Disciplines · Audits

Euterpe → most SOTA, industry-leading AI DAW — master build plan

Euterpe already has world-class backend/domain depth (multi-model routing, mastering DSP, music intelligence, voice) and a proven real-time generation edge (MRT2 wired + verified live).

9sections71 minread

On this page

Status doc + build log. Date: 2026-06-05. Derived from the 6-agent gap analysis (AI/DAW/UX census × 2026 industry SOTA research).

The thesis#

Euterpe already has world-class backend/domain depth (multi-model routing, mastering DSP, music intelligence, voice) and a proven real-time generation edge (MRT2 wired + verified live). The gaps that separate it from a leading DAW are three structural gates plus a set of differentiators that win leadership. Fill the gates, then own the differentiators.

Hard rule (CLAUDE.md): zero stubs. Every DSP block, UI surface, and AI feature ships real, tested logic or an honest fail-closed seam. DSP is deterministic math → unit-tested against known-correct values. Commit + push (branch AND main) per slice.

Gate P0 — Real-time audio engine (unblocks everything)#

A WASM/Rust DSP engine on AudioWorklet: the thing Euterpe most lacks (today all DSP is delegated). New lib libs/euterpe/audio-engine/ (Cargo workspace, mirrors realtime-engine):

  • dsp-core (pure rlib, cargo-tested): oscillators (sine/saw/square/triangle, PolyBLEP anti-aliased), ADSR envelopes, biquad filters (RBJ cookbook: LP/HP/BP/notch/peak/shelf), a polyphonic synth voice, dynamics (compressor/ limiter with knee), delay line, FDN/Schroeder reverb, a parametric EQ, a mix bus + gain/pan, sample playback + linear/cubic resampling, a meter (peak/RMS/ LUFS-K). All unit-tested against known DSP behavior (−3 dB at cutoff, ADSR shape, sine RMS=0.707, limiter ceiling, etc.).
  • dsp-graph: a real-time node-graph processor (block-based, lock-free param smoothing) that hosts the above as nodes with routing — the engine core.
  • dsp-wasm (cdylib, wasm-bindgen): exposes the graph to JS; built to wasm32-unknown-unknown (wasm-pack), staged for the browser.
  • @euterpe/audio-engine (TS): an AudioWorklet processor that runs the WASM graph at the 128-sample quantum, plus a main-thread control API (createTrack, insertEffect, setParam, transport, record). SharedArrayBuffer ring buffers. Reuse @euterpe/core (notes/MIDI/audio-codec) + @euterpe/studio (graph model).
  • Real-time I/O: getUserMedia capture + AudioWorklet output; transport (play/stop/record/loop/click), punch in/out.

Verification: cargo test --workspace (DSP correctness), wasm build, an AudioWorklet smoke (headless) producing non-silent audio, latency math.

Gate P1 — Render the actual DAW (over existing logic)#

The studio-web app is ~120 lines of JSX over ~4000 lines of unrendered logic. Build real React surfaces + a dark-first design system (@euterpe/ui):

  • Timeline (WebGPU/Canvas; the culling/LOD math already exists in shell-runtime) — tracks, clips, ruler, playhead, drag/zoom/snap.
  • Mixer (channel strips, faders, meters, sends, mute/solo) over studio/mixer.
  • Piano-roll / MIDI editor over studio/midi-editing.
  • Effects rack + instrument browser over studio/effects|instruments (driven by the P0 engine).
  • Project browser / transport / settings / audio-device picker.
  • Wire the keyboard-shortcut, autosave, permission, PWA logic that already exists.

Verification: Playwright + next dev (render, interact, play audio through the engine), per-surface.

Gate P2 — Table-stakes AI (mostly wiring existing logic)#

Per the research these earn parity, not leadership — but are mandatory:

  • Stem separation — on-device (Demucs/ONNX) + cloud tier; wire the existing samples/stem-separation interface to a real model.
  • AI mastering — surface the real master/mastering-chain DSP as an adjustable, stem-aware assistant (quick win; the DSP is already real).
  • AI mixing assistant — wire master/mix-assistant (full-session balance, per-track smart EQ/comp, resonance suppression).
  • Transcription — wire transcribe (stem-separate → audio-to-MIDI/chords).
  • Pitch/time — polyphonic editor over voice/vocal-processing + a real PSOLA/phase-vocoder in dsp-core.

Gate P3 — Differentiators to OWN (where leadership is won)#

  • AI Session Players (Logic's #1 differentiator; almost no one matches) — elevate accompany/backing-band + theory to generative Drummer/Bass/Keys that follow a chord track in real time, output as editable MIDI tracks.
  • Conversational copilot + text-to-DAW-action — an MCP-style agent over the studio domain (read project state + act: create tracks, place MIDI, insert/parametrize FX, route) with contextual memory. Build on agents/.
  • Real-time jam as a first-class instrument — promote the verified MRT2 panel into a timeline generator-track.
  • Inline AI UX — Ableton pattern (preview → Apply → undo → re-roll) + Suno pattern (section-scoped regen, take lanes, comping) across every AI action.
  • Provenance UX + compliance — SynthID/C2PA labels; EU AI Act Art. 50 (Aug 2 2026) + CA SB 942 (Jan 1 2026). Time-boxed.
  • Live collaboration + presence cursors — surface collab/sync-engine; add Figma-style presence (the unclaimed industry frontier).

Gate P4 — Reach + polish#

Keyboard-first + screen-reader a11y (REAPER+OSARA bar), Tauri desktop (config exists, unwired), mobile, AAX/Pro-Tools plugin form factor.

Sequencing#

P0 → P1 in lockstep (engine + the surfaces that drive it), then P2 (wire existing AI), then P3 (differentiators), then P4. Each gate ships in tested, committed slices. Already-in-hand assets accelerate P2/P3 (mastering DSP, routing engine, MRT2, music intelligence, collab logic).

P2 integration map (recon 2026-06-05 — exact existing libs to wire)#

All pure-TS, real algorithms (no creds), operate on Float32Array buffers or note data:

  • @euterpe/master (libs/euterpe/master/src/) — runMasteringChain, analyzeTrack, meterLoudness (BS.1770), analyzeDynamicRange, analyzeStereoField, detectProblems, analyzeHeadroom, predictTranslation, generateReport, intelligentEqMatching, referenceTrackMatching, multibandCompression, finalLimiting. I/O = MasteringAudioBuffer {channels, data: Float32Array[], sampleRate, length}. → Mixing Assistant / Auto-Master: needs an OFFLINE RENDER of the session. Add a --target web wasm build (alongside no-modules) + a main-thread renderOffline(secs) that runs the engine into a buffer, then feed analyzeTrack/detectProblems/generateReport.
  • @euterpe/theory (libs/euterpe/theory/src/) — harmony.* (romanNumeralAnalysis, detectIIVI, analyzeFunctionalHarmony, voice-leading), melody.* (contour, motif, retrograde/invert/augment, detectHooks), groove.* (analyzeSwing, analyzeMicroTiming, analyzePocket, classifyDrumPattern). I/O = chord symbols / MIDI note arrays / hit lists. → Chord-progression generator + groove/humanize for the step grid (note data — no offline render needed; cleanest next slice).
  • @euterpe/core (libs/euterpe/core/src/) — scales/ (100+ scales/modes/ragas), chords/ (voicings, inversions, quality detect), keys/ (key detection). → scale/key- aware step grid + chord tools (replace the local SCALES in pattern-gen.ts).
  • @euterpe/transcribe (libs/euterpe/transcribe/src/) — detectPitch (autocorrelation), detectOnsets (spectral flux), generateMidiEvents, estimateTempo, quantizeToGrid. I/O = audio buffer → DetectedNote[]. → Audio→MIDI feature.
  • @euterpe/restore (spectral subtraction / Wiener / de-click) → a "Clean Up" insert.
  • @euterpe/acoustics (Bark/mel, masking, equal-loudness) → masking-aware Smart EQ.

Easiest first (note data, no new infra): theory chord-gen + core scales on the step grid. Marquee (needs offline-render infra): @euterpe/master Mixing Assistant on the master bus.

  • P2.1 (first real AI/theory wiring) chord-progression generation via @euterpe/core. chord-progressions.ts uses the shared library's symbolToMidi (real chord-symbol → voiced MIDI: triads/sevenths/extensions/slash) — no duplicated theory; the DAW only arranges chords across the grid. 4 preset progressions (I–V–vi–IV, ii–V–I, etc.) + a generateChords reducer action + StepGrid progression buttons. Verified: 6 vitest tests pin the real theory (C→{C,E,G}, Cmaj7→adds B, Dm7 correct) + grid placement → 182 app tests; app+spec tsc clean (the full @euterpe/core barrel typechecks under the no-DOM config). NOTE: @euterpe/core resolved via vitest test.alias (a resolve.alias entry corrupts vite's config bundler — stale-cache symptom); declared as app dep + transpilePackage for the Next build (needs pnpm install to link, like audio-engine-web).
  • P1.7/export offline render + WAV bounce. Engine render_offline(frames) resets ALL DSP state (added reset to Adsr/SynthVoice/PolySynth + Source/Track.reset_state + master limiter/meters), plays the arrangement from 0 into fresh L/R buffers, and leaves the transport stopped at 0 — a deterministic mixdown. dsp-wasm render_offline returns interleaved stereo; the worklet bounces on a 'bounce' message and transfers the buffer back; AudioEngine.renderOffline/bounceWav + a pure-TS WAV encoder (16-bit PCM) + a "⤓ Bounce WAV" transport button (downloads 4 bars). Verified: deterministic-bounce + reset cargo test (60 cargo tests), a compiled-wasm Node render_offline test (deterministic energy, transport reset), WAV-header + clamp/scale tests → 8 audio-engine-web vitest; 182 app vitest; app+lib tsc clean. This unlocks the marquee @euterpe/master Mixing Assistant (analyze the bounced buffer).
  • P2.2 (marquee) Mixing Assistant via @euterpe/master. mix-analysis.ts deinterleaves a bounce into an AnalysisAudioBuffer and runs the library's REAL DSP (mixAnalysis.meterLoudness / analyzeDynamicRange / analyzeHeadroom / detectProblems) → a consolidated MixReport (LUFS, true peak, dynamic+loudness range, headroom, clip count, health score, problems w/ suggestions). useDawEngine.analyze() bounces 4 bars → analyzeBounce → report; a "✓ Analyze Mix" transport button + MixReportPanel render it. Verified: 3 vitest tests (−6 dBFS sine → plausible LUFS+peak; hot signal → low headroom; silence safe) → 185 app tests; app+spec tsc clean (full @euterpe/master barrel typechecks under no-DOM). @euterpe/master resolved via test.alias; declared as app dep + transpilePackage. The DAW now self-critiques its mix with real mastering DSP.
  • P2.3 AI Master & Export via @euterpe/master applyMasteringProfile. mastering.ts marshals an offline bounce through the library's genre mastering profile (real EQ curve + chain) for all 10 genres (pop/rock/hip-hop/electronic/jazz/classical/metal/ r&b/country/ambient) → mastered interleaved buffer. useDawEngine.masterExport(genre) bounces → masters → WAV download; the transport gets a genre s and dims/accent-highlights rows by scale membership. Verified: 5 vitest tests (C major = {0,2,4,5,7,9,11}, D minor-pentatonic transpose, every curated scale resolves, unknown→empty, octave-independent membership) → 193 app tests; app tsc clean. Four real Euterpe-lib AI/theory integrations now live in the DAW (chords, mix analysis, mastering, scale mode).
  • P1.8 swing/groove. Pattern gains a swing field (0..0.9); the engine's advance_sequencer lengthens on-beat steps and delays off-beat steps by that fraction (per-pair timing) for a shuffle feel. Threaded through dsp-wasm set_track_pattern (swing arg) → worklet → setPattern command → reducer PatternState + a setSwing action → a StepGrid swing slider. Verified: a cargo test proves a 0.5-swing off-beat step is delayed vs straight (61 cargo tests); a setSwing reducer test (clamp + re-emit + no-op without a pattern) → 194 app tests; the compiled-wasm tests carry the new arg (8 audio-engine-web tests); app tsc clean.
  • P1.9 sampler / audio-clip tracks (the biggest remaining capability). dsp-core Sampler gains load(); the engine sequencer retriggers a Source::Sampler on hit steps (pitched by note−60, so a sampler track is a drum/melodic lane); Engine + dsp-wasm gain add_sampler_track / load_sample / trigger_sample / stop_sample / set_sampler_rate|loop|gain. The worklet handles addSampler/loadSample(transfer)/ trigger/loop/gain; AudioEngine exposes loadSample (decode→transfer) / triggerSample / setSamplerLoop. The reducer's "audio" track now creates a real sampler (addSampler, not addSilent) and pattern actions work on sampler tracks too; a SamplerPanel (file load → decodeAudioData → loadSample, trigger, loop) shows for audio tracks and the step grid sequences them. Verified: cargo tests (load→trigger→audio, sequenced hits fire) + a dsp-wasm native test → 62 cargo tests; app+lib tsc clean; 194 app + 8 wasm vitest. The DAW now has real audio tracks alongside synths.

Gate P3 — differentiators (in progress)#

  • P3.1 conversational command bar (text-to-DAW-action — the 2026 SOTA differentiator). command-parser.ts parseCommand(text, session) → { actions: DawAction[], feedback } recognizes transport, add synth/audio, tempo, master gain, inserts (reverb/delay/ comp/eq/limiter on an explicit "track N" or the selection), mute/solo, pan (left/ right/center/number), euclid N, chord progressions (jazz/minor), swing N, gain (louder/quieter/dB), clear. A CommandBar UI feeds a line in and dispatches the result. Pure + fully tested: 12 vitest (every intent + track resolution + a round-trip proving parsed actions apply + gibberish → no-op). 206 app tests; app tsc clean. The rules are an LLM-swappable seam (same DawAction output).

  • P2.5 audio→MIDI transcription via @euterpe/transcribe. transcribe.ts windows a mono buffer into N steps and runs the library's real autocorrelation pitch detector (estimateF0 → DetectedPitch.midiNote per window) → a monophonic pattern. A loadPattern reducer action sets a whole pattern (used by transcription); the SamplerPanel gets an "Audio → MIDI" file input (decode → transcribe → loadPattern on the selected track). Verified: 3 vitest on the real DSP (440/880 Hz both detected as pitch-class A — robust to autocorrelation's octave ambiguity; silence → empty; N steps returned) + a loadPattern reducer test → 210 app tests; app + spec tsc clean. Fixed a pre-existing swing-less PatternState literal the spec-config tsc surfaced. Five Euterpe-library AI integrations now live: core chords/scales, master analysis/mastering, transcribe pitch-detection.

  • P1.10 pattern transforms — the classic compositional ops on the step grid: reverse (retrograde), invert (mirror about the pattern center), transpose ±1, octave ±12. pattern-transforms.ts (pure, clamped, sorted) + a transformPattern reducer action + StepGrid buttons. Verified: 6 vitest on the transforms (reverse order, transpose+ clamp, octave, invert about center incl. single-note + empty no-op) + a reducer test (reverse/octaveUp re-emit, no-op without a pattern) → 217 app tests; app+spec tsc clean.

  • P3.2 song/timeline arrangement — pattern chains (the #1 structural gap). A track can hold a Vec chain played bars_per_pattern bars each (4/4); the sequencer selects the active slot by elapsed bars and releases held notes on slot change. Done ALLOCATION-FREE (no per-block pattern clone — the slot index is cheap and only the active step's notes clone on step change, as before), holding the RT-safety bar. Engine set/clear_track_chain + track_chain_index; dsp-wasm flat multi-pattern encoding; worklet setChain/clearChain (flattens number[][][]) + AudioEngine. setTrackChain/clearTrackChain. Verified: a cargo test (two 1-bar patterns switch at the bar boundary + wrap) → 63 cargo tests; a dsp-wasm native test (chain switches per bar across the flat encoding) → 7 dsp-wasm; lib tsc clean. Arrangement UI (pattern-bank view) deferred — engine/wasm/API complete + tested.

  • P3.3 AI Session Player (research's #1 differentiator, cf. Logic Session Players). session-player.ts generateAccompaniment(symbols, numSteps, style) writes a musically-appropriate part from a chord progression using @euterpe/core's real voicings (symbolToMidi): 'bass' (roots on downbeats, low octave), 'arp' (chord tones cycled one-per-step), 'chords' (block voicings). A generateAccompaniment reducer action + StepGrid "♪ Bass"/"♪ Arp" buttons (over I–V–vi–IV). Verified: 4 vitest (bass roots+pitch classes, arp in-chord cycling, block chords, empty→empty) + a reducer test → 222 app tests; app+spec tsc clean. SIX Euterpe-lib AI integrations live: core chords/scales, master analysis/mastering, transcribe, session-player.

  • P3.4 content provenance / AI-disclosure (2026 compliance — EU AI Act Art.50 Aug 2 2026 + CA SB 942 Jan 1 2026). Patterns carry an origin (manual/euclidean/chords/ accompaniment/transcribe; a hand-edit re-marks 'manual'); provenance.ts summarizeProvenance(session) → {aiAssisted, features[], label}. The WAV encoder gains an optional RIFF LIST/INFO chunk (ISFT + ICMT), embedded in bounce AND mastered exports with the provenance label; an "AI-assisted" transport badge. Verified: 4 provenance vitest (none/euclidean/multi-feature/edit-clears) + a WAV-metadata test (LIST/INFO/ICMT present; byte-identical without metadata) → 226 app + 9 wasm tests; app+spec+lib tsc clean.

  • P3.5 MIDI export (interop). midi-export.ts encodeSmf(tracks, tempoBpm) writes a Standard MIDI File (format 1): MThd + a tempo MTrk + one MTrk per track-with-pattern (track-name meta, note on/off pairs with delta-times, gate = one step, 480 ticks/qtr). A "⤓ MIDI" transport button exports the arrangement (tracks with notes) as a .mid. Verified: 4 vitest parse the bytes (valid MThd format-1/division, tempo meta = 500000 µs/qtr @120, note-on/off pitch pairs, MThd + N+1 MTrk count) → 230 app tests; app tsc clean (TS5.7 Uint8Array→BlobPart cast). WAV + MIDI export both shipped.

  • P3.6 undo/redo (the universal DAW feature, + the inline-AI preview→undo foundation). Done CORRECTLY with engine replay so view-state and audio stay in sync: the DawController snapshots the (immutable) session before each mutating dispatch; undo/ redo restore a snapshot then reset the engine (new Engine.clear_tracks / dsp-wasm reset_engine / worklet 'reset' command) and replay rebuildCommands(session) — a pure reconstruction of the full command stream (tempo, master, tracks, synth patches, inserts incl. EQ bands, mix, patterns). Cmd/Ctrl+Z + ↶/↷ transport buttons. Verified: 16 dsp-graph cargo tests (clear_tracks → silence → rebuildable), 4 rebuildCommands vitest, 3 controller undo/redo vitest (restore+replay, empty no-op + redo invalidation, view-only not snapshotted) → 237 app + 9 wasm tests; app+spec+lib tsc clean. Documented limit: a sampler track's loaded buffer (engine-side binary, not in the session model) isn't replayed — re-load if needed; all modeled state restores.

  • P4.1 keyboard-first transport shortcuts (a11y — REAPER+OSARA-style). keyboard- shortcuts.ts transportShortcut(key, session) → DawAction: Space = play/stop, digits 1–9 = select track; deliberately avoids the A–K note row and the Cmd/Ctrl+Z undo binding. Wired into the DawApp window keydown (ignored while typing; via a sessionRef so it doesn't re-subscribe on meter ticks). Verified: 3 vitest (Space, digit select + out-of-range, no collision with note keys) → 240 app tests; app+spec tsc clean.

  • P4.2 metronome / click. The engine emits a post-master click on each beat (1.5 kHz accent on the 4/4 downbeat, 1 kHz otherwise, ~40 ms decay), beat-accurate within the block and only while playing. Engine set_metronome + dsp-wasm set_metronome + worklet 'metronome' command + AudioEngine.setMetronome + a ◷ transport toggle. Verified: a cargo test (silent when stopped, audible clicks while playing over a silent mix, goes silent when disabled) → 64 cargo tests; app+lib tsc clean, 240 app + 9 wasm tests.

  • P4.3 project save / load. project-io.ts serialize/deserializeProject (versioned JSON of the arrangement — tracks/patches/inserts/mix/patterns/tempo/master; transient meter/peak/held-notes/playhead stripped on save, reset on load). DawController. loadSession replaces the session, clears history, and reset+replays the engine via rebuildCommands. Save/Open buttons in the transport (download JSON / file input). Verified: 3 vitest (round-trip preserves the arrangement, transient runtime fields reset on load, malformed/wrong-version rejected) → 243 app tests; app+spec tsc clean.

  • P1/mix master-bus EQ. The engine applies a per-channel ParametricEq to the whole mix before the safety limiter (Engine.set_master_eq_band, reset on clear_tracks); dsp-wasm set_master_eq_band + worklet 'setMasterEqBand'. Modeled as mix state (session.masterEq) so it persists through undo/redo (rebuildCommands replays it) AND project save/load (project-io). A 3-band MasterEqPanel (low/mid/high). Verified: a cargo test (master low+high cut attenuates the mix, resets flat on clear_tracks) → 65 cargo tests; reducer

    • rebuild + project-io round-trip vitest → 245 app + 9 wasm tests; app+spec+lib tsc clean.
  • P1/mix master-bus compressor — completes the EQ → comp → limiter master chain. A stereo-LINKED compressor (one gain from max(|L|,|R|)) via a new dsp-core Compressor.process_gain seam; Engine.set_master_comp (bypassed by default) applied between master EQ and limiter, reset on clear_tracks. dsp-wasm set_master_comp + worklet; modeled as session.masterComp (enabled/threshold/ratio/makeup) so it persists through undo/redo + project save/load; MasterEqPanel gains a comp section (toggle + threshold/ratio/makeup). Verified: a cargo test (enabled comp reduces the steady-state peak vs raw) → 66 cargo tests; reducer + rebuild (replayed when enabled, omitted when off) + project round-trip vitest → 247 app + 9 wasm tests; tsc clean.

  • P1/UX track rename + recolor. renameTrack / setTrackColor reducer actions (pure view-state, no engine commands, persist in save/load); ChannelStrip name is an inline editable input and the color dot cycles an 8-color palette. Verified: a reducer test (rename/recolor update view-state, emit no commands) → 248 app tests; app+spec tsc clean.

  • P1/mix master-comp gain-reduction meter. Engine.master_comp_gr_db (0 when bypassed) → dsp-wasm master_comp_gr_db → folded into the meter event (MeterSnapshot.masterCompGr) → a live "GR −x dB" readout in the master panel when the comp is on. app+lib tsc clean; 248 app + 9 wasm tests (66 cargo).

  • P1/seq per-step velocity (accents/dynamics — the most musically-impactful sequencer gap). Pattern gains a step_velocities: Vec<f32> (empty/short → falls back to the pattern velocity) + velocity_at(step); the sequencer drives each synth note-on AND each sampler hit (set_gain) at its step velocity. dsp-wasm set_track_pattern takes a flat step_velocities: &[f32]; the worklet forwards stepVelocities as a Float32Array; messages.ts setPattern carries it. Modeled as PatternState.stepVelocities kept in lockstep with steps by the reducer (emptyPattern fills 1.0; generators/transcription resize via fitVelocities preserving extant accents; a setStepVelocity action edits one, clamped; retrograde reverses the lane with the steps, in-place transforms leave it aligned) → persists through undo/redo (session-rebuild) + save/load (project-io). StepGrid gains a vertical per-step velocity lane (disabled on rests). Verified: cargo per_step_velocity_scales_note_loudness (quarter-velocity step ≥2× quieter) → 67 cargo; a wasm binding test + a Node compiled-wasm test (per-step 0.25 ≥2× quieter across the boundary) → 10 engine-web vitest; 2 reducer tests (clamp/lane-sizing/no-op + retrograde travels accents) → 250 app tests; app+spec+lib tsc clean; worklet rebuilt.

  • P3/seq per-step probability (generative trig conditions — Elektron / Ableton 12). Each step gains a 0..1 fire chance, re-rolled every loop; a skipped trig holds the previous note (a tie). Determinism preserved: each Track owns a SplitMix64-seeded xorshift64* PRNG (seeded by track index so probabilistic tracks decorrelate) that rewinds on reset_state, so render_offline bounces are reproducible; the PRNG is only consumed when prob ∈ (0,1), so all-1.0 patterns render bit-identically to before. Pattern gains step_probabilities + probability_at; dsp-wasm set_track_pattern takes a flat step_probabilities; worklet forwards stepProbabilities; messages.ts setPattern carries it. Reducer: PatternState.stepProbabilities (lane fitter generalized to fitUnitLane, shared with velocity), a setStepProbability action (clamped), all generators/transcode keep the lane sized, retrograde reverses it with the steps, persists via session-rebuild

    • project-io. StepGrid gains a second vertical lane ("Prob"). Verified: cargo per_step_probability_gates_triggers_deterministically (p=0 silent, 0<half<full, render_offline reproducible) → 73 cargo; a wasm binding test + a Node compiled-wasm test → 11 engine-web vitest; 2 reducer tests (clamp/sizing/no-op + retrograde) → 252 app tests; app+spec+lib tsc clean; worklet rebuilt. Sequencer now does velocity + swing + probability per step — Elektron-class trig expressiveness.
  • P3/seq per-step gate length (note length — staccato↔legato per step), completing the velocity/probability/gate per-step triad. The synth note now releases mid-step once the gate (a fraction of the step) elapses; ≥1 holds to the next step (legato = today's behaviour). Engine: Pattern.step_gates + gate_at; Track tracks seq_gate_off_at (block- granular release, reset on stop/chain-change) — samplers are one-shots so gate is synth-only; the new_step refactor keeps swing/probability/chain intact (all-default patterns render identically). dsp-wasm set_track_pattern takes a flat step_gates; worklet forwards stepGates; messages.ts setPattern carries it. Reducer: PatternState. stepGates (fitUnitLane, fallback 1), a setStepGate action (clamped), all generators keep the lane sized, retrograde reverses it with the steps, persists via session-rebuild + project-io. StepGrid gains a third vertical lane ("Gate"). Verified: cargo per_step_gate_shortens_note_sustain (staccato ≥2× less energy than legato) → 76 cargo; a wasm binding test + a Node compiled-wasm test → 12 engine-web vitest; 2 reducer tests (clamp/sizing/no-op + all-three-lanes retrograde) → 254 app tests; app+spec+lib tsc clean; worklet rebuilt. Per-step expressiveness complete: how loud (velocity) · whether (probability) · how long (gate) · groove (swing) — modern step-sequencer parity.

  • P3/seq variable pattern length + polymeter. A setPatternLength reducer action resizes a pattern's grid (1..64 steps; grow pads empty steps + default lanes, shrink truncates, surviving indices keep their notes + all three per-step lanes via fitUnitLane). No engine change — each track already loops its own pattern length, so different lengths per track = real polymeter. StepGrid gains an 8/16/24/32 length selector. Persists via session-rebuild

    • project-io (length is intrinsic to the steps array). Verified: 1 reducer test (grow-pads / shrink-truncates / surviving notes+velocity preserved / clamp 0→1 & 999→64 / same-length + no-pattern no-ops) → 255 app tests; app+spec tsc clean. App-only slice (no cargo/worklet rebuild). Sequencer no longer locked at 16 steps — odd meters + polymeter.
  • P3/seq adjustable resolution (beat subdivision) — the other half of grid control. A setStepsPerBeat reducer action (1..12) re-interprets the grid at a new rate; the engine's sequencer already keys timing off steps_per_beat, so re-emitting the pattern re-times playback (verified: patternCurrentStep at 3/beat @120 BPM → 8000 samples/step). StepGrid gains a ⅛ / ⅛T / 16th / 16T / 32nd selector — triplet feels (3, 6) now reachable. Persists via session-rebuild + project-io. Verified: 1 reducer test (rate change re-emits

    • re-times, content preserved, clamp 0→1 & 99→12, same-value + no-pattern no-ops) → 256 app tests; app+spec tsc clean. App-only slice. Full grid control: length × resolution × per-step velocity/probability/gate × swing.
  • P3/seq per-step ratchet (retrigger rolls — the sequencer capstone, à la Elektron/Polyend). A step fires N evenly-spaced sub-hits across its duration (1=normal, up to 8). Engine: the Track schedules sub-hits in the same-step branch (seqratchet_n/done + cached onset/len/ notes/velocity, reset on stop) for both synth (note-off→note-on re-attack) and sampler (retrigger) — ratchet > 1 overrides the gate so they don't fight, and the step's notes are _moved into the cache (no extra audio-path allocation). Deterministic (no RNG) → bounces reproducible. dsp-wasm set_track_pattern takes a flat step_ratchets (u8); worklet forwards stepRatchets; messages.ts setPattern carries it. Reducer: PatternState.stepRatchets via a new integer fitRatchetLane (1..8), a setStepRatchet action, all generators keep the lane sized, retrograde reverses it, persists via rebuild + save/load. StepGrid gains a fourth vertical lane ("Ratchet"). Verified: cargo per_step_ratchet_retriggers_within_a_step (ratchet 4 ≥2.5× the trigger energy of 1; render_offline reproducible) → 78 cargo; a wasm binding test + a Node compiled-wasm test → 13 engine-web vitest; 2 reducer tests (clamp/round/sizing/no-op + all-four-lanes retrograde) → 257 app tests; app+spec+lib tsc clean; worklet rebuilt. Step sequencer is now Elektron-class: velocity · probability · gate · ratchet per step, plus swing, variable length & resolution.

  • P1/mix shared aux/reverb send bus (the first parallel-FX routing — beyond per-track inserts). The Engine gains a stereo send bus (two summed scratch buffers) + a shared Reverb (two mono Reverbs, medium-hall fixed); each Track taps a post-fader send into it, the wet returns to the master bus (so it still runs through master EQ/comp/limiter). RT- safe (buffers sized once, cleared per block; send only written when level>0) and deterministic (reverb reset in render_offline + clear_tracks). dsp-wasm set_track_send + worklet 'trackSend' + messages.ts trackSend command. Modeled as TrackState.sendLevel so it persists through undo/redo (session-rebuild emits trackSend when >0) AND save/load (project-io, with an old-file default). ChannelStrip gains a "Reverb Send" slider. Verified: cargo aux_send_adds_a_reverb_tail (send=0 → silent after decay; send=0.8 → reverb tail ≥100× louder) → 80 cargo; a wasm binding test + a Node compiled-wasm test → 14 engine-web vitest; 2 app tests (clamp/default + rebuild replays trackSend) → 258 app tests; app+spec+lib tsc clean; worklet rebuilt. Mixer now has parallel FX sends, not just inserts — multiple tracks share one cohesive reverb space.

  • P1/mix tunable send reverb — completes the send bus into a fully-controllable aux. Engine set_send_reverb(room, damp) tunes the shared reverb (reset to the default hall in clear_tracks so undo/replay stays deterministic). dsp-wasm set_send_reverb + worklet 'setSendReverb' + messages.ts setSendReverb command. Modeled as session.sendReverb {room, damp} (default {0.72, 0.4}); a setSendReverb action; rebuild replays it only when tuned (engine reset restores the default); project-io persists it (old-file default). MasterEq panel gains a "Reverb Send" Room + Damping section. Verified: cargo send_reverb_room_size_lengthens_the_tail (room 0.95 late-tail > room 0.2) → 81 cargo; a wasm binding test → 13 dsp-wasm; 2 app tests (clamp/default-emit + rebuild omits default / replays tuned) → 260 app tests; app+spec+lib tsc clean; worklet rebuilt. The shared reverb is now a real, tunable hall — per-track sends + global room/damp.

  • P1/mix sidechain compression (the EDM "pumping" duck — the most distinctly-SOTA mixer feature). The master compressor can DETECT on a chosen key track instead of the mix: each Track optionally taps its post-fader signal into a stereo KEY bus (it still sounds), and the master comp's detector reads the key bus when a key is set. Engine set_master_comp_key (manages per-track is_key flags; -1 = self-detect; reset in clear_tracks), key buffers cleared per block, RT-safe. dsp-wasm set_master_comp_key(i32 — i64 marshals as JS BigInt and breaks the worklet/Node call) + worklet 'setMasterCompKey' + messages.ts command. Modeled as MasterCompState.sidechainTrackId (preserved across comp-param edits); a setMasterCompKey action; rebuild replays it AFTER the tracks exist (the key references a track); project-io merges over the default so old files load. Master panel gains a "Sidechain key" track