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