Step Sequencer
A multi-track step sequencer with a strip-based architecture. Each strip groups one or more sounds with a shared effects chain (saturation, glue compressor, DJ filter, volume, pan) and per-sound step sequencing with Song Mode arrangement.
Available at /synth/sequencer (read the sequencer overview).
Quick Links
| Document | Description |
|---|---|
| overview | Architecture overview, plugin pattern, module types |
| engine | initSynth orchestrator, signal chain wiring, renderToBuffer |
| distortion | Distortion pipeline, curve generation, chain builder |
| preset-manager | Preset CRUD, localStorage persistence |
| template-manager | Template system for constrained randomisation |
| playback | AudioContext lifecycle, preview, VU meter, download |
| sequencer-audio | Stereo master bus, strip audio chains, send FX |
| sequencer-persistence | Serialization, auto-save, snapshots |
| sequencer-song | Song mode arranger, slice system |
Architecture
init-sequencer.js
→ restores saved state from localStorage (or starts fresh)
→ creates SequencerEngine
→ calls buildSequencerUI(container, seq) (DOM builder)
sequencer.js (orchestrator, ~1187 lines)
├── addStrip / addSoundToStrip / removeSoundFromStrip
├── setStep / setVelocity / setChance / setMod
├── start / stop / _scheduleLoop (lookahead scheduler)
├── renderStripBuffer (WAV loop export)
├── _playNote (trigger + sends + sidechain)
├── undo / _saveUndoSnapshot
├── mute / solo / pan
├── sound snapshot CRUD
└── Song Mode gating (_songPlayback, _savedSongState)
sequencer-audio.js (Web Audio graph, ~427 lines)
├── ensureMasterBus (stereo master chain)
├── initStripAudio / updateStripAudio (strip chain)
├── buildOfflineMaster / buildOfflineStripChain
└── buildSaturationCurve
sequencer-persistence.js (save/restore/snapshots, ~363 lines)
├── serialize / save (debounced 150ms) / restore
├── project snapshot CRUD
├── sound card snapshot CRUD
└── createStep / cloneSteps / _normalizeModRoute
sequencer-song.js (Song Mode, ~161 lines)
└── SongMode class — slice CRUD, activateSlice, setCellSnapshot
AudioContext from playback.js (shared). Sound rendering uses
renderToBuffer() from engine.js.
Strips
A strip is a container that groups sounds with a shared audio processing chain. Each strip renders as a card containing:
┌─ Strip Card ─────────────────────────────────────────────────────┐
│ [name] [Pan ○] [M] [S] [WAV] [✕] │
│ │
│ Comp: [On/Off] Thresh [===○==] -12 Ratio [===○==] 4 ... │
│ (always visible) │
│ │
│ ┌─ sound card ───┐ ┌─ sound card ───┐ ┌─ Sat ─┐ ┌─ Pan ─┐ │
│ │ [snap□] ... │ │ [snap□] ... │ │ [fdr] │ │ [fdr] │ │
│ │ RS RC RT RV RM │ │ RS RC RT RV RM │ └───────┘ └───────┘ │
│ │ Snap List Undo │ │ Snap List Undo │ ┌─ Filt ─┐ ┌─ Vol ─┐│
│ └────────────────┘ └────────────────┘ │ [fdr] │ │ [fdr] ││
│ └───────┘ └───────┘│
│ │
│ [+ Add Sound] │
└───────────────────────────────────────────────────────────────────┘
Effect cards (Saturation, Compressor, DJ Filter, Volume, Pan) are shown per
strip alongside the sound cards. Each effect card has a vertical fader
(48px wide, flex height, writing-mode:vertical-lr).
Strip Audio Chain
source → noteGain (per-trigger envelope)
→ volGain (strip volume)
→ duckGain (sidechain ducking, gain=1 normally)
→ Saturator (tanh waveshaper, drive 0–1)
→ Glue Compressor (6ms attack, 12dB soft knee, auto makeup gain)
→ DJ Filter (blend 0→1 maps LPF→Flat→HPF, springs to neutral at bar boundary)
→ strip EQ (3-band: lowshelf 250Hz, peaking 1kHz, highshelf 8kHz)
→ StereoPannerNode (pan −1..+1)
→ _mergeL/R (ChannelMerger(2))
→ _masterGain (0.5 headroom)
→ _glueComp (stereo-linked compressor, threshold −30dB, ratio 4:1)
→ ChannelSplitter(2)
→ _limSumL / _limSumR (distortion return summing nodes)
→ _limiterL / _limiterR (brickwall −6dB, 20:1)
→ ChannelMerger(2) → destination
Send FX (reverb, stereo distortion) returns inject into _limSumL/R. See
Send FX below.
The duckGain node sits between volGain and the saturator. Its gain is
normally 1. When a sidechain source fires, _playNote schedules a gain dip:
gain → 1 − (velocity × amount^0.5) over attack ms, then recovers over
release ms. See Sidechain Ducking below.
Patterns (Removed)
Patterns were removed during the Turing Machine refactoring. Each sound now
has a single step array. Step state is managed per-sound with activeSteps
determining the loop length (16–128). Song Mode provides arrangement via
slices that reference sound card snapshots.
SequencerEngine
State
| Property | Type | Description |
|---|---|---|
sounds |
Array | List of sound objects (shared across all strips) |
strips |
Array | List of strip objects |
bpm |
number | Tempo 40–300 (default 120) |
scaleRoot |
string | Global key root (C–B, default C) |
scaleName |
string | Scale type (chromatic, major, minor, pentatonic, blues) |
defaultOctave |
number | Base octave for Note tab (1–9, default 3) |
totalSteps |
number | Fixed at 128 (steps stored per sound) |
isPlaying |
boolean | Transport state |
Each strip:
| Property | Type | Description |
|---|---|---|
id |
string | Unique identifier |
name |
string | Editable strip name |
soundIds |
array | Ordered list of sound IDs in this strip |
volume |
number | 0–1 master volume (default 0.75) |
pan |
number | −1–+1 stereo pan (default 0) |
mute |
boolean | Mute toggle |
solo |
boolean | Solo toggle (overrides mute, sums soloed strips) |
compressor |
object | { amount: 0–1 } single-fader glue compressor |
saturation |
object | { amount: 0–1 } tanh waveshaper drive |
filter |
object | { blend: 0.5 } DJ filter (0=LPF, 0.5=Flat, 1=HPF) |
sendReverb |
number | 0–1 reverb send level |
sendRuina |
number | 0–1 stereo distortion send level |
ducks |
array | Ducking targets: [{ soundId, amount, attack, release }] |
Each sound/track:
| Property | Type | Description |
|---|---|---|
id |
string | Unique identifier |
name |
string | Editable track name |
engineId |
string | Engine type (kick, snare, hihat, bia-clone, juno, neuro) |
engine |
object | Reference to the SynthEngine |
presetName |
string | Loaded preset name (null = defaults) |
activeSteps |
number | How many steps this track plays (16–128, default 16) |
params |
object | Current synth parameters |
stages |
array | Current distortion stages |
steps |
array | 128 elements of {active, velocity, chance, mod, note} |
legato |
boolean | Sustain through consecutive notes (no noteOff between steps) |
sidechain |
object | Ducking config: {sourceId, amount, attack, release} |
modRoute |
array | Modulation routing: [{param, atten, bipolar}] |
_snapshotName |
string | Active sound card snapshot name (persisted) |
_snapshotDirty |
boolean | True when unsaved changes exist |
_undoHistory |
array | Max 10 undo snapshots of {params, stages, steps, eq, sidechain, sends, modRoute} |
Per-step Properties
| Property | Range | Default | Description |
|---|---|---|---|
active |
bool | false | Pad on/off (no default triggers) |
velocity |
0–1 | 0.85 | Volume multiplier |
chance |
0–100 | 100 | Probability (%) that the step fires |
mod |
0–1 | 0 | Modulation amount (routed via modRoute) |
note |
-1–71 | -1 | Scale note index (-1 = off, 0+ = note in current scale across 3 octaves) |
Strip Methods
| Method | Description |
|---|---|
addStrip(name) |
Creates a new empty strip |
removeStrip(stripId) |
Removes strip and all its sounds |
addSoundToStrip(stripId, engineId) |
Creates a new sound in the strip |
removeSoundFromStrip(stripId, soundId) |
Removes a sound from the strip |
Sound Methods
| Method | Description |
|---|---|
loadPreset(soundId, name) |
Loads a preset snapshot, re-renders |
randomiseSound(soundId) |
Randomises unlocked params, re-renders |
randomiseVelocities(soundId) |
Sets random velocity (0.3–1.0) on active steps |
randomiseChance(soundId) |
Sets random chance (0–100%) on active steps |
randomiseTriggers(soundId) |
Randomises active steps (≈40% density) |
randomiseMod(soundId) |
Sets random mod (−1.0–1.0) on active steps |
undo(soundId) |
Restores previous state from undo history |
setStep(soundId, idx, active) |
Toggles a step, extends/contracts activeSteps |
setVelocity(soundId, idx, v) |
Sets velocity 0–1 (click-drag) |
setChance(soundId, idx, chance) |
Sets chance percentage 0–100 (click-drag) |
setMod(soundId, idx, mod) |
Sets mod value 0–1 (click-drag) |
setBpm(n) |
Sets tempo 40–300 |
reRenderSound(soundId) |
Re-renders a single track |
reRenderAll() |
Re-renders all tracks |
auditionSound(soundId) |
Plays the track once immediately |
start() / stop() |
Transport control |
renderStripBuffer(stripId, opts) |
Renders strip to WAV via OfflineAudioContext |
saveSoundSnapshot(soundId, name) |
Saves sound card snapshot to localStorage |
loadSoundSnapshot(soundId, name) |
Loads sound card snapshot from localStorage |
listSoundSnapshots() |
Lists all saved sound card snapshots |
deleteSoundSnapshot(name) |
Deletes a sound card snapshot |
setPan(stripId, pan) |
Sets strip pan −1..+1 |
Persistence
| Method | Description |
|---|---|
_save() |
Debounced (150ms) localStorage write |
restore(data) |
Restores full state including strips + patterns |
saveSnapshot(name) |
Serializes full state and saves as named snapshot to localStorage('sequencer_snapshots') |
loadSnapshot(name) |
Persists snapshot to auto-save key then reloads page |
deleteSnapshot(name) |
Removes a named project snapshot |
listSnapshots() |
Returns [{name, date, data}] |
exportSnapshot(name) |
Returns plain object for JSON download |
importSnapshot(data) |
Saves as snapshot + persists to auto-save + reloads |
saveSoundSnapshot(soundId, name) |
Saves per-sound param/steps snapshot to localStorage('sequencer_sound_snapshots') |
loadSoundSnapshot(soundId, name) |
Restores per-sound params/steps |
listSoundSnapshots() |
Returns all saved sound card snapshots |
deleteSoundSnapshot(name) |
Removes a saved sound card snapshot |
Scheduler
The scheduler uses a lookahead approach with setTimeout (batching) rather
than requestAnimationFrame, scheduled in waves via _scheduleNextWave():
_scheduleWave():
// Every ~50ms, schedule notes for the next ~1s window
while (_nextEventTime < _lookaheadEnd):
for each sound:
step = sound.steps[sound._currentStep]
vel = step.active AND chance pass ? step.velocity : 0
_playNote(sound, _nextEventTime, vel, stepIdx)
sound._currentStep = (sound._currentStep + 1) % sound.activeSteps
_globalStep++
_nextEventTime += stepDuration
// Song Mode auto-advance at bar boundary
if _songPlayback and _globalStep >= pendingSliceStart:
activate next slice
setTimeout(_scheduleWave, 50)
_playNote Flow
_playNote is the core per-step function. It handles three paths:
noteOn/noteOff path (Neuro — engines that implement both functions):
1. Chain built once per sound (sound._liveChain), reused for all steps
2. Pre-note: if previous note active AND (velocity == 0 OR not legato), calls
noteOff on the previous note (ramps gain to 0, stops old oscillators)
3. If velocity > 0 and step has a note, calls noteOn to create fresh
oscillators/filters/LFO at the new frequency
4. Post-note: unless sound.legato is true, schedules a setTimeout to call
noteOff at 90% of the step duration
5. With sound.legato = true: notes sustain through consecutive steps
(no noteOff between them); the next noteOn creates new oscillators
while the old ones continue playing; noteOff only fires on gaps (velocity 0)
trigger path (kick, snare, hihat, BIA — one-shot engines):
1. Calls sound.engine.trigger() for each active step
2. Velocity 0 steps are skipped
After triggering, sends (reverb, stereo distortion) and sidechain ducking are wired if applicable.
Effect Cards
All four effect cards share the same vertical fader design (48px wide, flex
height, writing-mode:vertical-lr orientation).
Saturation (Sat)
Uses a tanh-based waveshaper. At 0% the curve is identity (no saturation).
At 100% the curve applies heavy tape-style saturation (tanh(x·11)/tanh(11)),
clipping peaks musically while preserving the waveform envelope.
| Fader | Range | Display |
|---|---|---|
| Drive | 0–1 | "Off" / percentage |
Glue Compressor (Comp)
Single-fader compressor with auto makeup gain:
| Fader | Range | Maps To |
|---|---|---|
| Amount | 0–1 | Threshold 0 → -30 dB, Ratio 1:1 → 10:1 |
Fixed parameters: 6ms attack, 60ms release, 12dB soft knee. Makeup gain increases proportionally (+0 to +6 dB) to compensate for gain reduction, keeping perceived volume consistent.
DJ Filter (Filt)
Spring-loaded filter that returns to neutral at the next bar boundary on release. Uses a single vertical fader:
| Position | Filter Type | Frequency |
|---|---|---|
| 0.0 | Lowpass | 20 Hz |
| 0.5 | Flat (neutral) | 20000 Hz |
| 1.0 | Highpass | 20 Hz |
Q is fixed at 0.7 for a smooth, musical response.
Volume (Vol)
Strip master volume fader. Range 0–100%, controls the per-strip output level before the master bus.
Pan
Strip panoramic fader. Range −100%–+100% (left–right), controls a
StereoPannerNode at the end of the strip's audio chain.
Send FX (Snd tab on sound card)
Each sound card has a Snd tab with two send faders:
| Control | Range | Description |
|---|---|---|
| Reverb | 0–100% | Send level to master reverb bus |
| Stereo Distortion | 0–100% | Send level to master distortion bus |
Sends are wired post-strip-chain, pre-glue-compressor, returning into
_limSumL/R nodes so reverb/stereo-distortion tails don't pump the glue compressor
but still pass through the brickwall limiter.
Master Bus
All strips sum into a shared stereo master bus:
_mergeL/R (ChannelMerger(2))
→ _masterGain (0.5 headroom)
→ _glueComp (stereo-linked compressor, threshold −30dB, ratio 4:1,
6ms attack, 60ms release, auto makeup gain)
→ ChannelSplitter(2)
→ _limSumL / _limSumR (stereo distortion return summing nodes)
→ _limiterL / _limiterR (brickwall −6dB threshold, 20:1 ratio, 1ms attack)
→ ChannelMerger(2) → ac.destination
Reverb return injects at _limSumL/R (post-glue, pre-limiter). Stereo distortion
return also injects at _limSumL/R. This prevents reverb/distortion tails from
pumping the glue compressor while still passing through the final brickwall
limiter.
The Master section (collapsible, default collapsed below transport controls) contains:
| Card | Description |
|---|---|
| Glue | Stereo-linked compressor (threshold, ratio, attack, release) |
| Reverb | Convolution reverb (mix, predelay, decay) |
| Stereo Distortion | Multi-mode stereo distortion (mode pills, drive, fold, crush, mix, width) |
| Volume | Master gain fader |
Step Grid
Each track has up to 8 pages of 16 steps (128 total):
[1] [2] [3] [4] [5] [6] [7] [8] ← page pills (blue = current,
┌──┬──┬──┬──┐ yellow = has triggers,
│1 │2 │3 │4 │ dimmed = beyond activeSteps)
├──┼──┼──┼──┤
│5 │6 │7 │8 │ 4×4 grid, step numbers 1–16
├──┼──┼──┼──┤ click to toggle pad on/off
│9 │10│11│12│ right-click for velocity prompt
├──┼──┼──┼──┤
│13│14│15│16│
└──┴──┴──┴──┘
- Clicking a dimmed page pill extends the track's step count to include it
- Active pads show warm amber glow (3px border, theme primary colour)
- Currently-playing pad has a red border highlight
- Opacity reflects velocity (faint = low velocity, bright = full)
Step Count
Each track's activeSteps grows when a step on a new page is activated:
- Page 2 (steps 16–31) → activeSteps = 32
- ...
- Page 8 (steps 112–127) → activeSteps = 128
Removing all steps from a page contracts the count back down.
Tab Controls
| Tab | Description |
|---|---|
| Trig | 4×4 step trigger grid with page pills (8 pages) |
| Vel | 4×4 velocity grid — click-drag up/down to adjust 0–1 |
| Chance | 4×4 probability grid — set each step's trigger chance 0–100% |
| Random | Randomise Steps (params + re-render) and Randomise Velocities |
| Duck | Sidechain ducking — coming soon |
| Choke | Choke groups — coming soon |
Page pill state is shared across all tabs — switching tabs preserves the current page, and page pills show active/trigger status in all tabs.
Transport Controls
Transport controls are rendered as clickable pill buttons (not inline selects). Clicking opens a modal dialog for the respective setting.
| Control | Shortcut | Description |
|---|---|---|
| ▶ Play / ■ Stop | Space | Toggles playback |
| BPM | — | Tempo 40–300 (pill opens modal) |
| Key (C–B) | — | Global scale root for Note tab (pill opens modal) |
| Scale (chromatic/major/minor/pentatonic/blues) | — | Scale note set for Note tab (pill opens modal) |
| Oct (1–9) | — | Base octave for Note tab (pill opens modal) |
| New | — | Create a new project snapshot (prompts for name) |
| Save | — | Overwrite the currently loaded project snapshot |
| Snapshots | — | Open snapshot manager (list/load/export/import/delete) |
| Song | — | Toggle Song Mode arranger view |
| ⛶ | — | Toggle fullscreen (hides nav/breadcrumbs/title) |
| + Add Strip | — | Creates a new empty strip |
The BPM/Key/Scale/Oct pills update immediately when a new value is selected and serve as the note pool for the Note tab on each sound card.
Project Snapshots
The sequencer supports named snapshots of the entire state (all strips, sounds,
BPM, master settings, EQ, sidechain, sends). Saved to
localStorage('sequencer_snapshots').
- New button in the transport bar prompts for a name and creates a new snapshot (flashes green checkmark on success)
- Save button overwrites the currently loaded snapshot (flashes green checkmark on success)
- Snapshots button opens a modal listing all saved snapshots with Load / Export / Import / Del buttons per entry
- Loading a snapshot persists it to
localStorage('sequencer_state')and reloads the page to rebuild the UI from the restored data - Export downloads a JSON file, Import reads a JSON file and saves as a new snapshot (also immediately loads it)
The _serialize() method captures everything — BPM, scale, master volume,
glue compressor params, reverb params, stereo distortion params, all strips with their
sound IDs, and all sounds with their full param state, steps, sidechain config,
mod routes, sends, and sound snapshots.
Sound Card Snapshots
In addition to project-level snapshots, each sound card can save/load its own
per-sound snapshots stored in localStorage('sequencer_sound_snapshots'):
- Snap (Save) button — saves current sound params, stages, steps, EQ, sidechain, sends, and mod route under a snapshot name
- List button — opens a modal listing all saved snapshots for this sound with Load / Del buttons
Sound card snapshots are included in project snapshot export/import via
data.soundSnapshots. The active snapshot name is displayed as a label next
to the List button, turning amber (_snapshotDirty) when the sound state
diverges from the saved snapshot.
Undo History
Each sound maintains an undo history (max 10 snapshots) in
sound._undoHistory. Every randomisation or step mutation pushes the
previous state. The Undo button in the sound card's icon bar restores
the most recent entry. Entries capture {params, stages, steps, eq, sidechain,
sends, modRoute}.
Sound Card Controls
| Control | Description |
|---|---|
| Name | Click to edit via dialog |
Engine (e.g. Kick Synth) |
Click to change via dropdown dialog (preserves steps) |
Preset (Default / saved) |
Click to load via dropdown dialog |
| 🎧 | Audition — plays the track once immediately |
| ⚙ | Open in synth page (same-tab, with preset pre-loaded) |
| ✕ | Remove sound from strip |
Sound Card Icon Bar
Each sound card has an icon bar with randomisation, snapshot, and undo buttons:
| Button | Action |
|---|---|
| RS (Randomise Sound) | Randomises unlocked param values, re-renders current tab |
| RC (Randomise Chance) | Sets random chance (70–100%) on active steps, re-renders Chance tab |
| RT (Randomise Triggers) | Randomises which steps are active (≈40% density), re-renders Trig tab |
| RV (Randomise Velocities) | Sets random velocity (0.3–1.0) on active steps, re-renders Vel tab |
| RM (Randomise Mod) | Sets random mod (–1.0–1.0) on active steps, re-renders Mod tab |
| Snap | Saves current sound state as a named snapshot |
| List | Opens snapshot list modal (load/delete) |
| Undo | Restores previous state (params + steps) from undo history |
Each randomisation button also re-renders the relevant tab directly (not just
the current tab), so the user sees the randomised values immediately. All
randomisation and step mutations set sound._snapshotDirty = true, turning
the snapshot label amber.
Step Object Integrity
Every step mutation uses _replaceStep(sound, idx, patch) which creates a
new object at the array index rather than mutating properties in-place.
This guarantees no two array positions can share the same object reference
(which would cause unrelated steps to change together). The randomisation
functions also create new step objects per-index instead of using forEach
with in-place mutation.
When loading saved data, restore and _loadPattern pad the steps array
to totalSteps (128) with fresh objects to prevent short arrays from
old-format saves.
Sound Card Tabs
Each sound card has a tab bar: Trig, Vel, Chance, Note, Duck, EQ, Snd, Mod.
Note Tab
Displays a 4×4 grid of step pads where each pad shows the note name
(e.g. C3, E♯4) or — for inactive steps. The note is determined by the
global Key and Scale transport controls and the Oct setting:
- Click an empty pad to set the first scale degree and activate the step.
- Click again to cycle through the scale's notes across 3 octaves.
- Right-click to clear the note (step becomes inactive).
- The octave range uses the transport Oct setting as the base.
- Note data is stored per-step as
note(index into the scale grid).
When a step has a note ≥ 0, _playNote overrides the sound's frequency:
freq = midiToFreq(semitone + (defaultOctave + octaveOffset) × 12).
Duck Tab (Sidechain Compression)
Configures ducking of this sound's volume triggered by another sound:
| Control | Range | Default | Description |
|---|---|---|---|
| Source | dropdown | None | Sound that triggers the duck |
| Amount | 0–100% | 50% | Max gain reduction (power curve applied) |
| Attack | 1–50 ms | 5 ms | Time to reach full duck |
| Release | 10–500 ms | 100 ms | Time to recover to full volume |
A real-time envelope canvas at the bottom of the tab shows the inverted duck
curve (gain reduction over time) using the same power-curve calculation as
_playNote.
When the source sound fires, _playNote schedules on the target strip's
duckGain node:
g = targetStrip.duckGain.gain
dip = max(0, 1 − velocity × amount^0.5)
g.linearRampToValueAtTime(dip, time + attack)
g.linearRampToValueAtTime(1, time + attack + release)
Sound Settings
Each sound card has a settings button that opens a modal dialog with:
| Control | Description |
|---|---|
| Open in Synth | Opens the standalone synth page with the current preset |
| Max steps | Loop length (16–128) |
| Legato | When enabled, notes sustain through consecutive steps without retriggering the envelope. noteOff only fires on gaps (velocity 0) or sequencer stop. |
Mod Tab
Modulation routing table. Each row assigns a step's mod value (0–1) to a
target synth parameter:
| Column | Description |
|---|---|
| Param | Dropdown of all engine params |
| Atten | 0–100% modulation depth |
| ± | Toggle bipolar (negative values allowed) |
| ✕ | Remove this route |
When a step with mod > 0 fires, _applyModLive clones the sound's params
and applies p[param] += mod × atten × (max − min) / 2 for each active route.
Snd Tab (Send FX)
Controls how much of this sound is sent to the master reverb and stereo distortion buses:
| Control | Range | Description |
|---|---|---|
| Reverb | 0–100% | Send level to master convolution reverb |
| Stereo Distortion | 0–100% | Send level to master stereo distortion |
Sends are connected each time the sound triggers (gated by stripPlaying),
routed post-strip-chain into the master bus summing nodes.
Song Mode
Song Mode replaces the multi-pattern system with a slice-based arranger. Accessed via the Song button in the transport bar.
Architecture
SongMode class (sequencer-song.js)
├── slices[] — array of slice objects
├── addSlice(name, bars) / removeSlice(idx) / renameSlice / cloneSlice
├── moveSlice(fromIdx, toIdx)
├── setCellSnapshot(sliceIdx, soundId, snapshotName)
├── activateSlice(idx)
├── totalSteps() — sum of all slices' bars × 16
├── getSliceAtStep(globalStep) — returns {slice, sliceStart, sliceEnd}
└── serialize / restore / save
Slice System
A slice represents a section of the arrangement and stores the sound card snapshot name assigned to each sound card column for that section:
| Property | Type | Description |
|---|---|---|
name |
string | Slice label (editable) |
bars |
number | Length in bars (1–64, default 4) |
cells |
object | { [soundId]: snapshotName } mapping |
When Song Mode is activated:
1. The current sound state is saved to _savedSongState
2. Each slice cell loads its assigned snapshot into the corresponding sound
3. The bar counter and transport display reflects the active slice
Arranger View
When Song button is toggled on, the strip rack is replaced by the Song Mode table:
┌─ Song ──────────────────────────────────────────┐
│ [▶] [Slice 1] [Slice 2] [Slice 3] [+ Add] │
│ 4 bars 8 bars 4 bars │
├─────────────────────────────────────────────────┤
│ Slice 1 Slice 2 Slice 3 │
│ Kick Default Fill Default │
│ Snare Default Default Default │
│ Hi-hat Default Open Default │
│ Neuro Verse Chorus Verse │
└─────────────────────────────────────────────────┘
Each cell shows the snapshot name for that sound in that slice. Clicking opens a floating picker. Each slice has controls: Up (reorder), Dn, Cp (clone), Go (jump to), ✕ (delete).
Playback
During playback in Song Mode:
- _songPlayback flag gates auto-advance logic
- At each bar boundary, checks if the current slice has ended
- End-of-slice detection: _globalStep >= currentSlice.bars × 16
- On slice end: resets _globalStep, loads next slice's cell snapshots,
shows NEXT indicator on the pending slice
- At end of last slice: stops playback and resets to first slice
- Go jumps to a slice mid-playback, resetting _globalStep and loading
that slice's snapshots
WAV Loop Export
Each strip has a WAV Loop button that renders the strip's audio to a stereo 24-bit WAV file:
renderStripBuffer(stripId, opts = { bars, passes })
→ builds OfflineAudioContext at project sample rate
→ runs N priming passes (default 3) to settle reverb/compressors
→ captures 1 loop of the strip (bars × 16 steps)
→ normalises peak to −1dB
→ encodes as stereo 24-bit WAV
→ triggers download: <Strip>-<N>bar-<BPM>.wav
The number of bars defaults to the sound with the most steps in the strip
(e.g. 32 steps = 2 bars). Uses buildOfflineMaster and
buildOfflineStripChain from sequencer-audio.js.
Tests
node --test tests/js/synth/**/*.test.js
15 dedicated sequencer tests cover: - Constructor defaults (bpm, sounds, strips) - addSoundToStrip creates valid tracks with correct step patterns - Engine switching (returns null for unknown engines) - Track removal from strip - BPM clamping - 128-step storage per track - Per-track activeSteps (independent expansion and contraction) - setStep / setVelocity / setChance round-trip - randomiseSound modifies params - start/stop transport and per-track step reset - auditionSound plays without error - Default step pattern (all steps off)
Files
| File | Role |
|---|---|
templates/synth_sequencer.html |
Sequencer page layout, fullscreen CSS, mobile responsive piano |
static/js/synth/sequencer.js |
SequencerEngine — strips, scheduling, WAV export, undo, sound snapshots, Song Mode gating |
static/js/synth/sequencer-ui.js |
DOM builder — transport pills, strip/sound/master cards, Song Mode table, snapshots UI |
static/js/synth/sequencer-audio.js |
Stereo master bus, strip audio chains, send FX, offline build helpers |
static/js/synth/sequencer-persistence.js |
Serialize/restore, project snapshots, sound card snapshots, createStep |
static/js/synth/sequencer-song.js |
SongMode class — slice CRUD, activateSlice, setCellSnapshot |
static/js/synth/init-sequencer.js |
Bootstrap — restore state, build UI |
tests/js/synth/sequencer.test.js |
Sequencer engine tests |
src/routers/synth.py |
GET /synth/sequencer |