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).

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 (CB, 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