Sequencer Audio — Stereo Master Bus & Strip Chains
Handles all Web Audio API graph construction: the stereo master bus (glue
compressor, send returns, per-channel limiters), per-strip audio chains
(volume → saturation → compressor → filter → EQ → panner), and the Ruina +
Reverb send effects. Extracted from the main sequencer.js to isolate
AudioContext-dependent code.
Files
static/js/synth/sequencer-audio.js (427 lines)
Stereo Master Bus
strips (mono → StereoPanner → L/R)
↓
_mergeL ─┐
├→ ChannelMerger(2) → _masterGain → _glueComp (stereo-linked DN)
_mergeR ─┘ ↓
ChannelSplitter(2)
├→ _limSumL ──→ _limiterL ──→ ┐
└→ _limSumR ──→ _limiterR ──→ ├→ ChannelMerger(2) → destination
↑
reverb.outputL ──→ _limSumL │
reverb.outputR ──→ _limSumR │
ruina.output(ch0) → _limSumL │
ruina.output(ch1) → _limSumR │
Built by ensureMasterBus(engine, ac):
- _mergeL/_mergeR — GainNodes receiving all strip outputs
- _masterGain — master volume control
- _glueComp — single DynamicsCompressorNode processing stereo (linked gain reduction)
- _limSumL/_limSumR — summing junctions post-glue, pre-limiter where send returns inject
- _limiterL/_limiterR — per-channel brickwall limiters (-6 dB, 20:1)
- Final ChannelMerger(2) ensures proper stereo mapping to destination
Strip Audio Chain
Built by initStripAudio(engine, stripId, ac?):
eqHigh → StereoPanner → ChannelSplitter(2)
└→ _mergeL (channel 0)
└→ _mergeR (channel 1)
Each strip chain (mono throughout):
volGain → duckGain → saturator (WaveShaper)
→ compBypass/compressor → makeupGain
→ filter → filterBypass
→ eqLow → eqMid → eqHigh
→ StereoPanner → _mergeL / _mergeR
Strip compressor routing
When compressor.amount > 0.01, the chain routes through the compressor with
auto makeup gain. The routing is tracked via chain._compRouted / _lastCompAmt
to avoid unnecessary disconnect/reconnect cycles.
StereoPanner
Each strip has a StereoPannerNode (mono → stereo). Its L channel connects to
_mergeL, R channel to _mergeR. Pan is controlled by strip.pan (−1 to +1).
Send Effects
Reverb
_playNote → revG (gain = sends.reverb) → _reverbInput → reverbNode.input
→ reverbNode.outputL → _limSumL
→ reverbNode.outputR → _limSumR
Ruina
_playNote → ruinaG (gain = sends.ruina) → _ruinaInput → ruinaNode.input
→ ruinaNode.output → ChannelSplitter(2)
→ ch0 → _limSumL
→ ch1 → _limSumR
Offline Rendering
Two functions support explicit AudioContext for WAV export:
buildOfflineMaster(engine, ac, includeSends)
Creates a full stereo master bus in an OfflineAudioContext. Returns
{ mergeL, mergeR, limSumL, limSumR, reverbNode, ruinaNode }.
buildOfflineStripChain(strip, ac)
Creates a strip's audio chain in an explicit AudioContext without storing on
the engine. Returns all nodes for immediate use. No side effects.
Exports
| Export | Signature | Purpose |
|---|---|---|
buildSaturationCurve |
(amount, len) |
Float32Array tanh waveshaper curve |
ensureMasterBus |
(engine, ac) |
Creates stereo master bus on first call |
initStripAudio |
(engine, stripId, ac?) |
Creates strip chain, skips if exists; ac optional for offline |
updateStripAudio |
(engine, stripId) |
Updates vol/solo/mute, saturator, compressor routing, filter, EQ, pan |
buildOfflineMaster |
(engine, ac, includeSends) |
Async — master bus for offline render |
buildOfflineStripChain |
(strip, ac) |
Strip chain for offline render |
Channel Count Pattern
StereoPannerNode and ruina output GainNode may not report 2 output channels
at connect() time. The pattern used throughout is:
var splitter = ac.createChannelSplitter(2);
panner.connect(splitter);
splitter.connect(destL, 0, 0); // ch0 → L
splitter.connect(destR, 1, 0); // ch1 → R
ChannelSplitterNode always has exactly numberOfOutputs outputs regardless
of input channel count.
See Also
docs/synth/sequencer.md— SequencerEngine overviewdocs/synth/modules/reverb.md— convolution reverb moduledocs/synth/modules/ruina.md— stereo distortion module