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

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 <select> + "♪ Master"
  button. Verified: 3 vitest tests (same-length/finite/in-range output, the
  profile measurably changes the audio, every genre runs) → 188 app tests; app
  tsc clean. Reuses the offline-render + WAV infra; closes the
  bounce→master→export loop.
- P2.4 scale mode (Ableton-style) via **@euterpe/core** scale library.
  scale-mode.ts uses findScaleByName → interval definitions to compute a
  root+scale's in-scale pitch classes (11 curated scales:
  Major/Aeolian/Dorian/…/pentatonics/blues). StepGrid gets root + scale
  <select>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<Pattern> 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 (seq*ratchet_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
  <select>. Verified: cargo sidechain_detects_on_the_key_not_the_mix (keying a
  loud pad's comp to a SILENT track drops gain-reduction ≥4× — proves it detects
  the key, not the mix) → 82 cargo; a wasm binding test + a Node compiled-wasm
  test (keyed- to-silent output louder than self-detect) → 15 engine-web vitest;
  2 app tests (set/clear/preserve + rebuild orders key after tracks) → 262 app
  tests; app+spec+lib tsc clean; worklet rebuilt. **Master bus now does
  sidechain ducking — kick-pumped mixes.**
- P3/gen Euclidean rotation — the defining Euclidean control + a "re-roll the
  groove" loop. pattern-gen `rotateOnsets(onsets, rotation)` rotates the
  Bjorklund onset pattern left (wraps; negative ok; full-loop = identity) before
  the arp is placed; generatePattern takes an optional `rotation` (threaded via
  the generatePattern action). StepGrid remembers the last Euclidean fill and
  shows a "↻ Rotate" button that re-emits the same density at rotation+1, so
  3/5/7 → rotate → rotate explores related grooves (each step undoable).
  Verified: 1 pattern-gen test (E(3,8) rotations x..x..x. → ..x..x.x / x..x.x..
  , −1≡7, loop-identity, onset-count preserved) + 1 reducer test (rotated groove
  same density, shifted, full-loop returns base) → 264 app tests; app+spec tsc
  clean. App-only slice (pure generation; no engine/wasm change). **The
  Euclidean tool now rotates — instant groove variations, the first taste of the
  inline "re-roll" workflow.**
- P1/mix second (delay) FX send — completes the standard pro "two-send" mixer
  (reverb + delay). Mirrors the reverb send: Engine gains a stereo aux Delay
  (two mono Delays, fully wet mix=1, default dotted-eighth 0.375 s / 0.35 fb) +
  a delay send bus; each Track taps a post-fader delay send (Track::process now
  writes out/reverb-send/delay-send/key in one pass), wet returns through the
  master chain. Tunable via set_send_delay(time, feedback); RT-safe +
  deterministic (reset in render_offline/clear_tracks). dsp-wasm
  set_track_delay_send + set_send_delay + worklet
  'trackDelaySend'/'setSendDelay' + messages.ts. Modeled as
  TrackState.delaySendLevel + session.sendDelay {timeSec, feedback} (defaults
  {0.375, 0.35}); setTrackDelaySend + setSendDelay actions; rebuild replays
  sends (>0) + tuned delay; project-io persists both (old-file defaults).
  ChannelStrip gains a "Delay Send" slider; master panel gains Time + Feedback.
  Verified: cargo aux_delay_send_returns_echoes_after_the_dry_note (send=0
  silent after decay; send=0.9 → echo returns ≥50×) → 84 cargo; a wasm binding
  test + a Node compiled-wasm test → 16 engine-web vitest; 3 app tests
  (clamp/default send + clamp delay tune + rebuild replays send/tuned-delay) →
  267 app tests; app+spec+lib tsc clean; worklet rebuilt. **Mixer now has both
  classic FX sends — reverb space + delay throws, each tunable.**
- P0/synth noise oscillator (the 5th waveform — essential for
  hats/snares/risers/wind/FX). dsp-core Oscillator gains a `Waveform::Noise`
  arm: pitch-independent xorshift32 white noise in [-1,1], with a per-oscillator
  RNG reset to a fixed seed in `reset()` so offline renders reproduce.
  waveform_from_u8 maps 4→Noise; the TS Waveform enum + synth-panel selector
  gain "Noise" (flows through the existing configureSynth path, persisted in the
  patch). Verified: cargo
  noise_is_broadband_bounded_zero_mean_and_reset_deterministic (in [-1,1],
  ~zero-mean, rms≈1/√3, ≫5000 zero-crossings/s, reset reproduces the exact
  stream) → 85 cargo; a wasm binding test (waveform 4 → sounding broadband
  voice) → 16 dsp-wasm; engine-web/app tsc clean; worklet rebuilt; 267 app tests
  unchanged. **The synth can now make noise — the whole percussion/FX palette
  (filtered-noise hats, snares, sweeps) is in reach.**
- P0/synth filter-type selection (LP/HP/BP) — pairs with the noise osc (noise+HP
  = hats, noise+BP = snares). The dsp-core SynthVoice's biquad was hardwired
  low-pass; it now holds a `filter_type` (default LowPass) used in the
  control-rate coeff refresh, with set*filter_type. dsp-wasm
  set_synth_filter_type(track, u8: 0 LP/1 HP/2 BP); the worklet's configureSynth
  handler also applies m.filterType. Modeled as SynthPatch.filterType
  (FilterMode 0|1|2) in the configureSynth command; project-io defaults it for
  old patches. Synth panel gains an LP/HP/BP selector. Verified: cargo
  filter_type_changes_the_spectral* balance (a low note through a 400 Hz LP
  keeps ≥1.5× the energy of HP) → 86 cargo; a wasm binding test (LP vs HP on a
  low note) → 17 dsp-wasm; 2 app tests (default LP + change mode emits it, other
  patch fields preserved) → 268 app tests; app+spec+lib tsc clean; worklet
  rebuilt. **Subtractive synth is complete: 5 waveforms × LP/HP/BP filter ×
  envelopes — real sound design (a noise+HP voice is a hi-hat).**
- P1/fx distortion (waveshaper) insert — a fundamental creative effect the
  insert palette lacked. New dsp-core `Waveshaper`:
  `mix·tone(tanh(drive·x)) + (1-mix)·x`, drive-normalized (÷tanh(drive)) so wet
  stays near unity, a one-pole "tone" LP, RT-safe; 3 cargo tests (mix=0 = exact
  bypass, drive saturates toward a square raising RMS at fixed peak, darker tone
  = less energy). dsp-graph `WaveshaperNode` (AudioEffect, name "distortion");
  dsp-wasm add_distortion(drive, tone, mix); worklet 'addDistortion' +
  messages.ts. Reducer: InsertKind += 'distortion' with a buildInsert case
  (default drive 8/tone 0.7/mix 1) + a session-rebuild case; insert-rack gains a
  "Dist" add button. Verified: cargo waveshaper trio + a wasm binding test (a
  sine note through a drive-12 insert raises RMS ≥1.2× vs clean) → 94 cargo (49
  dsp-core + 27 dsp-graph + 18 dsp-wasm); engine-web/app tsc clean; worklet
  rebuilt; 2 app tests (add-insert default params/command + rebuild replays it)
  → 269 app tests. **Tracks can now be driven/saturated — the insert palette is
  EQ · comp · distortion · reverb · delay · limiter.**
- P0/synth cutoff LFO (the synth's modulation source — movement/wobble for
  pads/basses). The SynthVoice gains a sine LFO (per-voice, deterministic phase
  accumulator advanced at the control rate) folded into the filter cutoff
  alongside the filter envelope: cutoff = base + env·level + depth·sin(lfo);
  set_lfo(rate_hz, depth_hz), depth 0 = off (existing patches unaffected), phase
  reset on voice reset (offline determinism). dsp-wasm set_synth_lfo; the
  worklet's configureSynth applies m.lfoRate/lfoDepth. Modeled as
  SynthPatch.lfoRate/lfoDepth in the command, project-io defaults for old
  patches; synth panel gains LFO Rate + Depth sliders. Verified: cargo
  cutoff_lfo_modulates_the_output_over_time (a deep LFO makes the windowed RMS
  swing ≥2× vs none) → 96 cargo (50 dsp-core); a wasm binding test (LFO wobbles
  the output) → 19 dsp-wasm; 1 app test extension (default-off + change emits
  rate/depth) → 269 app tests; app+spec+lib tsc clean; worklet rebuilt. **Synth
  now has full modulation: amp env + filter env + cutoff LFO — wobble basses,
  evolving pads, sweeps.**
- P0/sampler reverse playback (reverse cymbals/hits — a staple). The dsp-core
  Sampler gains a `reversed` flag: trigger starts from the tail and `process`
  walks the read position backward (direction-aware bounds: forward ends at n,
  reverse below 0; both wrap when looping, else stop) — interpolation is
  position-based so it reads correctly either way. engine set_sampler_reversed →
  dsp-wasm → worklet 'samplerReversed' → AudioEngine .setSamplerReversed; the
  SamplerPanel gains a "Reverse" checkbox (imperative, like the existing Loop
  control). Verified: cargo reverse_plays_the_clip_backwards_and_stops (a ramp's
  forward output rises, reverse falls, reverse starts at the tail, one-shot
  stops at 0) → 98 cargo (51 dsp-core); a wasm binding test (reverse's first hit
  ≥4× the energy of forward's, tail-first) → 20 dsp-wasm; engine-web/app tsc
  clean; worklet rebuilt; 269 app tests. **The sampler can now play clips
  backwards — reverse risers, swells, chopped hits.**
- P1/fx bit-crusher (lo-fi) insert — distinct digital degradation alongside the
  smooth tanh distortion. New dsp-core `BitCrusher`: bit-depth quantization
  (round to 2^bits amplitude levels) + sample-rate reduction (sample-and-hold
  every Nth input) + dry/wet mix, RT-safe; 3 cargo tests (mix=0 bypass, 2-bit →
  a handful of quantization levels vs 16-bit's full detail, downsample 4 →
  groups of 4 held samples). dsp-graph BitCrusherNode (name "bitcrusher");
  dsp-wasm add_bitcrusher(bits, downsample, mix); worklet 'addBitcrusher' +
  messages.ts. Reducer: InsertKind += 'bitcrusher' (default 6-bit / ÷4 / wet) +
  buildInsert + session-rebuild cases; insert rack gains a "Crush" button.
  Verified: bitcrusher trio + a wasm binding test (a 2-bit crush cuts distinct
  sample values to <⅓ of clean) → 102 cargo (54 dsp-core + 27 dsp-graph + 21
  dsp-wasm); 2 app tests (add-insert + rebuild) → 270 app tests; engine-web/app
  tsc clean; worklet rebuilt. **Insert palette = EQ · comp · distortion ·
  bitcrush · reverb · delay · limiter.**
- P1/fx chorus (ensemble) insert — completes the modulation-FX family
  (movement/width). New dsp-core `Chorus`: a short ~15 ms delayed copy whose tap
  is swept by a sine LFO (built on the interpolating DelayLine), mixed with dry
  → a moving comb that thickens; params rate (Hz) / depth (0..1 of a ~7 ms
  swing) / mix; RT-safe + deterministic. 3 cargo tests (mix=0 bypass, the wet
  tap alters the signal, the moving delay sweeps the comb so windowed RMS swings
  ≥2× a fixed delay's). dsp-graph ChorusNode; dsp-wasm add_chorus(rate, depth,
  mix); worklet 'addChorus' + messages.ts. Reducer: InsertKind += 'chorus'
  (default 0.8 Hz / 0.5 / 50% wet) + buildInsert + session-rebuild; insert rack
  gains a "Chorus" button. Verified: chorus trio + a wasm binding test (a swept
  chorus makes the windowed RMS move ≥2× vs dry) → 106 cargo (57 dsp-core + 27
  dsp-graph + 22 dsp-wasm); 2 app tests (add-insert + rebuild) → 271 app tests;
  engine-web/app tsc clean; worklet rebuilt. **Insert palette = EQ · comp ·
  distortion · bitcrush · chorus · reverb · delay · limiter — a full creative FX
  rack.**
- P3/copilot command-bar coverage for the new capabilities (ties the
  conversational seam to everything built this session). command-parser.ts now
  understands: the new creative inserts (distort/overdrive→distortion,
  bitcrush/crush/lofi→bitcrusher, chorus); aux SENDS ("reverb send 40", "delay
  send 30" → setTrackSend/setTrackDelaySend — handled _before_ the insert loop
  so "reverb"/"delay" there don't add an insert); synth WAVEFORM
  (sine/saw/square/ triangle/noise → configureSynth); and synth FILTER mode
  (lowpass/highpass/bandpass + lpf/ hpf/bpf → configureSynth). All resolve an
  explicit "track N" or the selection. Verified: 3 new parser tests (creative
  inserts, sends-before-inserts, waveform+filter) → 15 command- parser tests,
  274 app total; app+spec tsc clean. App-only slice. **The text-to-DAW copilot
  now drives the whole new feature set — "distort track 2", "reverb send 50",
  "noise", "highpass".**
- P0/synth unison detune (the defining "fat" sound — supersaw / detuned bass).
  SynthVoice gains a second Oscillator detuned by `detune_cents`; the two run at
  0.5 gain each so 0 cents (phase-locked from note_on) sums back to a single
  oscillator (existing patches unchanged), while a spread drifts the second osc
  → beating/thickness. Both oscs reset on note_on/reset (offline determinism).
  dsp-wasm set_synth_detune; the worklet's configureSynth applies m.detuneCents.
  Modeled as SynthPatch.detuneCents in the command, project-io default for old
  patches; synth panel gains a "Detune" (cents) slider. Verified: cargo
  unison_detune_beats_while_unison_is_steady (30¢ → windowed RMS swings ≥2× the
  steady 0¢ unison) → 108 cargo (58 dsp-core); a wasm binding test (detune
  beats) → 23 dsp-wasm; 1 app test extension (default-unison + change emits
  detuneCents) → 274 app tests; engine-web/app tsc clean; worklet rebuilt. **The
  synth is now genuinely fat: 2 detunable oscillators × 5 waveforms × LP/HP/BP
  filter × amp/filter envelopes × cutoff LFO.**
- P1/workflow delete track (a fundamental gap — there was no way to remove a
  track). A deleteTrack reducer action filters the track out, RE-INDEXES the
  remaining tracks (ids match the engine's sequential creation order), remaps
  id-bearing references (selection + master-comp sidechain key: shift down past
  the hole, clear if it pointed at the deleted one), and — since the engine
  can't drop a single track — returns a full `rebuildCommands(next)` (reset +
  replay), the same path as undo/redo (so it's undoable; the documented
  sampler-buffer-not-replayed limitation applies). ChannelStrip gains a ✕ delete
  button. Verified: 1 reducer test (delete-middle re-indexes A,C→0,1 +
  sidechain/selection follow; delete the keyed+selected track clears the key +
  reselects in range; reset+2× addSynth emitted; missing-id no-op) → 275 app
  tests; app+spec tsc clean. App-only slice. **Tracks can now be deleted — the
  mixer is fully editable (add · rename · recolor · delete).**
- P1/workflow duplicate track (layer/vary sounds — the complement to delete). A
  duplicateTrack action makes a deep, independent `structuredClone` copy
  (transient fields reset, name + " copy") spliced in right after the source,
  re-indexes by position, shifts id-bearing references (sidechain key) up past
  the insertion, selects the new copy, and returns a full rebuildCommands(next)
  (reset + replay, undoable — same path as delete). ChannelStrip gains a ⧉
  duplicate button. Verified: 1 reducer test (A,B → A,"A copy",B re-indexed;
  copy carries the pattern but is a distinct object; sidechain key shifts up;
  reset+3× addSynth; missing-id no-op) → 276 app tests; app+spec tsc clean.
  App-only slice. **Full track lifecycle: add · duplicate · rename · recolor ·
  delete.**
- P1/workflow reorder tracks (move up/down — organize the mixer). A moveTrack
  action swaps a track with its neighbor, re-indexes by position, swaps the two
  old-ids in id-bearing references (sidechain key), selects the moved track at
  its new slot, and returns rebuildCommands(next) (reset+replay, undoable).
  ChannelStrip header gains ▲▼ buttons. Verified: 1 reducer test (move B up →
  B,A,C re-indexed; sidechain key A 0→1 follows; top-up / bottom-down /
  missing-id all no-op) → 277 app tests; app+spec tsc clean. App-only slice.
  **Track lifecycle complete: add · duplicate · rename · recolor · reorder ·
  delete.**
- P0/synth portamento/glide (pitch slides — 303 acid lines, lead/bass slides).
  SynthVoice eases `freq` toward a `target_freq` each control block by a
  one-pole `glide_coeff` (time→coeff, 1 = instant). PolySynth tracks the last
  note's pitch and, when glide is on, triggers each new note via
  `start_glide(from=last_freq, note)` so it slides in (works for sequenced +
  live; mono steals the voice for a clean single-pitch slide). Snaps + resets
  the target on note*on/reset/offline. dsp-wasm set_synth_glide; the worklet's
  configureSynth applies m.glideSec. Modeled as SynthPatch.glideSec in the
  command, project-io default; synth panel gains a "Glide" (ms) slider.
  Verified: cargo glide_slides_pitch_from_the* previous_note (a mono synth's 2nd
  note A2→A3 measured low early ~115 Hz → high late ≥195 Hz) → 110 cargo (59
  dsp-core); a wasm binding test (glide slides up) → 24 dsp-wasm; 1 app test
  extension (default-off + change emits glideSec) → 277 app tests;
  engine-web/app tsc clean; worklet rebuilt. **The synth glides — portamento
  basses, acid slides, expressive leads.**
- P1/transport panic — all-notes-off (stuck-note rescue, MIDI/keyboard hangs).
  Engine all_notes_off releases every live + sequenced note
  (PolySynth.all_notes_off gates every voice + clears the glide origin; samplers
  stop; stop_sequencer per track) — the transport keeps running, so a playing
  sequencer re-fires on the next step. dsp-wasm all_notes_off + worklet
  'panic' + messages.ts; a `panic` reducer action (command-only, view-state
  untouched) + a "⊘ Panic" transport button. Verified: cargo
  all_notes_off_panics_held_notes (a held synth note + looping sampler → after
  panic + release decay = silent) → 112 cargo (28 dsp-graph); a wasm binding
  test → 25 dsp-wasm; 1 app test extension (panic emits the command, leaves the
  transport playing) → 277 app tests; engine-web/app tsc clean; worklet rebuilt.
  **One button kills every stuck note.**
- P1/master adjustable limiter ceiling (the last hardcoded master-chain value —
  was fixed at -0.3 dBFS). Engine set_master_ceiling_db sets both limiters'
  ceiling (reset to -0.3 in clear_tracks for deterministic replay). dsp-wasm
  set_master_ceiling_db + worklet 'setMasterCeiling' + messages.ts. Modeled as
  session.masterCeilingDb (default -0.3, clamped -24..0); a setMasterCeiling
  action; rebuild replays only when non-default; project-io persists (old-file
  default). Master panel gains a "Limiter Ceiling" slider. Verified: cargo
  master_ceiling_is_adjustable (a -12 dB ceiling caps the driven output ≥2×
  lower than -0.3, each under its ceiling) → 113 cargo (29 dsp-graph); 2 app
  tests (clamp/default + rebuild omits default / replays tuned) → 279 app tests;
  engine-web/app tsc clean; worklet rebuilt. **Master chain fully controllable:
  gain → EQ → comp(+sidechain) → adjustable-ceiling limiter, with the GR
  meter.**
- P1/workflow New Project (completes the New/Save/Open project trio — there was
  no way to start fresh). A newProject handler in the DAW shell calls
  controller.loadSession( createDawSession()) → resets every track + the
  mix/transport and rebuilds the engine empty. Transport bar gains a "New"
  button beside Save/Open. Verified: 1 controller test (after adding two tracks,
  the New path clears to 0 tracks + null selection + emits a reset) → 280 app
  tests; app+spec tsc clean. App-only slice (wiring the existing loadSession
  path). **Project lifecycle complete: New · Save · Open (+ WAV/MIDI export, AI
  master).**
- P1/workflow tap tempo. A pure tap-tempo.ts: bpmFromTaps(timestamps) averages
  the recent inter-tap intervals → BPM (drops intervals after a >2 s gap, caps
  the run at 6, clamps 20–300), and pushTap(taps, now) appends a stamp /
  restarts after a long gap. The transport bar gains a "Tap" button feeding
  performance.now() (the time source is the test-double seam). Verified: 6
  helper tests (500 ms→120 / 1 s→60 / 250 ms→240 BPM, uneven averaging, clamp,
  gap-drop, run restart/cap) → 286 app tests (21 files); app+spec tsc clean.
  App-only slice (pure helper + UI button). **Tap a beat to set the tempo.**
- P0/synth sub-oscillator (osc2 octave offset — fat bass / octave layers,
  completing the dual-osc design). SynthVoice's osc2 gains a coarse
  `osc2_semitones` offset folded into its pitch (freq2 = freq·2^(semitones/12 +
  detune/1200)); 0 = same octave (unchanged), -12 = a sub oscillator, +12 = an
  octave-up layer. dsp-wasm set_synth_osc2_semitones; the worklet's
  configureSynth applies m.osc2Semitones. Modeled as SynthPatch.osc2Semitones,
  project-io default; synth panel gains a Sub / 0 / +8ve selector. Verified:
  cargo osc2_semitones_layers_a_sub_or_octave (+12 adds high partials → more
  zero crossings; -12 materially changes the waveform) → 115 cargo (60
  dsp-core); a wasm binding test (sub changes the sound) → 26 dsp-wasm; 1 app
  test extension (default-0 + change emits osc2Semitones) → 286 app tests;
  engine-web/app tsc clean; worklet rebuilt. **The synth's two oscillators are
  fully independent (detune + octave) — supersaws, sub-bass, octave stacks.**
- P1/sampler start offset (trim leading silence / chop a one-shot without
  re-decoding). The Sampler gains a `start_frac` (0 = head, 1 = tail);
  `trigger(0.0)` — the sequencer + manual default — now begins at that offset,
  while an explicit positive sample position still overrides it
  (tests/scrubbing). Reverse honors the offset by trimming the tail. Engine
  set_sampler_start → dsp-wasm set_sampler_start → worklet `samplerStart` →
  AudioEngine.setSamplerStart; the sampler panel gains a Start 0–100% slider
  (imperative, like loop/reverse — not session state). Verified: cargo
  start_frac_skips_into_the_clip (0.5 offset on a ramp starts mid-clip; explicit
  position overrides; reverse trims the tail) → 61 dsp-core; a wasm binding test
  (a clip silent for its first half → a 0.5 offset jumps into the loud half) →
  27 dsp-wasm (117 cargo total); engine-web 16 + app 286 tests; tsc clean across
  both packages; worklet rebuilt. **Every sampler hit can now skip into the clip
  — tight one-shots from sloppy recordings, beat-chops from a single sample.**
- P2/noise-gate insert (downward expander — the missing table-stakes dynamics
  tool, after compressor/limiter). dsp-core NoiseGate: a peak-follower detector
  (fast attack, slow `env_rel` decay) bridges a tone's zero crossings so the
  gate doesn't chatter; opens above `threshold_db`, attenuates by `range_db`
  (≤0; -80 ≈ mute, -12 = gentle duck) below it, with a hold time and separate
  attack(open)/release(close) one-poles. dsp-graph GateNode (boxed AudioEffect,
  mono in-place); wasm add_gate(track, thr, range, atk, hold, rel). Modeled as
  InsertKind 'gate' → addGate EngineCommand → worklet add_gate; reducer +
  session-rebuild replay + command-parser (gate/noisegate/expander) + an
  insert-rack 'Gate' button. Verified: 3 cargo unit tests (passes loud above
  threshold at unity; attenuates a sub-threshold tone ~80 dB; opens on a
  transient then closes on the quiet tail) → 64 dsp-core; a wasm binding test (a
  low-velocity note collapses through the gate while a full-velocity note passes
  ≥80%) → 28 dsp-wasm (121 cargo total); app daw-session + session-rebuild +
  command-parser tests (287 app, 16 engine-web); fmt+clippy clean; worklet
  rebuilt. **Tighten drums, kill mic bleed / amp hiss between hits — per-track,
  in the insert chain, real envelope-follower DSP.**
- P2/transient-shaper insert (a.k.a. transient designer — independent
  attack/sustain shaping a threshold compressor can't do; modern punch tool).
  dsp-core TransientShaper: two level-followers track the rectified signal at
  different speeds — while the signal rises into a transient the fast follower
  leads the slow (`diff>0` = attack region), while it decays the fast falls
  below (`diff<0` = sustain region); `diff` normalised by the slow envelope
  makes detection level-independent, then two amounts (−1..+1) scale the gain.
  dsp-graph TransientNode → wasm add_transient(track, attack, sustain).
  InsertKind 'transient' → addTransient EngineCommand → worklet add_transient;
  reducer (default attack 0.5) + session-rebuild replay + command-parser
  (transient/punch/designer) + a 'Trans' insert-rack button. Verified: 3 cargo
  unit tests (attack +0.9 raises the onset peak; sustain −0.9 cuts decay-tail
  energy; neutral = bypass) → 67 dsp-core; a wasm binding test (attack-boost
  sharpens a sampler hit's onset ≥10%) → 29 dsp-wasm (125 cargo total); app
  daw-session + session-rebuild + command-parser tests (288 app, 16 engine-web);
  fmt+clippy clean; worklet rebuilt. **Punchier or drier drums on demand —
  level-independent onset/body sculpting in the per-track insert chain.**
- P3/live ARPEGGIATOR (a performance differentiator, not an effect — first new
  note-routing layer). New dsp-graph `arp.rs` `Arp`: captures held notes while
  armed and re-fires them as a tempo-synced single-note line on the track's
  synth — modes Up/Down/UpDown(no repeated endpoints)/Random, rate in
  steps/beat, 1–4 octave span, per-step gate (staccato↔legato). Block-granular
  off the transport playhead (like the step sequencer); a pure performance tool
  over the LIVE note stream, so it's inert during the offline render (held set
  is empty there) — zero determinism impact. Track gains arp-aware
  note_on/note_off (route to the arp when armed, else straight to the synth) +
  advance_arp in the engine loop; all_notes_off/reset clear it. wasm
  configure_arp + arp_note(track) getter. Modeled as TrackState.arp (ArpState —
  persisted), a configureArp reducer action + session-rebuild replay; a full
  synth-panel arp section (On toggle, mode buttons, rate ⅛–1/32, octaves, gate).
  ALSO fixed a pre-existing reload gap: session-rebuild's configureSynth now
  replays the extended voice params (filterType/lfo/detune/ osc2/glide), not
  just the base 8. Verified: 6 cargo arp unit tests (Up/Down/UpDown/octave
  sequences exact; advance() cycles 60→64→67→60 at the step rate; releasing all
  held silences it) → 35 dsp-graph; a wasm binding test (held triad arps
  ascending over a beat via arp_note) → 30 dsp-wasm (132 cargo total); 3 app
  tests (configureArp maps mode→int + merges; rebuild replays enabled-arp +
  skips disabled; rebuild replays synth extras) → 291 app, 16 engine-web;
  fmt+clippy clean; worklet rebuilt. **Hold a chord, get a tempo-locked arpeggio
  — the first live note-transform layer, fully persisted and reload-safe.**
- P3/MIDI file IMPORT (round-trips the existing export — drag a `.mid` →
  sequencer tracks). New `midi-import.ts` `decodeSmf`: a full SMF parser
  (formats 0/1) — MThd division (rejects SMPTE), MTrk delta-times, running
  status, the channel-voice family (note on/off incl. vel-0-as-off,
  CC/bend/aftertouch skipped), meta events (tempo/track-name/end), SysEx skip;
  pairs note-ons↔offs FIFO into absolute-tick MidiNoteEvents. `notesToSteps`
  quantises onsets onto a steps/beat grid (nearest-grid); `importMidi` ties it
  together (tempo + per-track steps). Wired into the app: an `importMidiFile`
  callback (file→ArrayBuffer→importMidi → setTempo + per-track
  addSynthTrack+loadPattern via the controller's atomic dispatchAll) + a '⤒
  MIDI' file button in the transport bar. Verified: 5 vitest tests — round-trip
  through encodeSmf (steps+rests+tempo recovered), multi-track + chords,
  **running-status** parse (hand-built bytes), non-MIDI rejection, coarser-grid
  re-quantisation. 296 app tests; tsc clean (pure-TS slice, no engine touch).
  **Bring arrangements in from any DAW — real SMF interop both ways.**
- P1/transport LOOP region (loop a section while tweaking — table-stakes
  transport). The engine gains a loop stored in BEATS
  (loop_start_beats/loop_end_beats, not samples, so it stays musically anchored
  across tempo changes): at each block boundary, when playing + enabled, it
  converts beats→samples at the live tempo and wraps the playhead from the
  region end back to its start — the sequencer/arp (which derive their step from
  the playhead) re-fire the loop for free. set_loop(enabled, startBeat,
  endBeat) + loop_active(); wasm set_loop; worklet setLoop; AudioEngine.setLoop.
  Modeled as DawSession.loop (LoopState beats, persisted via project-io) + a
  setLoop reducer (partial-merge) + session-rebuild replay (when enabled); a
  transport-bar '↻ Loop' toggle + a 1/2/4/8-bar length select. Verified: a wasm
  binding test (loop [0,0.5]beat @120BPM → playhead wraps < 12000 samples after
  200 blocks, free-runs once disabled) → 31 dsp-wasm (133 cargo total); 2 app
  tests (setLoop merges + emits; rebuild replays enabled/skips disabled) → 298
  app, 16 engine-web; fmt+clippy clean; worklet rebuilt. **Loop the first N bars
  and tweak — beat-locked so it survives tempo changes, fully persisted.**
- P3/live SCALE-LOCK (snap played notes into key — second note-routing
  transform, on the same Track seam as the arp; Ableton-12-style scale
  awareness). New dsp-graph `scale.rs` `ScaleLock`: holds an absolute 12-bit
  pitch-class mask (bit i = pitch class i in-scale); `snap(note)` passes
  in-scale notes through and otherwise searches outward (≤6 semitones, ties
  resolve down) to the nearest in-scale pitch. Track::note_on/note_off snap
  FIRST (deterministic → the pitch triggered is the one released), then route
  through the arp/synth — so scale-lock + arp compose. engine
  configure_scale_lock(track, enabled, mask) → wasm (mask as u32, &0xfff) →
  worklet → AudioEngine. Modeled as TrackState.scaleLock (ScaleLockState
  {enabled, root, scale}, persisted); the reducer derives the mask from the
  existing `scalePitchClasses(root, scale)` (reuses @euterpe/core scale defs) +
  session-rebuild replay; a synth-panel Scale-Lock toggle + root/scale selects.
  Verified: 4 cargo scale unit tests (in-scale passthrough; C#→C / F#→F / A#→A
  snaps; disabled/empty/chromatic passthrough) → 39 dsp-graph; a wasm binding
  test (a C# played under C-major-lock sounds at C's fundamental, audibly lower
  than the un-locked C#, via zero-crossing counts) → 32 dsp-wasm (138 cargo
  total); 2 app tests (mask = 2741 for C major + rotates for D; rebuild replays
  enabled/skips disabled) → 300 app, 16 engine-web; fmt+clippy clean; worklet
  rebuilt. **Play anything, stay in key — composes with the arp for an in-scale
  arpeggiator.**
- P2/master STEREO WIDTH (mid/side) — rounds out the master chain to
  gain→EQ→comp→width→limiter. In engine.process, after the master compressor and
  before the safety limiter, a mid/side fold scales the side signal:
  mid=(L+R)/2, side=(L−R)/2·width, L=mid+side, R=mid−side. width 1 = unchanged
  (a cheap identity skip), 0 = mono (mono-compatibility check), >1 = wider.
  set_master_width (clamped 0..2) + reset to 1 in clear_tracks; wasm
  set_master_width; worklet setMasterWidth; AudioEngine.setMasterWidth. Modeled
  as DawSession.masterWidth (persisted) + a setMasterWidth reducer (clamped) +
  session-rebuild replay (skips the default 1) + a 'Stereo Width' slider
  (mono…200%) in the master panel. Verified: a dsp-graph engine test (a
  hard-left note stays panned at width 1 — R≈0 — and collapses to L==R at
  width 0) → 40 dsp-graph (139 cargo total); 2 app tests (clamp + emit; rebuild
  replays non-default/skips 1) → 302 app, 16 engine-web; fmt+clippy clean;
  worklet rebuilt. **Widen the mix or check mono-compatibility in one slider.**
- P3/conversational COMMAND-BAR expansion — extend the text→DAW-action parser to
  reach the recently-built surface so the whole DAW is text-addressable (the
  LLM-swappable copilot seam). Added branches: **loop** ("loop 4 bars" / "loop
  off" → setLoop in beats), **stereo width** ("width 150" / "mono", placed
  BEFORE the master-gain branch so "master width 150" means width not gain),
  **arpeggiator** ("arp up/down/updown/random" / "arp off" → configureArp),
  **scale- lock** ("scale c minor" / "key of d major" / "scale f# dorian" /
  "scale off" → configureScaleLock, parsing a root note + mapping spoken scale
  words to the library's SCALE_NAMES, longest-match). The root-note regex uses a
  negative lookahead `\b([a-g])(#|b|s)?(?![a-z])` so a bare letter doesn't read
  the start of a longer word ("dorian" ≠ D root) while still catching "f#"/"eb".
  Pure-TS slice (no engine touch). Verified: 4 new command-parser tests (loop,
  width incl. the master-width-vs-gain precedence, arp modes, scale from spoken
  key) → 306 app tests; tsc clean. **The conversational bar now drives
  transport/loop/master/synth/arp/scale/inserts/sends — one line of text per
  action.**
- P1/per-insert BYPASS (universal A/B for every insert — closes the
  set-and-forget gap; before this, only EQ bands were editable after adding).
  Track gains a parallel `insert_bypass: Vec<bool>` (pushed false on
  push_insert); the process loop skips a bypassed insert so the dry signal flows
  through unprocessed. set_insert_bypass(i, on); wasm set_insert_bypass(track,
  insert, bypassed) (via track_mut, like set_eq_band); worklet setInsertBypass;
  AudioEngine.setInsertBypass. Modeled as InsertState.bypassed (persisted) + a
  toggleInsertBypass reducer + session-rebuild replay (the bypass command is
  emitted AFTER the insert's add so the index exists) + an insert-rack on/byp
  chip per insert (dims + strikes through when bypassed). Verified: a dsp-graph
  engine test (a gutting EQ insert, bypassed → the dry level is restored ≥1.3×)
  → 41 dsp-graph (140 cargo total); 2 app tests (toggle flips + emits both
  states; rebuild replays bypass after the add) → 308 app, 16 engine-web;
  fmt+clippy clean; worklet rebuilt. **A/B any effect in the chain without
  deleting it — every insert, not just EQ.**
- P1/editable COMPRESSOR insert (a compressor you can't tune is half-useless;
  until now only EQ bands were live-editable). wasm set_comp_params(track,
  insert, threshold/ratio/attack/release/ makeup) downcasts the chain node to
  CompressorNode → comp_mut() (the set_eq_band pattern); worklet setCompParams;
  AudioEngine.setCompParams. A generic setInsertParams reducer merges the params
  onto the InsertState and, for compressor kind, emits the live setCompParams
  (full set, defaults for untouched fields) — extensible to gate/transient
  later. Persistence is automatic: session-rebuild's addCompressor already reads
  insert.params, so a retuned comp replays correctly on reload/undo with no
  rebuild change. The insert rack gains Thresh/Ratio/Attack/Release sliders on
  compressor inserts (mirroring the EQ band editor). Verified: a wasm binding
  test (the same note + comp retuned high-threshold/low-ratio vs
  low-threshold/high-ratio → the latter clamps ≥30% quieter) → 33 dsp-wasm (141
  cargo total); 1 app reducer test (params merge + full-set command) → 309 app,
  16 engine-web; fmt+clippy clean; worklet rebuilt. **Dial in the compressor
  after adding it — threshold/ratio/attack/release, live and persisted.**
- P1/editable GATE + TRANSIENT inserts (completes the editable-dynamics theme —
  all three dynamics processors are now tunable after adding; their core params
  were stuck at defaults). wasm set_gate_params
  (threshold/range/attack/hold/release) + set_transient_params (attack/sustain)
  downcast to GateNode.gate_mut() / TransientNode.shaper_mut() (same pattern as
  comp/EQ); worklet setGateParams/setTransientParams; AudioEngine wrappers. The
  generic setInsertParams reducer now emits the right live command per insert
  kind (comp/gate/transient); persistence stays automatic (session-rebuild's
  add\* already read insert.params). Insert rack gains
  Thresh/Range/Attack/Release sliders on gate inserts and Attack/Sustain
  (−100..+100) on transient inserts. Verified: 2 wasm binding tests (gate
  retuned to a threshold below vs above a note → passes vs silences ≥4×;
  transient attack 0→0.9 → onset peak rises) → 35 dsp-wasm (143 cargo total); 1
  app reducer test (gate + transient full-set commands) → 310 app, 16
  engine-web; fmt+clippy clean; worklet rebuilt. **Every dynamics insert is now
  fully dialable — gate threshold/range, transient attack/sustain.**
- P1/editable REMAINING inserts (distortion/bitcrusher/chorus/reverb/delay) —
  now ALL 9 non-EQ inserts (and EQ) are live-tunable after adding; the chain is
  fully editable. 5 wasm setters
  (set_distortion/bitcrusher/chorus/reverb/delay_params) downcast to the chain
  node (WaveshaperNode/BitCrusherNode/ChorusNode/ReverbNode/DelayNode via their
  \*\_mut() accessors, ReverbNode also has set_mix); 5 worklet handlers +
  AudioEngine wrappers + messages. The setInsertParams reducer gained the 5
  cases (full param sets, defaults for untouched). The insert rack's per-insert
  editors were refactored to a single data-driven PARAM_CONFIG (kind → slider
  defs with label/min/max/step/def/fmt), replacing the three hand-written
  comp/gate/transient blocks and covering all 8 editable non-EQ kinds uniformly.
  Verified: a wasm binding test (distortion drive 1→30 retune → RMS rises as it
  saturates) → 36 dsp-wasm (144 cargo total); 1 app reducer test (all 5 emit the
  right full-set command) → 311 app, 16 engine-web; fmt+clippy clean; worklet
  rebuilt. **The entire insert chain is now editable + persisted + bypassable —
  a complete per-track effects rack.**
- P1/insert REORDER (effect order matters — comp→dist ≠ dist→comp; inserts were
  append-only). Track::swap_inserts(a,b) swaps the boxed nodes AND their bypass
  flags (state moves with them, so a reorder doesn't reset reverb tails); wasm
  move_insert(track, from, to); worklet moveInsert; AudioEngine.moveInsert. A
  moveInsert reducer ('up'/'down') reorders InsertState[] and RENUMBERS ids to
  the new chain positions (ids === engine chain index, so
  setInsertParams/setEqBand/bypass stay correct) + emits a single swap command
  (NO full rebuild → no DSP reset). Persistence is automatic: session-rebuild
  replays inserts in array order with the renumbered ids. Insert rack gains ▲▼
  buttons per chip (disabled at the ends). Verified: a dsp-graph test
  (distortion→high-cut vs swapped — nonlinear, so the orders don't commute →
  outputs differ >5%) → 42 dsp-graph (145 cargo total); 1 app reducer test
  (reorder + renumber + emit + no-op at the end) → 312 app, 16 engine-web;
  fmt+clippy clean; worklet rebuilt. **Drag the comp before or after the
  distortion — reorder the chain live, state-preserving and persisted.**
- P3/VOLUME AUTOMATION playback (the first piece of the automation system — a
  real new capability, the engine reads gain breakpoints synced to the
  transport). Track gains `static_gain_db` (the fader value when no automation
  is active) + `volume_automation: Vec<(beat, dB)>`; process() drives the strip
  gain per-block from the playhead beat via a linear interpolation between
  breakpoints (clamped to the ends), reverting to the static fader when stopped
  or cleared. set_volume_automation (sorts + restores static on clear); engine
  set_track_volume_automation (parallel beat/dB arrays); wasm + worklet
  (Float32Array) + AudioEngine.setVolumeAutomation. Modeled as
  TrackState.volumeAutomation (AutomationPoint[], persisted) + a
  setVolumeAutomation reducer (sort + clamp; empty clears) + session-rebuild
  replay; channel-strip Fade-In/Fade-Out (4-bar) + Clear-Auto buttons. Verified:
  a wasm binding test (a 4-beat −60→0 dB fade-in → the late window is ≥8× louder
  than the early window) → 37 dsp-wasm (146 cargo total); 2 app tests
  (sort+clamp+emit+clear; rebuild replays/skips empty) → 314 app, 16 engine-web;
  fmt+clippy clean; worklet rebuilt. **Tracks can fade in/out over the
  arrangement — automation breakpoints played back from the transport, the
  foundation for fuller parameter automation.**
- P3/PAN AUTOMATION (completes strip automation — auto-pan / stereo movement
  over the arrangement). Refactored the volume-automation interpolation into a
  free `interp_automation(points, beat, fallback)` (linear, clamped) reused by
  both lanes. Track gains `static_pan` + `pan_automation`; process() drives the
  strip pan per-block from the playhead beat (reverts to static when stopped/
  cleared). engine set_track_pan_automation + wasm + worklet (Float32Array) +
  AudioEngine. Modeled as TrackState.panAutomation (PanPoint[], persisted) + a
  setPanAutomation reducer (sort + clamp −1..1) + session-rebuild replay; a
  channel-strip ⇄ Pan toggle (sets a 4-bar L→R sweep, clears when active).
  Verified: a wasm binding test (a 4-beat −1→1 auto-pan → L≫R early, R≫L late,
  ≥4× each) → 38 dsp-wasm (147 cargo total); 2 app tests (sort+clamp+emit;
  rebuild replay) → 316 app, 16 engine-web; fmt+clippy clean; worklet rebuilt.
  **Tracks can fade AND sweep across the stereo field over time — both strip
  parameters automate from the transport.**
- P3/FILTER-CUTOFF AUTOMATION (the iconic filter sweep — synth tracks).
  SynthVoice gains set_base_cutoff(hz) (updates just the base cutoff, leaving
  the filter env/LFO modulation intact). Track gains static_cutoff_hz (mirrored
  from configure_synth, so it can be restored) + cutoff_automation
  Vec<(beat,Hz)>; process() applies the interpolated cutoff to every voice
  per-block via source.configure (reverting to static when stopped/cleared).
  wasm configure_synth now also calls t.set_static_cutoff(cutoff_hz); engine
  set_track_cutoff_automation + wasm/worklet (Float32Array)/AudioEngine. Modeled
  as TrackState.cutoffAutomation (CutoffPoint[], synth-only, persisted) + a
  setCutoffAutomation reducer (sort + clamp 20–20kHz, ignores non-synth) +
  session-rebuild replay; synth-panel ⟋ Sweep Up / ⟍ Down filter-automation
  buttons. Verified: a wasm binding test (a 50Hz→18kHz sweep on a 523Hz saw →
  the late/open window is ≥2× the dark early window) → 39 dsp-wasm (148 cargo
  total); 2 app tests (synth-only + clamp + emit; rebuild replay) → 318 app, 16
  engine-web; fmt+clippy clean; worklet rebuilt. **The classic filter sweep is
  automatable — volume, pan, AND cutoff now play back from the transport.**
- P2/STEM EXPORT (render each track isolated → one WAV per track; a real
  collaboration/mixing deliverable). engine render_track_offline(track, frames)
  temporarily solos the track and reuses the offline-render path (full master
  chain), restoring the prior solo state after; wasm render_track_offline
  (interleaved) + worklet 'bounce' gained an optional `track` field →
  render_track_offline; AudioEngine.renderTrackOffline (a bounce request
  carrying `track`). A daw-app exportStems callback loops the tracks, renders
  each, encodes a WAV, and downloads 'euterpe-stem-N-name.wav'; a transport-bar
  '⤓ Stems' button. Verified: a wasm binding test (two sequenced tracks at
  different pitches → each stem non-silent + the two differ; a pattern-less
  track's stem is silent; the full mix is louder than one stem; solo state
  restored) → 40 dsp-wasm (149 cargo total); 318 app, 16 engine-web; tsc clean;
  worklet rebuilt. NB: stems come from PATTERNS (offline render resets live
  notes for determinism). **Export every track as its own WAV — bring the
  arrangement into another DAW or hand off stems.**
- P3/velocity HUMANIZE (loosen mechanical patterns with seeded accent variation
  — a distinct sequencer transform, not a note transform). New pure
  `humanizeVelocities(velocities, amount, seed)` in pattern-transforms.ts: a
  mulberry32-seeded ±amount jitter kept in [0.05, 1] — the controlled randomness
  IS the feature and it's deterministic (reproducible), not Math.random faking a
  result. A humanizePattern reducer seeds from the current velocities (so
  re-applying varies while staying deterministic per pattern) + emits the
  pattern; a step-grid ✲ Humanize button + a command-bar "humanize 30" branch.
  Pure-TS (no engine touch — velocities already flow through setPattern).
  Verified: 3 helper tests (amount-0 no-op; same-seed determinism + range +
  variation + different-seed differs; centered mean) + 1 reducer test (changes +
  in-range + command + determinism + amount-0 no-op) + 1 command-parser test →
  323 app tests; tsc clean. **One click loosens a robotic groove — seeded,
  reproducible velocity humanization.**
- P3/SEND AUTOMATION (reverb + delay sends over time — automated throws/swells;
  completes automation across every track mix parameter: gain · pan · cutoff ·
  reverb-send · delay-send). Track gains static_send/send_automation +
  static_delay_send/delay_send_automation (set_send/ set_delay_send now mirror
  the static + only apply when no automation); process() drives both send levels
  per-block from the playhead via interp_automation. engine + wasm
  set_track_send_automation / set_track_delay_send_automation; worklet
  (Float32Array) + AudioEngine. Modeled as
  TrackState.sendAutomation/delaySendAutomation (SendPoint[], persisted) + a
  combined setSendAutomation/setDelaySendAutomation reducer (shared case, sort +
  clamp 0..1) + session-rebuild replay; channel-strip ↗ Verb / ↗ Delay swell
  toggles (0→100% over 4 bars). Verified: a wasm binding test (a sustained held
  note with the reverb send swelling 0→1 → the late wet window is louder than
  the dry early window) → 41 dsp-wasm (150 cargo total); 2 app tests (both lanes
  clamp+emit+independence; rebuild replays both) → 325 app, 16 engine-web;
  fmt+clippy clean; worklet rebuilt. **Automated reverb/delay throws — every
  track mix parameter now automates from the transport.**
- P3/MASTER-GAIN AUTOMATION (the whole-mix build-up / drop — the iconic
  electronic-music move; automation now spans the master bus too, not just
  tracks). `interp_automation` made pub(crate) so engine.rs reuses the same
  curve engine. Engine gains static_master_gain_db + master_gain_automation; the
  master process loop drives master_gain per-block from the playhead beat
  (reverting to the static fader when stopped/cleared); set_master_gain_db
  mirrors the static; clear_tracks resets it. wasm set_master_gain_automation +
  worklet (Float32Array) + AudioEngine. Modeled as
  DawSession.masterGainAutomation (AutomationPoint[], persisted via
  project-io) + a setMasterGainAutomation reducer (sort + clamp) +
  session-rebuild replay; master-panel ⟋ Build / ⟍ Drop / ✕ clear buttons.
  Verified: a wasm binding test (two tracks, master −60→0 dB over 4 beats → the
  late window is ≥8× the early) → 42 dsp-wasm (151 cargo total); 2 app tests
  (sort+clamp+emit+clear; rebuild replay) → 327 app, 16 engine-web; fmt+clippy
  clean; worklet rebuilt. **Automation is now complete across the whole console
  — 6 lanes: track volume/pan/ cutoff/reverb-send/delay-send + master gain. The
  classic build/drop is one click.**
- P1/PIANO-ROLL engine — note CLIP playback (variable-length timed notes on a
  looping beat timeline; the model behind a piano-roll, vs the step grid). New
  dsp-graph `clip.rs` NoteClip:
  `ClipNote{start_beat,length_beats,pitch,velocity}` + a loop length; playback
  by STATE RECONCILIATION — each block compute which pitches should sound at the
  current (looped) beat and diff against what's sounding, firing note on/off to
  match (robust under block-granular timing + looping; a pure function of the
  playhead → replays deterministically in the offline render, so clips are part
  of the arrangement). Track holds an Option<NoteClip> + set/clear/advance
  methods; the engine loop calls advance_note_clip alongside the sequencer +
  arp; stop_sequencer releases clip notes. engine + wasm set_track_note_clip
  (parallel start/length/pitch/velocity arrays + loop length) +
  clear_track_note_clip; worklet (Float32Array/Uint8Array) +
  AudioEngine.setNoteClip/ clearNoteClip. Verified: 4 cargo clip unit tests
  (fires a note inside its span + releases past its end; longer notes sustain
  more beats; empty = silent; release_all silences) → 46 dsp-graph; a wasm
  binding test (a 2-note 4-beat clip renders non-silent + deterministic offline;
  clearing silences) → 43 dsp-wasm (151 cargo total); 16 engine-web; fmt+clippy
  clean; worklet rebuilt. **The engine now plays real piano-roll clips —
  variable note lengths + sub-step timing, looped and deterministic. App model +
  reducer + the canvas/SVG roll component follow.**
- P1/PIANO-ROLL model + reducer — TrackState.noteClip (NoteClipState {notes:
  ClipNote[], lengthBeats}, synth-only, persisted via project-io's TrackState
  spread). A setNoteClip reducer sanitizes (start≥0, length≥0.05, pitch
  round+clamp 0–127, velocity clamp) + sorts by start, then emits the engine
  setNoteClip command (parallel start/length/pitch/velocity arrays + loop
  length); an empty note list → clearNoteClip + drops the clip. A clearNoteClip
  action too. session-rebuild replays the clip. Verified: 1 reducer test
  (sanitize + sort + emit; empty clears) + 1 rebuild test → 330 app tests; tsc
  clean. **The piano-roll clip is fully modeled + persisted; the editing
  component (the visual roll) is next.**
- P1/PIANO-ROLL component (the visual editor — closes the biggest DAW gap). Pure
  edit/layout helpers in `piano-roll-helpers.ts` (noteIndexAt over the half-open
  span, toggleNote add/remove, removeNoteAt, setAllVelocities, transposeClip —
  all immutable, 5 unit tests). An SVG `PianoRoll` component: a 3-octave (C2–C5)
  pitch×beat grid with black/white-key row shading, bar/beat grid lines, and the
  clip's notes drawn as accent-coloured bars (opacity = velocity); click a cell
  to add a note of the selected length (¼/½/1/2 beats, ¼-beat snap) or remove
  the note covering it; bar-length (1/2/4) + Clear controls. Wired into daw-app
  under the step grid for synth tracks; dispatches setNoteClip/clearNoteClip
  (the reducer + engine clip do the rest). Note math is fully unit-tested; the
  SVG renders via the DOM (no canvas, no browser run needed — the in-browser
  pixel pass stays deferred for the Mac-freeze hazard but the component
  type-checks
  - mounts). Verified: 5 helper tests → 335 app tests; tsc clean. **The
    piano-roll is real: author variable-length notes on a looping timeline,
    played back by the engine's deterministic note-clip scheduler. The DAW's
    single biggest gap is closed.**
- P1/MIDI RECORDING (play → record → edit — completes the piano-roll workflow).
  New pure `clip-recorder.ts` ClipRecorder: captures note on/off tagged with
  their transport beat into ClipNotes (closing still-held notes at the end
  beat), with `quantizeClip`/`quantizeBeat` grid-snapping — deterministic, no
  audio/DOM needed. Wired into daw-app: a Record toggle creates a recorder,
  seeks to 0 + plays, and routes the keyboard through a `recordDispatch` that
  feeds note on/off to the recorder at the live playhead beat (computed from
  playheadSamples·tempo/ (60·sampleRate)); stopping calls finish(endBeat,
  1/16-quantize) and dispatches setNoteClip to drop the captured performance
  into the piano roll. A transport-bar '● Rec' button (needs a selected synth
  track). Verified: 6 recorder unit tests (timed capture; held-note close;
  orphan note-off ignored; quantize start+length; finish-quantize; reset) → 341
  app tests; tsc clean. **Record a performance live and it lands in the piano
  roll, quantized and editable — the full play→record→edit loop.**
- P1/PIANO-ROLL drag editing (completes the roll into a full editor). Pure
  helpers moveNote (shift start≥0 + pitch clamp 0–127) + resizeNote (Δlength,
  min 0.25), immutable + tested. The SVG component swaps click-only for pointer
  handlers: mousedown grabs a note (move, or resize from its right ⅓) or arms an
  empty-cell add; mousemove tracks a snapped (¼-beat / semitone) delta with a
  live preview on the dragged note; mouseup commits — a zero-delta grab removes
  the note (click), a drag moves/resizes it, an empty cell adds. Verified: 2 new
  helper tests (move clamps start/pitch + no-op out of range; resize min-clamps)
  → 343 app tests; tsc clean. **The piano-roll is a complete editor — click to
  add/remove, drag to move, drag the edge to resize, all snapped and
  previewed.**
- P3/STREAMING SOURCE — routing the AI generator into a DAW track (the genuine
  MRT2↔DAW gap; the MRT2 live path itself was already built + fail-closed-tested
  by prior sessions — sidecar on the real streaming API, runtime step-op + live
  conditioning, BFF route feeding live note/drum control, panel stereo decode +
  audio-ref; the steady-gathering-quiche plan in context was stale). New
  dsp-graph `stream.rs` StreamSource: a fixed-capacity mono ring buffer the host
  fills with decoded PCM (the MRT2 generator's async WS frames) and the audio
  thread drains one sample/ block — bridging async bursty network audio into the
  synchronous alloc-free block so the AI generator becomes a first-class mixer
  track (through inserts/sends/automation/master). Fail- closed: underrun →
  silence (never fabricated), overrun → drop-oldest (bounded latency). New
  Source::Stream variant + Track.stream_mut; wasm
  add_stream_track(capacity)/push_stream → samples dropped; worklet
  addStream/pushStream + AudioEngine.addStreamTrack/pushStreamSamples (transfers
  the buffer). Verified: 5 cargo ring-buffer tests (in-order playback;
  underrun-silence; overrun-drop-oldest-bounds-latency; ring wraparound; clear)
  → 51 dsp-graph; a wasm binding test (idle silent → pushed PCM plays through
  the master → drained back to silence) → 44 dsp-wasm (162 cargo total); 16
  engine-web; fmt+clippy clean; worklet rebuilt. **The on-device AI generator's
  audio can now flow into a mixable DAW track — the engine + JS API are wired; a
  'generator' track kind feeding it from the realtime controller is the
  remaining app glue.**
- P3/GENERATOR TRACK KIND — makes the StreamSource a first-class DAW track.
  TrackKind gains 'generator'; an addGeneratorTrack reducer creates the track +
  emits the addStream engine command (96000-sample / ~2 s ring buffer);
  session-rebuild replays generator tracks as addStream; a '+ AI Gen' button +
  command-bar "add a generator". The synth-specific panels (synth/piano-roll/
  keyboard) guard on kind==='synth', so a generator shows just its channel strip
  — gain/pan/mute/ solo/sends/inserts/automation all apply, so streamed AI audio
  flows through the full mix. Verified: a reducer test (kind generator +
  addStream command) + a rebuild test + a command-parser test → 346 app tests;
  tsc clean. **Add an AI-generator track from the UI; pushStreamSamples (wired
  in the prior slice) feeds it. The remaining glue is the /realtime MRT2 WS
  connection pushing its decoded frames into this track's buffer
  (deploy-verified with the real engine).**
- P3/MRT2-WS → GENERATOR-TRACK BRIDGE (closes the last-mile AI↔DAW wiring). New
  `realtime-stream-bridge.ts`: `interleavedStereoToMono` (downmix the 48k stereo
  frames), `routeRealtimeMessage` (binary → mono audio frame, string → JSON
  envelope type, malformed ignored), and `bridgeRealtimeStream(socket, cb)`
  wiring a WebSocket-like (open/message/error/ close) to the sink + a disposer —
  the socket is INJECTED so it's fully unit-tested. daw-app: a generator track
  shows a Connect/Disconnect MRT2 panel; connect opens a WebSocket to
  `/v1/generation/realtime` (the same BFF endpoint the standalone panel uses)
  and bridges its decoded frames into the track via pushStreamSamples — so the
  on-device AI generator's audio flows through that track's
  inserts/sends/automation/master. Fail-closed: on error/close → disconnect + a
  "backend unavailable" message (never fabricated audio); the disposer detaches
  handlers + closes; unmount cleans up. Verified: 3 bridge tests (downmix;
  binary/string/malformed routing; socket wiring + disposer) → 349 app tests;
  tsc clean. **The full AI↔DAW path is wired end to end: add an AI Gen track →
  Connect MRT2 → the live generator streams into the DAW mix. Frame
  routing/downmix/socket-wiring are mock-tested; the live stream runs against
  the deployed BFF + real magenta_rt engine.**
- P4/NATIVE DESKTOP SHELL (Tauri v2) — the named "native builds" gap. A
  complete, deploy-ready desktop wrapper for the studio-web DAW under
  `apps/euterpe-studio-web/ src-tauri/`: the app crate
  (`Cargo.toml`/`build.rs`/`src/{main,lib,commands}.rs`),
  `capabilities/default.json`, real PNG icon set (hand-rolled PNG encoder →
  Euterpe-purple "E"), and a fixed `tauri.conf.json` (corrected the removed
  `next export` CLI → `EUTERPE_TAURI=1 next build`; added the COOP/COEP headers
  Tauri must serve since static export can't; `withGlobalTauri`; window label).
  The non-trivial, security-relevant logic lives in a dependency-light
  `euterpe-desktop-core` crate (serde only): `sanitize_stem_filename`
  (path-traversal/absolute/Windows-reserved safe), `dedupe_filenames`
  (case-insensitive), `validate_project_json` — 9 cargo tests. Native commands
  (thin wrappers over core): `export_stems` (folder-wide WAV write + containment
  check), `save/load_project` (validated `.eup` I/O), `reveal_in_file_manager`.
  Web side wired via `src/daw/desktop-bridge.ts` using the injected
  `window.__TAURI__` global (no `@tauri-apps/api` dep): `isDesktop`,
  `bytesToBase64`, `stemsPayload`,
  `nativeExportStems/SaveProject/LoadProject/Reveal/ PickDirectory` — 6 vitest
  tests; `exportStems` now prefers a single native folder-export + reveal under
  desktop, falling back to per-file browser download. `next.config.mjs`
  conditionally static-exports under `EUTERPE_TAURI=1`. Verified: core 9 cargo
  tests; bridge 6 vitest + app tsc clean + 355 app tests; JSON + cargo manifest
  valid; **`cargo fetch` resolves the full graph** (tauri 2.11, dialog/fs/
  opener, wry 0.55); the `OpenerExt::reveal_item_in_dir` API confirmed against
  the resolved plugin source; all capability permission ids confirmed present.
  **Boundary: the full `cargo check`/`tauri build` compiles the WebKit/wry tree
  (~400 crates, multi-GB) — not run on the 16 GB laptop to avoid swap-thrash
  (CLAUDE.md memory rule); it runs in CI/packaging. Everything short of that
  compile is verified.**
- P4/AUDIO-PLUGIN FORM FACTOR (CLAP + VST3 instrument) — the named "plugin form
  factor" gap. New standalone crate `libs/euterpe/instrument/` exposes the
  tested dsp-core PolySynth as a plugin usable inside other DAWs (the instrument
  engine is the right unit — a whole DAW doesn't load as a VST). `params.rs`:
  pure normalized- [0,1] ↔ engine-unit mapping (log cutoff 20Hz–20kHz, ADSR
  curves, detune cents, Q, dB→linear gain) — 8 cargo tests vs exact known values
  (e.g. cutoff(0.5)=632.46Hz geometric midpoint). `engine.rs`:
  `EuterpeInstrument` applies a ParamSnapshot onto every voice, consumes MIDI
  NoteEvents, renders blocks — 5 cargo tests against the real PolySynth
  (silent→note→release decay, determinism, gain scaling, polyphony). `plugin.rs`
  (feature `plugin`): the nih-plug host binding — 19 automatable params with
  engineering-unit readouts, sample-accurate MIDI, CLAP+VST3 exports. Written
  against the FETCHED nih-plug source (every symbol confirmed:
  Plugin/ClapPlugin/ Vst3Plugin items, Float/IntParam,
  NoteEvent::{NoteOn,NoteOff,Choke}, timing(), const_default, export macros).
  nih-plug is an optional git dep behind the feature so default
  `cargo test`/build compiles only dsp-core + this crate (confirmed: nih_plug
  absent from the default dep tree). Verified: 13 cargo tests;
  `cargo tree --features plugin` resolves the full graph (nih_plug git
  f36931f7 + clap-sys + vst3-sys); adversarial stub scan clean. **Boundary:
  `cargo build --features plugin` compiles nih-plug + CLAP/VST3 system bindings
  (needs the host audio toolchain) — runs in CI/packaging, not on the 16 GB
  laptop. Engine, mapping, dep graph, and binding API correctness all verified
  short of that compile.**

- P4/ACCESSIBILITY (a11y) — the named "a11y" P4 item. New `src/daw/daw-a11y.ts`:
  pure, tested screen-reader strings — `transportStatus` (a polite live-region
  message: Stopped/Playing/Recording, loop bars, BPM), `meterStatus` (peak
  dBFS + LUFS + explicit clipping flag),
  `rangeAria`/`toggleAria`/`trackButtonLabel`, and an `srOnly` style. Wired into
  the shared controls so it covers every instance at once: `Slider` now carries
  `aria-label` + engineering-unit `aria-valuetext` (a reader says "Master, -3.0
  dB", not "0.43"); the peak `Meter` bars expose `aria-valuetext` in dBFS +
  valuemin/max and the meter group an `aria-label` summary; the transport
  `<header>` is a labelled `role="toolbar"` with a visually-hidden
  `role="status" aria-live="polite"` region announcing transport changes, plus
  `aria-label`s on the symbol-only undo/redo/metronome buttons; channel-strip
  mute/solo gain proper `aria-label`s (were bare "M"/"S"). Baseline was 68
  buttons / 11 labels / 0 live regions / 0 slider valuetext. Verified: 4
  daw-a11y vitest tests (status/meter/aria strings) → 359 app tests; tsc clean;
  stub scan clean.

- P3/LIVE AUDIO-INPUT RECORDING — capture mic/line into an audio track (the last
  table-stakes DAW capability the notes flagged as platform-bound). Pure
  `audio-capture.ts`: `CaptureBuffer` (accumulates unbounded Float32 capture
  blocks — COPIES them since the worklet reuses its buffer — into one contiguous
  take; frames/durationSec/finish/reset), `RecordingSession`
  (idle→armed→recording→idle state machine rejecting illegal transitions,
  returns the target track on stop), `downmixToMono` — 6 vitest. daw-app
  `recordInput(track)` toggle: getUserMedia({audio}) → MediaStreamSource →
  ScriptProcessor appends downmixed mono to the CaptureBuffer (routed through a
  muted gain so it pulls without monitoring); on stop →
  `engine.loadSample(track, finish())` so the take becomes a playable clip on
  the track. Fail-closed: getUserMedia denial → no fabricated take; unmount
  tears down the stream/nodes. SamplerPanel gains a labelled ●/■ Record Input
  button. Verified: 6 capture tests → 365 app tests; tsc clean; stub scan clean.
  **Boundary like the Stop-hook MRT2 case: the pure capture logic is tested
  here; the live getUserMedia/ScriptProcessor capture runs in the browser
  (tsc-verified, not headless-runnable).**

- P3/LIVE MIDI-HARDWARE INPUT — play/record the DAW synth from a connected
  controller (the DAW had none; only the /realtime MRT2 panel used WebMIDI).
  Pure `web-midi.ts` `parseMidiMessage(data)`: decodes raw MIDI status/data
  bytes → note events (0x90 note-on, 0x90-vel-0 + 0x80 → note-off, channel
  nibble ignored, data masked to 7 bits, CC/bend/clock/ malformed → null) — 6
  vitest with byte vectors. daw-app `toggleMidiInput`:
  `navigator.requestMIDIAccess()` → attaches `onmidimessage` to every input
  port, routing decoded events through the SAME `recordDispatch` the on-screen
  keyboard uses (so a controller both plays the selected synth track and records
  into the active clip when armed); reads the target track per-message so track
  selection stays live. Fail-closed: no Web MIDI / denied access → honest error,
  no events; disable + unmount detach every port handler. A labelled MIDI-In
  toggle above the keyboard. Verified: 6 web-midi tests → 371 app tests; tsc
  clean; stub scan clean.
- AUDIT-DRIVEN / TIME-STRETCH + PITCH-SHIFT WARP — surfaced by a deep audit as
  the one genuinely-missing SOTA DAW capability (the Sampler only did coupled
  resampling; no independent duration/pitch). Full 7-layer slice end-to-end:
  dsp-core `timestretch.rs` WSOLA (waveform-similarity OLA, Hann-COLA unity
  gain) + `time_stretch`/`pitch_shift`/`resample_to_len` — 6 cargo tests vs the
  defining invariants (stretch changes length yet keeps zero-crossing density;
  octave pitch-shift doubles frequency at constant length) → dsp-graph re-export
  → dsp-wasm free fns `time_stretch_buffer`/`pitch_shift_buffer` (+1 binding
  test) → **wasm rebuilt (wasm-pack, light)** → worklet `loadSampleWarped`
  handler (warps on the audio thread via the worklet's wasm instance, then
  `load_sample`) → `messages.ts` cmd + `AudioEngine.loadSampleWarped` →
  engine-web spec drives the **real compiled wasm** (warp crosses the WASM
  boundary; 17 tests) → daw-app retains decoded buffers per track
  (non-destructive re-warp; recorded takes too) + `warpSample` → SamplerPanel
  Warp section (Tempo ×0.5–2 stretch + Pitch ±12 st, Apply/Reset,
  aria-labelled). Verified: dsp-core 73 + dsp-graph 51 + dsp-wasm 45 cargo;
  engine-web 17 (real wasm); 371 app; all tsc/fmt/clippy/stub-scan clean. **The
  audit's finding that the DAW kernel (audio-engine) is stub-free is now matched
  by closing its one real feature gap.**
- NATIVE COMPILES PROVEN ON-BOX (2026-06-06) — both previously "deploy-bound"
  native scaffolds were compiled here at restricted parallelism (`-j2`,
  memory-monitored; free% held 33–39 throughout, no swap-thrash): (1)
  `cargo build --features plugin` on `libs/euterpe/instrument` → exit 0 in ~25s,
  produced `libeuterpe_instrument.dylib` (nih-plug + clap-sys + vst3-sys
  compiled + linked, `nih_export_clap!`/`nih_export_vst3!` entry points). (2)
  `cargo check` on `apps/euterpe-studio-web/src-tauri` → exit 0 in ~1m36s, zero
  errors/warnings, and `tauri_build` generated `gen/schemas/` (validating
  tauri.conf.json + capabilities + commands.rs opener/fs/dialog). The plugin
  binding + the Tauri desktop shell genuinely compile against real Tauri 2.11 /
  wry / nih-plug — not just resolve. Only the final bundle steps (`tauri build`,
  `cargo xtask bundle`) remain, and those need only packaging, not code.

## Build log (append per slice)

- P0.1 dsp-core: oscillators (PolyBLEP) + ADSR + RBJ biquad (8 types). 14 cargo
  tests vs known DSP anchors; fmt+clippy clean. Commit 74aeeafd21.
- P0.2 dsp-core: dynamics (feed-forward soft-knee compressor + brickwall
  limiter)
  - metering (sample-peak, sliding RMS, ITU-R BS.1770 K-weighted LUFS). 22 cargo
    tests total; the LUFS −3.01 LKFS anchor (full-scale 997 Hz single channel)
    pins the K-weighting coeffs to the standard; compressor hits the −10.5 dB
    4:1 ratio target; limiter holds its ceiling. fmt+clippy clean. Commit
    7df7b7c68e.
- P0.3 dsp-core: time/space/mix — fractional-interpolated DelayLine + feedback
  Delay (damped, dry/wet), Freeverb (8 damped combs ∥ 4 allpasses, canonical
  tunings SR-scaled), and mixer (equal-power constant-power pan + ChannelStrip +
  StereoBus). 32 cargo tests; echoes land at exact multiples w/ fb^n decay,
  reverb tail is dense+decaying+stable & bigger room rings longer, pan is −3 dB
  center / isolated hard L-R / L²+R²=1 across the sweep. fmt+clippy clean.
  Commit 238026d670.
- P0.4 dsp-core: instrument/EQ set — 5-band ParametricEq (low-shelf/3 peak/
  high-shelf), variable-rate interpolating Sampler (resampling + semitone
  pitch + loop), and a subtractive SynthVoice (osc→resonant LP w/ filter-env,
  amp env; control-rate coeff refresh) + PolySynth (free-voice-then-oldest
  stealing). 44 cargo tests; flat EQ transparent / +6 dB peak lifts its center /
  shelf cut hits bass not treble, rate 2.0 doubles pitch via zero-crossings +
  exact unit-rate reproduction, voice sounds→silences after release + lower
  cutoff = less energy, poly allocates/steals/frees. fmt+clippy clean.
  **dsp-core primitive set done.**
- P0.5 dsp-graph crate: the real-time mixing engine over dsp-core. AudioEffect
  trait + insert nodes (eq/comp/limiter/delay/reverb), Track (mono Source →
  insert chain → gain/pan strip), Engine (transport clock + track sum → stereo
  master gain+limiter + peak/LUFS meters), live note routing, solo/mute. All
  alloc-free after construction (scratch sized once). 51 cargo tests total (7
  new engine): silent→silence, synth note→stereo sound, hard-left isolates L,
  master limiter caps a hot mix, playhead advances only while playing, mute/solo
  gating, insert-chain EQ cut measurably quieter. fmt+clippy clean.
- P0.6 dsp-wasm crate: the wasm-bindgen surface (WasmEngine) exposing the engine
  to JS/AudioWorklet — add synth/silent tracks, configure_synth, note on/off,
  gain/pan/mute/solo, insert builders (compressor/limiter/reverb/delay/EQ) with
  downcast-based set_eq_band (added as_any_mut to the AudioEffect trait),
  transport, master gain, peak+LUFS readouts, process(&mut[f32]L,&mut[f32]R).
  Compiles to wasm32-unknown-unknown (verified) AND natively; 3 native binding
  tests (note→audio, inserts indexed+editable, transport). Offline build via
  cached wasm-bindgen 0.2.122. fmt+clippy clean. 57 cargo tests total.
- P0.7 @euterpe/audio-engine-web TS package: the browser front-door. Main-thread
  AudioEngine class (AudioContext + AudioWorkletNode; compiles wasm, posts the
  WebAssembly.Module to the worklet; full control API:
  tracks/synth-config/notes/
  gain-pan-mute-solo/inserts/EQ-band/transport/master; deterministic
  track+insert indices; onMeter LUFS/peak stream). Worklet processor
  (engine-processor.template
  - concatenated wasm-bindgen no-modules glue → euterpe-engine-processor.js via
    build-worklet.mjs). wasm-opt bulk-memory/nontrapping flags fixed the
    optimizer. Verified: build-worklet emits a valid bundle
    (glue+processor+registerProcessor), tsc clean, and **4 vitest tests drive
    the COMPILED wasm in Node** (initSync from bytes, same path the worklet
    uses) — silent→note→real stereo audio, transport advance, live EQ-band edit
    across the WASM boundary, finite LUFS readback. **P0 audio engine COMPLETE:
    Rust DSP → WASM → AudioWorklet → TS, fully tested.**

### Gate P1 — render the DAW UI (in progress)

- P1.1 DAW session core (apps/euterpe-studio-web/src/daw/): a pure,
  framework-free reducer (mirrors the realtime-mrt2 controller pattern) that
  folds UI intents into immutable view-state AND emits the EngineCommand[] to
  post to the engine. Track + insert ids assigned sequentially to match the
  engine's own creation order (no audio-thread round-trip). Covers tracks
  (synth/audio), gain/pan/mute/solo, synth-patch config, live notes (held-note
  tracking), insert builders, EQ-band edits, transport (play/stop/tempo/seek),
  master gain, meter folding; plus display helpers (dB/pan/bars:beats
  formatting, computer-keyboard→MIDI, note names). Added AudioEngine.postCommand
  seam + a DOM-free `@euterpe/audio-engine-web/messages` subpath so the pure
  core compiles under ES2022-only (no DOM pulled in). 20 vitest tests (clamps,
  immutability, command emission, solo/mute audibility, helpers); all 160 app
  tests green; tsc clean. Wired the package into the app (dep + transpile +
  vitest alias).
- P1.2 DawController + React DAW surface. DawController (engine sink injected)
  holds the session, runs the reducer, posts commands, notifies React — 6 vitest
  tests with a recording fake sink (command stream verified for
  add/note/insert/mix/transport + default scene + full mixing gesture). React
  surface (app/daw/page.tsx + src/ components/daw/): dark-first theme tokens,
  Slider/Meter controls (dB-scaled peak bars + LUFS), ChannelStrip
  (gain/pan/mute/solo), SynthPanel (waveform + ADSR + filter), InsertRack (add
  comp/limiter/reverb/delay/EQ + 3-band EQ editor), TransportBar
  (play/tempo/position/master+meter), playable Keyboard (pointer +
  computer-keyboard, releases on unmount), and DawApp tying it together via a
  useDawEngine hook (boots AudioEngine from a user gesture, lays a default
  scene, streams meters). Asset-sync script copies the worklet+wasm into
  public/audio on predev/prebuild. Verified: app tsc clean (DOM+JSX config), 166
  app vitest tests green, asset sync emits both files. NOTE: live in-browser
  visual pass deferred — the box's 16 GB RAM makes a next dev + browser run
  risky (documented hazard); the engine produces real audio (Node-verified in
  P0.7) and all reducer/controller behavior is unit-tested, so the React layer
  is the thin untested binding (as with the existing realtime page).
- P1.3 step sequencer (engine + binding). dsp-graph: a per-track looping Pattern
  (per-step MIDI note lists, steps_per_beat subdivision) driven
  sample-accurately by the transport clock — Engine advances each track's
  sequencer at every block's start playhead, firing note on/off at step
  boundaries (≤ one quantum jitter), releasing on stop and rearming on seek.
  dsp-wasm: set_track_pattern (flat step-lengths + notes encoding to fit
  wasm-bindgen), clear_track_pattern, track_step. Verified: 3 new dsp-graph
  tests (step index advances + loops with the clock; sound only while playing;
  stop releases held notes) + 1 dsp-wasm native test + 1 audio-engine-web test
  driving the COMPILED wasm pattern in Node. 58 cargo tests, fmt+clippy clean,
  rebuilt+synced the worklet artifact.
- P1.4 step-grid UI. Engine protocol gains setPattern/clearPattern commands
  (worklet flattens steps[][]→the wasm flat encoding). Reducer:
  ensurePattern/toggleStep/ clearPattern actions (auto-create a 16-step bar,
  keep step notes sorted, toggle on/ off) + a patternCurrentStep helper
  mirroring the engine's sequencer math for the UI highlight. StepGrid component
  (pitch × step grid for the selected synth, playing column highlighted from the
  transport playhead) wired below the mixer. Verified: 3 new reducer tests
  (toggle/clear/current-step) → 169 app vitest tests; app + lib tsc clean;
  worklet rebuilds with the setPattern handler. **DAW now sequences: live
  synth + mixer + effects + step patterns, all driven through the tested core.**
- P1.5 per-track metering. dsp-graph Track gets a post-fader PeakMeter
  (peak-hold ballistic); the Engine resets the meter of any track silenced by
  another's solo and exposes track_peak(i); dsp-wasm forwards it; the worklet
  gathers per-track peaks into the meter event; AudioEngine + the reducer's
  meter action fold them into each TrackState.peak (by id); ChannelStrip renders
  a dB-scaled level bar (green/amber/red). Verified: 1 new dsp-graph test
  (sounding track meters >0, silent ~0, muted decays, solo-out forces 0) → 59
  cargo tests; 169 app + 5 wasm vitest; app+lib tsc clean; worklet rebuilt with
  track_peak.
- P1.6 algorithmic pattern generation (the first "intelligence" feature).
  pattern-gen.ts: real Bjorklund Euclidean rhythms + scale-quantized
  ascending-arp note placement (major/minor/dorian/pentatonics). A
  generatePattern reducer action fills the selected synth's grid + emits
  setPattern; StepGrid gets Euclidean-fill buttons (3/5/7 pulses, pentatonic
  arp). Verified vs canonical patterns: E(3,8)=x··x··x· (tresillo),
  E(5,8)=x·xx·xx· (cinquillo), E(4,16) four-on-the-floor; exact pulse count +
  every note in-scale + MIDI clamp; reducer test confirms 4 onsets + pentatonic
  notes. 7 new tests → 176 app vitest; app tsc clean.
