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.