Three.js From Zero · Article s9-08

S9-08 Procedural Audio

Season 9 · Article 08

Procedural Audio

No samples. Synthesize every footstep, gunshot, explosion, engine note from oscillators + noise + filters. Infinite variations, tiny size, perfectly tuned.

1. Building blocks

  • Oscillators: tones. Sine / square / saw / triangle.
  • Noise: buffer of random values. Ambient, impact, hiss.
  • Filters: shape the spectrum. Lowpass = muffled, highpass = bright.
  • Envelopes: volume/filter over time via AudioParam scheduling.
  • Combinations: stack them, modulate each other.

2. Kick drum

function kick() {
  const t = ctx.currentTime;
  const osc = ctx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(120, t);
  osc.frequency.exponentialRampToValueAtTime(35, t + 0.1);  // pitch drop

  const g = ctx.createGain();
  g.gain.setValueAtTime(1, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);      // decay

  osc.connect(g).connect(ctx.destination);
  osc.start(t);
  osc.stop(t + 0.5);
}

3. Footstep

function footstep(surface) {
  const t = ctx.currentTime;
  const noise = ctx.createBufferSource();
  noise.buffer = makeNoiseBuffer(0.1);  // 100ms white noise

  const filter = ctx.createBiquadFilter();
  filter.type = surface === 'grass' ? 'lowpass' : 'highpass';
  filter.frequency.value = surface === 'grass' ? 800 : 2000;
  filter.Q.value = 1;

  const g = ctx.createGain();
  g.gain.setValueAtTime(0.3, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + 0.08);

  noise.connect(filter).connect(g).connect(ctx.destination);
  noise.start(t);
}

4. Engine (continuous)

const engine = ctx.createOscillator();
engine.type = 'sawtooth';
engine.frequency.value = 80;

const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 400;
engine.connect(filter).connect(ctx.destination);
engine.start();

// Each frame, update based on RPM
function updateEngine(rpm) {
  engine.frequency.setTargetAtTime(80 + rpm * 0.1, ctx.currentTime, 0.05);
  filter.frequency.setTargetAtTime(400 + rpm * 3, ctx.currentTime, 0.05);
}

5. Randomization for variation

function kickVariant() {
  const freq = 100 + Math.random() * 40;      // 100-140
  const decay = 0.3 + Math.random() * 0.2;    // 0.3-0.5
  const amp = 0.8 + Math.random() * 0.3;
  // ... use these values
}

Every footstep slightly different. Avoids "machine gun same-sound-50-times" effect.

6. Live demo — try the sounds

7. Snare = kick + noise

function snare() {
  const t = ctx.currentTime;
  // Tonal component (bottom)
  kickAt(200, 150, 0.15, t);
  // Noise burst (top)
  noiseBurst('highpass', 1500, 0.15, t);
}

8. AudioWorklet for complex DSP

Need FM synthesis, custom wave shaping, modular patches? AudioWorklet runs JS in the audio thread at sample rate.

await ctx.audioWorklet.addModule('processor.js');
const node = new AudioWorkletNode(ctx, 'my-processor');
node.connect(ctx.destination);

9. Takeaways

  • Oscillator + noise + filter + envelope = every sound.
  • Pitch envelopes on osc for percussive.
  • Filtered noise for impacts/wind/fire.
  • Randomize params for variation.
  • AudioWorklet for sample-accurate custom DSP.