4. Sound Design
This tutorial covers every instrument and effect, and shows how to combine them into polished sounds.
Synth machines compared
vasynth — virtual analog
Classic oscillator → filter → ADSR. Best for leads, basses, pads, and arpeggios.
// Acid bass: high resonance, short decayvasynth({ wave: 'sawtooth', cutoff: 300, resonance: 12, decay: 0.05, sustain: 0, release: 0.05 })
// Supersaw pad: detune with pwm, slow filtervasynth({ wave: 'sawtooth', cutoff: 800, resonance: 2, pwm: 15, attack: 0.8, release: 2.0 })
// Soft flute: triangle, high cutoff, gentle ADSRvasynth({ wave: 'triangle', cutoff: 5000, attack: 0.15, decay: 0.1, sustain: 0.8, release: 0.4 })pwm is a detune amount in cents applied with a slight random spread — larger values = fatter, thicker sound.
fmsynth — frequency modulation
FM synthesis produces complex timbres by modulating one oscillator’s frequency with another. Two key parameters:
| Parameter | Default | Effect |
|---|---|---|
ratio | 1 | Modulator frequency relative to carrier. Integer ratios = harmonic tones |
modIndex | 1 | Modulation depth — 0 = sine, higher = brighter/noisier |
attack / release | 0.01 / 0.4 | Amplitude envelope |
indexAttack / indexDecay | 0 / 0.5 | Modulation envelope — shapes brightness over time |
// Bell: high ratio, index that decaysfmsynth({ ratio: 7, modIndex: 3, indexAttack: 0, indexDecay: 0.6, release: 2.0 })
// Electric piano: ratio 14, moderate indexfmsynth({ ratio: 14, modIndex: 0.8, indexDecay: 0.5, attack: 0.001, release: 0.8 })
// Bright pluck: high index, very shortfmsynth({ ratio: 3, modIndex: 6, indexDecay: 0.2, release: 0.15 })
// Sub bass: ratio 0.5, low modIndexfmsynth({ ratio: 0.5, modIndex: 0.3, attack: 0.01, release: 0.2 })Acoustic-style machines
These model physical instruments and have fewer parameters to tweak — mostly gain, release, and timbre-specific controls.
// Piano — velocity-sensitive acoustic piano modelpiano({ gain: 0.7 })
// Keys — electric piano (Rhodes-like)keys({ gain: 0.6 })
// Mallet — marimba/xylophonemallet({ gain: 0.7 })
// Modal — physical string/tube resonancemodal({ frequency: 200, decay: 2.0, gain: 0.6 })
// Strings — bowed string ensemblestrings({ attack: 0.4, release: 1.0, gain: 0.4 })Timbral comparison: same melody, different machines
Paste each block and compare:
// Electric piano feelfunction* song(ctx) { const inst = keys({ gain: 0.6 }) const prog = chords(8, scales(4, 'Eb4:major'), 'Imaj7,IVmaj7,V7,I') yield cast(inst, prog, seq(4, '0,2,4,2,3,4,2,0'), ctx)}// Marimba feelfunction* song(ctx) { const inst = mallet({ gain: 0.7 }) const prog = chords(8, scales(4, 'C4:major pentatonic'), 'I,IV,I,V') yield cast(inst, prog, seq(4, '0,2,4,2,3,4,2,0'), ctx)}// FM bellfunction* song(ctx) { const inst = fmsynth({ ratio: 7, modIndex: 2, indexDecay: 1.0, release: 2.5, gain: 0.5 }) const prog = chords(8, scales(4, 'C4:major'), 'Imaj7,IVmaj7,V7,Imaj7') yield cast(inst, prog, seq(4, '0,2,4,2,3,4,2,0'), ctx)}Effects
Effects are inserted between a machine and the master bus using chain():
const synth = chain(vasynth({ wave: 'sawtooth' }), eq({ freq: 200, gain: -6 }), reverb({ mix: 0.3 }))Or apply a single effect directly:
const synth = reverb(vasynth({ wave: 'sawtooth' }), { mix: 0.4 })eq — equalizer
Boost or cut a frequency band:
eq(machine, { type: 'lowshelf', freq: 200, gain: 4 }) // bass boosteq(machine, { type: 'highshelf', freq: 8000, gain: -6 }) // high cut (darkens)eq(machine, { type: 'peaking', freq: 2000, gain: 3, q: 1 }) // presence boosteq(machine, { type: 'highpass', freq: 120 }) // remove sub rumbleeq(machine, { type: 'lowpass', freq: 5000 }) // soft filtercomp — compressor
Reduces dynamics — makes quiet notes louder relative to loud ones:
comp(machine, { threshold: -18, ratio: 4, attack: 0.003, release: 0.1 })reverb — convolution reverb
Adds acoustic space:
reverb(machine, { mix: 0.15 }) // subtle roomreverb(machine, { mix: 0.5 }) // large hallreverb(machine, { mix: 0.85 }) // wash / ambientpingpong — stereo delay
Creates rhythmic echos:
pingpong(machine, { time: 0.25, feedback: 0.4, mix: 0.3 }) // 1/4-note delaypingpong(machine, { time: 0.375, feedback: 0.5, mix: 0.25 }) // dotted 1/8Distortion effects
saturate(machine, { drive: 0.5, mix: 0.6 }) // subtle warmth / tape saturationdistort(machine, { drive: 0.85, mix: 0.8 }) // heavy overdriveChorus, flanger, phaser
Modulation effects that add movement and width:
chorus(machine, { rate: 0.5, depth: 0.4, mix: 0.5 }) // lush chorusflanger(machine, { rate: 2.0, depth: 0.6, mix: 0.5 }) // jet-flangerphaser(machine, { rate: 0.3, stages: 4, mix: 0.6 }) // slow phase sweepChaining multiple effects
const synth = chain( vasynth({ wave: 'sawtooth', cutoff: 2000 }), (m) => eq(m, { type: 'highpass', freq: 80 }), (m) => comp(m, { threshold: -16, ratio: 3 }), (m) => reverb(m, { mix: 0.25 }),)LFO — parameter modulation
lfo() creates a low-frequency oscillator that modulates a synth parameter over time. Pass it as the parameter value instead of a number:
const wobble = lfo({ rate: 2, min: 400, max: 2000, shape: 'sine' })const synth = vasynth({ wave: 'sawtooth', cutoff: wobble })LFO shapes and rates
lfo({ rate: 0.5, min: 800, max: 4000, shape: 'sine' }) // slow sine sweeplfo({ rate: 4, min: 200, max: 2000, shape: 'square' }) // tremolo / auto-wahlfo({ rate: 1, min: 600, max: 2000, shape: 'saw' }) // rising ramplfo({ rate: 0.2, min: 0.3, max: 0.9, shape: 'triangle' }) // slow volume swell (on gain)| Parameter | Default | Meaning |
|---|---|---|
rate | 1 Hz | Cycles per second |
min | — | Minimum output value |
max | — | Maximum output value |
shape | 'sine' | 'sine' 'triangle' 'saw' 'square' |
offset | 0 beats | Phase offset relative to the cycle |
LFO on different parameters
// Wobble cutoff (wah effect)const synth = vasynth({ wave: 'sawtooth', cutoff: lfo({ rate: 3, min: 300, max: 3000, shape: 'sine' }) })
// Vibrato (pitch oscillation via pwm)const synth = vasynth({ wave: 'triangle', pwm: lfo({ rate: 5, min: -30, max: 30, shape: 'sine' }) })
// Tremolo (volume oscillation on FM)const synth = fmsynth({ ratio: 2, modIndex: lfo({ rate: 6, min: 0.5, max: 4, shape: 'square' }) })Complete sound design example
Lush ambient pad with chorus and reverb, plus a bell lead with FM:
function* song(ctx) { const pad = chain( vasynth({ wave: 'sawtooth', cutoff: lfo({ rate: 0.3, min: 500, max: 1800, shape: 'sine' }), attack: 0.8, release: 2.0, gain: 0.3 }), (m) => chorus(m, { rate: 0.4, depth: 0.5, mix: 0.6 }), (m) => reverb(m, { mix: 0.5 }), ) const bell = chain( fmsynth({ ratio: 7, modIndex: 2.5, indexDecay: 1.2, release: 3.0, gain: 0.5 }), (m) => reverb(m, { mix: 0.4 }), )
const key = scales(4, 'G4:major') const prog = chords(16, key, 'Imaj7,IVmaj7,IIm7,V7')
yield cast(pad, prog, seq(4, '{0,2,4},~,~,~'), ctx) yield cast(bell, prog, seq(8, '0,_,_,2,_,4,_,_'), ctx)}Next
5. Song Structure → — use ctx.cycle, arrange(), and meta events to build evolving, multi-section songs.