Skip to content

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 decay
vasynth({ wave: 'sawtooth', cutoff: 300, resonance: 12, decay: 0.05, sustain: 0, release: 0.05 })
// Supersaw pad: detune with pwm, slow filter
vasynth({ wave: 'sawtooth', cutoff: 800, resonance: 2, pwm: 15, attack: 0.8, release: 2.0 })
// Soft flute: triangle, high cutoff, gentle ADSR
vasynth({ 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:

ParameterDefaultEffect
ratio1Modulator frequency relative to carrier. Integer ratios = harmonic tones
modIndex1Modulation depth — 0 = sine, higher = brighter/noisier
attack / release0.01 / 0.4Amplitude envelope
indexAttack / indexDecay0 / 0.5Modulation envelope — shapes brightness over time
// Bell: high ratio, index that decays
fmsynth({ ratio: 7, modIndex: 3, indexAttack: 0, indexDecay: 0.6, release: 2.0 })
// Electric piano: ratio 14, moderate index
fmsynth({ ratio: 14, modIndex: 0.8, indexDecay: 0.5, attack: 0.001, release: 0.8 })
// Bright pluck: high index, very short
fmsynth({ ratio: 3, modIndex: 6, indexDecay: 0.2, release: 0.15 })
// Sub bass: ratio 0.5, low modIndex
fmsynth({ 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 model
piano({ gain: 0.7 })
// Keys — electric piano (Rhodes-like)
keys({ gain: 0.6 })
// Mallet — marimba/xylophone
mallet({ gain: 0.7 })
// Modal — physical string/tube resonance
modal({ frequency: 200, decay: 2.0, gain: 0.6 })
// Strings — bowed string ensemble
strings({ attack: 0.4, release: 1.0, gain: 0.4 })

Timbral comparison: same melody, different machines

Paste each block and compare:

// Electric piano feel
function* 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 feel
function* 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 bell
function* 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 boost
eq(machine, { type: 'highshelf', freq: 8000, gain: -6 }) // high cut (darkens)
eq(machine, { type: 'peaking', freq: 2000, gain: 3, q: 1 }) // presence boost
eq(machine, { type: 'highpass', freq: 120 }) // remove sub rumble
eq(machine, { type: 'lowpass', freq: 5000 }) // soft filter

comp — 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 room
reverb(machine, { mix: 0.5 }) // large hall
reverb(machine, { mix: 0.85 }) // wash / ambient

pingpong — stereo delay

Creates rhythmic echos:

pingpong(machine, { time: 0.25, feedback: 0.4, mix: 0.3 }) // 1/4-note delay
pingpong(machine, { time: 0.375, feedback: 0.5, mix: 0.25 }) // dotted 1/8

Distortion effects

saturate(machine, { drive: 0.5, mix: 0.6 }) // subtle warmth / tape saturation
distort(machine, { drive: 0.85, mix: 0.8 }) // heavy overdrive

Chorus, flanger, phaser

Modulation effects that add movement and width:

chorus(machine, { rate: 0.5, depth: 0.4, mix: 0.5 }) // lush chorus
flanger(machine, { rate: 2.0, depth: 0.6, mix: 0.5 }) // jet-flanger
phaser(machine, { rate: 0.3, stages: 4, mix: 0.6 }) // slow phase sweep

Chaining 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 sweep
lfo({ rate: 4, min: 200, max: 2000, shape: 'square' }) // tremolo / auto-wah
lfo({ rate: 1, min: 600, max: 2000, shape: 'saw' }) // rising ramp
lfo({ rate: 0.2, min: 0.3, max: 0.9, shape: 'triangle' }) // slow volume swell (on gain)
ParameterDefaultMeaning
rate1 HzCycles per second
minMinimum output value
maxMaximum output value
shape'sine''sine' 'triangle' 'saw' 'square'
offset0 beatsPhase 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.