Synth Framework — Overview
File Layout
static/js/synth/
core.js DSP utilities (encodeWAV, ADSR, normalize, math helpers)
registry.js register(id, engine) / get(id) for sequencer integration
distortion.js DistortionPipeline, curve generation, chain builder, UI
ui-builder.js Builds per-module param sections, envelope/waveform canvas, lock sync
preset-manager.js Generic preset CRUD with localStorage, manifest.json, list UI
playback.js AudioContext lifecycle, playBuffer with VU meter, WAV download
engine.js initSynth() orchestrator + renderToBuffer() headless render
midi-player.js SMF parser + AudioContext-timed MIDI scheduler
sequencer.js Step sequencer: strips, patterns, scheduler, effect chain, Turing Machine
sequencer-ui.js Sequencer DOM rendering: sound cards, step grid, effect controls
modules/
filter.js Low-pass BiquadFilter
eq.js 3-band shelving/peaking EQ
compressor.js Dynamics compressor
limiter.js Brickwall limiter (-6 dB, 20:1)
ott.js Multiband upward/downward compressor (OTT)
clarity.js Dynamic mud + air reduction
chorus.js BBD stereo chorus
reverb.js Convolution reverb (async OfflineAudioContext tail)
delay.js Stereo ping-pong tempo-synced delay (sequencer-ready)
osc.js Per-trigger oscillator bank
noise.js Cached noise buffer source
sub.js Sub oscillator (sine at freq/2)
pitch.js Frequency ramp (pitch decay)
terrain.js Wave terrain shaper network (BIA-specific)
tear.js LFO for terrain modulation (BIA-specific)
volume.js AD gain envelope
engines/
kick.js SynthKick — bass drum
snare.js SynthSnare — snare drum
hihat.js SynthHihat — hi-hat
bia-clone.js SynthMono — BIA Clone terrain synth
juno.js SynthJuno — polyphonic Juno-inspired
neuro.js SynthNeuro — sub/neurofunk bass
init-*.js Page bootstrap files (load engine, init UI, wire keyboard/MIDI)
Unified Engine Contract
Every engine follows the same pattern:
export var SynthX = {
id: 'x',
modules: [ /* module definitions */ ],
defaults: { params: { ... }, stages: [] },
presetsKey: 'x_synth_presets',
getDuration(params) { ... },
buildLiveChain(ctx, params, stages) {
var c = {};
c.masterGain = ctx.createGain();
c.masterGain.gain.value = 0;
var chainInput = c.masterGain;
for (var i = this.modules.length - 1; i >= 0; i--) {
var mod = this.modules[i];
if (mod.type === 'processor' && mod.build) {
var result = mod.build(ctx, params, chainInput, 1);
chainInput = result && result.input ? result.input : result;
} else if (mod.type === 'distortion') {
chainInput = buildDistortionChain(ctx, stages || [], chainInput);
}
}
c.input = chainInput; // per-note sources connect here
return c;
},
trigger(ctx, chain, params, time, velocity) {
// Create per-note sources connected to chain.input
var noteGain = ctx.createGain();
noteGain.gain.setValueAtTime(0, time);
...
noteGain.connect(chain.input);
},
updateChain(chain, params) { /* update baked-in params */ },
};
The backward loop iterates the modules array from last to first. Each
processor module's build() wraps the previous chainInput — its output connects
to chainInput, and it returns its input node (or { input, output, nodes }).
Module Types
| Type | Loop? | Purpose |
|---|---|---|
source |
Skipped | Oscillators, noise, terrain — handled by trigger() |
processor |
Wrapped | Filter, compressor, chorus, clarity, limiter — wired by backward loop |
distortion |
Wrapped | buildDistortionChain — inserted between processors |
master |
Skipped | Volume envelope — baked into buildLiveChain (type master, not processor) |
builtin |
Skipped | Post-render normalisation (applied in renderToBuffer Promise) |
Legacy render() vs Unified buildLiveChain + trigger
The old pattern had a standalone render() function that built sources and
connected to chainInput. This duplicated the processor wiring logic and
led to subtle differences between live playback (sequencer/midi) and WAV export.
The unified pattern uses buildLiveChain + trigger for everything:
- Live playback (sequencer audition, keyboard, MIDI) calls trigger() at audioCtx.currentTime
- WAV export calls trigger() at time 0 on an OfflineAudioContext
The engine.js renderToBufferInternal falls back to the unified path when
engine.render is falsy and engine.buildLiveChain + engine.trigger exist.
Lifecycle
- Bootstrap:
initSynth(engine)fetchespresets/{id}/default.json, builds UI - Live chain:
trigger()called by keyboard, sequencer, or MIDI player - Export:
renderToBuffer(engine, params, stages)builds offline chain, renders, applies builtins, returns buffer - Presets:
PresetManagermanages save/load via localStorage + built-in manifest.json