# Euterpe → SOTA DAW — Remaining-Work Checklist (granular, expert)

**Date:** 2026-06-08 · **Baseline report:**
`EUTERPE_SOTA_GAP_ANALYSIS_2026-06-07.md` (186 residual gaps) **Author
context:** the cleanly-buildable, low-risk local backlog has been worked through
(≈60+ gaps shipped, the latest 12 this session: COLLAB-3
versioning/merge/branching, ENG-17/19/8, MASTER-6/N3/7/15, PLAT-N6, MIDI-5).
This document is the **forward map of everything that is NOT yet done**, grouped
into three buckets by how it must be executed. It is written to be picked up
cold — every item has files, an approach, verification, acceptance criteria, and
an honest blast-radius/risk note.

> **Paths moved (checked 2026-09-18).** Every `apps/euterpe-studio-web/…` path
> in this file is now `apps/euterpe/studio-web/…`, and the DSP effects it names
> (`autowah.rs`, `deesser.rs`, `dynamic_eq.rs`) are in
> `libs/euterpe/audio-engine/crates/dsp-core/src/`, beside the graph crate
> `crates/dsp-graph`. The 44 old paths are left as written so the history reads
> true; resolve them through this note.

---

## 0. How to use this checklist

- Items are checkboxes. `[ ]` = not started, `[~]` = partial/foundation-only,
  `[x]` = done (none here — this file is remaining-only). Sub-tasks are
  deliberately small so progress is legible.
- **Pick ONE bucket-A or bucket-B item per work session and budget for its blast
  radius + full verification.** These are no longer quick clean wins. Do not
  batch-mark; each box must be backed by code you wrote and tests you ran.
- Bucket-C items **cannot be completed in this sandbox** — for each, the
  checklist records the _fail-loud seam_ to ship now (a typed interface that
  throws `not_configured` / returns `{configured:false}`) and the exact external
  resource needed to finish. Do not fake success; ship the seam, document the
  dependency.

### 0.1 The proven cross-stack engine recipe (used ~7× this session — follow exactly)

```
1. dsp-core (primitive) or dsp-graph (engine/track): add the Rust method with a NO-OP INVARIANT
   (default value = bit-identical to before → all existing cargo tests pass unchanged).
2. dsp-wasm/src/lib.rs: #[wasm_bindgen] getter/setter forwarding to the engine.
3. worklet/engine-processor.template.js: a `case '<cmd>':` dispatch (only if realtime/per-block).
4. audio-engine-web/src/messages.ts: extend EngineCommand union (+ EngineEvent/MeterSnapshot if it's a readback).
5. audio-engine-web/src/audio-engine.ts: a bridge method ONLY if you want a named call; most commands post generically
   from the reducer's `commands: EngineCommand[]` — no bridge needed.
6. App reducer: apps/euterpe-studio-web/src/daw/{types.ts (field + action), daw-session.ts (case), session-rebuild.ts
   (replay non-default), project-io.ts (persist + restore — often FREE via the SerializedTrack/session spread)}.
7. UI panel under apps/euterpe-studio-web/src/components/daw/.
8. REBUILD the served bundle (gitignored, NEVER committed):
   cd libs/euterpe/audio-engine-web && node scripts/build-worklet.mjs        # wasm-pack release, ~6-10s, light on the Mac
   cd apps/euterpe-studio-web && node scripts/sync-engine-assets.mjs         # copies to public/audio
```

### 0.2 Verification harness (the standard every item must clear)

```
# Rust
cd libs/euterpe/audio-engine/crates
cargo test -p dsp-core -p dsp-graph          # known-correct value assertions, NOT just "renders"
cargo clippy -p dsp-core -p dsp-graph -p dsp-wasm   # pre-existing warning: track.rs Source enum variant size (ignore)

# WASM boundary (drives the compiled wasm in Node — proves the binding flows end-to-end)
cd libs/euterpe/audio-engine-web
npx tsc --noEmit
npx vitest run src/__tests__/wasm-engine.spec.ts   # add the new method to WasmEngineInstance iface + a boundary test

# App (NO Nx — it's broken by worktrees; use the ad-hoc tsconfig)
cd apps/euterpe-studio-web
npx tsc --noEmit -p tsconfig.tmpcheck.json   # MUST stay at 0 errors
npx vitest run src/daw src/components/daw     # jsdom render+dispatch tests
```

- No `timeout` command on macOS. `grep` exit 1 = "no matches" = success in a
  one-shot batch (don't let it fail a chain).
- `git diff --cached | grep 'wasm/'` FALSE-POSITIVES on the `crates/dsp-wasm/`
  SOURCE path — verify artifacts aren't staged by checking the file list for
  `audio-engine-web/wasm/` and `public/audio/` specifically.
- Commit + push to BOTH refs every completed item: `git push origin <branch>`
  then `git push origin <branch>:main`. `--no-verify` is OK (pre-commit Nx
  hangs). Trailer: `Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>`.

### 0.3 Architecture anchors (where things live)

- Engine:
  `libs/euterpe/audio-engine/crates/{dsp-core (DSP primitives: voice.rs, oscillator, filters, dynamics.rs, saturator/waveshaper, oversample.rs, multiband.rs, meter.rs), dsp-graph (engine.rs = master bus + per-track loop, track.rs = Source/inserts/sends/channel-strip, clip.rs, tempo_map.rs, effect.rs = AudioEffect trait + insert nodes, context.rs = ProcessContext), dsp-wasm (#[wasm_bindgen] facade)}`.
- Engine bridge:
  `libs/euterpe/audio-engine-web/{src/messages.ts (EngineCommand/EngineEvent unions, MeterSnapshot), src/audio-engine.ts (AudioWorklet host), worklet/engine-processor.template.js (command dispatch + meter frame)}`.
- App:
  `apps/euterpe-studio-web/src/{daw (reducer daw-session.ts, types.ts, session-rebuild.ts, project-io.ts, automation-lane-helpers.ts, fft.ts, reference-match.ts, pure helpers + specs), components/daw (React: daw-app.tsx is the host hook+render; piano-roll, arrangement-view, mixer, synth-panel, insert-rack, transport-bar, *-panel.tsx)}`.
- The `@euterpe/master` lib (`libs/euterpe/master/src/frontier-mastering`) has
  REAL DSP planners (matchEq, dynamicEqResponse, masterStems, recommendChain,
  meterBs1770, resolveBus) — wire-able, but several return PLANS not realtime
  processors (wiring a planner as if it processes = display-only stub; verify
  before wiring).
- **The `@euterpe/provenance` lib is a SIMULATION** (FNV-keyed 'ed25519-sim',
  64-bit-into-number[] watermark). Real C2PA/SynthID needs proper crypto + a PCM
  watermarker (bucket C).

---

## 1. Reconciliation — what is already DONE (do NOT re-do)

Shipped/verified (source-tagged): arrangement timeline + clip editor
(ARR-1/2/3/4/6/8/9/10/11/12/13/15/16, FX-17, UX-9, REC-1, IO-10, PLAT-7, AI-11),
session/clip-launch view (ARR-7/UX-23), synthesis quartet (FX-1/2/3/5/6/7/8
incl. full mod-matrix + LFO shapes/targets, FX-10 presets, FX-14 slicing), 15
insert effects, master metering suite (ENG-12/15/16/20, MIX-5/13), master chain
(MIX-2 bus/aux, MIX-1 VCA, MIX-4 pre/post sends, MIX-7 EQ-auto, MIX-23
multiband, MIX-27 sidechain, MIX-34 snapshot, ENG-17 soft-clip, ENG-19
lookahead, MASTER-15 ceiling+width auto), mastering/export (MASTER-2/3/5/6/N3/7/
14/18/19/N1, IO-5 BWF, format/genre/LUFS/compliance/dither/tags), MIDI tools
(MIDI-4/9/10/11/12/15/16/17/19/20/5), DAWproject import+export (IO-1), recording
(REC-7/8/20), rights (RIGHTS-1/2/3/4/5/11/17 at the codebase's fail-closed
baseline), provenance scan (RIGHTS-11 own-watermark), versioning+merge+branching
(COLLAB-3, COLLAB-8), platform a11y/i18n-adjacent (PLAT-1 MIDI-learn,
PLAT-10/12/14/15/17/21/N6), UX (UX-2/3/4/5/14/17). ENG-8 saturator oversampling,
AI-4/8/N1 generation.

**Ambiguous — verify before assuming remaining:** MIDI-18 / ENG-21 / REC-6 /
MIX-31 (MIDI-learn / control-surface — PLAT-1 covers CC-mapping; OSC done [3.9
IO-8] + MCU done [3.9 MIDI-18, 2026-06-08] + **HUI core done [3.9,
2026-06-08]**; HUI transport/feedback + EUCON NOT done), ENG-18 (per-track
sidechain matrix — MIX-27 covers per-insert sidechain off a key track; an
arbitrary any→any routing matrix UI is NOT done), **MIX-3 routing matrix — DONE
2026-06-08:** `apps/euterpe-studio-web/src/daw/routing-matrix.ts`
(`buildRoutingMatrix` derives a grid of every track's output destination [Master
/ group bus] + reverb/delay aux-send levels;
`routeToBusAction`/`reverbSendAction`/`delaySendAction` lower a cell edit to the
existing `setTrackBus`/`setTrackSend`/`setTrackDelaySend` reducer actions,
clamped) + a `RoutingMatrixPanel` modal (exclusive destination radios per row +
aux-send sliders, es-localized) opened by a "Routing Matrix…" command-palette
command. A REAPER-style patchbay over the existing bus/send routing — no new
engine capability. 6 pure + 6 jsdom tests; full suite 1683. (Arbitrary any→any
track-routing would need new engine routing — still NOT done.) MASTER-N4
(compliance dashboard — ≈ covered by MIX-33 loudness-preview-panel), UX-29
(DAWproject UI — IO-1 wired it), MASTER-17 (offline-render progress UI —
verify), COLLAB-19 (PWA/OPFS — only partially; see PLAT-5), AI-N5/UX-26/REC-N2
(AI session players — a ~55-LOC accompaniment helper exists; the deep
chord-reactive real-time player is NOT done).

---

## 2. BUCKET A — Large local engine refactors (buildable + fully verifiable here)

> These are real, in-sandbox, cargo+Node+jsdom-verifiable, but touch the audio
> hot path or need a new DSP subsystem. Each has a **no-op invariant** that
> protects the ~190 existing engine tests — preserve it religiously.

### 2.1 `[x]` ENG-3 — Plugin Delay Compensation (PDC) framework · P1 · effort L ✅ DONE 2026-06-08 (+ REC-9 folded in)

**Why here:** real DAWs auto-report per-plugin latency and delay-align all
tracks so the mix stays phase-coherent. Euterpe's only current latency sources
are the oversampler FIRs (Waveshaper/Saturator) — small, but the _framework_ is
the gap.

> **Implementation note:** the PDC ring is a single **MONO** delay applied to
> the post-insert block **before the channel strip** (not the proposed
> `pdc_l/pdc_r` post-strip stereo pair). Delaying the mono pre-strip signal
> uniformly delays the main bus output AND every post/pre-fader send AND the key
> bus — i.e. the "full correctness" option (sends delayed too), achieved with
> one ring instead of stereo rings + separate send handling. REC-9's manual
> nudge is folded in.

- [x] **Latency reporting (foundation, zero audio change):**
  - [x] `dsp-core`: expose `Waveshaper::latency()` + `Saturator::latency()` =
        `if mix==0 {0} else {self.oversampler.latency()}` (honest about the
        zero-latency bypass/off paths). Verified by a normalized
        cross-correlation delay-detector (gain-invariant — a raw sample compare
        fails because unity-drive waveshaper has 1.31× inherent gain).
  - [x] `dsp-graph/effect.rs`: added `fn latency(&self) -> usize { 0 }` to
        `AudioEffect`; overridden on `WaveshaperNode`/`SaturatorNode` →
        `self.<inner>.latency()`. (All other nodes inherit 0.)
  - [x] `dsp-graph/track.rs`: `pub fn insert_latency(&self) -> usize` = sum of
        `latency()` over **non-bypassed** inserts.
- [x] **Compensation (hot path — guard the no-op):**
  - [x] `Track`: per-track mono PDC delay ring (`pdc_ring: Vec<f32>`, `pdc_pos`,
        `pdc_len`) + `set_pdc_delay(samples)`. `pdc_len==0` → ring skipped
        entirely → **bit-identical** (no-op invariant tested); `set_pdc_delay`
        reallocs only on size change (`==pdc_len` guard → no per-block alloc in
        steady state); `reset_state` clears the ring (bounce determinism).
  - [x] In `Track::process`, the post-insert mono block runs through the PDC
        ring **before the strip**, so the main output + ALL sends + key bus
        inherit the same delay (full correctness, one ring).
  - [x] `dsp-graph/engine.rs`: each `process()` block computes
        `base = max insert_latency`; per track
        `delay = base + (nudge − min_nudge) − insert_latency` (raises the
        reference by the most-negative nudge so no realized delay is < 0);
        recomputed every block so a bypass toggle re-aligns next block.
  - [x] `pub fn reported_latency(&self) -> usize` on the engine (= base) for the
        "PDC: N samples" readout.
  - [x] **REC-9 manual track-delay nudge** folded in:
        `Track::set_pdc_nudge`/`pdc_nudge` + engine
        `set_track_delay_samples(i64)`/`track_delay_samples`.
- [x] **Bridge + UI:** dsp-wasm `engine_latency_samples()` +
      `set/track_delay_samples` (i32 — i64→JS BigInt breaks the number
      boundary); meter frame carries `latencySamples`; EngineCommand
      `setTrackDelay`; audio-engine.ts bridge; transport-bar shows "PDC N smp";
      channel-strip "Track Delay" ±2400-sample nudge slider.
- [x] **Tests:** cargo — clean + 4× distortion stems stay phase-aligned within 1
      sample (normalized cross-correlation); `pdc_delay(0)` is bit-identical
      (no-op invariant); `insert_latency` sums only non-bypassed; per-track
      delay math + REC-9 nudge. Node-boundary `engine_latency_samples()` returns
      16 for a 4× distortion + nudge round-trip. jsdom — transport-bar PDC
      readout + channel-strip nudge slider dispatch. (cargo dsp-core 148 +
      dsp-graph 106 + dsp-wasm 49; app 1188 green; clippy clean; wasm
      rebuilt+synced.)
- **Acceptance:** ✅ two tracks with differing insert latency stay phase-aligned
  at the master; 0-latency sessions are bit-identical; the engine reports its
  added latency.
- **Risk/blast radius:** MEDIUM. Touches every track's output path. Mitigation =
  the `pdc_len==0` byte-identical fast path (all existing scheduler tests have
  no oversampled inserts → unaffected). Dynamic-bypass ring resize is the subtle
  part — test bypass-toggle mid-playback. NB: REC-9 (PDC + track-delay nudge) is
  the same feature + a manual per-track delay offset — folded in.

### 2.2 `[x]` ENG-2 — Sample-accurate / sub-block automation · P2 · effort M ✅ DONE 2026-06-08

**Why here:** automation is currently evaluated **once per process block**
(block-granular). Audio-rate filter sweeps and fast moves zipper. Pro DAWs
render automation per-sample (or in fine sub-block slices).

- [x] Identified all sites: `track.rs` volume/pan/cutoff/send/delay-send +
      per-insert-param; `engine.rs` master gain/ceiling/width.
- [x] Granularity: **CTRL = 16-sample sub-blocks** (matches the synth voice's
      internal control rate).
- [x] `track.process` refactored into a dispatch:
      `has_active_automation() && playing` → a `while` loop over CTRL slices,
      each calling `apply_automation(beat)` then `process_slice(start,end)`
      (extracted render→inserts→PDC→strip); else a single full-width slice =
      **byte-identical** to before (no-op invariant). `engine.process` master
      loop re-evaluates `apply_master_automation(beat)` at each `i % CTRL == 0`
      while playing; else once per block. All nodes are per-sample streaming so
      slicing is bit-identical when params don't change; the one block-rate node
      (auto-wah envelope filter) simply tracks at the slice rate when a track
      _also_ has automation (finer = the intended sub-block behaviour, no
      regression — the fast path sees one full slice).
- [x] **Tests:** cargo — `constant_automation_equals_the_static_parameter`
      (no-op invariant: constant automation renders bit-identically to the
      static fader); `sub_block_automation_ramps_gain_within_a_single_block` (a
      steep ramp climbs monotonically across CTRL slices — block-granular would
      be flat); `master_gain_automation_is_evaluated_sub_block` (a master fade
      reaches full within the block). All 92 scheduler + automation tests still
      green (148 dsp-core + 109 dsp-graph + 49 dsp-wasm; clippy clean; wasm
      rebuilt+synced). No wasm/app API change.
- **Acceptance:** ✅ audio-rate cutoff/volume/master sweeps are sub-block-smooth
  (CTRL-rate); sessions without automation are bit-identical.
- **Risk:** MEDIUM-HIGH. Touches the hot loop + 92 scheduler tests. Mitigated as
  planned: the no-op invariant test was written FIRST, the
  empty/stopped-automation fast path is byte-identical (one full-width slice),
  and all 92 scheduler + automation tests stayed green through the refactor.

### 2.3 `[~]` ENG-14 — Selectable FFT window / size / overlap for spectral metering · P3 · effort M ✅ CORE+UI DONE 2026-06-08 (live-visual = browser pass)

**Why here:** the live spectrum uses a WebAudio `AnalyserNode` whose window is
FIXED (Blackman) and only `fftSize` is settable. Pro analyzers expose window
TYPE + size + overlap. Requires the app's OWN STFT render (tap master output →
window → FFT).

- [x] New `apps/euterpe-studio-web/src/daw/spectral.ts` (reuses `fft.ts`):
      `windowSample`/`makeWindow` + `coherentGain` for
      Rectangular/Hann/Hamming/Blackman/Blackman–Harris (periodic form → clean
      coherent gains), `windowedSpectrum` (amplitude-corrected: a bin-centred
      unit sine reads ≈1.0 for ANY window via 2/Σwindow), `binFrequency`,
      `dominantBin`, `leakageEnergy`, `spectrumBars` (log-spaced dB display
      reducer).
- [x] STFT analyzer wired into `SpectrumView`: when a `window` prop is set it
      taps `analyser.getFloatTimeDomainData` (the documented copy approach) →
      `windowedSpectrum` → `spectrumBars`, replacing the fixed-window
      `getByteFrequencyData` path (kept as the fallback). FFT size drives
      `analyser.fftSize`.
- [x] UI: `SpectrumPanel` adds window-type + FFT-size selectors (window TYPE is
      the thing the AnalyserNode can't do), persisted UI-local to localStorage.
      (Overlap is inherent in the animation-frame re-tap — documented, not a
      separate selector.)
- [x] **Tests:** 9 pure — coherent gains == known values
      (1.0/0.5/0.54/0.42/0.35875); a bin-centred sine reads ≈1.0 + dominant bin
      correct for every window; Blackman–Harris leaks <¼ of rectangular for an
      off-bin tone; `binFrequency` correct; `spectrumBars` floors silence +
      peaks on the tone. 4 jsdom — window/size selectors default, reflect,
      persist, restore. (220 component tests green; app tsc 0.)
- **Acceptance:** ✅ window/size selectable via the own-STFT (changes
  resolution/leakage — verified numerically); spectrum reads correct bin
  frequencies. ⏳ live-canvas _visual_ confirmation = browser pass (no browser
  MCP here); the render path is unchanged from the shipped fixed-window
  spectrum, only its source bars changed. **REMAINING:** browser-visual
  acceptance pass.
- **Risk:** LOW-MEDIUM. Mostly additive (new STFT path); the tap from the
  worklet output is the integration point to design carefully (a meter SAB or a
  periodic `getFloatTimeDomainData` copy).

### 2.4 `[x]` ENG-9 / IO-N2 — Élastique/Rubber-Band-class elastic time-stretch + warp markers · P2 · effort L ✅ DONE 2026-06-08 (both halves)

**Why here:** WSOLA stretch (`warpSample`) exists; ENG-9 wants
formant-preserving, transient-aware, pitch-independent stretch
(élastique/Rubber-Band quality) + per-clip warp markers (ARR-5/IO-N2:
non-destructive grid-following stretch).

- [x] **Élastique-class stretch quality (DONE 2026-06-08):**
      `dsp-core/timestretch.rs` already had transient-locked WSOLA; added a
      from-scratch **offline phase vocoder** (per-bin instantaneous-frequency
      phase propagation on the existing `fft.rs`, Hermitian-mirrored, unity-gain
      weighted-overlap-add) + a **quality flag**
      `StretchMode::{Beats,Tones,Complex}` — Beats = WSOLA (the default,
      **bit-identical to before**, no-op-invariant tested), Tones = phase
      vocoder (sustained/tonal), Complex = phase vocoder + **transient
      phase-reset** at detected onsets (crisp attacks + smooth sustains).
      `time_stretch_mode` dispatches; `time_stretch` delegates to Beats.
      **Formant preservation** for pitch shift: a **cepstral spectral envelope**
      captured from the source is re-imposed on the shifted buffer per STFT
      frame (`env_orig/env_shift`), so harmonics move but resonances stay (no
      chipmunk) — `pitch_shift_mode(.., mode, preserve_formants)`; `pitch_shift`
      delegates. Wired end-to-end: dsp-wasm
      `time_stretch_buffer_mode`/`pitch_shift_buffer_mode` → worklet
      `loadSampleWarped` → audio-engine-web
      `loadSampleWarped(.., mode, preserveFormants)` + `StretchMode` type →
      daw-app `warpSample` → sampler-panel engine selector + "preserve formants"
      toggle. **Tests:** cargo dsp-core 174 / dsp-graph 109 / dsp-wasm 50
      (Beats==WSOLA bit-identical; PV length/pitch/level; Complex sharper
      transients than Tones; formant preservation restores the spectral centroid
      while pitch still doubles), clippy clean; compiled-wasm boundary spec 32
      (mode + formant fns on the rebuilt artifact); app tsc 0, sampler-panel 15,
      full daw suite 1339. (commit
      `feat(euterpe): phase-vocoder stretch modes + formant-preserving pitch shift`.)
- [x] **Warp markers (DONE 2026-06-08):** implemented as a **non-destructive
      buffer re-render** rather than a per-clip content-read (the engine's
      track-owns-one-`Sampler` model makes a re-render the clean fit).
      `dsp-core/timestretch.rs`
      `warp_to_markers(input, src_beats, dst_beats, spb, mode)` builds
      (source-sample, target-sample) anchors, implies a `(0,0)` start + an
      unwarped tail, and pitch-preserving-stretches each inter-anchor segment by
      its local ratio via `time_stretch_mode`. **No-op invariant:** no anchors →
      input unchanged; an identity map is byte-identical (unit-ratio segments
      are copied).
- [x] **Bridge + reducer (DONE):** dsp-wasm `warp_buffer_markers` → worklet
      `loadSampleWarpMarkers` → audio-engine-web
      `loadSampleWarpMarkers(.., samplesPerBeat, mode)` → daw-app
      `warpSampleMarkers` (renders from the **retained original** buffer, so
      re-applying replaces not compounds) → sampler-panel **warp-markers
      editor** (add / seed / snap-to-grid / edit src→dst / remove / apply /
      reset; Apply gated on `warpMarkersAreActive`). New pure
      `daw/warp-markers.ts` (`sanitizeWarpMarkers` sorts + drops
      zero-length/invalid + clamps targets monotonic, `snapMarkersToGrid`,
      `evenWarpMarkers`, `warpMarkersAreActive`).
- [x] **Tests (DONE):** cargo — warp no-op invariants + a region-doubling test
      via energy centre-of-mass (robust to WSOLA grain overlap; clicks proved
      too pathological for exact-peak assertions); compiled-wasm boundary spec
      re-times a burst to the right beat; warp-markers pure spec 6;
      sampler-panel jsdom (add/edit/apply, seed/reset, omit-without-handler).
      cargo dsp-core 176 / dsp-graph 109 / dsp-wasm 50; app tsc 0; full daw
      suite 1348. (commit
      `feat(euterpe): warp markers — grid-following … IO-N2`.)
- **Acceptance:** ✅ a stretched drum loop keeps transient definition
  (Beats/WSOLA transient lock); ✅ warp markers grid-lock content
  non-destructively (original retained, re-rendered on apply). ⏳ **REMAINING
  (deeper follow-ups):** markers are runtime-local (samples aren't persisted in
  the session, like the existing warp/velocity layers); a **waveform
  drag-to-place** overlay, **per-clip** warp markers, and **true realtime
  streaming-warp** (vs. buffer re-render) are the documented next steps.
- **Risk:** MEDIUM. Phase-vocoder quality is hard to verify objectively — assert
  measurable proxies (transient sharpness, fundamental preservation), not
  "sounds good".

### 2.5 `[x]` FX-9s — Spectral / STFT effects (vocoder, spectral gate, spectral delay/freeze) · P2 · effort L ✅ COMPLETE 2026-06-08 (STFT framework + gate + freeze + spectral-delay + VOCODER)

**Why here:** needs a real STFT engine (overlap-add framing) inside an insert
node — a new DSP subsystem, not a per-sample fx.

- [x] Built the STFT framework in `dsp-core`: new dependency-free `fft.rs`
      (radix-2 FFT/IFFT — round-trip identity + sine-peak tests) + `stft.rs`
      `Stft` (streaming weighted-overlap-add, periodic Hann, 75 % overlap = hop
      size/4, COLA-normalized → identity callback reconstructs the input
      exactly, verified by cross-correlation at the reported latency = size−1).
      Reusable by future spectral fx / ENG-14 / AI mastering.
- [x] FOUR spectral nodes shipped: `SpectralGate` (per-bin dBFS threshold, STFT
      de-noiser) + `SpectralFreeze` (capture-and-hold the spectrum with per-bin
      natural phase advance → a sustained drone) + `SpectralDelay` (per-bin
      **frequency-dependent** delay: highs lag lows by up to 24 hops × `spread`
      → a spectral smear impossible with a time-domain delay; dry/wet `mix`) +
      **`SpectralVocoder`** (FX-9s final piece, 2026-06-08): a **self-carrier
      channel vocoder** — averages the modulator's magnitude into `bands`
      frequency bands (a coarse formant envelope that smooths away the pitch
      harmonics) and imposes that envelope on an internally-synthesised
      broadband **noise carrier** (a fresh deterministic per-bin random phase
      each frame, Hermitian-mirrored → real out) → whisperization that keeps the
      vowel's formant colour; dry/wet `mix`. Single-input design that fits the
      mono insert model — the classic two-input modulator+carrier vocoder is a
      sidechain-keyed enhancement (needs the sidechain KEY BLOCK, currently only
      a scalar `sidechain_level` in ProcessContext → an engine-wide lifetime
      change, deferred). All → `*Node` impls with `latency()` → the framework
      latency so ENG-3 PDC delay-aligns them.
- [x] Insert-rack wiring per the recipe for all four (dsp-wasm add/set;
      messages; worklet;
      `InsertKind 'spectralgate'`/`'spectralfreeze'`/`'spectraldelay'`/`'spectralvocoder'`;
      reducer build + setInsertParams; session-rebuild; insert-rack KINDS 'Spec
      Gate'/'Freeze'/'Spec Dly'/'Vocoder' + PARAM_CONFIG: gate=dBFS threshold,
      freeze=Hold toggle, delay=spread+mix, vocoder=bands+mix). wasm
      rebuilt+synced.
- [x] **Two-input (sidechain-keyed) channel vocoder DONE 2026-06-09 — the
      documented enhancement, the "deferred" note below is now CLOSED.**
      `dsp-core/stft.rs` `ChannelVocoder` — **two synchronized STFTs**: the
      modulator's per-band magnitude envelope is whitened off the carrier and
      re-imposed (the carrier keeps its pitch but speaks the modulator's vowels
      — robot voice/talkbox), `bands` + dry/wet `mix`. **Avoided the engine-wide
      ProcessContext lifetime ripple** the note feared: the modulator reaches
      the insert via an **additive, defaulted** `AudioEffect` seam
      (`feed_sidechain(&key)` + `wants_sidechain()`, no-op for all existing
      nodes), so no `&ProcessContext` site changed and the 142 prior dsp-graph
      tests stay byte-identical. The engine snapshots the **key bus** to mono
      and feeds it to a track's vocoder before processing, gated on
      `wants_sidechain()`. The modulator is the existing MIX-27 key bus (pick a
      key source in the master panel); without one the vocoder is silent at full
      wet (a true vocoder needs a modulator — honest).
      `dsp-graph SidechainVocoderNode` (carrier = insert input, modulator = fed
      key) → `dsp-wasm add_sidechain_vocoder`/`set_sidechain_vocoder_params` →
      worklet → `InsertKind 'sidechainvocoder'` → reducer/ session-rebuild →
      insert-rack **"Robot Voc"** button. **Tests:** dsp-core 218 (+2
      ChannelVocoder), dsp-graph 148 (+1 engine: silent w/o modulator, sounds
      when the key bus feeds it), dsp-wasm 55 (+1 binding), WASM-boundary 56 (+1
      real-wasm via the key bus), app reducer +3, full src/daw+components
      **2058**; clippy clean. (commits
      `two-input channel vocoder — engine + wasm` +
      `two-input vocoder insert UI`.) Remaining polish: a dedicated per-vocoder
      modulator selector (it currently shares the master-comp key bus).
- [x] **Tests:** cargo — STFT identity reconstruction (delay == latency());
      reset determinism; gate removes a quiet tone / passes a loud one; freeze
      sustains through silence + holds pitch + passes un-frozen; spectral delay
      smears highs later than lows (output centroid rises over time) + dry-mix
      == the bare framework; **vocoder: mix=0 == the bare framework, the
      noise-carrier output's spectral centroid tracks the modulator's formant
      (low→low, high→high), reset-deterministic, reports framework latency**;
      FFT round-trip + sine-peak. Node-boundary — gate reports 511, **vocoder
      reports 1023 latency across the rebuilt WASM boundary**. jsdom — reducer
      add/edit + rebuild for all four. (164 dsp-core + 109 dsp-graph + 49
      dsp-wasm cargo; 1320 daw; clippy clean [only the pre-existing
      Source-variant-size warning]; tsc 0.)
- **Acceptance:** ✅ gate de-noises, freeze sustains, spectral-delay smears by
  frequency, **vocoder imposes the modulator's formant on a noise carrier**;
  STFT reconstruction is exact (COLA).
- **Risk:** MEDIUM-HIGH. New subsystem + latency — done after ENG-3, so each
  node PDC-aligns automatically. The two-input/sidechain-keyed vocoder (the one
  enhancement) is now **also shipped** (2026-06-09) — via an additive
  `feed_sidechain` AudioEffect seam, so it needed NO ProcessContext key-block
  refactor. FX-9s is complete.

### 2.6 `[x]` ENG-N2 / ENG-11 — WASM SIMD vectorization + large-session profiling · P2 · effort M-L ✅ DONE 2026-06-09

**Why here:** the DSP hot path is scalar; the 2025-26 web-DAW pattern compiles
to WASM SIMD128 (4-wide). ENG-11 = profile + SIMD-sum the master/bus mix for
many-track sessions.

- [x] **Portable `F32x4` abstraction (DONE 2026-06-09):**
      `libs/euterpe/audio-engine/crates/dsp-graph/src/simd.rs` — a 4-wide f32
      vector with two `cfg`-selected backends: on `wasm32`+`simd128` it lowers
      to `core::arch::wasm32` `v128` ops (`f32x4_add/mul/abs/max`, safe
      `f32x4()` lane-constructor load + `extract_lane` store — **no raw-pointer
      `v128_load`**, so the crate's `#![forbid(unsafe_code)]` holds); on every
      other target (incl. the native `cargo test` host) a `[f32; 4]` fallback
      the autovectorizer still lowers to NEON/SSE. Only add/mul/abs/max are used
      — **no FMA** — and `mac` does an explicit mul-then-add, so the two
      backends are **bit-identical** for the finite inputs the mixer sees (the
      no-op invariant).
- [x] **Vectorized the engine.rs hot loops (DONE 2026-06-09):** (1) the
      **group-bus → master mixdown** — the canonical
      `bus[i] += group_bus[k][i] * g` multiply-accumulate, now
      `crate::simd::mac` (per bus, per channel, per block); (2) the **per-track
      sidechain-key detector** — `max_i max(|key_l[i]|, |key_r[i]|)`, now
      `crate::simd::abs_peak` (runs **once per track per block**, so it scales
      with track count — the bigger structural win). Both are drop-in over
      disjoint engine fields; all 116 prior dsp-graph engine tests stay green
      (the no-op invariant) + 7 new simd-kernel tests (bit-exact `assert_eq` vs
      a scalar oracle for **every length 0..=64** so each remainder size is
      covered, a 4096-sample mix within 1e-6, shorter-slice + empty edge cases,
      abs-peak across both channels).
- [x] **`+simd128` enabled in the wasm build (DONE 2026-06-09):** new
      `libs/euterpe/audio-engine/.cargo/config.toml`
      (`[target.wasm32-unknown-unknown] rustflags = ["-C","target-feature=+simd128"]`,
      scoped to wasm so native tests are untouched) + `--enable-simd` added to
      the `dsp-wasm` wasm-opt flags. **Verified the shipped artifact actually
      contains SIMD:** an opcode-byte scan of `wasm/dsp_engine_bg.wasm` finds
      real `f32x4.abs`×2 + `f32x4.max`×2 (the `abs_peak` kernel),
      `f32x4.add`×14 + `f32x4.mul`×74 (the `mac` kernel + autovectorized loops);
      module validates. The **runtime feature check** is
      `libs/euterpe/audio-engine-web/src/wasm-simd.ts` `wasmSimdSupported()`
      (the canonical 31-byte `() -> v128` probe via `WebAssembly.validate`,
      never throws) + `wasmSimdCapability()` — a fail-loud capability gate (5
      tests incl. mocked unavailable/throwing hosts). **Honest scope:** one SIMD
      binary ships (simd128 is baseline since Chrome 91 / FF 89 / Safari 16.4);
      the `cfg` scalar **source** fallback exists, but a _dual-binary_ runtime
      auto-switch (ship engine-simd + engine-scalar, pick in the loader) is the
      documented optional tail — unnecessary at current browser support, where
      the check fails loud instead.
- [x] **Large-session bench + cpuLoad (DONE 2026-06-09):** a cargo
      `large_session(n)` helper (¾ synth / ¼ silent tracks round-robin across
      the 4 group buses with non-unity trims + a sidechain key) backs two tests
      — `large_64_track_session_ renders_finite_and_audible` (the checklist's
      64-track acceptance: every sample finite + the mix audible) and an
      `#[ignore]`'d `large_session_bench_reports_realtime_ratio` (prints the
      real-time headroom; **measured: 10 s of 64-track audio in 3.86 s
      native-release = 2.6× real-time, 38.6 % of one core**). The existing
      **cpuLoad meter** (worklet process-time ÷ budget → MeterSnapshot → the
      transport-bar `CPU N%`) already surfaces this headroom for any session
      size; the SIMD work lowers that number directly — no new meter wiring
      needed.
- [x] **Tests:** cargo — SIMD vs scalar bit-identical for every length + within
      1e-6 on a 4096 random mix; the 64-track render completes + is finite
      (dsp-core 185 / dsp-graph 117 +1 ignored bench / dsp-wasm 50, clippy clean
      bar the pre-existing enum-size note). WASM boundary — a **32-track / 4-bus
      / sidechain-key** session renders finite + audible through the _real
      simd128 wasm_ (`wasm-engine.spec.ts`, 36 tests) + `wasm-simd.spec.ts` (5).
      App tsc 0; full audio-engine-web suite 77 green.
- **Acceptance:** ✅ measurable headroom on a 64-track session (2.6× real-time
  native; the cpuLoad meter reflects the wasm gain); ✅ output unchanged within
  fp tolerance (bit-identical kernels, all 116 engine tests + 36 boundary tests
  green through the SIMD wasm). ⏳ a _dual-binary_ scalar-fallback build is the
  only documented (optional) tail.
- **Risk:** MEDIUM — discharged. The scalar-equivalence test is bit-exact (not
  merely 1e-6), the real v128 artifact is verified present, and the no-op
  invariant holds end-to-end through both the native fallback and the compiled
  wasm.

### 2.7 `[x]` ENG-N3 / MIX-28 / AI-20 — Immersive / surround / Dolby Atmos output + ADM authoring · P1/P2 · effort XL ✅ DONE 2026-06-09 (parallel path, 3 increments)

**Why here:** the engine is stereo end-to-end (`track.rs:1` "mono source →
stereo via pan"). Surround/Atmos = a multi-channel bus architecture + object
panning + ADM BWF export (IO-N6). Built the checklist's recommended **parallel
multi-channel path**: the realtime engine stays stereo (all ~190 engine tests
untouched), surround is an _offline_ render + export capability.

- [x] **N-channel bed + 3D panner (Increment 1/3, DONE 2026-06-09):**
      `dsp-graph/src/surround.rs` — `SpeakerLayout` (5.1/7.1/7.1.4 with real
      ITU/Dolby azimuth/elevation positions, channel order
      `L R C LFE … surrounds … tops`), a **proximity-on-the-sphere
      constant-power 3D panner** (`pan_gains(az, el, spread)`: object + speakers
      as unit vectors, weight `(½+½·cos θ)^sharpness`, LFE excluded, Σ gain² = 1
      so loudness is constant as the object moves; `spread` widens the lobe),
      `render_object` (composite a mono object into the N-channel bed), ITU-R
      **BS.775 stereo fold-down**, and a documented **spherical-head ITD/ILD
      binaural** monitor (Woodworth ITD + head-shadow ILD — a real named model;
      true measured-HRTF is the external tail). Exposed via dsp-wasm free fns
      (`surround_pan_gains`/`channel_count`/`channel_labels`/`downmix_stereo`/
      `binaural_stereo`). **8 cargo tests** (hard-rear→rear, −90°→Lss,
      overhead→tops, constant-power+LFE-silent, spread, bed accumulation, BS.775
      centre/hard-left, binaural-left-louder-and-earlier) + 1 dsp-wasm binding +
      1 WASM-boundary.
- [x] **ADM BWF writer (Increment 2/3, DONE 2026-06-09):**
      `audio-engine-web/src/adm-bwf.ts` — a self-contained ADM BWF:
      `encodeAdmBwf` frames a multi-channel RIFF/WAVE with `fmt`+`bext` (EBU R98
      602-byte ext)+`chna` (BS.2088 channel allocation, one
      audioTrackUID/track→track+pack format)+`axml` (BS.2076
      `audioFormatExtended`)+`data`. `buildAdmXml` emits a fully
      self-referential ADM
      (audioProgramme→audioContent→audioObject→audioPackFormat→audioChannelFormat[+audioBlockFormat
      speaker position, **inline** so no BS.2094 common-defs
      dependency]→stream/track format→audioTrackUID); azimuth negated into ADM's
      anticlockwise convention; LFE gets a lowPass; **D/M/E/N stem groups** →
      distinct objects/contents/packs with the right `<dialogue>` value. **8
      tests** (XML parsed with @xmldom: well-formed, one
      channelFormat+trackUID/channel, azimuth negation, **full IDRef referential
      integrity**, D/M/E/N + dialogue; binary: RIFF/chunk order+sizes, 602-byte
      bext, chna records, channel-count fail-loud, axml round-trip).
- [x] **Per-track 3D position + export wiring (Increment 3/3, DONE
      2026-06-09):** `audio-engine-web/src/surround-pan.ts` — a main-thread TS
      port of the panner (the wasm runs in the AudioWorklet, out of reach for
      the offline mixdown), held bit-close to the Rust original by a **boundary
      cross-check** (TS gains vs the compiled `surround_pan_gains` within 1e-5);
      `renderSurroundBed` composites per-track mono objects. App:
      `TrackState.pan3d {azimuth,elevation,spread}` (display-only, default
      centre) + a `setTrackPan3d` reducer (clamp + azimuth wrap, **no engine
      command** → session-rebuild untouched, the key-signature pattern),
      persisted free via project-io. A **"Surround (3D)" control** in the
      channel strip + an **"Export Immersive Surround (ADM BWF, 7.1.4)"**
      command that renders each audible track's stem → mono → positions it via
      the panner → `encodeAdmBwf`. 5 surround-pan + cross-check +
      reducer/project-io/channel-strip/command tests.
- [x] **Tests:** cargo — a hard-rear object routes energy to the rear channels
      (✅) + downmix/binaural; ADM chunk structure validates (✅, @xmldom +
      binary RIFF walk). dsp-core 185 / dsp-graph 125(+1 ignored) / dsp-wasm 51;
      WASM boundary 38; audio-engine-web 91; app tsc 0; full
      daw+components 1722.
- **Acceptance:** ✅ a 7.1.4 session renders to a valid (self-contained) ADM BWF
  — `fmt/bext/chna/axml/data`, referentially consistent ADM; ✅ binaural monitor
  is plausible (spherical-head ITD/ILD — lateralisation cues verified). **Honest
  scope:** the deliverable is a **channel-based 7.1.4 bed** (tracks baked into
  the bed channels via the panner) — the standard immersive master; _dynamic_
  ADM objects (each track a live `audioObject` with time-varying position
  blocks) + measured-HRTF binaural + realtime surround monitoring are the
  documented advanced follow-ups.
- **Risk:** HIGH/XL — discharged via the parallel path: the stereo core + its
  ~190 tests are byte-untouched (no channel-count refactor of the hot loop),
  surround is an additive offline capability with its own cargo + boundary +
  jsdom verification.

### 2.8 `[~]` ENG-7 / ENG-N1 — OPFS-streamed sample playback + SharedArrayBuffer lock-free transport · P1 · effort L ✅ SAB RING DONE 2026-06-08 (OPFS stream + headers = browser tail)

**Why here:** samples are RAM-resident; SOTA streams from OPFS with a small
prebuffer, and uses SAB ring buffers (needs COOP/COEP cross-origin isolation
headers). Partly browser-infra (overlaps bucket B), but the ring-buffer +
prebuffer logic is local + testable.

- [x] **COOP/COEP cross-origin-isolation DONE 2026-06-09:** `next.config.mjs`
      serves `Cross-Origin-Opener-Policy: same-origin` +
      `Cross-Origin-Embedder-Policy: require-corp` for the browser build (Tauri
      shell serves the same pair), enabling `SharedArrayBuffer` (also unblocks
      PLAT-5/COLLAB-19). Backed by a **canonical, tested policy module**
      `libs/euterpe/audio-engine-web/src/cross-origin-isolation.ts` —
      `crossOriginIsolationHeaderEntries()` (the values next.config mirrors,
      asserted by a test so drift is caught),
      `isCrossOriginIsolated`/`sharedArrayBufferConstructible`/
      `canUseSharedArrayBuffer` capability predicates, and a typed
      `CrossOriginIsolationError` fail-loud seam. `SabRing.create` now guards on
      `SabRing.isSupported()` → throws the typed error (not an opaque
      `SharedArrayBuffer is not defined`) when a tab isn't isolated. 8 tests.
      **(Remaining browser tail: confirm the live app still loads under
      `require-corp` with no blocked cross-origin subresource — switch to
      `credentialless` (also exported) if a third-party CDN asset lacks CORP.)**
- [x] **Streaming scheduler core DONE 2026-06-09:**
      `libs/euterpe/audio-engine-web/src/streaming-sampler.ts`
      `SampleStreamScheduler` — the deterministic playback _policy_ for
      OPFS-streamed samples: keeps a `prebufferFrames` window ahead of the
      playhead, `planNextFetch(ringFree)` returns the next
      `{sourceStart, length}` to read (capped by chunk granularity, ring
      backpressure, and source/loop end), `onFetched`/`onConsumed` advance the
      source + playhead cursors, with **loop wrap-around** (starts at
      loop.start, wraps at loop.end, never ends) and **one-shot EOF + drain**
      (partial final chunk, `isEnded` only after source exhausted AND buffer
      drained). + `frameByteOffset` for the read offset. 9 Node tests (prebuffer
      fill / backpressure / consume-resumes / partial-final / end-after-drain /
      zero-length / loop-wrap / loop-never-ends). Feeds the `SabRing` (2.8
      above).
- [ ] **OPFS read adapter (REMAINS — browser tail):** open a
      `FileSystemSyncAccessHandle`, on each `planNextFetch()` `read()` `length`
      frames at `frameByteOffset(sourceStart,…)`, push into the ring, call
      `onFetched`; the worklet `pull`s + reports `onConsumed`. Pure plumbing
      around the tested scheduler, but OPFS + the worklet read side need a real
      browser to verify. _Re-read 2026-09-18: "needs a real browser" is not a
      blocker. Headless Chromium under Playwright exposes OPFS and
      `FileSystemSyncAccessHandle` inside a dedicated worker, and the repository
      already drives Chromium that way. Write the adapter and verify it in a
      Playwright chromium spec that writes a known sample file to OPFS, streams
      it through the ring and compares the frames the worklet pulled with the
      source._
- [x] **SAB lock-free ring DONE 2026-06-08:**
      `libs/euterpe/audio-engine-web/src/sab-ring.ts` `SabRing` — a single-
      producer/single-consumer f32 ring over a `SharedArrayBuffer`, lock-free
      via `Atomics` on the read/write cursors only (the acquire/release ordering
      SPSC needs, no mutex); one slot kept empty so full/empty are
      distinguishable and the cursors never overflow; partial `push`/`pull`
      report their count (honest underrun); `pullOrSilence` zero-fills a short
      block; `create`/`wrap` split producer + consumer over the same SAB. **8
      Node tests** — accounting, in-order round-trip, partial-push-near-full (no
      overwrite), wrap-around, honest-underrun→silence, producer/consumer cursor
      visibility across two wrappers, a 1000-sample stream through a 64-slot
      ring (exact order, no loss/dupes), clear. (commit
      `feat(euterpe): lock-free SPSC … ring buffer`.)
- [x] **Tests (ring):** pure ring-buffer produce/consume across wrap +
      underrun→silence-not-garbage — DONE (Node). ⏳ Browser pass for the actual
      OPFS stream remains (documented).
- **Acceptance:** a long sample plays from OPFS without a full RAM load; no
  audible underruns at moderate buffer sizes.
- **Risk:** MEDIUM; the COOP/COEP header change affects the whole app (test the
  app still loads cross-origin-isolated).

---

## 3. BUCKET B — Local infrastructure (buildable, heavier, some need a browser pass)

> In-sandbox-codeable but each is a substantial subsystem or needs
> browser/visual verification (no browser MCP here) or a new dependency. Build
> the pure/testable core fully; document the browser-only acceptance check
> honestly.

### 3.1 `[~]` MIDI-2 / UX-27 / IO-N7 / PLAT-N3 — Notation / score editor + MusicXML round-trip · P1/P2/P3 · effort L-XL ✅ PURE INTEROP + STATIC SCORE RENDER DONE 2026-06-08

- [x] Pure `notation.ts`: `NotationNote` model ↔ MusicXML;
      `midiToPitch`/`pitchToMidi` (sharp spelling, C4 = middle C), `noteValue`
      (plain/dotted/triplet/irregular-fallback), `quantizeNotes(grid)` (snaps a
      swung performance, never collapses a note to 0).
- [x] MusicXML 4.0 export (`notesToMusicXml`) + import (`musicXmlToNotes`,
      DOMParser): measure-splitting by time sig, rests for gaps, chords (shared
      onset via `<chord/>`), barline-crossing notes split into tied segments +
      merged back on import. Timing authoritative via `<duration>`/`<divisions>`
      so it round-trips exactly for grid-clean durations. Wired into the app:
      TransportBar ⤓ XML / ⤒ XML buttons + a `project.exportMusicXml` palette
      command (export the selected track's note clip; import a `.musicxml` file
      as a new note-clip track).
- [x] **Static score render (DONE 2026-06-08):** pure engraving geometry
      `apps/euterpe-studio-web/src/daw/score-layout.ts` — `diatonicStep`
      (octave×7 + letter; sharps share their natural's line), `accidentalFor`,
      `stemUp` (B4-and-above ⇒ down), `glyphFor` (head fill / stem / flags /
      dots from `notation.noteValue`), `ledgerSteps` (middle C gets its ledger;
      the space just below the staff gets none), `noteY`/`stepToY` (higher pitch
      ⇒ smaller y; top line F5 at `topPad`), and `layoutScore` (notes positioned
      by beat, barlines per `beatsPerBar`, length rounded up to whole bars).
      Rendered by `components/daw/score-view.tsx` `ScoreView` — an SVG **single
      treble staff**: 5 staff lines, clef, barlines, note heads (filled/open),
      stems (up/down), un-beamed flags, accidentals, ledger lines, augmentation
      dots. Wired: a **"♪ Score" toggle** in the piano-roll (parallel to the
      PLAT-9 "⌨ Accessible grid"), showing the engraved view of the selected
      synth track's clip (`beatsPerBar` threaded from the session). **Tests:**
      12 pure (staff steps incl. C#4-shares-C4-line, accidentals, stem
      direction, glyph-by-duration, ledger lines for middle C / A3 / high A5 /
      none-in-the-space, vertical monotonicity, layout barlines + bar-rounding +
      per-note placement) + 5 jsdom (5 staff lines + clef + barline-per-bar +
      note count; accidental for a sharp + ledger for C4; eighth flag + open
      half head; multi-bar; empty staff) + 1 piano-roll toggle. App tsc 0; full
      `src/daw`+`src/components/daw` suite 1461 green; stub scan clean.
- [x] **Tests:** 11 pure MusicXML tests + a TransportBar UI-wiring test (app
      tsc 0) — plus the 12+5+1 score-render tests above.
- [x] **Grand staff / bass clef (DONE 2026-06-08):** `score-layout.ts` gains a
      `Clef` type + a bass staff (G2..A3, steps 18–26) and
      `layoutScore(..., clef)` with a `'grand'` mode. The grand staff reuses ONE
      continuous step→y mapping — the 4-step gap between the treble bottom line
      (E4) and the bass top line (A3) is exactly the middle-C gap, so C4 lands
      on its ledger halfway between the staves — and routes each note to its own
      staff (`staffForStep`: treble ≥ middle C, bass below), measuring stem
      direction + ledger lines against THAT staff's lines
      (`stemUpForMiddle`/`ledgerStepsForLines`). The treble default path is
      **byte-identical** (the public `stemUp`/`ledgerSteps` now delegate to the
      parameterized helpers — all 17 prior score tests unchanged). `ScoreView`
      auto-selects the grand staff when any note falls below middle C, rendering
      the bass staff lines + bass clef (𝄢) + a joining brace + full-height
      barlines (override via a `clef` prop). **Tests:** 7 new (staffForStep
      routing; grand bass-line positions + full-height barline span + taller
      height; per-staff stem/ledger for C4/C3/C2; treble no-op) + 3 jsdom (auto
      grand = 10 lines + bass-clef + brace; high-only stays treble; explicit
      override). App tsc 0; full suite 1538 green; stub scan clean.
- [x] **Beaming (DONE 2026-06-08):** `score-layout.ts`
      `computeBeams(layout, notes, metrics)` — real beat-grouped beaming: runs
      of consecutive flagged notes (eighth and shorter) that share a beat, are
      time-contiguous (a rest gap breaks the run), and sit on the same staff
      join under **one slope-clamped beam line** instead of individual flags.
      Stems unify to one direction (the group's mean pitch vs that staff's
      middle line) and extend/retract so every stem ends exactly on the beam
      (with a minimum-stem floor). **Secondary beams** for
      sixteenths-and-shorter: a full segment across each maximal flags≥L
      sub-run, or a short **hook** for an isolated one. Chords (simultaneous
      onsets) + lone flagged notes are not beamed (keep flags) — honestly
      documented. `ScoreView` suppresses the per-note flag + stem for beamed
      notes and draws the group's stems + beam lines
      (`data-testid="beam"`/`beam-stem`/`beam-group`). **Tests:** 13 pure
      (`score-beaming.spec.ts`: beat-grouping into 2-note groups,
      quarter/rest/lone/chord break beaming, full secondary for two 16ths + hook
      for an isolated 16th, low-up/high-down stems, stem-ends-on-the-beam-line,
      flat→horizontal beam, slope clamp, grand-staff no-beam-across-the-split +
      bass-run beams on the bass staff) + 2 jsdom (eighth-run draws beams +
      suppresses flags; a quarter between eighths keeps flags). App tsc 0; full
      `src/daw`+`src/components/daw` 1597 green; stub scan clean.
- [x] **Key signatures (DONE 2026-06-08):** new pure
      `apps/euterpe-studio-web/src/daw/key-signature.ts` — a real
      **line-of-fifths** key model (MusicXML `<fifths>`, −7…+7) with key-aware
      enharmonic spelling: `spellInKey` picks the letter+alteration whose lof is
      closest to the key's diatonic window `[fifths−1, fifths+5]` (tie-broken to
      the key's sharp/flat orientation), so a flat key respells A♯→B♭ **onto a
      different staff line** (`keyAwareStep`), and `accidentalGlyphInKey` prints
      only what the key doesn't already cover (a key-sharped F prints no ♯; an F
      that sounds natural in a sharp key prints a **♮** to cancel).
      `keySignatureSteps` lays the ♯/♭ block on its conventional treble/bass
      staff positions; `MAJOR_KEYS` is the 15-key selector. **No-op invariant:**
      `fifths === 0` delegates to `midiToPitch` (legacy all-sharp spelling) so
      the default score + all prior tests are byte-identical.
      `score-layout.layoutScore`/ `computeBeams` take a `fifths` param
      (default 0) and route placement through the key-aware spelling + reserve a
      key-signature column (zero-width at fifths 0); `score-view` renders the
      ♯/♭ block (both staves on a grand staff) and a **Key selector** sits
      beside the "♪ Score" toggle in the piano-roll (display-only — does not
      alter playback). **Tests:** 21 pure (`key-signature.spec.ts`: catalog,
      `keyAlterForLetter`, spelling incl. the F-natural-in-G case + octave
      naming across an enharmonic boundary, key-aware step shifts A♯/B♭ to
      different lines, accidental suppress/natural/print, key-sig staff steps) +
      6 `score-layout` (no-op default, reserved column shifts notes/barlines,
      accidental suppress/♮, B♭ respelled to a higher line, grand-staff key sig
      on both staves) + 3 `score-view` jsdom (no accidentals in C, ♯ block +
      suppressed per-note accidental, both-staves grand sig) + 1 piano-roll
      selector. App tsc 0; full `src/daw`+`src/components/daw` 1627 green; stub
      scan clean. (TS-only — no engine/wasm change.)
- [x] **MusicXML time-signature + tempo round-trip (DONE 2026-06-08):**
      `musicXmlToNotes` also parses `<time>` (returned as the bar length in
      quarter beats, `beats·4/beat-type`, so 3/4→3 and 6/8→3) and the
      `<sound tempo>` directive, returning `null` for either when the file omits
      it (so a session keeps its own). The import handler dispatches
      `setTimeSignature` + `setTempo` only when present — adopting the file's
      meter + tempo exactly as the **MIDI import already adopts its tempo**
      (consistent precedent). 2 notation tests (3/4 + 90 bpm round-trip incl.
      6/8→3; null-when-omitted).
- [x] **Session-level key + MusicXML `<key><fifths>` round-trip (DONE
      2026-06-08):** promoted the score key from piano-roll-local UI state to a
      real **session property** `DawSession.keySignatureFifths` + a
      `setKeySignature` reducer action (clamped −7…+7, **display-only → emits no
      engine command**, so session-rebuild is untouched), persisted through
      `project-io` (back-compat: absent → 0). `musicXmlToNotes` now parses
      `<key><fifths>` and returns it, so the existing `notesToMusicXml`
      `keyFifths` export **round-trips**: the MusicXML export command passes
      `session.keySignatureFifths`, and the import dispatches
      `setKeySignature(fifths)` so an imported score carries its key onto the
      staff. The piano-roll Key selector now reads the session key + dispatches
      the action (consistent + persisted across reload). **Tests:** 6 session
      (`time-signature.spec`: default 0, set-no-command, clamp/round,
      rebuild-emits-nothing, save/load persist, old-project → 0)
  - 1 notation MusicXML round-trip (E♭/A major + absent) + 2 piano-roll
    (selector dispatches; renders under the session-key prop). App tsc 0; full
    suite 1656 green; stub scan clean.
- [x] **Time-signature display (DONE 2026-06-08):** `score-layout` engraves a
      `PlacedTimeSig` (numerator over denominator, centred in a reserved column
      **after the key signature**; on both staves of a grand staff) — completing
      the score's clef + key + time front matter. `layoutScore` takes a
      `timeSignature` param defaulting to **`null` → zero-width →
      byte-identical** (pure tests unchanged); `score-view` always passes the
      session meter as `beatsPerBar`/4 so the rendered score shows it. 4 layout
      tests (default-null, reserves-column/shifts-notes, after-the-key-sig,
      grand-both-staves) + 2 score-view (treble 3/4 numerals, grand 4-glyph).
      Full suite 1697. + a **tempo marking** (♩ = N above the staff, drawn when
      the score-view is given the session tempo; sits in the top headroom so it
      shifts nothing) — 1 score-view test. The score's clef + key + time + tempo
      front matter is now complete.
- [x] **Measure-internal accidental carry (DONE 2026-06-08):**
      `score-layout.measureAccidentals` resolves each note's printed accidental
      with a stateful per-bar pass (time-ordered, reset at every barline; the
      per-letter baseline at a bar's start is the key signature): an accidental
      persists for the rest of its bar at that exact **letter+octave**, so a
      repeated altered note isn't re-marked and a note **returning to the key's
      pitch within the bar is explicitly cancelled with a ♮** (fixing a real
      latent bug — a C♮ after a C♯ in the same bar previously drew nothing and
      read as a second C♯). Per-octave (C♯4 doesn't suppress C♯5); resets at the
      next barline. A single note (or no same-bar/same-octave repeat) is
      identical to the stateless spelling, so the key-signature tests are
      unaffected. **Tests:** 6 (`score-layout.spec`: first-marks-not- repeat,
      returns-cancel-with-♮, barline-reset re-marks, per-octave independence,
      single-note identity, key-diatonic repeat stays unmarked) + the score-view
      ♯-then-♮ render test updated to assert the now-correct cancellation.
- [x] **Multi-part MusicXML export (DONE 2026-06-08):**
      `notesToMultiPartMusicXml(parts, opts)` exports the **whole arrangement**
      as one printable score — a `<part-list>` of `<score-part>`s + a `<part>`
      per track, sharing the key/time signature, the tempo direction emitted
      once. Refactored the single-part `notesToMusicXml` to share a
      `partMeasures` helper (the existing MusicXML tests confirm the single-part
      output is byte-identical). Wired: an "Export Full Score (MusicXML, all
      tracks)" command gathers every note-clip track. **Multi-part IMPORT also
      done** (full round-trip): `musicXmlToParts(xml)` parses **every** `<part>`
      (name from `<part-list>` + notes via the shared `parsePartNotes` helper,
      byte-identical single-part refactor) → the import handler creates **one
      track per part** (adopting the file's key/time/tempo). 5 tests (export: N
      parts share key + one tempo, each `<part>` round-trips, empty → one part;
      round-trip: every part recovers name+notes incl. a 2-beat note,
      single-part → one part). The whole arrangement now round-trips through
      MusicXML.
- [x] **Click-to-edit on the staff (DONE 2026-06-08):** the score is now
      **editable** — `score-layout` gains the inverse of the engraving:
      `stepToMidi(step, fifths)` (a staff line/space → its key-diatonic pitch),
      `scorePointToNote(sx, sy, layout, …)` (an SVG point → a grid-snapped
      `{ beat, pitch }`, `null` left of the first note column), and
      `toggleScoreNote` (click an empty spot to add a note, click an existing
      note to erase it). `score-view` takes an optional `onEdit`: when supplied
      the SVG becomes `role="application"` with a click handler that maps the
      click (via `getBoundingClientRect` → viewBox coords) → `scorePointToNote`
      → `toggleScoreNote` → the new clip; the piano-roll passes `onEdit`
      dispatching `setNoteClip`. **Tests:** 4 pure (stepToMidi incl. key-sharped
      F, point→note snap + null-in-clef-area, toggle
      add/remove/different-pitch) + 3 jsdom (read-only role=img without onEdit;
      click adds the note at the beat+line; click on an existing note removes it
      — the rect is mocked so client coords map 1:1 to the viewBox). The
      drag-to-MOVE gesture stays browser-bound. Full suite 1710.
- [x] **Multi-voice engraving (DONE 2026-06-09):** overlapping rhythmic lines
      now render as independent **voices** with per-voice stems + beaming,
      instead of one stem-by-pitch. New
      `apps/euterpe-studio-web/src/daw/score-voices.ts` `assignVoices` — groups
      notes into chord-units (same onset+duration → one stem), then **greedy
      free-voice assignment** (each unit → a voice whose previous unit has
      ended, nearest in pitch for smooth voice-leading), voices ordered by mean
      pitch DESC (voice 0 = top line). `layoutScore` tags each
      `PlacedNote.voice` + flips the stem by voice when polyphonic (top up /
      next down); `computeBeams` beams **each voice independently** (no beams
      across voices) following that voice's direction (`buildBeamGroup` gains a
      `forcedStemUp`); `score-view` renders both directions (added a `stem`
      testid). **No-op invariant:** monophonic content + plain block chords are
      all voice 0 ⇒ stems stay pitch-based ⇒ byte-identical (the 73 prior score
      tests pass unchanged). **Tests:** 5 voice-separation
      (mono/chord/held-bass/two-distinct-rhythms/ empty-single) + 3 layout
      (voice tagging + stem flip, mono no-op, beams-stay-within-voice) + 2
      score-view (both stem directions render, mono no-op); full
      src/daw+components **2053 green**. **MusicXML `<voice>`/`<backup>`
      multi-voice round-trip also DONE 2026-06-09** (commit
      `MusicXML multi-voice export/import`): `partMeasures` gates on >1 voice →
      `multiVoiceMeasures`/`fillVoiceMeasures` (each voice fills every bar, then
      a `<backup>` rewinds for the next voice; `noteElement` gains `<voice>`);
      the importer `parsePartNotes` walks each measure's children in order,
      applying `<backup>`/`<forward>` to the cursor — so a polyphonic clip
      exports correctly for MuseScore/Sibelius and round-trips (monophonic stays
      byte-identical, no `<voice>`/`<backup>`; +2 round-trip tests; suite 2055).
- [x] **Cross-voice note-head/stem collision avoidance DONE 2026-06-09 (the last
      engraving tail, commit
      `cross-voice note-head/stem collision avoidance`):** `PlacedNote.headDx` +
      `resolveHeadCollisions` in `score-layout.ts` — when a stem-up voice and a
      stem-down voice sound at the **same beat on the same staff** within a
      diatonic 2nd (heads would overlap), the lower (stem-down) voice's heads +
      stems are displaced clear (the standard two-voice resolution). **Gated on
      `polyphonic`** so monophonic content — incl. a block chord, whose
      pitch-based stems mix up/down — keeps `headDx 0` → **byte-identical**
      (no-op invariant); `computeBeams` reads `x + headDx` so beamed displaced
      heads keep aligned stems; `score-view` renders head + ledgers +
      accidental + dots + stem at `x + headDx`. **Tests:** +6 pure (displace
      within a 2nd / no-displace a 3rd+ / no-displace across beats /
      single-note + block-chord no-ops) + 1 jsdom (heads at distinct cx); all 84
      score-layout/voices/beaming/view tests green; app tsc 0; stub scan clean.
- **Acceptance:** ✅ MusicXML round-trips losslessly for pitch/rhythm; ✅ a clip
  renders as an engraved score (treble, or an auto **grand staff** with a bass
  clef when it reaches below middle C — note heads, stems, accidentals, ledgers,
  dots, barlines, **beat-grouped beaming with secondary beams/hooks**, **key
  signatures with key-aware enharmonic spelling + measure-internal accidental
  carry + time signature + tempo marking**, **multi-voice separation with
  per-voice stems + beaming** + **cross-voice head/stem collision avoidance**),
  all geometry verified); ✅ the staff is **editable by click** (add/remove
  notes); ✅ **multi-voice MusicXML round-trip** (`<voice>`/`<backup>`). ⏳
  **REMAINING:** **drag-to-move** a note on the staff (browser-bound) is the
  only engraving tail left.
- **Risk:** L-XL; interop + render (grand staff + beaming + key sigs +
  accidental carry + time/tempo + **multi-voice**
  - **collision avoidance**) + **click-to-edit** shipped (pure/jsdom-verified).
    The drag-to-move gesture is the separate browser remainder. Honest limit:
    treble + grand staves, key-aware spelling within a single accidental.

### 3.2 `[~]` MIDI-1 / UX-24 / FX-N2 / IO-3(expr) / REC-N3 — MPE / per-note expression · P1 · effort L-XL ✅ ENGINE + LIVE INPUT + CLIP EXPRESSION LANES (engine→bridge→model→editor UI) DONE 2026-06-09 (canvas-drag + live recording = tails)

- [x] **Engine:** `SynthVoice` gains per-note `bend_semitones` / `pressure` /
      `timbre` (set live, independent of the channel-wide LFO/matrix): bend sums
      into the pitch (`play_freq`), pressure swells the amp `amp_mul`, timbre
      opens the filter cutoff (+up to 6 kHz). All default 0 = **bit-identical**
      to a non-MPE voice (no-op invariant tested + all 164 prior dsp-core tests
      green). Cleared on every note-on so a fresh note starts neutral.
      `PolySynth::note_expression(note, dim, value)` finds the voice(s) sounding
      `note` and routes dim 0/1/2 → so two held notes bend/press/brighten
      independently. Threaded through `Track`/`Engine`/`dsp-wasm`
      `note_expression(track, note, dim, value)` (with the same scale-lock
      snap).
- [x] **Capture:** `apps/euterpe-studio-web/src/daw/mpe.ts` `MpeRouter` — tracks
      the note live on each MPE member channel and decodes per-channel
      **pitch-bend (0xE0, 14-bit → ±48 st default), channel-pressure (0xD0), and
      CC74 timbre/slide** into per-note `note_expression` events; note-on/off
      maintain the channel→note map. Wired into the Web-MIDI handler →
      `engine.noteExpression(selectedSynthTrack, note, dim, value)` (note-on/off
      still play via the normal path).
- [x] Bridge: messages `noteExpression` + worklet dispatch +
      `AudioEngine.noteExpression` bridge. wasm rebuilt+synced.
- [x] **Tests:** cargo — the **MPE invariant** (two voices on the same note:
      bend one an octave up via the engine, only its zero-crossing rate
      doubles), pressure swells RMS, timbre raises high-frequency
      (first-difference) energy, neutral expression is bit-identical, retrigger
      clears the bend, PolySynth routes to the sounding note + no-ops an absent
      note. Node-boundary — `note_expression` pressure swells the master RMS
      across the rebuilt WASM boundary. 7 jsdom (`mpe.spec.ts`: per-channel
      routing, channel independence, pressure/CC74, no-live-note ignore,
      note-off frees, vel-0 = note-off, reset). (169 dsp-core + 109 dsp-graph +
      49 dsp-wasm cargo; 1327 daw; clippy clean; tsc 0.)
- [x] **Clip-side expression curves — ENGINE (DONE 2026-06-09):**
      `dsp-graph/src/clip.rs` `NoteExpression` (per-note bend/pressure/timbre
      breakpoint curves, `(beat-offset, value)` relative to the note start)
      stored **parallel to** the notes (ClipNote stays `Copy`), sorted together
      by start beat. `NoteClip::advance` interpolates each _sounding_ note's
      curves at its note-local beat (`eval_curve`, linear + hold-endpoints,
      empty→skip) and emits `Track::note_expression(pitch, dim, value)` per
      block (control-rate, absolute → smoothly tracks the curve).
      `NoteClip::with_expressions` constructor; `new` delegates with empty
      curves. **No-op invariant:** an all-default expression renders
      **bit-identical** (asserted), so every existing clip/engine test is
      untouched. 4 cargo tests (eval_curve interp/hold/empty, empty-expression
      bit-identical render, a bend curve 0→+12 st ~doubles the clip voice's
      zero-crossing rate, a pressure curve swells RMS). dsp-graph 129; clippy
      clean.
- **Acceptance:** ✅ two held notes with independent pitch bends bend
  independently (cargo + the `MpeRouter` channel-isolation test); ✅ a live MPE
  controller's pitch-bend/pressure/timbre route to the right per-note voice; ✅
  **a clip note carries bend/pressure/timbre curves that play back through the
  synth** (engine, cargo-verified).
- [x] **Ragged-array wasm bridge (DONE 2026-06-09):** `set_track_note_clip_mpe`
      (engine + dsp-wasm) takes the flat note arrays
  - a **flat ragged expression encoding** (per curve: note-idx / dim / count +
    concatenated beats/values), reconstructs one `NoteExpression` per input
    note, replicates across ratchet sub-notes. cargo binding + WASM-boundary
    tests (a bend curve 0→+12 st lifts the clip render's pitch an octave through
    the real simd128 wasm).
- [x] **App model + persistence + bridge wiring (DONE 2026-06-09):**
      `ClipNote.expression` + `NoteExpression` types; `note-expression.ts`
      ragged encoder; a `setNoteExpression` reducer (set one note's curves by
      sorted index, re-emit the clip through the MPE bridge) + `setNoteClip` now
      PRESERVES expression; the `setNoteClip` command + session-rebuild carry
      the encoding only when present (no-expression command byte-identical); the
      worklet routes to `set_track_note_clip_mpe`; project-io persists it free.
      Encoder + reducer + project-io round-trip tests (app daw 1401;
      audio-engine-web 93; tsc 0).
- [x] **Per-note expression editor UI (DONE 2026-06-09):**
      `components/daw/note-expression-panel.tsx` `NoteExpressionEditor` (under
      the piano roll for the selected synth track) — a note selector + a
      per-dimension (bend st / pressure / timbre) breakpoint editor: add a
      `(beat-offset, value)` point, edit its beat/value, remove it; every edit
      dispatches `setNoteExpression` so the curve plays through the MPE bridge +
      persists. 3 jsdom tests.
- [x] **Visual SVG expression LANE (DONE 2026-06-09):**
      `components/daw/note-expression-lane.tsx` `NoteExpressionLane` draws each
      dimension's breakpoint curve over the note's duration (x = beat-offset, y
      = value, inverted) and **edits by click**: empty lane → add a grid-snapped
      point at the mapped `(beat, value)`; on a point → remove it. Pure
      coord↔value mapping
      (`beatToX`/`xToBeat`/`valueToY`/`yToValue`/`pointHitTest`/`snapBeat`) so
      it's browser-free-verifiable; the click handler is jsdom-tested by mocking
      `getBoundingClientRect` (the score click-to-edit pattern). 6 tests.
      components/daw 336 green.
- [x] **Drag-to-move points (DONE 2026-06-09):** the lane interaction is a
      mouse-down→move→up state machine — empty-lane down adds a snapped point;
      down on a point begins a window-tracked **drag** (snapshot-indexed so it
      stays valid even when carried past another point in beat order); a down-up
      with no movement is a tap = remove. The drag math reuses the pure mapping,
      jsdom-verified (add / tap-remove / drag-to-+max). components/daw 337
      green.
- **Acceptance:** ✅ a clip note's bend/pressure/timbre curves are **fully
  editable** — a visual lane with click-to-add, tap-to-remove and
  **drag-to-move**, plus a precise breakpoint editor — and play back through the
  full app→worklet→engine path (boundary-verified) + survive save/load. **The
  MIDI-1 clip-expression LANES feature is complete at the in-sandbox baseline;
  the only remainder is live-MPE RECORDING into the curves (genuine hardware: an
  MPE controller).**
- **Risk:** L-XL; landed across 6 verified increments (engine → ragged bridge →
  app model+persistence → breakpoint editor → visual lane → drag-to-move) via
  the proven cross-stack recipe + a strict no-op invariant. Only live-MPE
  recording remains.

### 3.3 `[x]` FX-18 / UX-10 / REC-2 / AI-N4 / IO-11 / REC-16 — Take comping (multi-take lanes + swipe + undo) · P1 · effort L ✅ DONE 2026-06-08

- [x] Data model: `TakeLane` / `CompSegment` / `TakeComp` in `types.ts` (a comp
      = stacked take lanes + a gap-free segment tiling of which lane plays per
      range + a boundary crossfade). `DawSession.takeComps` +
      `nextCompSeq`/`nextLaneSeq`.
- [x] Comp engine (`apps/euterpe-studio-web/src/daw/take-comp.ts`, pure, 15
      tests): `createComp`/`addLane`/`laneFromClip`, `compSetRange`
      (**swipe-to-select** — splits boundary segments, replaces the interior,
      merges adjacent same-lane runs back into one, always re-yielding a
      gap-free tiling), `setCrossfade`, `laneAtBeat`, and `resolveCompClips` —
      the active take per range materialized as arrangement clips, each internal
      boundary **overlapped by the crossfade** so the earlier clip fades out
      while the next fades in over the same span = a genuine equal-power
      crossfade (rendered by the existing arrangement-clip fade engine, so
      playback is reused — no new DSP).
- [x] Reducer (6 actions:
      createTakeComp/addTakeLane/setCompRange/setCompCrossfade/applyTakeComp/deleteTakeComp)
      with a `materializeComp` helper that drops the comp's previously-owned
      clips, resolves fresh ones, mints ids, and resyncs the track
      (`arrangementCommand`) — and **re-materializes LIVE** when an
      already-applied comp is edited, so painting the comp updates playback.
      project-io persists comps (backward-compat) + the materialized clips, so a
      comp survives save/load.
- [x] Loop-record capture into take lanes: seeded from the track's live note
      clip (each "+ Take" snapshots the current clip) — the honest in-sandbox
      path since multi-take _recording_ needs a mic (the REC-5 looper from 3.4
      feeds it there).
- [x] **UI** `take-comp-panel.tsx` (`TakeCompPanel`): per-lane row of beat-cells
      you click/paint to assign the comp range, a crossfade slider, Apply (▸/●
      Live), + Take, delete; wired into daw-app via `TakeCompSection` for the
      selected synth track. REC-16 undo: apply/edit actions emit engine commands
      → the undo controller snapshots them.
- [x] **Tests:** 15 pure (engine: tiling invariant, swipe split/merge, overlap
      crossfade math, deep-copy seeding) + 6 reducer
      (create/addLane/apply→song-mode/live-re-materialize-on-swipe/delete/save-load
      round-trip/undo boundary) + 3 jsdom (cell paint dispatch,
      apply/crossfade/add/delete, applied-state). App tsc 0; full
      `src/daw`+`src/components/daw` 1281 green; stub scan clean.
- **Acceptance:** ✅ comping across N takes plays the selected take per range
  with crossfaded boundaries; ✅ undo restores the prior comp (command-emitting
  boundary). Takes seeded from clips per the checklist's stated verification
  path; live mic multi-take recording into lanes is the REC-3/5/12 browser
  acceptance.
- **Risk:** L — landed via the public clip API + a single `materializeComp`
  helper (reuses `arrangementCommand`), so the hot arrangement path was
  extended, not rewritten; the no-op invariant (sessions with no comps are
  byte-identical) holds.

### 3.4 `[~]` REC-3 / REC-5 / REC-12 / REC-N5 — Recording capture: punch, looper, multitrack, retro-capture · P1/P2 · effort L ✅ PURE LOGIC (ALL 4) + REC-N5 WIRED 2026-06-08

- [x] New `apps/euterpe-studio-web/src/daw/recording-capture.ts` — all four
      capture primitives, pure + unit-tested (21 domain tests), layering on the
      existing single-take `audio-capture.ts`
      (`CaptureBuffer`/`RecordingSession`):
  - [x] **REC-3 punch-in/out:** `punchRegionFrames` (beats→frames at tempo) +
        `applyPunch` — splices a take into an existing track buffer over
        `[in,out)` only, **equal-power-crossfading** both seams (`gOut²+gIn²=1`,
        verified the incoming reads `sin(t·π/2)` and outgoing `cos(t·π/2)`);
        outside the region the original is byte-exact; the fade clamps to half
        the region so the two crossfades never overlap.
  - [x] **REC-5 looper/overdub:** `Looper` (fixed loop length, round-robin write
        head, `overdub(block, feedback)` — `feedback=1` layers forever / `<1`
        decays older layers / silence-start makes the first pass a plain
        record); wrap + `loopCount`/`position`/`clear` tested.
  - [x] **REC-12 multitrack arm:** `MultitrackRecorder` — per-armed-track
        `CaptureBuffer`s, routed `append` ignores un-armed tracks (no fabricated
        take), `finishAll` returns only non-empty takes keyed by id;
        arm/disarm/`armedTracks` tested.
  - [x] **REC-N5 retrospective MIDI:** `RetroMidiBuffer` — always-on ring of
        recent notes vs a free-running beat clock, `capture(now, length)`
        re-zeroes the last N beats to a grid-aligned clip, closes still-held
        notes at `now`, FIFO-pairs overlapping repeats of one pitch, excludes
        pre-window onsets, clamps the window at the song start, `trim` bounds
        the ring.
- [x] **REC-N5 wired END-TO-END (verified here):** `RetroMidiBuffer` fed from
      `recordDispatch` (the single keyboard+MIDI note choke point) against a
      `performance.now`-anchored tempo clock that advances whether or not the
      transport plays; a `captureRetro` callback commits the last 4 bars to the
      selected synth track's clip; TransportBar "⤓ Capture" button + jsdom test.
      So "Capture recovers an un-armed take" is real + verifiable without a mic.
- [x] **Tests:** 21 pure (`recording-capture.spec.ts`) + 1 jsdom (TransportBar
      Capture button) — app tsc 0, full `src/daw` + `src/components/daw` suite
      green (1242). Adversarial stub scan clean.
- **Acceptance:** ✅ REC-N5 "Capture" recovers an un-armed take (wired +
  verified); ✅ punch region+crossfade / looper-overdub / multitrack-arm pure
  cores verified by domain tests. ⏳ **REMAINING (browser/hardware-bound):**
  routing the live mic through the Looper (loop-record mode) and a multi-channel
  interface through `MultitrackRecorder`, and splicing `applyPunch` into the
  recorded sample on stop — these consume the actual `getUserMedia` audio path,
  so they're the documented mic/browser acceptance; the logic they'd call is
  built + tested.
- **Risk:** L; mic/multichannel capture needs a browser + interface. Pure logic
  built + tested per the checklist's stated deliverable; the mic-bound
  integration is left honest rather than fake-wired against a single stream.

### 3.5 `[~]` PLAT-5 / COLLAB-19 — PWA: service worker + OPFS offline-first · P1 · effort L ✅ CACHE POLICY + SW + GATED REGISTRATION 2026-06-08 (offline-boot = browser gate; OPFS-stream remains)

- [x] **Pure cache-strategy module (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/pwa-cache.ts` —
      `strategyForRequest(req, origin)` is the tested routing core:
      **network-first** for navigations/documents (a new deploy always wins
      online; the cached shell is the offline net), **cache-first** for
      immutable hashed assets + `/audio/*` (the Rust engine WASM + worklet) +
      the manifest, **stale-while-revalidate** for other same-origin GETs,
      **network-only** for cross-origin / non-GET (never cache third-party or
      mutations). + `CACHE_VERSION`/`cacheName`/`isStaleCache` (versioned
      cleanup), `PRECACHE_URLS` (shell + engine wasm + worklet),
      `isPrecacheUrl`. 7 pure tests.
- [x] **Service worker (DONE 2026-06-08):** `public/sw.js` — a hand-rolled SW
      mirroring the policy: install precaches the shell+engine (per-URL
      `allSettled` so a missing optional asset never fails install), activate
      deletes stale `euterpe-cache-*`
  - `clients.claim`, fetch routes through
    `cache-first`/`network-first`(offline→cached shell
    `/`)/`stale-while-revalidate`. Conservative (navigations never served
    cache-first → no stale-app footgun). The `manifest.webmanifest` already
    existed + is linked in `app/layout.tsx`.
- [x] **Registration (DONE, opt-in seam):** `src/components/sw-register.tsx`
      `ServiceWorkerRegister` — registers `/sw.js` **only when
      `NEXT_PUBLIC_EUTERPE_SW === '1'`** (added to the layout body). The opt-in
      gate is deliberate: a SW persists across deploys and the _offline boot_
      must be confirmed in a real browser before it's enabled in production, so
      by default it's inert + safe (renders/registers nothing).
      `shouldRegisterServiceWorker(env, hasSW)` pure predicate + 3 jsdom tests
      (no-register-by-default, registers when opted in). App tsc 0; standard
      gate green (1468).
- [x] **Offline persistence queue core DONE 2026-06-09:**
      `apps/euterpe-studio-web/src/daw/offline-sync-queue.ts`
      `OfflineSyncQueue<T>` — the deterministic sync-on-reconnect policy:
      `enqueue(key, payload)` coalesces repeated saves per key (latest payload
      wins, FIFO position kept), `drainBatch(max)` yields pending writes in
      order (empty while offline), `setOnline` returns the offline→online
      reconnect cue, and `ack(key, seq)` removes a flushed entry **only if no
      newer edit arrived mid-flight** (the seq-match seam that makes "save again
      during a save" lossless) — a failed flush stays pending simply by not
      being acked. 8 Node tests (coalesce / FIFO+max / offline-gate+reconnect /
      ack / lossless-ack-in-flight / retry-on-no-ack / clear /
      re-enqueue-after-ack).
- [ ] **OPFS write adapter (REMAINS — browser tail):** open a
      `FileSystemSyncAccessHandle` per project/sample path, drive it from
      `drainBatch()`→write→`ack(key, seq)`, wire `setOnline` to the
      `online`/`offline` events. Pure plumbing around the tested queue; OPFS
      storage + the reconnect lifecycle need a real browser to verify.
      (Streaming _read_ side ties to ENG-7/2.8; the `SabRing` transport for it
      shipped above.) _Re-read 2026-09-18: as for the read adapter — verify in a
      Playwright chromium spec: write through the queue, reload the page, read
      back the same bytes; toggle `context.setOffline` to exercise the reconnect
      path._
- [x] **Tests:** pure cache-strategy decisions (7) + registration gating (3).
      **Offline behavior still REQUIRES a browser pass** (set
      `NEXT_PUBLIC_EUTERPE_SW=1`, build, load once, reload offline → cached
      shell + engine serve) — NOT claimed here.
- **Acceptance:** ✅ the cache policy + SW + gated registration ship, fully
  unit-tested; ⏳ "app shell loads offline after first visit" is the documented
  browser-acceptance gate (enable the flag + verify); OPFS project persistence
  remains.
- **Risk:** MEDIUM; unverifiable-offline-in-sandbox is the honesty seam —
  plumbing built + tested, registration gated OFF by default so it can't break
  the live app, browser offline-pass is the gate to enable. Not marked fully
  done.

### 3.6 `[~]` IO-6 / PLAT-N2 / MASTER-9 — Lossy/lossless export codecs (FLAC / Opus / AAC / MP3 / ALAC / Ogg) · P1/P2 · effort M ✅ FLAC + AIFF + ALAC + encodeCodec DISPATCHER DONE (lossy codecs = fail-loud seam, need WASM deps)

- [x] **FLAC (DONE 2026-06-08, NO new dependency):** a real from-scratch FLAC
      encoder `libs/euterpe/audio-engine-web/src/flac.ts` (`encodeFlac`) —
      `fLaC` + STREAMINFO + frames with CONSTANT/FIXED-predictor (orders 0–4,
      min-coded-size selection) subframes + partitioned-Rice residuals (5-bit
      params), CRC-8 header + CRC-16 footer, UTF-8 frame numbers, explicit
      sample-rate codes (CoreAudio rejects the read-from-STREAMINFO code),
      optional VORBIS*COMMENT for the provenance label. 16-bit, matching
      `encodeWav`. Wired: daw-app `bounce('flac')` + command-palette "Bounce to
      FLAC (lossless)". **Verified TWO independent ways:** (1) a from-scratch
      FLAC \_decoder* in the test round-trips the whole multi-frame stream
      **bit-exactly** (lossless, every frame CRC-16 validated; 48k stereo /
      44.1k mono / tiny / silence / vorbis-comment); (2) macOS `afconvert`
      (CoreAudio) decodes it bit-exact — external real-FLAC proof. (commit
      `feat(euterpe): real lossless FLAC encoder…`)
- [x] **AIFF (DONE 2026-06-08, NO new dependency):** a real from-scratch AIFF
      encoder `libs/euterpe/audio-engine-web/src/aiff.ts` (`encodeAiff`) —
      Apple's lossless PCM interchange, the big-endian IFF sibling of WAV/FLAC:
      `FORM`/`COMM`/`SSND` chunks, 16-bit signed **big-endian** PCM, the sample
      rate as an **80-bit IEEE-754 extended float** (`writeExtended`; 48 kHz →
      the canonical `40 0E BB 80 …`), optional `ANNO` annotation for the
      provenance label. Wired: daw-app `bounce('aiff')`
  - command-palette "Bounce to AIFF (lossless)". **Verified TWO independent
    ways:** (1) a from-scratch AIFF _decoder_ in the test reads the extended
    rate back + round-trips the 16-bit PCM **bit-exactly** (48k stereo sine /
    44.1k mono / silence / annotation); (2) macOS `afconvert` (CoreAudio)
    transcodes it AIFF→WAV without error — external real-AIFF proof. 5 tests.
    (Lossless PCM ⇒ no WASM dep, like FLAC.)
- [x] **ALAC (DONE 2026-06-09, NO new dependency):** a real from-scratch **Apple
      Lossless** encoder `libs/euterpe/audio-engine-web/src/alac.ts`
      (`encodeAlac`) — a faithful port of Apple's open-source reference codec
      (Apache-2.0): matrix mix (`mixRes=0` independent channels) → adaptive-FIR
      **dynamic predictor** (`pc_block`, 8 taps) → **adaptive-Golomb** entropy
      coder (`dyn_comp`), with a per-packet **escape** to raw PCM on
      incompressible input, wrapped in a **CAF** container (caff/desc/kuki/info/
      pakt/data + BER packet table + 24-byte ALACSpecificConfig magic cookie).
      Exact int32 wrap via `Math.imul`+`|0` so residuals/coefs are bit-identical
      to the C (which is what makes it CoreAudio- decodable). 16-bit,
      mono+stereo, the always-correct "fast" param set (no brute search / no
      mid-side matrix — documented size enhancement, not a correctness gap).
      **Verified TWO independent ways:** (1) a from-scratch ALAC _decoder_ in
      the same module (`decodeAlacCaf`: AG-decode + inverse predictor
  - un-mix) round-trips every packet **bit-exact** (multi-frame / partial-frame
    / silence-zero-run / incompressible-escape; mono + stereo); (2) macOS
    `afconvert` (CoreAudio's independent decoder) decodes the `.caf` **bit-exact
    with the exact frame count** — external real-ALAC proof. 9 tests. ALAC, like
    FLAC/AIFF, is _lossless_ and openly specified ⇒ needs no WASM dep (corrects
    the prior "ALAC needs WASM deps" grouping). Wired: daw-app
    `bounce('alac')` + palette "Bounce to ALAC".
- [x] **`encodeCodec(interleaved, sampleRate, format, opts)` dispatcher (DONE
      2026-06-09):** `libs/euterpe/audio-engine-web/src/codec.ts` — one entry
      point owning the format→encoder map + MIME + extension + availability.
      Routes wav/flac/aiff/alac to their real encoders; for the dep-blocked
      lossy formats (mp3/opus/aac/ogg) it throws a typed `CodecUnavailableError`
      (`code:'codec_unavailable'`) — an honest **fail-loud seam**, never
      fabricated audio; a `LOSSY_ENCODERS` registry lets a vendored WASM codec
      slot in with zero call-site change.
      `CODEC_INFO`/`SUPPORTED_CODECS`/`AVAILABLE_CODECS` are the single source
      of truth. The daw-app `bounce()` now selects every lossless container
      through it. 8 tests (each lossless format's container magic + ALAC
      round-trip + channel/comment threading + fail-loud lossy
  - UnknownCodecError + registry integrity).
- [ ] **MP3 / Opus / AAC / Ogg (REMAIN — need WASM codec deps):**
      `@wasm-audio/lamejs` MP3, `opus-encoder` Opus, etc. NEW dependency edge
      (pnpm catalog) — deferred on this memory-constrained box (a fresh
      `pnpm install` risks thrashing per the macOS guidance). The lossy codecs
      are patented psychoacoustic encoders not worth hand-rolling; they fail
      loud via `encodeCodec` until vendored. _Re-read 2026-09-18, install first:
      the memory caveat was about the Mac. On the Linux server, add the encoders
      through the pnpm catalog (check `free -m` first; if the worktree's link
      farm is incomplete, link the packages from `.pnpm` as the repo's own note
      describes rather than running a full install): an MP3 encoder
      (LAME-derived, LGPL — record the licence), Opus and Ogg Vorbis. AAC stays
      a fail-loud seam until its encoder's licence is read and recorded; say so
      in `encodeCodec`'s error. **Verify:** each codec round-trips a reference
      tone within a stated SNR and the container parses with ffprobe._
- **Acceptance:** ✅ exported FLAC **+ AIFF + ALAC** decode to bit-exact
  lossless audio (own decoders + afconvert); ✅ unified `encodeCodec` dispatcher
  with a fail-loud seam for the lossy set. ⏳ MP3/Opus/AAC/Ogg remain (WASM
  dep).
- **Risk:** MEDIUM; remaining lossy codecs = dependency weight + license review.
  WAV/BWF/FLAC/AIFF/ALAC are the verified lossless defaults; `encodeCodec` is
  the one selector.

### 3.7 `[~]` UX-1 — Resizable/dockable panel workspace + saveable screensets · P1 · effort L ✅ LAYOUT MODEL + SCREENSETS + WORKSPACE SHELL DONE 2026-06-08 (drag-dock/free-float = browser tail)

- [x] **Layout model (DONE 2026-06-08, pure):**
      `apps/euterpe-studio-web/src/daw/workspace.ts` — a registry-driven
      panel-layout reducer: per-panel **visibility / collapse / order** + a
      resizable **sidebar width**, and named **screensets** (Logic/Cubase-style
      snapshots). `defaultLayout`/`defaultWorkspace` (all visible, source order,
      380 px sidebar = **byte-identical to the pre-workspace fixed stack** →
      non-breaking), `isPanelVisible`/`isPanelCollapsed`,
      `setPanelVisible`/`setPanelCollapsed`/`togglePanelCollapsed`,
      `movePanel(dir, amongIds)` (swaps with the _visible_ neighbor even when
      hidden/absent panels are interleaved), `setSidebarWidth` (clamped
      240–680), `saveScreenset`/ `recallScreenset` (deep-copy → edits don't
      mutate the snapshot)/`deleteScreenset`/`resetWorkspace`, `reconcileLayout`
      (drops registry-gone ids, appends new ones visible, prunes orphan flags),
      `orderedVisiblePanels`, and `serializeWorkspace`/`parseWorkspace`
      (fail-loud-tolerant — null/garbage/wrong-shape ⇒ default, never throws;
      drops nameless screensets + a dangling active pointer). The
      `WORKSPACE_PANELS` registry is the 23 DAW panels.
- [x] **Workspace shell (DONE, React):** `components/daw/workspace-shell.tsx` —
      `WorkspaceControls` (a screenset switcher: Save-as / Recall / Delete; a
      sidebar-width number input; Reset; and a disclosure listing every present
      panel with a show/hide checkbox, ↑/↓ reorder, and a collapse toggle) +
      `CollapsedPanelBar` (the slim title bar a collapsed panel becomes; click
      to expand). Presentational — all state lives in the host via the pure
      model.
- [x] **Wired into the DAW (DONE):** `daw-app.tsx` now renders its ~23 top-level
      panels through a **panel registry** (`panelNodes` id→node, conditional
      panels = a null node that's skipped) driven by `orderedVisiblePanels` — so
      visibility, order, collapse, and the work-area **sidebar width** are all
      live + persisted to `localStorage` (`euterpe-workspace`). Default layout
      reproduces the prior fixed stack exactly (all panels, source order, 380
      px).
- [x] **Tests:** 22 pure (`workspace.spec.ts`: defaults/registry-integrity,
      hide/show + flag-pruning, collapse, reorder incl. interleaved-absent +
      boundary no-ops, sidebar clamp, screenset save/overwrite/recall-deep-copy/
      delete-clears-active/reset-keeps-screensets, reconcile drop/append/prune,
      serialize↔parse round-trip, fail-loud parse, drop-nameless-screenset) + 11
      jsdom (`workspace-shell.spec.tsx`: panel list + visibility checkbox
      state/toggle, hidden-count label, reorder fire + boundary-disabled,
      collapse toggle, sidebar-width change, screenset save (disabled when
      empty) / recall / delete on the selection, reset, collapsed-bar expand).
      App tsc 0; full `src/daw`+`src/components/daw` suite 1582 green; stub scan
      clean.
- [x] **Drag-to-reorder panels (DONE 2026-06-08):** a pure
      `workspace.reorderPanel(state, id, beforeId, amongIds)` — moves `id` to
      just before `beforeId` among the present panels (derived in current
      display order) and rewrites the master order so interleaved non-present
      panels keep their slots; no-op on same/absent id. The WorkspaceControls
      rows are now `draggable` with a ⠿ grip + HTML5 DnD handlers (dragStart
      records the id, drop fires `onReorder(dragged, target)`) — testable by
      `data-id` target, not coordinates, so jsdom-verifiable. 3 pure
      (drop-before / no-op / interleaved-preserve) + 2 jsdom (drag C→A fires
      onReorder; drop-on-self no-ops). Full suite 1702.
- [x] **Drag-to-resize the sidebar splitter (DONE 2026-06-08):** a
      `ResizeHandle` (workspace-shell) — a vertical `role="separator"` handle
      between the sidebar and the work area that, on mouse-down, captures the
      start x + width and tracks the move on `window` (so the drag continues
      past the thin handle), reporting `clampSidebarWidth(startWidth + Δx)` (the
      same 240–680 clamp the reducer applies) until mouse-up; wired into daw-app
      driving `wsSetSidebarWidth`. The drag math is jsdom-tested (3: delta→width
      both directions, min/max clamp, listener-detached-after-mouseup); the live
      splitter feel is the browser tail. Full suite 1689.
- **Acceptance:** ✅ a screenset round-trips (save → diverge → recall restores;
  persisted across reload); ✅ panels show/hide, reorder, collapse, and the
  sidebar resizes — all jsdom-verified driving the real render; ✅
  **drag-to-resize the sidebar** (mouse-drag handler, jsdom-tested math); ✅
  **drag-to-reorder panels** (HTML5 DnD → `reorderPanel`, jsdom-tested). ⏳
  **REMAINING (browser-bound):** **drag-to-dock zones / free-floating windows +
  tabbed docks** — the remaining deluxe presentation gestures (the
  keyboard/click controls + the resize/reorder drags drive the identical layout
  state).
- **Risk:** L; landed additively — the model + shell are self-contained + fully
  tested, and the default layout is byte-identical to the prior fixed stack, so
  the daw-app render refactor (panels → registry map) is behavior-preserving.

### 3.8 `[~]` IO-2 / IO-N3 / ARR-N4 / PLAT-N1 / FX-N1 — Deep interchange (DAWproject depth, AAF/OMF/FCP-XML, device-state preset) · P2 · effort M-L ✅ DAWPROJECT AUTOMATION DEPTH + FX-N1 DEVICE PRESET DONE 2026-06-08

- [ ] DAWproject round-trip DEPTH: **volume + pan automation + session time
      signature + audio-clip fades** now round-trip (the highest-value missing
      fidelity). `dawproject.ts`: Channel `<Volume>`/`<Pan>` carry `id`s;
      per-track `<Lanes>` emit
      `<Points unit="linear|normalized"><Target parameter="vol-/pan-{id}"/><RealPoint time value/>…`;
      a lane is emitted for any track with clips OR automation; import parses
      `<Points>` → `setVolumeAutomation`/`setPanAutomation` (dB↔linear,
      pan↔normalized). `<TimeSignature numerator>` is now session-derived
      (`beatsPerBar`) on export + read back on import. **ARR-8 audio-clip
      fades** round-trip as standard DAWproject `<Clip fadeInTime fadeOutTime>`
      (in the clip time unit; omitted when zero; 2 new round-trip tests) —
      genuinely interoperable, unlike Euterpe-specific insert params.
      Round-trips through the existing `.dawproject` export/import (palette
      command + import picker). **STILL TODO:** crossfades (cross-clip),
      device/insert state (murky — built-in-effect params don't map to a
      standard device taxonomy), note-expression, time-warp. _2026-09-18: open
      for an agent. The residue is the note's own list: cross-clip crossfades,
      note expression and time-warp round-trip; device and insert state only
      where DAWproject defines a standard device._
- [x] **Aux SENDS round-trip DONE 2026-06-09:** reverb + delay sends now
      interchange via the standard DAWproject
      `<Channel><Sends><Send destination type="pre|post"><Volume.../></Send></Sends>`
      form. `dawproject.ts` emits two effect-role return-bus tracks
      (`aux-reverb`/`aux-delay`, only when any track has sends) so each
      `<Send destination>` resolves to a real channel id; per-track reverb/delay
      send **levels + pre/post-fader** + send-level **automation** (`<Points>`
      targeting `send-rev-/send-dly-{id}`) all serialize. Import skips the
      aux-bus tracks (recognized by id, so engine ids stay aligned) and parses
      sends back to
      `setTrackSend`/`setTrackDelaySend`(+`…PreFader`)/`setSendAutomation`/
      `setDelaySendAutomation`. 3 new round-trip tests (emit-only-when-present /
      level+pre-fader / send automation); full dawproject suite 25 green; app
      tsc 0. (Crossfades aren't a first-class DAWproject primitive — they're
      authored as overlapping clips with complementary fades, which the existing
      `fadeInTime`/`fadeOutTime` round-trip already covers.)
- [ ] AAF/OMF/FCP-XML post-production interchange — pure XML/binary writers.
      **FCPXML DONE 2026-06-08:** `apps/euterpe-studio-web/src/daw/fcpxml.ts` —
      a pure FCPXML 1.10 export+import for Final Cut Pro / DaVinci Resolve /
      Premiere. Each track → one continuous, song-aligned audio stem `<asset>`;
      each arrangement clip → an `<asset-clip>` slicing that stem at its
      timeline boundaries on its own lane. Beats↔seconds through the session
      tempo; sample-accurate rational FCPXML times (`N/Ds`) at a 48 kHz timebase
      (`secondsToFcpTime`/`parseFcpTime`, gcd-reduced); tempo+time-signature
      carried in the base `<gap>`'s `<metadata>` (`md key="com.euterpe.*"`) so
      our round-trip recovers them (FCP preserves unknown `<md>`). Honest
      stem-baked semantics: track structure/names/clip boundaries/tempo/time-sig
      round-trip to sample accuracy; synth→audio (FCPXML knows only audio) and
      per-clip trim collapses to 0 (baked into the song-aligned stem). Wired: a
      "Export FCPXML…" palette command + handler that **renders each track's
      real song-aligned stem** (`engine.renderTrackOffline`+`encodeWav`) and
      zips `project.fcpxml`+`Stems/*.wav` (so the referenced media genuinely
      exists), and `.fcpxml` import in `loadProject` (+ the Open accept).
      **Tests:** 13 pure/jsdom (rational time round-trip/gcd/zero/
      malformed-throw, `stemFileName` slug, well-formed FCPXML structure +
      assets/asset-clips/lanes + gap metadata, empty-session validity, import
      throws on non-FCPXML + tempo-fallback, full export→ import round-trip
      fidelity) + 1 command-wiring test. App tsc 0; full
      `src/daw`+`src/components/daw` suite 1499 green; stub scan clean. (commit
      `feat(euterpe): FCPXML post-production interchange…`.) **AAF substrate —
      CFB/OLE2 container DONE + INDEPENDENTLY VERIFIED 2026-06-09 (incr 1,
      commit `MS-CFB (OLE2) compound-file container`):**
      `apps/euterpe-studio-web/src/daw/cfb.ts` — a from-scratch, dependency-free
      MS-CFB v3 (512-byte sector) Compound File Binary writer **+ reader**:
      header + FAT + a real **red-black directory tree** (siblings sorted by the
      CFB name comparator, provably-valid colouring) + mini-FAT/mini-stream for
      <4 KiB streams + regular sectors for larger ones; documented ≈7 MiB
      header-DIFAT cap (fail-loud beyond it). Verified TWO independent ways like
      the FLAC/ALAC codecs: (1) `readCfb` round-trips the storage tree + every
      stream's bytes bit-exactly — 23 tests incl. **red-black-tree validity
      asserted from the serialized directory** for n=1..9 + fail-loud edges; (2)
      macOS `file(1)` recognises the output as a "Composite Document File V2
      Document" (an independent OLE2 parser's verdict). app tsc 0. ⏳
      **REMAINING — AAF/OMF _object model_ is reference-data-bound (HONEST
      FINDING 2026-06-09, refines the bucket-B assumption):** an _importable_
      AAF needs the AAF **baseline MetaDictionary** — hundreds of
      class/type/property definitions keyed by **exact AUIDs** — plus a real AAF
      validator (the AAF SDK / pyaaf2 carries that baseline as data). **Neither
      is on this box** (`which aafconvert/aaf2` → none; no pyaaf2; no reference
      `.aaf`). Authoring the object graph blind would mean _fabricating AUIDs_
      (wrong + unverifiable) — exactly the anti-fabrication line. So the
      verifiable substrate (the CFB container) is shipped; the importable
      AAF/OMF object graph is gated on the AAF SDK baseline + a validator, like
      the other real-tool-acceptance gates documented across bucket B. (OMF =
      Bento container, same object-model/validator dependency.) _2026-09-18:
      open for an agent under the install-first rule. The AAF SDK is open
      source: build it, write one reference `.aaf` with it, and use its dump
      tool as the validator the note asks for, so that the object graph is
      checked against a real reader instead of authored blind. OMF follows only
      if the same route works._
- [x] **FX-N1 device-state preset (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/device-preset.ts` — capture a track's
      **full device** (instrument `SynthPatch` + insert chain with per-insert
      params/bypass/sidechain + channel strip: gain/pan/pan-law/both aux
      sends/pre-fader flags/bus) into a portable `.euterpe-device.json` (like an
      Ableton `.adg` rack / a Logic channel-strip patch), the file-portable
      counterpart to the localStorage FX/synth-preset libraries.
      `extractDevicePreset` (pure, deep-copy), `serializeDevicePreset`,
      `parseDevicePreset` (validates an untrusted string → `{error}` fail-loud
      on wrong/absent format/version, tolerant: drops non-numeric params +
      clamps strip ranges, never fabricates a half-preset), `applyDevicePreset`
      lowers a preset to the **existing** reducer actions in dependency order
      (configureSynth → setInsertChain → per-insert toggleBypass/setSidechain by
      index → strip setters), and `importDeviceText` composes parse+apply.
      Cross-DAW `.dawproject` device mapping is deliberately NOT faked (no
      standard taxonomy for the built-in-effect param space — the "murky" note);
      a faithful **native** preset is the honest deliverable. Wired: insert-rack
      "⤓/⤒ Device preset" buttons (export → download; import → file picker,
      applied to the selected track) via daw-app
      `saveDevicePreset`/`loadDevicePreset` (the hook does the I/O + fail-loud
      error surface; the component just forwards the File, matching the codebase
      pattern). **Tests:** 9 pure incl. a **full reducer round-trip** (extract
      from a configured source track → serialize → import → apply onto a fresh
      track → the device state deep-equals the source) + parse fail-loud +
      param-drop/clamp + apply ordering; 3 jsdom (buttons omitted without
      callbacks, export invokes the handler, import forwards the File). App tsc
      0; full `src/daw`+`src/components/daw` suite 1429 green; stub scan clean.
- [x] **Tests:** volume+pan automation round-trips (dB/pan recovered within
      float tol; `<Points>`/`<Target>` emitted) + audio-clip fade round-trip
      (`fadeInTime`/`fadeOutTime` recovered; omitted when zero) —
      `dawproject.spec.ts` (22 tests green; tsc 0). + device-preset round-trip
      (9) + insert-rack device buttons (3).
- **Acceptance:** ✅ a DAWproject with volume/pan automation + clip fades
  round-trips; ✅ a track's full device (instrument + FX chain + strip)
  round-trips as a portable preset file (FX-N1); ✅ the arrangement exports as a
  self-contained FCPXML + stems bundle and round-trips timeline
  structure/timing/tempo (2026-06-08). ⏳ AAF/OMF binary post-production writers
  remain (CFB/OLE2 + Bento containers — video-editorial handoff with embeddable
  essence).
- **Risk:** M-L; format fidelity is fiddly but pure + testable (no engine).
  Automation depth + FX-N1 shipped; AAF/OMF is additive.

### 3.9 `[~]` IO-9 / IO-7 / IO-8 — Sample/loop import (SFZ/Decent Sampler), scripting (Lua/Python), OSC/REST control API · P2/P3 · effort M each ✅ IO-9 SFZ + IO-7 DSL + IO-8 OSC/REST + MIDI-18 MCU + HUI-CORE DONE 2026-06-08 (HUI transport/master/feedback + EUCON + Lua host remain)

- [x] **IO-9 SFZ (DONE 2026-06-08):** pure parser
      `apps/euterpe-studio-web/src/daw/sfz.ts` → resolved multi-zone map
      (global/master/group/region inheritance, note-name|numeric keys C4=60,
      `key=` shorthand, lokey/hikey/lovel/hivel, pitch_keycenter,
      tune/pitch/transpose, volume, loop_mode, offset, seq round-robin,
      comments, default_path; unknown opcodes preserved) +
      `regionForNote`/`regionPitchOffset`/`sfzZonePlan`. **Engine key-zones
      (cargo-verified, no-op invariant):** `SampleLayer` gains
      key_min/key_max/root_key, `Sampler::add_zone` + `trigger_note`
      (zone-select + transpose by note−root_key) — a single full-range layer is
      **bit-identical** to the legacy
      `set_rate_semitones(note−60)`+`trigger_velocity` (both step-grid sites now
      call `trigger_note`, all dsp-graph tests green). Wired dsp-wasm/engine
      `add_sampler_zone` → worklet → `addSamplerZone` → daw-app `loadSfz`
      (parse + decode-by-basename + one zone/region) → sampler-panel "Load .sfz"
      multi-file picker. **Tests:** cargo dsp-core 180 / dsp-graph 109 (4 new),
      clippy clean; boundary spec 34; SFZ parser 16 + sampler-panel jsdom; full
      daw 1366. (commit
      `feat(euterpe): SFZ instrument parser + key-zone multi-sample engine`.)
      **Decent Sampler `.dspreset` ALSO DONE** (2026-06-08, commit
      `feat(euterpe): Decent Sampler (.dspreset) parser…`): `decent-sampler.ts`
      parses the XML into the SAME `SfzInstrument` model (groups→group→sample
      inheritance, rootNote/loNote/hiNote, tuning→cents, volume, loop), reusing
      the identical zonePlan→engine path; loadSfz dispatches by extension; 7
      jsdom tests. ⏳ REMAINING: per-region multi-`.wav` LOAD is browser-bound
      (decodeAudioData); held-note polyphony (notes route through PolySynth, not
      the sampler) is the follow-up.
- [x] **IO-7 DONE 2026-06-08 (safe restricted DSL, not a sandbox):**
      `apps/euterpe-studio-web/src/daw/script-host.ts` — `parseScript` (pure)
      compiles a tiny line-based DAW-control language (tempo / synth|audio
      "name" / gain|pan|mute|solo| rename <track> / bounded `repeat <n> … end`
      with `$i`) into typed `ScriptCommand[]` + line-numbered errors;
      `executeScript` maps them to reducer actions, resolving 1-based track
      indices to ids re-read after each command. **No `eval`, no host access,
      bounded loops** (n ≤ 1024, ≤ 10k cmds; nested/unclosed/runaway rejected,
      fail loud — no partial apply) — sidesteps the "no arbitrary code" sandbox
      risk by NOT being a JS/Lua interpreter. Wired: `script-console.tsx`
      modal + daw-app `runScript`
  - command-palette "Automation Script…". 7 pure + 2 jsdom tests; full daw 1391.
    (commit `feat(euterpe): safe automation-script DSL + console`.) ⏳ A full
    Lua/JS scripting host (general expressions/variables/nesting) needs a
    Worker/wasm sandbox (follow-up).
- [x] **IO-8 OSC/REST control surface (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/osc-control.ts` — three pure,
      fully-tested layers: (1) a from-scratch **OSC 1.0 binary codec**
      (`encodeOscMessage`/`decodeOscPacket`: address + comma type-tag string +
      big-endian args `i`/`f`/`d`/`s`/`b`/`T`/`F`/`N`/`I`, 4-byte aligned, plus
      `#bundle` decode + flatten — fail-loud on a misaligned packet); (2)
      **address/route → action** — `oscToAction` (`/transport/play`, `/tempo`,
      `/transport/seek`, `/track/<n>/gain|pan|mute|solo|volume`, `/master/gain`)
      and `restToAction` (`PUT /track/3/gain {db:-6}`, POST/PUT verbs only, GET
      = read) both yield the same typed `ControlAction` (values clamped to the
      reducer's safe ranges; mute/solo read `T`/`F` as absolute or no-arg as
      toggle); (3) **execution** — `applyControl` resolves a 1-based track index
      against the live list (re-read each call) and drives a `ControlRuntime`
      (the reducer, via the app), failing loud on an out-of-range track. A real
      `OscControlBridge` WebSocket client (injectable socket) decodes each
      binary frame → action → runtime, proving the **frame→decode→map→apply**
      chain end-to-end without a network. Wired: daw-app `ControlRuntime`
      adapter
      (play/stop/toggle/tempo/seek-beats→samples@48k/gain/pan/mute-set-or-toggle/solo/master) +
      `RemoteControlPanel` (bridge-URL connect, live status, recent-activity
      log) + command-palette "Remote Control (OSC)…". **Tests:** 22 pure (codec
      round-trip for every type + blob padding + double precision + bundle
      flatten + fail-loud; oscToAction/restToAction for every address/route
      incl. clamping + toggle-vs-set; applyControl track resolution +
      out-of-range; the bridge: binary frame → runtime, text-frame ignored,
      malformed-logged-not-thrown, error→close, bundle-in-order, default
      `new WebSocket` factory) + 4 jsdom (RemoteControlPanel
      connect/disconnect/status/log). App tsc 0; full
      `src/daw`+`src/components/daw` suite 1417 green; stub scan clean. **OSC
      feedback (2026-06-08):** `oscFeedbackMessages(state)` encodes the live
      mixer/transport as the inverse of `oscToAction`'s address set (round-trips
      back through `decodeOscMessages`+`oscToAction`);
      `OscControlBridge.send` +`sendFeedback` push it over the socket when
      connected, and a daw-app effect feeds it on every session change — so a
      TouchOSC/Lemur surface's faders/toggles track the DAW (the surface
      physically reflecting it is the network/hardware tail). ⏳ REMAINING: the
      live UDP/WS **bridge process** (TouchOSC/`osc-js` relay) is the external
      dependency.
- [x] **MIDI-18 MCU control surface (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/mcu-control.ts` — a pure
      Mackie-Control-Universal MIDI decoder `decodeMcu(midi) → ControlAction[]`
      reusing the IO-8 `ControlAction`/`applyControl` executor verbatim (no new
      arbitrary-code path). Decodes **faders** (14-bit Pitch-Bend: ch 0–7 →
      strip gain, ch 8 → master, via `faderToDb`/`dbToFader14` linear-in-dB over
      [−60,+12]), **V-Pots** (CC 0x10–0x17 endless encoders → a new
      `trackPanRelative` action: low bits = ticks, bit 6 = sign), the **jog
      wheel** (CC 0x3c → a `seekRelative` transport scrub), **buttons** (Note-On
      press: Mute 0x10–0x17 / Solo 0x08–0x0f → toggle; transport Play 0x5e /
      Stop 0x5d) — everything else → `[]` (releases + unowned messages). Added
      the `trackPanRelative` variant + an optional `nudgeTrackPan` to
      `ControlRuntime` + `applyControl` handling (unsupported runtime → honest
      `ok:false`, not a silent no-op). **Wired:** a `createControlRuntime`
      factory now backs BOTH the OSC bridge and a new MCU MIDI path; the
      once-installed Web-MIDI handler routes input through
      `decodeMcu`→`applyControl` when the RemoteControlPanel's new **"Control
      surface (MIDI): Off / Mackie Control (MCU)"** selector is set
      (es-localized). **Bidirectional feedback (2026-06-08):**
      `mcuFeedback(state)`/`encodeMcuFader`/`encodeMcuButtonLed` encode the live
      mixer/transport (motorized faders + mute/solo/transport LEDs) — the exact
      inverse of the decode (encode→decode round-trips a fader to its dB) — and
      a daw-app effect `port.send`s them to the MIDI output when MCU mode is on
      (the motors physically moving is the hardware tail). **Tests:** 24 pure
      (every message type incl. master + dB endpoints + 14-bit LSB/MSB, V-Pot
      direction/ticks/zero, mute/solo/play/stop, release/unmapped/truncated →
      []; `faderToDb`/`dbToFader14` round-trip; `applyControl` relative-pan
      resolve/unsupported/ out-of-range; decode→apply chain) + 2 jsdom (panel
      selector renders + reports the mode change; omitted without a handler).
      App tsc 0; full `src/daw`+`src/components/daw` suite 1518 green; stub scan
      clean.
- [ ] **MIDI-18 HUI control surface (CORE DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/hui-control.ts` — a **stateful** HUI (Pro
      Tools) decoder `HuiDecoder.decode(midi) → ControlAction[]` reusing the
      same `ControlAction`/`applyControl` executor. Unlike MCU's self-contained
      messages, HUI rides entirely on Control-Change and is stateful (exactly
      the "stateful" the checklist flagged): a **fader** is a CC _pair_ — MSB on
      CC 0x00–0x07, LSB on CC 0x20–0x27, accumulated into a 14-bit position →
      `trackGain` (reusing MCU's `faderToDb` law); a **button** is a _zone/port_
      pair — CC 0x0F selects the zone, the next CC 0x2F carries `port | 0x40`
      (press), with strip-zone (0x00–0x07) port 0x02 = Mute, 0x03 = Solo →
      toggle; **V-Pots** on CC 0x40–0x47 decode the same relative pan encoding
      (bit 6 = dir, low 6 = ticks) → `trackPanRelative`. Constants follow a real
      Mackie Baby HUI reference (matthewmx86/mackie-hui-osc) + the
      reverse-engineered HUI fader/zone format (Avid's HUI is a proprietary
      frozen legacy protocol — no public spec). **Wired:** the
      RemoteControlPanel selector gains a third option ("HUI (Pro Tools)",
      es-localized); the once-installed Web-MIDI handler routes input through a
      **persistent** `HuiDecoder` ref (statefulness needs one instance across
      messages) → `applyControl` when HUI mode is on. **Tests:** 14 pure
      (`hui-control.spec.ts`: fader MSB-emits- nothing /
      LSB-completes-the-14-bit-pair / mid-travel dB / sticky-MSB; V-Pot
      dir/ticks/zero; zone-then-port mute/solo, sticky zone across ports,
      release/unmapped/no-zone → []; non-CC + truncated ignored; reset clears
      MSB+zone; decode→apply chain for fader/mute/V-Pot) + 1 jsdom (panel offers
      the HUI option + reports the mode change). App tsc 0; full
      `src/daw`+`src/components/daw` suite 1642 green; stub scan clean.
      _2026-09-18: open for an agent. The note names no residue beyond "core".
      What a HUI host still owes a surface is the reverse direction: answering
      the surface's ping, and sending fader, LED and display feedback. Proving
      it on a physical HUI surface needs the hardware and is not claimed._
- [x] **HUI bidirectional feedback (DONE 2026-06-08):**
      `encodeHuiFader`/`encodeHuiButtonLed`/`huiFeedback` — the symmetric
      inverse of the decode: a motorized fader is driven by the **same** CC
      MSB/LSB pair (`dbToFader14`→ the 0x0z/0x2z pair) and a switch LED by the
      **same** zone/port pair (CC 0x0F zone + CC 0x2F `port|0x40`), so a fader
      feedback round-trips back through `HuiDecoder` to its dB and a mute/solo
      LED round-trips back to that strip's toggle. `huiFeedback(state)` emits
      the present faders + every strip's mute + solo LED; a daw-app effect
      `port.send`s them to the MIDI output when HUI mode is on (motors moving =
      the hardware tail). 3 new tests (fader/LED round-trip through the
      decoder + the full feedback stream). Full suite 1686.
- [x] **HUI transport (DONE 2026-06-09):** the transport zone (0x0e) now decodes
      — Rewind (port 1) / Fast-Forward (port 2) → a one-bar `seekRelative` nudge
      (`HUI_TRANSPORT_NUDGE_BEATS`), Stop (port 3) → `stop`, Play (port 4) →
      `play`; Record (port 5) decodes to `[]` (the typed `ControlAction` set has
      no record verb — honest, not fabricated). Sourced from the
      reverse-engineered HUI spec and **corroborated by the protocol's
      structural signature**: across all five buttons the press value equals the
      port with bit 6 set (0x41/0x42/0x43/0x44/0x45) — the same press-bit
      encoding the channel-strip zone already uses, so it rides the existing
      zone/port machinery unchanged. `huiFeedback` now also lights the
      Play/Stop/ Record transport LEDs (zone 0x0e) from
      `HuiSurfaceState.playing`/`recording`. 5 new tests (Play/Stop, Rewind/FFwd
      nudge, Record→[], release no-op, transport-LED feedback); HUI suite 22
      green; app tsc 0. ⏳ REMAINING (HUI follow-ups): the **master** fader +
      **track-select** — their HUI addressing isn't corroborated here, left
      honest rather than guessed. **EUCON** (Ethernet, not MIDI) is a separate
      transport, out of scope for a MIDI decoder.
- [x] **Tests:** SFZ parse → zone map (DONE); a script that adds a track
      produces the right actions (DONE); OSC message → action (DONE — 22
      osc-control tests incl. the full decode→map→apply chain).
- **Acceptance:** ✅ an SFZ instrument loads with zones; ✅ a script automates a
  session; ✅ OSC controls transport (verified via the bridge
  decode→action→runtime chain against a mock runtime). ⏳ a live OSC
  controller + bridge process is the hardware acceptance; MCU/HUI protocol + a
  full Lua/JS scripting host are the documented follow-ups.
- **Risk:** M; scripting needs a careful sandbox (no arbitrary code on the
  user's machine) — addressed by the restricted DSL (IO-7) + the typed
  action-only control surface (IO-8); neither path can execute arbitrary code.

### 3.10 `[x]` PLAT-3 — Internationalization (i18n) framework · P2 · effort L (mechanical) ✅ COMPLETE 2026-06-09 — framework + switcher + EVERY component wrapped in t() (coverage-guard-enforced) + a 100%-translated en+es locale (only proper nouns [Push/MPC/Maschine] + technical codes [ISRC/UPC/LUFS] stay as-is)

- [x] **i18n framework (DONE 2026-06-08, dependency-free):**
      `apps/euterpe-studio-web/src/daw/i18n.ts` — a tiny gettext-style core
      using the **"default language as key"** convention (the English source
      string IS the msgid), so `translate(locale, key, params)` does
      catalog-lookup → `{param}` interpolation → **English fallback** for any
      untranslated key. This makes adoption _non-breaking_: wrapping a literal
      in `t('Play')` renders identically (and existing render tests keep
      passing) until a locale supplies a translation. `LOCALES` (en + es to
      start) + `MESSAGES` (en empty = all keys fall through; es translates the
      wired strings) + `isLocale` guard. React binding
      `components/daw/locale-context.tsx`: `LocaleProvider` (active locale,
      localStorage-persisted, tolerant), `useT()` (the bound translator —
      **works without a provider too**, defaulting to English, so unwrapped
      components are unaffected), `useLocale()`, and a `LocaleSwitcher`
      (`<select>` of locale endonyms, live switching). Wired: the whole DAW tree
      is wrapped in `LocaleProvider` + a compact `LocaleSwitcher` in the app
      header; the **Remote-Control (OSC) panel** is converted end-to-end as the
      first localized surface (title/Connect/Disconnect/Close/status all
      translate in es). **Tests:** 8 pure (default-key passthrough, es lookups,
      untranslated→English fallback, `{param}` interpolation,
      unknown-locale→default, `isLocale`, catalog integrity) + 5 jsdom (useT
      without provider = English, es provider renders Spanish + falls back,
      switcher lists endonyms + switches live
  - persists) + 1 jsdom (the OSC panel renders Spanish under an es provider).
    App tsc 0; full `src/daw`+`src/components/daw` suite 1443 green; stub scan
    clean.
- [x] **Component string-sweep COMPLETE 2026-06-09 (every DAW component
      localization-ready — 26 surfaces):** all user-facing
      `title`/`aria-label`/placeholder literals across the DAW are wrapped in
      `t(...)` with the non-breaking default-key pattern (English source = the
      msgid → text-asserting specs stay green via the English fallback), with es
      translations for the high-value short labels (long technical tooltips fall
      back to English — the translator-pass concern). **Converted:** the
      previously-done set (OSC panel, command palette [whole command surface],
      automation-script console, asset browser, channel-strip) **+ this
      session:** transport bar (core controls + project menu + master-output +
      interchange), piano-roll (32), sampler-panel (19+placeholder), synth-panel
      (18), arrangement-view (17), song-structure-panel (12), session-scenes
      (8), insert-rack (7), chord-track (5), + the 12 small panels
      (version-history/generator/mix-snapshots/master-eq/folders/take-comp/
      audio-output/vca/tempo-map/split-sheet/song-form-bar/step-grid),
      keyboard-shortcuts + workspace-shell, the audio-visualizer analyzers,
      controls/Meter (loudness units), and **daw-app chrome** — which required
      **moving `<LocaleProvider>` up to `app/daw/page.tsx`** (a component can't
      consume the context it provides; `useLocale`/`useT` are no-provider-safe
      so the move is non-breaking). es-provider + English-fallback jsdom tests
      on transport-bar + piano-roll; every changed component's existing spec
      stays green (default-key fallback); app tsc 0.
- [x] **Coverage guard + straggler closure (2026-06-09):** a
      `src/components/daw/i18n-coverage.spec.ts` scans every DAW component and
      **fails if any `title`/`aria-label`/`placeholder` literal is left
      unwrapped** — the "eventual lint rule" implemented as a TEST (no change to
      the app's fragile ESLint config, no whole-app-lint risk). **It immediately
      earned its keep: it caught 15 components my first pass had missed** — all
      the **placeholders** (the original regex only targeted title/aria-label) +
      partial wrapping in channel-strip (11 titles) and transport-bar (31 more
      master/loudness/tags strings). Those are now all wrapped (`useT` added
      where absent). **Genuinely zero hard-coded user-facing literals remain**
      (the guard is green); 406 component + i18n tests pass.
- [x] **es locale 100% COMPLETE 2026-06-09:** translated EVERY wrapped string to
      quality Spanish — the 5 core editing surfaces (transport-bar +
      piano-roll + channel-strip + sampler + synth) then every remaining panel
      (arrangement, scenes, song-structure, insert-rack/device-presets, Meter
      loudness readouts, master-EQ, version-history, generator, take-comp,
      tempo-map, lyrics, app chrome). An extraction over all components finds
      **zero untranslated strings** — only universal proper nouns (Ableton Push
      2 / Akai MPC / NI Maschine) + technical format codes (ISRC / UPC / LUFS /
      dBTP / `ws://host:port`) intentionally stay as-is. ~150 es entries added
      this session; the i18n catalog-integrity spec stays green (no dup keys).
- [x] **Tests:** locale switch renders translated strings; missing-key fallback
      to the default locale; per-component es + English-fallback render tests
      (transport-bar, piano-roll); **the i18n-coverage regression guard** (zero
      unwrapped literals).
- **Acceptance:** ✅ the framework renders ≥2 locales (en + es) and surfaces
  localize live via the switcher; ✅ **no hard-coded user-facing strings remain
  — every DAW component is wrapped in `t()`, enforced by the coverage-guard
  test**; ✅ **the es locale is 100% complete** (every string translated;
  verified zero untranslated). **PLAT-3 is fully done.**
- **Risk:** L (breadth, not depth); touches nearly every component. Framework
  landed non-breaking; the sweep is interleavable safely thanks to the
  default-key fallback (no test churn).

### 3.11 `[~]` UX-7 — Unified semantic asset/sample/preset browser (embeddings + similarity + drag-drop) · P1 · effort L ✅ EMBEDDINGS + FIND-SIMILAR + SAMPLE TIMBRE + UNIFIED BROWSER PANEL DONE 2026-06-08

- [x] New `apps/euterpe-studio-web/src/daw/timbre-embed.ts` — a real,
      deterministic, classic-DSP feature extractor (no neural weights), two
      corpora in one cosine-similarity space:
  - [x] **Sample (PCM):** `sampleFeatures` — spectral **centroid**, **rolloff**
        (85 % energy), **flatness** (Wiener-entropy, ≈1 noise / ≈0 tonal),
        **ZCR**, **crest**, + a coarse 8-octave-band envelope (the perceptual
        axes an MFCC summarizes), from the magnitude spectrum (`fft.ts`) of the
        buffer's loudest window. `sampleVector` log-normalizes the Hz axes +
        centres to ≈[-1,1].
  - [x] **Preset (`SynthPatch`):** `patchFeatures` — a 13-axis timbral
        descriptor mapped from the synthesis parameters (brightness from cutoff,
        harmonic richness from waveform/FM/additive, the percussive↔sustained
        envelope, filter sweep, resonance, LFO motion, unison width, sub weight,
        glide), `patchVector` **perceptually weighted** (brightness ×1.7 +
        richness ×1.5 dominate, the way spectral centroid dominates heard
        timbre).
  - [x] `cosineSimilarity` + `rankSimilar` (seed-excluding, top-N) +
        `brightnessLabel`.
- [x] **Wired:** preset **"≈ Similar"** in `synth-panel` — ranks every factory +
      user preset by cosine-similarity to the live patch and surfaces the
      closest 4 as quick-apply chips (name + match %). Sample **timbre readout**
      in `sampler-panel` — `brightnessLabel` + centroid Hz + tonal/noisy
      flatness for the loaded buffer (memoized).
- [x] **Tests:** 12 pure (`timbre-embed.spec.ts`) — cosine
      identity/orthogonality/zero-vector; deterministic centred patch axes;
      brighter filter ⇒ higher brightness; saw richer than sine, FM/additive
      raise richness; a dark sustained bass ranks closer to another bass than a
      bright pluck; `rankSimilar` over the **real factory catalog** excludes the
      seed + orders descending + beats a bright lead; PCM: high sine centroid >
      low sine, noise flatter/brighter/higher-ZCR than a tone, empty buffer ⇒
      zeros (no fabricated timbre), two noises rank above noise-vs-tone. + 3
      jsdom (preset find-similar surfaces + applies; sampler timbre readout
      shows/omits). App tsc 0; full `src/daw`+`src/components/daw` suite green
      (1257); stub scan clean.
- [x] **Unified browser surface (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/asset-browser.ts` — aggregates the
      scattered preset libraries (factory + user synth patches + FX chains) into
      ONE `BrowserAsset[]` (`buildAssets`/ `collectAssets`), `searchAssets`
      (fuzzy-subsequence over name/category/kind), `rankAssetsByPatch`
      (cross-library timbre ranking — reuses `patchVector`+`rankSimilar`,
      synth-only, FX excluded), and `applyAsset` → reducer actions (synth →
      `configureSynth`, FX → `setInsertChain`).
      `components/daw/asset-browser-panel.tsx` `AssetBrowserPanel` — a
      searchable modal list (kind/source/category badges, `draggable` rows), a
      **"≈ Similar"** toggle (when a seed patch is present) ranking every synth
      asset by similarity to the selected track with a match %, and click-Apply.
      Wired: a "Asset Browser…" palette command (Tracks) + daw-app applies to
      the selected track (the modal-state + seed-patch from `selected`); chrome
      localized via `t()` (es seeded). **Tests:** 9 pure (aggregate
      kinds/sources, fuzzy search, identity-ranks-first + FX-excluded +
      descending + topN, apply→actions) + 6 jsdom
      (list/search/apply/similar+match%/no-seed-hides-similar/es-localized). App
      tsc 0; full suite 1484 green; stub scan clean.
- [x] **Session sample library (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/sample-library.ts` — closes the "samples
      are user-loaded per track, not a collection" gap. A `LibrarySample` is
      `{ id, name, pcm, sampleRate, vector, features }` where the timbre
      vector + features are computed **once** from the PCM via the existing
      `sampleFeatures`/ `sampleVector` (the same cosine space as the preset
      browser). Pure ops: `makeLibrarySample` (no fabricated timbre on an empty
      buffer), `addSample` (replace-by-id-in-place / append, non-mutating),
      `removeSample`, `searchSamples` (fuzzy name), `rankSamplesBySeed`
      (cosine-rank the library by similarity to a seed sample, seed-excluded),
      `sampleTimbreLabel` (brightness · tonal/noisy). **Wired:** daw-app
      registers every file loaded via `loadSampleFile` into a session library
      (id = file name → reloading replaces) + records each track's loaded sample
      as its similarity seed; a refactored `applySampleToTrack` powers both
      file-load and `loadLibrarySample` (re-load a library sample onto any
      track). The sampler-panel gains a **"Sample library"** section: a fuzzy
      search box, a **"≈ Similar"** toggle (when the track has a loaded sample)
      that timbre-ranks the library with a match %, and click-to-load rows (name
      · timbre label). **Tests:** 10 pure (`sample-library.spec.ts`:
      make/empty-buffer, add-replace/append/non-mutating, remove, fuzzy search,
      similarity rank excludes-seed + low-tone-closest-to-low-tone +
      noise-least-similar + topN + absent-seed, timbre label) + 5 jsdom
      (sampler-panel: lists + click-loads, fuzzy filter, similar toggle + match
      % + seed-excluded, no-seed-hides-toggle, omitted-without-handler). App tsc
      0; full `src/daw`+`src/components/daw` suite 1671 green; stub scan clean.
- **Acceptance:** ✅ "find similar" from a seed preset surfaces timbrally-close
  presets; ✅ a unified, searchable browser lists every synth + FX preset with
  cross-library similarity ranking + apply-to-track; samples carry a real
  spectral timbre readout; ✅ a **session sample library** collects every loaded
  sample with timbre search + similarity ranking + load-onto- track. ⏳
  **REMAINING:** **drag-and-drop** placement of an asset/sample onto a track
  (UX-7's headline gesture — browser-bound; the rows are `draggable` /
  click-load is the verifiable equivalent), and **cross-session persistence** of
  the sample library (the decoded audio in IndexedDB/OPFS — the
  browser/OPFS-storage tail; the in-session collection + ranking ships here).
- **Risk:** L; neural embeddings would be bucket C — this classic-DSP feature
  vector is the real, honest in-sandbox version.

### 3.12 `[x]` UX-28 — Wire (or retire) the studio / studio-runtime view-model libraries · P2 · effort M ✅ RE-AUDITED + CLEANED 2026-06-09 (user-approved "clean house first")

- [x] **Deep re-audit DONE 2026-06-09 (4 parallel agents, ~37k LOC, all 18
      modules read):** the old "all unwired / ZERO importers / one consumer"
      claim was **STALE**. Truth: `@euterpe/studio` has **three** live
      `calliope` consumers — `calliope/muse` reference-track-analyzer uses
      `aiFeatures.classifyDrumHits/detectDrumPattern`; `calliope/bridge`
      audio-tools-bridge uses `@euterpe/studio/daw-engine`
      `createStream/pushToStream/getStreamStatus/stopStream`; `calliope/bridge`
      virtuoso-migration-bridge uses `@euterpe/studio/project-management`
      migration registry. **Every one of the 18 modules is REAL production code
      with passing specs — zero stubs found.** It's a parts-bin of SOTA-DAW
      features built but never wired into the shipped app.
- [x] **Clean house DONE 2026-06-09 (user sign-off "clean house first"; audit
      verified zero functionality loss):** removed the 6 genuinely-superseded
      modules —
      `studio-runtime/{coproducer-copilot, creator-experience, operator-admin, review-mastering-delivery}`
      (→ shipped DAW copilot/reducer, `@euterpe/workflows`,
      `@euterpe/master`+`@euterpe/distribution`,
      `@euterpe/ops`+`@euterpe/evals`) and `studio/{timeline, midi-editing}` (→
      shipped DAW view-state + live editing). Barrels updated. **KEPT**
      `studio/daw-engine` (consumed — streaming API, NOT deletable; corrects an
      agent's "remove" error) + `studio/project-management` +
      `studio/ai-features` (consumed). Verified: studio-runtime tsc 0 / 36
      tests; studio tsc 0 / 728 tests; calliope/bridge + calliope/muse tsc 0.
- [ ] **BUCKET D — wire-in the surviving feature surface (the build-on roadmap,
      prioritized):** these are real SOTA-DAW capabilities the shipped app
      LACKS, to port in (DSP algorithms → Rust `dsp-graph` nodes; state/control
      models → the TS reducer):
  1. **`studio/mixer`** ⭐ — arbitrary sends/returns, nestable buses,
     monitor/control-room section (dim/mono/talkback/cue), AFL/PFL solo,
     feedback-safe routing matrix (shipped: 4 fixed buses + 2 hardcoded sends).
     _2026-09-18: open for an agent, in four parts, because "the mixer" is four
     deliverables. Read `apps/euterpe/studio-web/src/daw/routing-matrix.ts` and
     `console-routing.ts` first: a routing matrix already exists, so state what
     it lacks before building. (a) Arbitrary sends and returns per track in the
     reducer and the Rust graph. (b) Nestable buses with cycle refusal, so a
     feedback route is rejected at edit time with the offending path named. (c)
     A monitor section: dim, mono, talkback and cue, outside the master bus. (d)
     AFL and PFL solo. **Verify:** reducer specs for each part, a `dsp-graph`
     test that a bus nested three deep sums to the expected samples and that a
     cycle is refused, and one real-browser check of the mixer surface._
     - [x] **Increment 1 DONE 2026-06-09 — pure routing model:**
           `apps/euterpe-studio-web/src/daw/console-routing.ts` — the console
           signal-routing graph (arbitrary `ReturnBus` + per-track `ConsoleSend`
           pre/post/post-pan + bus→bus output) with **feedback-loop prevention**
           (`wouldCreateFeedbackLoop`), `detectCycles`, `topologicalBusOrder`
           (engine summing order, throws on a loop), `validateRouting` (cycles +
           dangling sends), and a no-op-invariant `defaultConsoleModel` that
           reproduces the shipped 4-bus + reverb/delay-send topology exactly. 11
           Node tests; app tsc 0.
     - [x] **Increment 2 DONE 2026-06-09 — Rust nestable group-bus routing:**
           `dsp-graph` group buses generalized from flat 4→master to
           subgroup→group→master (per-bus output target; topological Kahn
           summing order; gains compound; `set_group_bus_output` rejects
           self/loops). NO-OP INVARIANT proven bit-identical (all→master = the
           prior kernel). Exposed via dsp-wasm. 133 cargo tests (+4), clippy
           clean, wasm rebuilt, 42 WASM-boundary tests (+3).
     - [x] **Increment 2b DONE 2026-06-09 — bridge + reducer wiring:**
           EngineCommand `groupBusOutput` + worklet dispatch;
           `DawSession.groupBusOutputs` + `setGroupBusOutput` action (reuses
           incr-1 `wouldCreateFeedbackLoop` so the reducer rejects loops exactly
           as the engine); session-rebuild replay + project-io persist. 8
           reducer tests; src/daw 1432 green.
     - [x] **Mixer UI DONE 2026-06-09 — per-bus output dropdown:** `BusPanel`
           gains a "→ Master / → Bus N" output selector per group bus, offering
           only loop-safe destinations (filtered via incr-1
           `wouldCreateFeedbackLoop`), dispatching `setGroupBusOutput`. 3 jsdom
           tests (default Master / route / loop-exclusion); app tsc 0. **The
           nestable group-bus routing feature is now COMPLETE end-to-end**
           (model → Rust engine → reducer/persistence → UI).
     - [x] **Increment 3a DONE 2026-06-09 — control-room / monitor section (full
           vertical):** the monitor-only post-master stage (after the master
           meters are read, so it never touches the mix, the render/bounce or
           the loudness/peak readouts) grew from the old engine-only mono/dim
           (no UI state) into a real control-room: **mono** sum, **dim** (now a
           configurable amount), monitor **mute**, **polarity** invert (none / L
           / R / both — flips one side then sums to Mono for the classic L−R
           difference / phase check), and a **control-room level** (the speaker
           volume, independent of the mix bus). Order = polarity → mono →
           level·dim → mute. Wired end-to-end: `audio-engine-web/src/monitor.ts`
           extended (`MonitorPolarity` + `applyMonitor` rewrite, RT-safe
           single-gain collapse) → worklet `engine-processor.template.js`
           mirrors it inline (`this.monitor` state + `setMonitor` case) →
           `messages.ts setMonitor` (+ `dimDb`/`polarity`/`levelDb`/`mute`,
           re-exports `MonitorPolarity`) → `audio-engine.ts` facade (options
           object). App side promoted from transient `useState` in `daw-app.tsx`
           to **reducer/session-backed** (persisted + undoable + rebuilt):
           `types.ts` `ControlRoomState` + `setControlRoom` action;
           `daw-session.ts`
           `DEFAULT_CONTROL_ROOM`/`controlRoomCommand`/`clampControlRoom`
           (level∈[−60,12] dB, dim∈[−60,0] dB) + reducer case emitting the full
           monitor command; `session-rebuild.ts` replays only when engaged
           (no-op default); `project-io.ts` persist + restore (back-compat
           default-merge for older files). UI: `transport-bar.tsx` control-room
           cluster — Mono/Dim/Mute quick toggles + a "CR ▾" popover (polarity
           select, level slider, dim-amount slider). **Tests:** monitor.spec 9
           pure (polarity-precedes-mono difference-cancel, level, level·dim
           compose, mute-overrides) + reducer (no-op default / patch-merge /
           dB-clamp / full-command) + rebuild-replay (engaged-only) + project-io
           round-trip + 3 transport-bar jsdom (toggle-dispatch / state-reflect /
           popover edits). lib tsc 0 + 139 green; app tsc 0; full
           src/daw+components 1775 green; worklet bundle
           rebuilt+synced+source-hash re-stamped; stub scan clean.
     - [x] **Increment 3b DONE 2026-06-09 — AFL/PFL/SIP solo modes (full
           vertical, real engine work):** the binary solo-in-place grew into
           three control-room solo modes. **SIP** (default) = the prior
           behavior, **byte-identical** (the 133 dsp-graph + 202 dsp-core tests
           pass unchanged). **AFL/PFL** are monitor-only **listen buses**: every
           track plays the mix normally (non-destructive — solo no longer
           mutes), and the soloed channels additionally tap their
           **after-fader** (AFL) or **pre-fader** (PFL) signal — both pairs are
           already computed by `strip.process_pre_post`, so the tap is one add —
           into a `solo_bus` that **replaces the speaker feed** after the master
           chain (master-gain scaled, safety-clamped). The full mix is still
           metered + is exactly what a **bounce** renders: `render_offline` sets
           an `offline` flag that **forces SIP** so the listen bus never leaks
           into a render or a per-track stem isolation. Wired end-to-end:
           `track.rs` `SoloTap` enum + `process_solo`/`process_inner` (the 9-arg
           `process` is unchanged → no test churn) → `engine.rs` `SoloMode`
           (Sip/Afl/Pfl, repr u8) + `solo_bus_l/r` + `set_solo_mode` + the
           rewritten solo loop + master-loop output overwrite (click captured
           for reuse, conditional-add to keep −0.0 bit-identity) →
           `dsp-wasm set_solo_mode` → worklet `setSoloMode` → `messages.ts` →
           reducer `soloMode` field + `setSoloMode` action (`SOLO_MODE_CODE`
           map) + rebuild-replay (non-SIP only) + project-io persist →
           transport-bar CR-popover SIP/AFL/PFL selector. **Tests:** 3 cargo
           (PFL-dwarfs-AFL-when-fader-down / no-solo-is-byte-identical-to-SIP /
           offline-bounce- ignores-listen-mode) + 1 WASM-boundary (real compiled
           wasm: soloed faded track, PFL RMS > AFL RMS × 8) + reducer
           (default-SIP / AFL→1 / PFL→2 mapping) + rebuild-replay + project-io
           round-trip + transport-bar jsdom. dsp-core 202 / dsp-graph 136 /
           clippy clean (only the documented Source-enum size warning); boundary
           47; lib 140; app tsc 0; full src/daw+components 1777 green; bundle
           rebuilt+synced+source-hash re-stamped; stub scan clean.
     - [x] **Increment 3c DONE 2026-06-09 — cue/headphone mix (full cross-stack
           vertical, real engine work):** a per-track **pre-fader cue send**
           summing into a control-room **cue bus** that the monitor can listen
           to — a separate headphone mix, independent of the channel faders.
           Built mirroring the 3b AFL/PFL listen-bus so it never touches the
           main mix, the meters or the bounce. `track.rs`: a `cue_send_level` +
           `set_cue_send` + a `process_cue` that taps `pre × cue` into cue
           slices (threaded through `process_inner`/`process_slice` with the
           **empty-slice guard** → `process`/`process_solo` pass empty cue
           slices → **byte-identical**; all 205 dsp-core + 142 dsp-graph tests
           pass unchanged). `engine.rs`: `cue_bus_l/r` + `cue_monitor` +
           `set_cue_monitor`/`set_track_cue_send`; when cue-active (and not
           offline) every track routes through `process_cue` and the cue bus
           replaces the speaker feed after the master chain (forced off during a
           bounce, like solo-listen). `dsp-wasm` setters → worklet →
           `messages.ts` 2 commands. App: `TrackState.cueSend` + `cueMonitor`
           session flag, `setTrackCueSend`/`setCueMonitor` reducers,
           session-rebuild replay (engaged/non-zero only), project-io persist
           (cueSend free via the track spread); UI: a per-strip **Cue Send**
           slider + a **Cue** toggle in the transport-bar control-room cluster.
           (Mic **talkback** = browser-bound mic input → honest seam, not
           built.) **Tests:** 2 cargo (cue-tap pre-fader/scales/zero + the no-op
           invariant via the unchanged suite) + **1 WASM-boundary** (real
           compiled wasm: a −60 dB-fader track is silent in the main mix but
           loud under cue monitoring — pre-fader proven) + 5
           reducer/persist/rebuild + 2 jsdom (strip slider + transport toggle).
           app tsc 0; cargo clippy clean (only the documented Source-enum
           warning); full src/daw+components **2021 green** + engine-web
           boundary **52 green**; wasm rebuilt+synced+source-hash re-stamped;
           stub scan clean.
     - [x] **Increment 4 DONE 2026-06-09 — arbitrary aux RETURNS with their own
           insert chains (the last mixer item; 2 sub-increments, real engine
           work). CLOSES `studio/mixer`.** NUM*AUX_BUSES=4 return buses, each
           receiving a per-track \_parallel* send and running its own **stereo**
           insert chain before folding back into the master — distinct from the
           group buses (routed output) and the 2 fixed reverb/delay returns.
           **Incr 4a (engine + wasm, commit
           `aux returns with per-bus insert chains — engine + wasm`):**
           per-track `aux_send`/`aux_send_pre` (track.rs) threaded via a flat
           `aux_l/aux_r` accumulator (bus k at `[k·n,(k+1)·n)`) through a new
           `process_aux` variant + an aux pair on
           `process_inner`/`process_slice` (empty slices on the common path →
           skipped); engine `aux_l/r` + `aux_gain` + two parallel mono insert
           chains per bus (L/R, like the reverb return's two `Reverb`s — the
           engine is mono-per-channel) + an `aux_active` gate (no track send AND
           no bus insert ⇒ the whole subsystem is skipped → **byte-identical**
           mix, the no-op invariant, proven by the 142 prior dsp-graph tests).
           Aux runs only on the base/render path (solo-listen & cue replace the
           master); the offline bounce forces SIP so a render includes the
           returns; `clear_tracks` drops the chains (rebuild replays) +
           `render_offline` resets their DSP state. dsp-wasm
           `set_track_aux_send`/`set_track_aux_send_pre_fader`/`set_aux_bus_gain`/`add_aux_insert`
           (reverb/delay/chorus/saturator/compressor/distortion/phaser/flanger
           factory, 100% wet)/
           `set_aux_insert_param`/`clear_aux_inserts`/`aux_insert_count`. 5
           cargo (send→master, return insert chain, return gain, L/R pan
           independence, inactive byte-identical) + 1 dsp-wasm binding + 2
           WASM-boundary (real-wasm aux return w/ saturator + unknown-kind
           fail-loud). **Incr 4b (app, commit
           `aux returns — reducer + persistence + UI`):**
           `TrackState.auxSends` + `DawSession.auxReturns {gain,inserts[]}` +
           `AUX_INSERT_KINDS`; reducer
           setTrackAuxSend/setAuxBusGain/addAuxInsert(rejects
           unknown)/clearAuxInserts → EngineCommands → worklet dispatch;
           session-rebuild replay (idle = no-op) + project-io persist
           (free/back-compat); a dockable **AuxReturnsPanel** (per-return gain +
           insert chain add/clear + per-track send sliders). 8 reducer + 4 panel
           jsdom; full src/daw+components **2043 green**; dsp-graph 147,
           dsp-wasm 54, boundary 55; clippy clean; wasm rebuilt+synced. **✅
           MIXER COMPLETE.** Remaining polish (documented): per-return-insert
           PARAM editing in the UI (the engine `set_aux_insert_param`
       - wasm exist; the UI adds inserts with musical defaults), and routing a
         return into a group bus (returns currently fold to master).
  2. **`studio/runtime-sota`** ⭐ — plugin host-planning
     (CLAP/VST3/AUv3/AAX/ARA), Ableton Link, MIDI 2.0 (7→32-bit), Push
     3/MPC/Maschine, sub-30ms remote-tracking budget, drummer Session-Player.
     - [x] **Drummer Session-Player DONE 2026-06-09 (pure-TS vertical):**
           `apps/euterpe-studio-web/src/daw/drum-groove.ts`
           `generateDrumGroove(style, opts)` — genre-aware drum-groove
           generation (real drumming knowledge, NOT a generic euclidean rhythm)
           emitting the engine's `number[][]` step→GM-pitch format so it drops
           straight into a step track via `loadPattern`. Eight styles
           (rock/pop/funk/hiphop/house/dnb/jazz/latin), each with its signature
           kick/snare/hat placement — backbeat, four-on-the-floor, 16th-hat
           funk, swung jazz ride, son-clave latin — plus a deterministic seeded
           end-of-phrase tom fill. UI: a "Drummer" row in the step-grid (style
           dropdown + ▣ Groove button) that derives the bar count from the
           pattern length and dispatches `loadPattern`. **Tests:** 7 pure (rock
           backbeat 2&4 / house four-on-floor + off-beat open hats + clap / funk
           16th-hats + syncopated kick / jazz rides-not-hats / bars·16 length +
           repeat / seeded fill determinism + fill-only-when-asked / every style
           non-empty) + 2 jsdom (rock + house grooves dispatch via loadPattern).
           app tsc 0; full src/daw+components 1845 green; stub scan clean.
     - [ ] ⏳ `runtime-sota`: **MIDI 2.0 model + Push/MPC/Maschine surface maps
           DONE 2026-06-09 (full vertical, pure-TS)**; Ableton Link + plugin
           host-planning remain bucket-C. (a) **MIDI 2.0 7→32-bit**
           `apps/euterpe-studio-web/src/daw/ midi2.ts` — the MIDI Association's
           official **Min-Center-Max scaling** (M2-104-UM):
           `scaleUp`/`scaleDown` + the `cc7to32`/`velocity7to16`/`value14to32`
           (+ inverses) wrappers, verified against the spec's fixed points
           (7-bit 127 → 0xFFFFFFFF, 0 → 0, 64 → 0x80000000; 14-bit center
           preserved; monotonic; round-trips). (b) **Programmable
           control-surface maps** `surface-map.ts` — a parameterized
           `ControlSurfaceMap` + generic `decodeSurface` turning grid
           controllers' user-assignable MIDI into the existing `ControlAction`
           set (relative encoders two's-complement/sign-bit, pitch-bend faders,
           note mute/solo, transport CC/note), with presets `push2` (Ableton's
           documented CC85 transport + CC71–78 encoders), `mpc`, `maschine`
           (user-mode templates) + `generic`. **Wired live:** `daw-app`
           control-surface handler routes MIDI through
           `decodeSurface(SURFACE_MAPS[mode], …) → applyControl` (falls through
           to the note path when no mixer action matches, so pads still play);
           the remote-control panel selector gains all four — alongside MCU/HUI.
           **Tests:** 8 midi2 + 11 surface-map (encoder decodings, Push/generic
           presets, registry) + remote-panel option set. app tsc 0; full
           src/daw+components **2012 green**; stub scan clean. (Ableton Link =
           network peer bridge, bucket-C; plugin host-planning = bucket-C 4.2
           seam shipped.)
  3. **`studio/instruments`** — granular synth + DX7-class multi-operator FM
     matrix (new Rust voices). _2026-09-18: open for an agent. The residue the
     note names is Ableton Link (its SDK is open source: install it and bridge a
     network peer) and the plugin host planning of bucket C; the granular and FM
     voices are item 3 of the same list._
     - [x] **Granular synth was ALREADY wired** (sampler-panel: enable + grain
           size/density/position/spray; `dsp-core` Sampler `set_granular` →
           dsp-wasm `set_sampler_granular`) — don't rebuild it.
     - [x] **Multi-operator (DX-style) FM DONE 2026-06-09 — full vertical
           (closes the instruments gap):** the synth voice grew from
           single-modulator FM to a **4-operator serial stack op4 → op3 →
           op2(`fm_*`) → carrier**. `dsp-core/voice.rs` adds op3/op4 (ratio +
           index + phase) + `set_fm_ops`; each op's instantaneous phase is
           offset by the modulator above it, and `*_index == 0` drops that
           operator — so with op3/op4 off the voice is **bit-identical** to the
           prior 2-op FM (the no-op invariant, asserted `assert_eq!`). Wired via
           the granular-setter pattern: `dsp-wasm set_synth_fm_ops` → worklet
           (in the `configureSynth` case, next to `set_synth_fm`) →
           `messages.ts configureSynth` (+ `fmOp3/4Ratio/Index`) → `SynthPatch`
           fields + reducer command builder + session-rebuild replay +
           project-io (free via the patch) → synth-panel **Op3/Op4 Ratio +
           Amount** sliders. **Tests:** 1 cargo (op3/op4-off bit-identical to
           2-op FM, then op3+op4 enrich the spectrum = more zero-crossings) + 1
           WASM-boundary (real wasm: the op-stack adds zero-crossings) + reducer
           (configureSynth carries the op fields). dsp-core 205 / clippy clean;
           boundary 50; lib 143; app tsc 0; full src/daw+components 1865 green;
           bundle rebuilt+synced+source-hash re-stamped; stub scan clean. (A
           full 6-op + 32-algorithm DX7 matrix is the heavier extension; this
           serial 4-op stack is the contained, complete first cut reusing all
           the synth plumbing.)
     - [x] **Full 6-operator DX7 FM matrix DONE 2026-06-09 — full vertical
           (CLOSES the instruments gap):** the heavier extension noted above is
           now shipped. `dsp-core/fm.rs` `FmCore` — **six** sine operators (each
           ratio or fixed-Hz + output level + its own ADSR) wired through the
           **canonical 32 DX7 algorithms** (transcribed verbatim from the
           MSFA/Dexed `fm_core.cc` byte table + ported bus model: per-op
           input/output mod-bus, `OUT_BUS_ADD` merge-vs-overwrite, averaged
           2-sample feedback incl. the **multi-operator** feedback loop of algos
           4 & 6), the transcription verified against the published DX7 carrier
           chart (algos 1/3/5/22/32). Integrated as a **voice mode** on
           `SynthVoice` (NOT a new `Source` variant): when enabled the FM
           carrier-sum replaces the subtractive oscillator while the voice's amp
           env / filter / velocity / glide / tuning / MPE all still apply (a
           hybrid); **disabled = the exact prior subtractive path,
           bit-identical** (no-op invariant, asserted). Cross-stack via the
           proven recipe: `dsp-wasm set_synth_fm6` bulk setter → worklet (folded
           into the `configureSynth` case) → `messages.ts` `fm6*` fields →
           `SynthPatch.fm6` + `DEFAULT_FM6_PATCH` → reducer (`configureSynth`
           merge) + `session-rebuild` replay + `project-io` persistence (free
           via passthrough; the command is byte-identical when a patch has no
           `fm6`) → a **`Fm6Editor`** in the synth panel (algorithm select +
           feedback + per-operator ratio/level/fixed-Hz/ADSR). **Tests:**
           dsp-core 216 (9 fm — chart match, clean single-carrier sine, additive
           carrier sum, FM sidebands, feedback enrichment, envelope-shaped
           timbre, deterministic, fixed-freq;
       - 2 voice: disabled-bit-identical +
         enabled-routes-through-amp/velocity/release) + dsp-wasm 53 (+1
         binding), clippy clean (only the pre-existing Source-enum size note);
         wasm rebuilt+synced; WASM-boundary 53 (+1 real-compiled-wasm FM
         sidebands); app tsc 0; full src/daw+components **2032** (+11: 7
         reducer/persist/rebuild + 4 Fm6Editor jsdom); stub scan clean. (commit
         `feat(euterpe): 6-operator DX7 FM synthesis voice (instruments)`.) ⏳
         Remaining (optional polish): per-operator velocity/keyboard scaling + a
         true DX7 4-rate/4-level EG (the operator EG is modeled as ADSR — a
         documented approximation; the routing/feedback/algorithm topology is
         exact).
  4. **`studio/effects`** — phaser, de-esser, per-track stereo-widener +
     multiband, FET/opto comp modes, selectable filter slopes (new Rust nodes).
     - [x] **Phaser DONE 2026-06-09 — full vertical:** `dsp-core/phaser.rs`
           (LFO-swept first-order allpass cascade + feedback + log sweep +
           stages; the audit-confirmed gap — engine had chorus/flanger but no
           phaser). 5 cargo tests (true bypass /
           allpass-preserves-energy-but-blend-notches / feedback stable /
           stage-count voicing / silence decay). Wired end-to-end: dsp-graph
           `PhaserNode` → dsp-wasm `add_phaser`/`set_phaser_params` → worklet →
           EngineCommand `addPhaser`/`setPhaserParams` → reducer `'phaser'`
           InsertKind (add + live-edit) → insert-rack "Phaser" button. cargo
           190+133, clippy clean, wasm rebuilt, 43 WASM-boundary tests (+1
           phaser-processes-audio), reducer test, src/daw+ components 1771
           green.
     - [x] **De-esser DONE 2026-06-09 — full vertical:** `dsp-core/deesser.rs` —
           a split-band sibilance ducker; the sibilant band is the
           **phase-coherent lowpass complement** (`band = x − lowpass(x)`, so
           `low+band=x` exactly → true bypass at unity), envelope-followed +
           downward-compressed (freq/threshold/ratio/amount), recombined. 4
           cargo tests (true bypass / ducks-loud-sibilance-but-passes-low-body /
           below-threshold-untouched / higher-ratio-ducks-harder). Wired
           end-to-end: `DeEsserNode` → dsp-wasm
           `add_deesser`/`set_deesser_params` → worklet → EngineCommands →
           reducer `'deesser'` InsertKind → insert-rack "De-Ess" button. 44
           WASM-boundary (+1) + reducer test; lib 132, src/daw+components 1771
           green. (NB: a biquad highpass is phase-shifted so `x − highpass`
           interferes — the lowpass-complement is the fix.)
     - [x] **Multimode filter DONE 2026-06-09 — full vertical:**
           `dsp-core/multifilter.rs` — a sweepable creative-filter insert
           (LP/HP/BP/Notch) built from a **cascade of identical biquads** so the
           slope is selectable (1–4 stages = 12/24/36/48 dB/oct), + resonance
           (Q) + dry/wet (true bypass at mix 0). The "filter slopes" audit gap —
           distinct from the EQ (tone shelves) and the synth's own filter. 5
           cargo tests (true bypass / LP-passes-lows-blocks-highs + HP-opposite
           / steeper-slope-rejects-more / BP-passes-center / notch-cuts-center).
           Wired end-to-end: `MultiFilterNode` → dsp-wasm
           `add_filter`/`set_filter_params` → worklet → EngineCommands → reducer
           `'filter'` InsertKind → insert-rack "Filter" button. cargo 199+133,
           clippy clean, wasm rebuilt, 45 WASM-boundary (+1), reducer test;
           src/daw+components green.
     - [x] **Comp circuit modes DONE 2026-06-09 — full vertical:**
           `dsp-core/dynamics.rs` `Compressor` gains a `CompCircuit` voicing
           (Clean/FET/Opto) reshaping the envelope coefficients — FET ≈4× faster
           attack + 2× faster release (punchy), Opto gentle attack +
           **program-dependent release** that slows as the GR deepens (LA-2A
           self-levelling). **Clean is the default + bit-identical** to before
           (gated; all 202 dsp-core tests incl. multiband/master pass
           unchanged). Wired: CompressorNode `set_param("circuit")` → dsp-wasm
           `set_compressor_circuit` → worklet → EngineCommand
           `setCompressorCircuit` → reducer (`circuit` param on the existing
           compressor insert, default 0; emitted on add/edit + replayed by
           session-rebuild for FET/Opto) → insert-rack **3-button Clean/FET/Opto
           selector**. 3 cargo tests (default-is-Clean, FET-attacks-faster,
           Opto-releases-slower), 46 WASM-boundary (+1), reducer test;
           src/daw+components 1772.
     - [x] **Per-track multiband compressor insert DONE 2026-06-09 — full
           vertical (closes the effects track):** the `MultibandCompressor`
           primitive (master-bus 3-band LR4 split) is now a per-track insert.
           `effect.rs` `MultibandNode` wraps it (mono — fed `(x,x)`;
           `set_enabled(true)`; a focused **8-param** surface = 2 crossovers +
           per-band threshold + per-band makeup gain, with ratio/attack/release
           fixed at musical defaults to keep it manageable; near-transparent
           defaults so adding it doesn't pump). Wired end-to-end via the proven
           recipe: dsp-graph re-export → dsp-wasm
           `add_multiband`/`set_multiband_params` (downcast like dynamiceq) →
           worklet `addMultiband`/`setMultibandParams` → `messages.ts` 2
           commands → `InsertKind 'multiband'` → daw-session buildInsert +
           setInsertParams branches + session-rebuild replay → insert-rack
           "Multiband" button + 8-slider PARAM_CONFIG. **Tests:** 2 cargo (a
           loud 50 Hz tone in the LOW band compresses by band-0 threshold / an
           out-of-band tone is untouched by mid-high thresholds) + 1
           WASM-boundary (real wasm: a saw note compressed across all bands
           renders < bare × 0.9) + reducer (defaults + live-edit emits
           setMultibandParams). dsp-core 204 / dsp-graph 139 / clippy clean;
           boundary 49; lib 142; app tsc 0; full src/daw+components 1862 green;
           bundle rebuilt+synced+source-hash re-stamped; stub scan clean.
     - [x] **Per-track stereo-widener DONE 2026-06-09 — full vertical (CLOSES
           the effects track).** The prior note was half-right (a true M/S
           widener can't be a _mono insert_) and half-wrong (it is NOT moot):
           built as a **channel-strip feature**, not an insert — the strip
           already emits the post-pan stereo pair (exactly like the master-bus
           width), so no "post-pan insert hook / architecture change" was
           needed. And it is _not_ moot for the mono engine: an off-centre mono
           source has correlated-but-unequal L/R, so M/S widening genuinely
           reshapes its image (0 = mono, 1 = unchanged, 2 = wide).
           `dsp-core ChannelStrip.set_width` + a `widen()` mid/side on **both**
           the post- and pre-fader pairs (so the main out + sends inherit it);
           **width 1 is bit-identical** (mid+side = a, mid−side = b → the no-op
           invariant, asserted; 14 prior mixer tests unchanged). Wired:
           `track.set_width` → `dsp-wasm set_track_width` → worklet `trackWidth`
           → `messages.ts` → `TrackState.width` + `setTrackWidth` reducer (clamp
           0..2) + session-rebuild replay (only when ≠ 1) + project-io persist
           (free via the track spread) → channel-strip **Width** slider.
           dsp-core 219 / dsp-graph 148 / dsp-wasm 55, clippy clean;
           WASM-boundary 57 (real-wasm pan collapse-to-mono / widen); app
           reducer 2 + channel-strip jsdom 1; full src/daw+components 2061.
           (commit `per-track stereo width (mid/side)`.) **The same seam-finding
           lesson as the vocoder: the "needs an architecture change" deferral
           dissolved once the right level (the channel strip, not an insert) was
           used.**
  5. **`studio/automation`** — touch/latch record modes, modulation palette
     (audio envelope-follower, perlin/S&H/walk), MIDI-learn-to-any-param with
     curves, Douglas-Peucker thinning.
     - [x] **DP thinning + modulation palette DONE 2026-06-09 (pure-TS
           vertical):** **Douglas-Peucker automation thinning** —
           `automation-lane-helpers.ts`
           `simplifyLane(points, spec, tolerance=0.02)` (iterative DP with
           perpendicular distance in NORMALIZED space — beat over the curve's
           own span, value via the lane's lin/log `valueToNorm`, so `tolerance`
           is a fraction of the lane range + a log cutoff lane thins
           perceptually; endpoints always kept) → a "Thin" button on every
           automation lane (disabled ≤2 points). **Modulation palette** —
           `automation-shapes.ts` gains `triangle`, `sampleHold` (S&H stepped
           levels with square edges), `randomWalk` (bounded, edge-reflected),
           `perlin` (1D value noise, smoothstep-interpolated); the random
           sources use a **seeded LCG** (Numerical-Recipes constants) so a seed
           reproduces the curve exactly (pure — no `Math.random`). The lane's
           shape row auto-renders them (maps over `AUTOMATION_SHAPES`).
           **Tests:** 4 DP (collinear→2 endpoints / keep-a-real-corner /
           tolerance-boundary drop-vs-keep / ≤2-no-op) + shapes (triangle peak,
           seeded-reproducibility + bounds for all 3 random sources, S&H
           held-flat) + 2 jsdom (Perlin fills + Thin collapses a dense collinear
           curve to 2). app tsc 0; automation specs 30 green; full
           src/daw+components green (the 4 unrelated drum-synth/HPSS/piano-roll
           failures were load-induced timeout flakes — all pass in isolation).
     - [x] **MIDI-learn response curves + cutoff target DONE 2026-06-09 (extends
           PLAT-1):** `web-midi.ts` gains `MidiCurve`
           (linear/exponential/logarithmic/inverted) + `applyMidiCurve`
           (reshapes the 0..127 CC value: exp eases in x², log opens fast √x,
           inverted flips the knob) applied in `midiCcToAction` before the
           parameter, a `curve?` on `MidiMapping`, and a new **`cutoff`** target
           (`ccCutoffToHz` log 20 Hz..20 kHz → `configureSynth({cutoffHz})`).
           UI: a per-mapping curve `<select>` + the Filter-cutoff target in
           `midi-learn-panel.tsx`; `daw-app` `setMidiMappingCurve` handler.
           **Tests:** 4 (cutoff log-map endpoints+mid / curve reshaping per type
           / midiCcToAction applies the learned curve / linear passthrough)
       - 1 jsdom (curve select dispatches + cutoff target present). app tsc 0;
         full src/daw+components 1849 green; stub clean.
     - [ ] ⏳ Remaining for `automation` — **ASSESSED 2026-06-09: genuine
           engine-runtime / live-interaction tail, NOT a sandbox-completable
           feature** (documented, not faked, per the anti-half-feature rule):
           (a) **touch/latch live WRITE record modes** — the event-driven
           write-capture (`isWriting` gates fader/pan/send/master moves at the
           playhead) already exists; touch/latch differ from plain write ONLY
           via a **continuous capture loop** that samples the held control value
           every frame during playback (and, for touch, reverts on release
           against live automation playback). Without that loop touch/latch are
           observationally identical to write, so the mode enum alone would be
           an inert control. The loop + pointer-down/up touch tracking is
           browser-interaction/timing-bound (like the other live-recording
           tails). (b) **Audio envelope-follower mod source** — the
           envelope-follower DSP primitive already ships **three times**
           (`autowah.rs` cutoff, `deesser.rs`, `dynamic_eq.rs`) + the MIX-27
           sidechain key-bus is a live audio-envelope→dynamics coupling; a
           _general_ "any track's live envelope → any parameter" mod source is a
           real-time routing paradigm distinct from the symbolic (offline-baked
           LFO/env/step) modulation system — an engine-runtime coupling, not a
           pure helper. Both are correctly left as runtime/interaction tails
           rather than shipped as half-features.
  6. **`studio/clip-hygiene`** — multi-mic polarity-flip + sample-accurate
     time-alignment, breath reduction, batch gain-match. _2026-09-18: "not
     sandbox-completable" no longer holds: the repository drives a real browser,
     and a Playwright run can hold a pointer down across frames. Open for an
     agent, in two parts. (a) Touch and latch write modes: the continuous
     capture loop that samples the held control every frame during playback,
     with touch reverting on release. **Verify:** a browser spec holds a fader
     for two seconds of playback and asserts the written points, and that touch
     returns to the underlying automation on release while latch holds. (b) An
     audio envelope follower as a modulation source, routed from any track to
     any parameter in `dsp-graph`, reusing the follower in `dsp-core`.
     **Verify:** a Rust test drives a gated sine through the follower into a
     gain parameter and asserts the modulated output against a reference
     envelope._
     - [x] **Per-track polarity / phase invert (Ø) DONE 2026-06-09 — full
           vertical (real engine work):** the classic "Ø" button on every
           channel — a mix-affecting polarity invert (negates the track in the
           mix, every send and the bounce), distinct from the monitor-only
           control-room polarity (3a). Implemented on the `dsp-core`
           `ChannelStrip` (a `sign` on the per-sample output so the post-fader
           pair, the pre-fader/send pair and the key bus all inherit the flip;
           **off = sign +1 = byte-identical**, the no-op invariant). Wired:
           `track.rs set_polarity_invert` → `dsp-wasm set_track_polarity` (via
           `track_mut`) → worklet `trackPolarity` → `messages.ts` → reducer
           `TrackState.polarityInvert` + `togglePolarity` action +
           session-rebuild replay (engaged-only) + project-io persist (free via
           the track spread) → channel-strip **Ø** button. **Tests:** 2 dsp-core
           (inverted+plain cancel sample-for-sample / off is bit-identical) + 1
           dsp-graph (inverting one of two identical synth tracks cancels >80%
           of the mix) + 1 WASM-boundary (same cancellation through the real
           compiled wasm) + reducer (toggle + trackPolarity command) +
           rebuild-replay + channel-strip jsdom. dsp-core 204 / dsp-graph 137 /
           clippy clean; boundary 48; lib 141; app tsc 0; full
           src/daw+components 1819 green; bundle rebuilt+synced.
     - [ ] ⏳ `clip-hygiene` DSP — **PURE ALGORITHMS DONE 2026-06-09 (2a)**:
           `apps/euterpe-studio-web/src/daw/clip-hygiene.ts` — the three
           buffer-analysis primitives, verified against synthetic signals with
           known answers. **Time-alignment**
           `findAlignmentLag(reference, signal, maxLag)` = normalized
           cross-correlation over [−maxLag,maxLag] (energy-normalized on the
           overlap so partial overlaps can't win; ties → smaller |lag|),
           recovering a known sample delay exactly (test: 37-sample delay → lag
           37, corr > 0.99; negative lag too). **Batch gain-match**
           `matchGainsRms(buffers, targetDb?)` → per-buffer dB to a common RMS
           target (default = loudest; silent buffers untouched) +
           `bufferRmsDb`/`bufferPeakDb` (full-scale sine = −3.01 dBFS; −6 dB per
           halving). **Breath reduction** `detectSilenceSegments`
           (sliding-window RMS below threshold for ≥ minDuration → spans;
           ignores short dips) + `breathReductionEnvelope` (click-free
           linear-faded ducking gain, deeper-duck on overlap) +
           `applyGainEnvelope`. **Tests:** 16 known-value (RMS/peak dBFS, delay
           recovery both signs + maxLag/empty guards, gain-match
           default/explicit-target/silent, silence gap detect + min-duration
           reject + all-loud, envelope floor/edge-fade/apply). app tsc 0; full
           src/daw+components **1968 green**; stub scan clean. **Remaining
           (2b):** wire end-to-end via `engine.renderTrackOffline` →
           `setTrackGain` (gain-match) / `setTrackDelaySamples` (alignment) + a
           panel. _2026-09-18: open for an agent. The residue is 2b as written:
           wire the three primitives through `engine.renderTrackOffline` to
           `setTrackGain` and `setTrackDelaySamples`, with a panel, verified in
           a real browser._
     - [x] **2b DONE 2026-06-09 — wired end-to-end + UI (CLOSES item 6):** a
           dockable **Clip Hygiene** panel (`clip-hygiene-panel.tsx`, registered
           in `workspace.ts`) drives three `useDawEngine` handlers that render
           tracks offline (`engine.renderTrackOffline` → `interleavedToMono`)
           and apply the 2a DSP via existing reducer actions: **Match levels**
           (render all tracks → `matchGainsRms` → `setTrackGain` per track,
           adding the dB delta), **Align** (render two selected tracks →
           `findAlignmentLag` ±50 ms → `planAlignmentDelays` →
           `setTrackDelaySamples` on the earlier one) and **Reduce breaths**
           (render track → `detectSilenceSegments` → `breathReductionEnvelope` →
           `applyGainEnvelope` → commit as a "(de-breathed)" audio track + mute
           the source, the bounce pattern). `planAlignmentDelays` is a pure
           tested helper. **Tests:** +1 model (planAlignmentDelays both
           signs/zero) + 5 jsdom panel (empty prompt, match-handler, align
           passes both ids, align-disabled-on-same-track, breath passes id). app
           tsc 0; full src/daw+components **1974 green**; stub scan clean. **✅
           ITEM 6 COMPLETE** (time-alignment + batch gain-match + breath
           reduction, DSP + wired + UI).
  7. **`studio-runtime/section-lyrics-editors`** — song-section arranger (energy
     curve / density grid / transition designer)
     - structured lyric/vocal-arrangement editor (delegate rhyme/meter to
       `@euterpe/lyrics`).
     * [x] **Increment 1a DONE 2026-06-09 — section-timeline arranger (full
           vertical, pure-TS):** the unwired 1519-LOC `section-lyrics-editors`
           parts-bin lib is now a shipped DAW feature via an app-native port
           (matching the BUCKET-D precedent —
           drum-groove/midi-harmony/arrangement-form/clip-edit all live in
           `src/daw/`, not lib imports; also sidesteps the lib's
           `section-lyrics-editors.ts:1214` `readonly Foo['field'][]`
           oxc-crash). `apps/euterpe-studio-web/ src/daw/song-structure.ts` — a
           tempo-independent (bars) section timeline where energy + the active
           instrument set ride on each `SongSection` (one serialisable array the
           reducer persists/undoes, vs the lib's 3 parallel structures). Carries
           over the lib's real algorithms verified vs known song-form values:
           12-kind template library (pop/EDM conventions — 8-bar verse/chorus,
           drop at peak energy), gapless **reflow** invariant,
           add/remove/reorder/resize, bars→sec projection, gap/overlap
           **validation**, **energy curve** (peak + mean-adjacent-step),
           **density** classification (sparse/balanced/dense/maximal), and the
           energy-delta **transition designer** (build/drop/sweep/
           fill/bridge/cut with element suggestions). Wired: `types.ts`
           `songStructure`+`nextSectionSeq` + 8 actions; reducer cases (pure
           view-state, `commands:[]`, ids from `nextSectionSeq`);
           `project-io.ts` persist+restore (back-compat default-empty);
           `workspace.ts` "Song Structure" dockable panel;
           `song-structure-panel.tsx` — section ruler (width ∝ bars, peak
           highlighted) with add-from-template / reorder / resize / lock /
           remove + per-section energy meter + density readout. **Tests:** 33
           pure (bars↔sec, templates, reflow/edit ops, projection, validation
           incl. hand-built gap/overlap, energy peak+meanStep, density
           thresholds, all 6 transition kinds) + 9 reducer (id-minting, atIndex,
           edit flow, save/load round-trip + legacy-empty) + 7 jsdom panel. app
           tsc 0; full src/daw+components **1913 green**; stub scan clean.
           **Remaining (1b/1c/1d):** energy/density EDITING UI +
           marker/generator connectors (1b), transition- designer UI (1c),
           structured lyric/vocal editor delegating to `@euterpe/lyrics` (1d).
     * [x] **Increment 1b DONE 2026-06-09 — energy/density EDITING + marker
           connector (full vertical, pure-TS):** the arranger panel grew from
           read-only readouts into a live editor. **Energy:** a per-section
           range slider → `setSongSectionEnergy` (the meter visual stays; peak
           section is highlighted live via `energyCurveOf`). **Density grid:**
           an instrument-rows × section-columns toggle grid
           (`density-{role}-{i}` cells, `aria-pressed`) →
           `toggleSongSectionInstrument` — the "arrangement density editor
           (sparse↔maximal)" subtask, with each section's
           sparse/balanced/dense/maximal class recomputed from its active set.
           **Connector:** a "→ Markers" button + `markStructureSections` reducer
           action projects the AUTHORED structure into timeline markers (bars →
           beats × `beatsPerBar`), using a distinct `"Section: <Kind>"` name
           prefix so it's idempotent AND coexists with the clip-derived
           `markSongSections` ("Section A/B") — manual markers preserved.
           **Tests:** +3 reducer (bar→beat boundaries,
           idempotent-+-manual-preserved, no-collision-with-markSongSections) +
           3 jsdom (energy slider dispatch, density-grid render + cell toggle,
           markers button). app tsc 0; full src/daw+components **1919 green**;
           stub scan clean. **Remaining (1c/1d):** transition-designer UI (model
           shipped in 1a), structured lyric/vocal editor (1d).
     * [x] **Increment 1c DONE 2026-06-09 — transition designer w/ persisted
           overrides (full vertical, pure-TS):** the 1a `designTransition` model
           gained **per-boundary overrides** so the designer is authorable, not
           just inferred.
           `SongStructure.transitions?: Record<boundaryKey, TransitionKind>`
           (back-compat optional); `boundaryKey(from,to)`;
           `setTransitionOverride` (kind | null to clear);
           `transitionsOf(structure, meter)` designs every adjacent boundary
           applying overrides. Overrides are **pruned on reflow** when an
           endpoint is removed (threaded through reflow + all edit ops) and
           **survive** edits that keep both endpoints. Reducer
           `setSongSectionTransition {fromId,toId,kind|'auto'}`; persisted free
           via the songStructure spread. UI: a "Transitions" section in the
           panel — one row per boundary (From→To, an
           Auto/build/drop/bridge/cut/sweep/fill `<select>` that highlights when
           overridden, + the live kind · Δenergy · suggested elements).
           **Tests:** +6 model (boundaryKey, transitionsOf order/count,
           override-honoured, clear-to-auto, reflow-prunes-stale,
           survives-unrelated-edit) + 2 reducer (override+clear, save/load
           round-trip) + 2 jsdom (per-boundary rows + override dispatch,
           single-section shows none). app tsc 0; full src/daw+components **1929
           green**; stub scan clean. **Remaining (1d):** structured lyric/vocal
           editor delegating to `@euterpe/lyrics`.
     * [x] **Increment 1d DONE 2026-06-09 — structured lyric editor (full
           vertical; rhyme/meter DELEGATED to `@euterpe/lyrics`):** the lyric
           half of item 7. `apps/euterpe-studio-web/src/daw/song-lyrics.ts`
           `analyzeLyrics(text)` parses a lyric document into
           `[verse]`/`[chorus]`-tagged lines and, **delegating to
           `@euterpe/lyrics/rhyme-meter`** (`countLineSyllables`,
           `analyzeRhyme`, `detectMeter` — the production phonetic engine, per
           the checklist directive), computes per-line syllable counts, a
           derived end-rhyme **scheme** (A/B/C grouping via pairwise
           `analyzeRhyme` + repeated-end-word grouping), and the dominant
           **metrical foot**. Wiring: narrow oxc-safe
           `@euterpe/lyrics/rhyme-meter` subpath added to `tsconfig.base.json`
           paths + app `vitest.config.ts` test-alias + lib `package.json` export
           subpath + app `package.json` dep (pnpm install clean no-op); session
           `lyrics: string` + `setLyrics` reducer + project-io persist/restore
           (back-compat empty). UI: a "Lyrics" editor in the panel — a textarea
           (`setLyrics`) + a live readout (lines · syllables · scheme · meter,
           then per-line rhyme-label + syllable + section-tag). **Tests:** 7
           analysis (empty, syllable counts vs known values, tag parsing, AABB +
           ABAB scheme derivation, repeated-end-word grouping, meter shape) + 1
           reducer (set + save/load + pure) + 2 jsdom (textarea dispatch +
           analysis render). app tsc 0; full src/daw+components **1939 green**;
           stub scan clean. **Item-7 core COMPLETE** (timeline + energy +
           density + transitions + lyrics). **Remaining tail:** per-section
           vocal-arrangement stack (lead/double/harmony) — 1e.
     * [x] **Increment 1e DONE 2026-06-09 — per-section vocal arrangement (full
           vertical, pure-TS) — CLOSES item 7:**
           `apps/euterpe-studio-web/src/daw/song-vocals.ts` — a stacked
           vocal-arrangement model (lead / double / harmony / adlib voices, each
           with semitone interval + pan + gain) with 5 **production-real
           presets**: solo (centred lead), doubled (lead + two unison doubles
           spread L/R — classic double-tracking), octave (+12), thirds
           (minor-3rd +3 & 5th +7 stack), choir (−5/+4/+7/+12 spread).
           `buildVocalArrangement` / `addHarmonyVoice` (clamps pan/gain,
           relabels to choir) / `removeVoice` (lead-protected) / `voiceCounts` /
           `describeArrangement`. Folded onto `SongSection.vocals?` (auto-prunes
           with the section, persists free); `setSectionVocals` updater; reducer
           `setSongSectionVocalPreset` + `clearSongSectionVocals`. UI: a
           per-section "♪" preset select + a voice-count summary. **Tests:** 9
           model (all 5 preset stacks vs known intervals/pans,
           add/remove/describe) + 2 reducer (attach+persist+clear,
           dropped-with-section)
       - 2 jsdom (preset select dispatch, summary render). app tsc 0; full
         src/daw+components **1952 green**; stub scan clean. **✅ ITEM 7
         COMPLETE** — section-lyrics-editors surface fully wired into the
         shipped DAW (timeline + energy curve + density grid + transition
         designer + structured lyric editor + vocal arrangement).
  8. **`studio/ai-features`** (build-on the symbolic side) — NL mixing commands,
     symbolic chord/key on MIDI clips, MIDI variation generation,
     arrangement/section analysis.
     - [x] **Symbolic chord/key on MIDI clips DONE 2026-06-09 (pure-TS vertical,
           verified vs music-theory ground truth):**
           `apps/euterpe-studio-web/src/daw/midi-harmony.ts` — symbolic harmonic
           analysis straight from note data (no audio/FFT, complementing the
           audio-domain `detectKey` in `audio-analysis.ts`). **Key detection** =
           Krumhansl–Schmuckler key-profile correlation over a
           **duration-weighted pitch-class histogram** (`detectKeyFromNotes` →
           {tonic, mode, name, confidence}). **Chord detection** =
           pitch-class-set template matching (`detectChord`) across 14 qualities
           (maj/min/dim/aug/sus2/sus4/ power + maj7/7/m7/m7b5/dim7/maj6/min6)
           with a `matched − 1.1·missing − 0.55·extra` score (root must sound;
           correctly handles the 5th-omitted dominant 7th and rejects
           rootless/partial guesses; inversion- + octave-invariant).
           `analyzeChordSegments` windows a clip and merges held chords into
           one-per-change; `analyzeClipHarmony` returns {key, chords}. UI:
           `harmony-readout.tsx` `HarmonyReadout` (memoized, pure display) wired
           under the piano-roll toolbar — live "Key: C major · C · F · G · C".
           **Tests:** 14 pure (every triad + 4 seventh types + omitted-5th C7 +
           sus/power + inversion-invariance + KS key on C-major scale / A-minor
           melody / G-major + I-IV-V-I cadence + histogram weighting + windowed
           progression merge) + 2 jsdom (readout renders key+chords / empty
           renders nothing). app tsc 0; full src/daw+components 1802 green; stub
           scan clean.
     - [x] **Roman-numeral harmonic-function analysis DONE 2026-06-09 (extends
           the chord/key detection):** `midi-harmony.ts`
           `romanNumeral(chord, key)` — the numeral base is the scale degree
           (spelled per the key's mode: a major key reads I/II/.../VII on its
           degrees, a minor key likewise; chromatic roots get ♭/♯ accidentals),
           and the CASE (upper = major quality, lower = minor) + suffix (°, +,
           7, maj7, ø7, sus…) come from the chord quality — so a borrowed ♭VII,
           a secondary-dominant II (V/V, uppercase by quality), and a viiø7 all
           read correctly. Surfaced as a third line in the piano-roll
           `HarmonyReadout` ("I · IV · V · I"). **Tests:** 4 pure (I-IV-V
           cadence / ii7-V7-Imaj7 jazz / minor i-iv-v lower-case /
           borrowed-♭VII + V/V + vii° + ø7) + 1 jsdom (the Roman line). app tsc
           0; full src/daw+components 1824 green.
     - [x] **Arrangement / section (song-form) analysis DONE 2026-06-09 (pure-TS
           vertical):** `apps/euterpe-studio-web/src/daw/ arrangement-form.ts` —
           derives a song's structure ("A B A B C") by clustering repeated clip
           content. `clipContentKey` is a stable content fingerprint (sorted
           notes + the placement fields that change what plays — ignores
           id/name/colour/ position), so identical-sounding clips share it.
           `analyzeSongForm` groups clips into section blocks by start beat,
           fingerprints each block as its sorted set of `(trackId, contentKey)`
           pairs, and labels blocks A/B/C… by first appearance (so a repeated
           multi-track section reuses its letter; the same parts on different
           lanes are a different section). UI: `song-form-bar.tsx` `SongFormBar`
           (pure, memoized) above the arrangement — coloured section chips per
           letter. **Tests:** 8 pure (content-key
           identity/order-independence/difference, A-B-A-B / A-A-A / A-B-C-A
           forms, multi-track block grouping, different-lane distinction,
           empty) + 2 jsdom (A-B-A-B chips / empty renders nothing). app tsc 0;
           full src/daw+components 1834 green; stub scan clean. **+ section
           markers from the form (2026-06-09):** a `markSongSections` reducer
           action derives navigation markers from `analyzeSongForm` ("Section
           A/B/…" at each block boundary, per-letter color), replacing prior
           auto-section markers (idempotent) while keeping manual ones; a "⌖
           Mark sections" button in the `SongFormBar`. 1 reducer + 1 jsdom test;
           suite 1861 green.
     - [x] **Extract chords → chord track DONE 2026-06-09 (connects the harmony
           detection to MIDI-9):** a `detectChordTrack` reducer action — the
           inverse of `applyChordTrack` — runs `analyzeChordSegments` on a
           track's note clip and replaces the chord track with the detected
           progression (per-beat detection, held chords merged, ids minted from
           `nextChordSeq`). UI: a "→ Chords" button in the piano-roll.
           **Tests:** reducer (C-then-G clip → chord track ['C','G'] at beats
           0/2 + empty no-op) + jsdom (button dispatches). app tsc 0;
           chord-track + piano-roll specs green; full suite green (the 4
           unrelated drum-synth/HPSS/piano-roll/a11y failures were load-induced
           timeout flakes — all pass in isolation).
     - [x] **NL mixing commands extended for the new features DONE 2026-06-09:**
           `command-parser.ts` gains natural-language verbs for this session's
           additions — **solo mode** ("solo mode pfl", "after-fader listen
           solo", "solo in place" → `setSoloMode`, placed before the plain solo
           toggle), **control-room monitor** ("monitor mono", "control room dim"
           → `setControlRoom`, gated behind the monitor context so the
           bare-"mono" master-width shortcut still works), and **polarity**
           ("invert phase track 2", "flip polarity 1" → `togglePolarity`).
           **Tests:** 3 (solo-mode AFL/PFL/SIP + plain-solo-still-toggles /
           monitor mono+dim + bare-mono-is-master-width / polarity invert) — 63
           green across the whole command surface (parser + copilot +
           daw-commands + script-host). app tsc 0; full src/daw+components 1852
           green.
     - [x] **DONE 2026-06-09 — conditional MIDI variation + deeper NL phrasing
           (full vertical, pure-TS) — CLOSES item 8:** (a) **Conditional
           generation** `apps/euterpe-studio-web/src/daw/midi-variation.ts`
           `generateConditionalVariation` — generates NEW material conditioned
           on the clip + the track's scale (distinct from `varyClipNotes`, which
           transforms): **'response'** (call-and-response — mirror the call's
           rhythm into the 2nd half, invert the contour around the call
           centroid, snap to scale, resolve the last note to the tonic),
           **'embellish'** (fill 3rd+ leaps with an in-scale passing tone),
           **'displace'** (rhythmic swing — keep pitches, push every other onset
           off-grid). Seeded LCG (no Math.random). Reducer
           `conditionalVaryTrack {trackId, mode}` (reads `noteClip` +
           `scaleLock`, grows length to fit the response). (b) **Deeper NL
           phrasing** — `command-parser.ts` gains a name-aware
           `resolveTrackLoose` ("the drums" → Drums track) + qualitative
           descriptors → **parameterized** effect chains: "punchier" → FET
           compressor (medium attack/fast release), "warmer"/"saturate" → gentle
           saturation, "fatten"/"thicker" → compound compressor + saturation;
           plus NL verbs for the 3 conditional modes. (Bare "punch" still maps
           to the transient-shaper insert.) **Tests:** 10 model (3 modes vs
           known music-theory: contour inversion + tonic resolution + in-scale,
           passing-tone insertion, pitch-preserving displacement, determinism,
           empty/amount-0 guards) + 9 integration (reducer
           response/embellish/no-op
       - parser response/embellish/displace/punchier-FET/warmer/fatten). app tsc
         0; full src/daw+components **1993 green**; stub scan clean. **✅ ITEM 8
         COMPLETE.**
  9. **`studio/clip-editing`** (minor) — join/consolidate, clip-reverse,
     repeat-to-fill, crossfade-curve selection. **`studio/generator-track`**
     (small) — provider-agnostic generator-track binding model.
     - [x] **Generator-track binding model DONE 2026-06-09 (provider-agnostic +
           honest seam):**
           `apps/euterpe-studio-web/src/ daw/generator-binding.ts` — a persisted
           `GeneratorBinding` (provider / prompt / drums / styleRef) so a
           generator track's configuration is part of the project (survives
           save/load + undo), not transient panel state. `GENERATOR_PROVIDERS`
           registry (on-device MRT2 now; cloud providers slot in) +
           `generatorProviderStatus` (the on-device provider is available; an
           unknown / cloud provider fails loud with the reason — never a
           fabricated success). `TrackState.generator` + `setGeneratorBinding`
           reducer (patch-merge, view-state only, persisted free via the track
           spread); `addGeneratorTrack` seeds the default. UI:
           `generator-panel.tsx` promoted from transient `useState` to the
           persisted binding (provider select + status readout +
           prompt/drums/style-ref now read/write the binding while still
           steering the live MRT2 stream). **Tests:** 3 pure (default /
           on-device-available / unknown-fails-loud) + 2 reducer (default seeded
           / patch-merge no-cmd)
       - 3 jsdom (provider ready / prompt persists via setGeneratorBinding /
         reflects a saved prompt). app tsc 0; full src/daw+components 1859
         green; stub scan clean.
     - [x] **Reverse + consolidate + repeat-to-fill DONE 2026-06-09 (pure-TS
           vertical):** `apps/euterpe-studio-web/src/daw/ clip-edit.ts` — three
           pure transforms over the arrangement clip model + the shared
           primitive `clipEffectiveNotes` (the notes a clip actually plays on
           its local timeline once the trim offset + `loopWithin` tiling are
           applied — the same placement math the preview renderer draws).
           `reverseClipNotes` mirrors each note in time (its end becomes its
           start; it's its own inverse); `consolidateClipNotes` bakes several
           same-lane clips into one spanning their union, repositioning each
           clip's effective notes by its song offset (offsetBeats 0 / loopWithin
           false result); `repeatCountToFill` is the tiling arithmetic. Reducer
           actions `reverseClip` / `consolidateClips` (same-lane only, mints one
           baked clip) / `repeatClipToFill` (fills up to the next same-lane clip
           or the loop-region end, capped at 256). UI: arrangement-view keyboard
           shortcuts — **R** reverse, **Cmd/Ctrl-J** consolidate (Logic/PT
           "Join"), **T** repeat-to-fill. Clips are standard `ClipState` so
           persistence/rebuild is free. **Tests:** 8 pure (reverse mirror +
           double-reverse-identity / effective-notes tiling+offset-clip /
           consolidate union + null / count-to-fill) + 6 reducer (reverse emits
           resync / consolidate bakes union + <2 no-op / repeat tiles to next
           clip + no-op) + 2 jsdom (R + T shortcuts dispatch). app tsc 0; full
           src/daw+components 1816 green; stub scan clean.
     - [x] **DONE 2026-06-09 — crossfade-curve selection (full cross-stack
           vertical) — CLOSES item 9:** a per-clip `fadeCurve` (0 = equal-power
           [default], 1 = linear, 2 = log/exp) applied in the engine's fade
           rendering, via the proven recipe. `dsp-graph/clip.rs`: a
           `fade_curve_shape(frac, curve)` (equal-power `sin(frac·π/2)` / linear
           `frac` / log `frac²`; `f(0)=0`, `f(1)=1` for all) + a `fade_curve`
           field + `set_fade_curve` + `fade_gain` calls it. **No-op invariant:
           curve 0 = the prior `sin` exactly** (an out-of-range curve also falls
           back to it) — all 141 dsp-graph + 205 dsp-core tests pass, +2 new.
           `dsp-wasm set_track_arrangement` gains a `clip_fade_curve: &[u8]`
           param → applied like `tempo_mult`; worklet passes `clipFadeCurve`;
           `messages.ts` (optional, back-compat). App: `ClipState.fadeCurve`,
           `arrangementCommand` emits `clipFadeCurve`, `setClipFadeCurve`
           reducer (re-emits the arrangement), persist free; an
           Equal-power/Linear/Log selector in the arrangement-view selection
           toolbar. **Tests:** 2 cargo (curve shapes + distinct rendered gains,
           no-op default) + **1 WASM-boundary** (real compiled wasm: a faded
           sample clip's midpoint is quieter under linear than equal-power) +
           reducer (set + emit + clamp + default) + jsdom (selector dispatch) +
           the exact-command test updated. wasm rebuilt+synced+source-hash
           re-stamped; app tsc 0; cargo clippy clean (only the documented
           Source-enum warning); full src/daw+components **2014 green** +
           engine-web boundary **51 green**; stub scan clean. (`generator-track`
           binding model was already done — see item 9's earlier `[x]`.) **✅
           ITEM 9 COMPLETE.**
- **Acceptance:** ✅ re-audit + cleanup done (6 dead modules removed, zero loss,
  all consumers green). ⏳ BUCKET D is the feature-build roadmap (each item = a
  Rust-node and/or reducer port + the cargo/node/jsdom harness).
- **Risk:** LOW for the cleanup (verified zero-loss); BUILD-ON items are M–XL
  each (engine + reducer + UI per feature).

### 3.13 `[x]` PLAT-9 — Screen-reader keyboard-navigable piano-roll / automation (treegrid) · P1 · effort L ✅ DONE 2026-06-08 (piano-roll treegrid + automation-lane keyboard editing)

- [x] Built as a **separate accessible view** (additive, zero-risk) rather than
      retrofitting the 775-line SVG roll: pure navigation model
      `apps/euterpe-studio-web/src/daw/a11y-grid.ts` (13 tests) — a pitch-row ×
      time-step grid over the note clip with `moveCursor`/`clampCursor` (up =
      higher pitch), `cellLabel` (SR readout "C4, beat 1.50, note, velocity
      102"), `toggleNoteAtCursor` (edit-in-place add/remove),
      `nudgeVelocityAtCursor`, `defaultGrid` (2-octave window centred on
      content), `pitchName` (reuses `notation.midiToPitch`).
- [x] `piano-roll-grid.tsx` `PianoRollGrid` — an ARIA **`treegrid`** with
      `role=row`/`rowheader`/`gridcell`, `aria-activedescendant` cursor,
      `aria-selected` + per-cell `aria-label`, arrow-key navigation, Enter/Space
      toggle, `[`/`]` velocity nudge, Home/End, click-to-place, and a polite
      `role=status` live region announcing the cell/edit. Edits dispatch the
      same `setNoteClip` as the visual roll (one clip, two interfaces). Toggled
      on via an "⌨ Accessible grid" button in the roll.
- [x] **Tests:** 13 pure (`a11y-grid.spec.ts`: geometry, cursor clamp, pitch
      naming, cell labels, toggle on/off, velocity clamp/no-op, default
      window) + 6 jsdom (`piano-roll-grid.spec.tsx`: treegrid role +
      activedescendant, arrow nav + live announce, Enter add/remove, `]`
      velocity, click-to-move) + 1 piano-roll toggle test. App tsc 0; full suite
      1301 green; stub scan clean.
- [x] **Automation lanes (2026-06-08):** `automation-a11y.ts` (8 pure tests) —
      `clampCursor`/`breakpointLabel` (lin/log/unit-aware, e.g. "Volume point 2
      of 5: beat 4.00, −3.00 dB") + `nudgeValue` (2 %-of-range linear /
      octave-wise for log cutoff) + `nudgeBeat` +
      `addAfterCursor`/`removeAtCursor`, all over the same
      `LanePoint[]`/`LaneSpec` the visual lane edits. Wired into
      `automation-lane.tsx`: a focusable `role=application` region with
      arrow-key breakpoint navigation + value/time nudge (Shift = ×5 / time) +
      Enter add + Delete remove, a polite status live region, dispatching via
      the same `commit`. 2 jsdom tests (ArrowDown nudges the value via
      `setVolumeAutomation`; Enter adds + announces).
- **Acceptance:** ✅ keyboard-alone navigate + edit **notes** (treegrid) AND
  **automation breakpoints** (the lane region) — jsdom-verified: focus moves by
  arrows, labels announce pitch/time/velocity + breakpoint position/value, edits
  dispatch. ⏳ a real NVDA/VoiceOver **AT pass** + transport-position
  announcements while playing remain (browser/AT-bound).
- **Risk:** L; both landed additive (parallel accessible interfaces), so the
  visual roll + lane are untouched. The actual SR experience still needs an AT
  pass — documented, not claimed.

### 3.14 `[~]` PLAT-N4 — Tempo/timecode sync (Ableton Link / MTC / LTC) · P2 · effort M ✅ MTC + LTC DECODE DONE 2026-06-08 · ✅ JAM-SYNC CHASE-WHILE-PLAYING DONE 2026-06-09 (varispeed rate-lock + LTC-audio + Link remain)

- [x] `apps/euterpe-studio-web/src/daw/timecode.ts` — pure SMPTE timecode + both
      decoders (14 tests):
  - [x] **SMPTE model:** `Timecode` + `timecodeToFrames`/`framesToTimecode`
        (incl. **29.97 drop-frame** with the drop-every-minute-except-tenth rule
        — frame counts round-trip; the 00:00:59;29 → 00:01:00;02 boundary
        verified), `timecodeToSeconds` (29.97 = 30/1.001 wall clock),
        `timecodeToSamples`/`samplesToTimecode`, `formatTimecode` (`;` for df).
  - [x] **MTC:** `MtcQuarterFrameAssembler` reassembles a full timecode from the
        8 quarter-frame nibbles (+ the rate bits → 29.97⇒df); robust to starting
        mid-run (only reports a set that began at piece 0).
  - [x] **LTC:** `decodeLtcFrame`/`encodeLtcFrame` (80-bit SMPTE frame, BCD
        fields + the 16-bit sync word — throws on a sync mismatch / wrong
        length, fail-loud), `modulateBiphase`/`decodeLtcAudio` (biphase-mark FM:
        a `0` = one full-period interval, a `1` = two half-period intervals; bit
        period self-estimated → SR/speed-agnostic) + `findLtcFrame` (sync-word
        alignment). Full **synthesize→modulate→audio-decode→align→parse**
        round-trip recovers the timecode.
  - [x] `timecodeDriftSamples` phase-sync (transport-vs-incoming drift for a
        chase controller).
- [x] **Wired:** a **SMPTE timecode readout** (30 fps) in the transport-bar
      (`samplesToTimecode`+`formatTimecode` of the playhead — a real consumer,
      jsdom-verified); **MTC quarter-frames** (`0xF1`) from the Web-MIDI input
      reassemble and **locate the playhead while the transport is parked**
      (chase-while-stopped).
- [x] **Jam-sync chase-WHILE-PLAYING DONE 2026-06-09 (commit
      `jam-sync timecode chase-while-playing`):** `timecode.ts`
      `chaseDecision(incoming, playheadSamples, sampleRate, opts)` — a pure,
      tested controller: while the transport plays, an incoming MTC frame
      re-locates it **only when drift exceeds a ~20 ms lock window** (jam-sync),
      so inside the window it free-runs from its own sample clock (no zipper).
      daw-app's MTC handler now chases while playing — reading the live
      `transport.playheadSamples` and dispatching `seek` on
      `chaseDecision().seekTo` — not only while parked. **Tests:** +5 pure
      (free-run within the window / re-seek when ahead+behind past it /
      exclusive threshold boundary / custom window); 19 timecode tests green;
      app tsc 0; stub scan clean.
- [x] **Tests:** 14 pure (`timecode.spec.ts`) + 1 jsdom (transport-bar
      readout) + 5 chase-controller pure. App tsc 0; stub scan clean.
- **Acceptance:** ✅ MTC/LTC frame decode + drop-frame + phase-sync verified
  from synthesized streams; ✅ MTC locates the parked transport; ✅ **jam-sync
  chase while _playing_** (re-locate on drift past the lock window — controller
  verified). ⏳ **REMAINING (engine/hardware/network-bound):** a _continuous_
  **varispeed rate-lock** (smoothly trimming playback speed to the external rate
  — a transport-engine change, vs. the jam-sync re-seek now shipped); the actual
  LTC **audio input** front-end; **Ableton Link** (UDP/WebSocket peer bridge —
  partial bucket-C); live-hardware MTC slaving (the MIDI-surface hardware tail).
- **Risk:** M; the decoders + the jam-sync chase controller are fully local +
  verified. Continuous varispeed rate-slaving + Link's transport are the
  network/engine remainder, documented not faked.

---

## 4. BUCKET C — External-blocked (cannot complete in this sandbox)

> These need native binaries, a backend server, paid API creds, or large model
> weights — none available here. **For each, ship the fail-loud seam now** (a
> typed boundary that refuses to fabricate: throws `not_configured`, returns
> `{configured:false}` / a 503 gate, or a capability flag reading "unavailable")
> and document the exact resource to finish. Fail loud beats fake success. Do
> NOT wire the provenance SIMULATION lib as if it were real.

### 4.1 `[ ]` ENG-1 / PLAT-2 — Native low-latency audio I/O (CoreAudio / ASIO / WASAPI / ALSA) · P1 · XL

- Needs a native shell (Tauri + `cpal`) running a native audio thread;
  impossible in a browser tab.
- **Seam:** keep the WebAudio path; expose a `capabilities.nativeAudio = false`
  flag + a "native driver (desktop build)" note.
- **To finish:** Tauri desktop build hosting the Rust engine on a native
  callback; bridge transport/meters over IPC.

### 4.2 `[~]` FX-11 / IO-4 / MIX-29 / AI-22 / UX-15 / REC-13 / PLAT-N5 / ARR-17 — VST3 / CLAP / AU plugin hosting · P0/P1/P2 · XL ✅ FAIL-LOUD SEAM SHIPPED 2026-06-08

- Needs a native plugin-host subprocess loading binary plugins; browsers can't.
  (In-browser WAM hosting is a narrower, separate path that COULD be local —
  consider WAM as a bucket-B alternative.)
- [x] **Seam SHIPPED (2026-06-08):**
      `apps/euterpe-studio-web/src/daw/plugin-host.ts` — a typed `PluginHost`
      interface (`scan`/`instantiate`) + `webPluginHost` whose ops throw
      `NotConfiguredError` (`code:'not_configured'`) instead of fabricating an
      empty success; a `CapabilityStatus` (reason + how-to-enable);
      `isNotConfigured` helper. insert-rack surfaces an honest "VST3/CLAP/AU
      hosting is desktop-build only" note. 4 unit + 1 jsdom test. (commit
      `feat(euterpe): fail-loud plugin-host seam…`) **To finish (real
      hosting):** native host (JUCE/clap-host) in the Tauri build, or implement
      WAM (Web Audio Modules) hosting behind the same interface.

### 4.3 `[ ]` COLLAB-1/2/4/5/6/7/9/10/11/12 · AI-16 · UX-22 · IO-13 · PLAT-N7 — Real-time collaboration + cloud projects · P0/P1 · XL

- Needs a backend: WebSocket CRDT sync server, Postgres + object store,
  auth/roles, presence, share links, activity feed, cloud render. COLLAB-3/8
  (local versioning/merge/branching) are the offline analog already shipped.
- **Seam:** the local version-store + merge UI stay; a `collabClient` interface
  returns `{connected:false}`; share/presence UI gated behind a "requires
  backend" capability. **To finish:** stand up the sync service
  (y-websocket/Liveblocks + DB/S3).

### 4.4 `[ ]` ARR-N1 / REC-N1 / IO-N4 / UX-25 / COLLAB-21 — Neural 4-stem separation (Demucs/HTDemucs) · P0/P1/P2 · XL

- Needs hundreds-MB model weights + an inference runtime (ONNX/candle in wasm or
  native). Absent here.
- **Seam:** a `StemSeparator` interface returning `{available:false}`; UI offers
  it as "download model / cloud" disabled.
- **To finish:** bundle/download Demucs weights + an in-browser ONNX-Runtime-Web
  (or native) inference path.

### 4.5 `[ ]` AI-14 / AI-1 / AI-12 / AI-7 / AI-N3 — On-device + cloud generative AI (Magenta-RT, Suno/Udio/Stable Audio, voice clone, inpaint, speech-to-text) · P1/P2 · XL

- On-device (AI-14) needs candle forward-pass + 2.4B weights; cloud
  (AI-1/12/7/N3) needs paid API creds. The BFF resolver + control-plane are
  fail-closed already (per the MRT2 memory).
- **Seam:** providers register from creds and fail-closed without them (existing
  pattern); on-device shows "model not loaded".
- **To finish:** wire the candle forward-pass + weights (on-device) or supply
  API creds (cloud) at deploy.

### 4.6 `[ ]` MIX-35 / RIGHTS-1(real) / RIGHTS-2(real) / RIGHTS-11(provider) / MASTER-N2 / AI-19(real) — Real C2PA Content Credentials + SynthID watermark · P0/P1 · L (but crypto-dep + sim-lib blocked)

- The current `@euterpe/provenance` lib is a SIMULATION (FNV 'ed25519-sim',
  64-bit-into-number[]). Real C2PA 2.1/ISO 22144 = JUMBF/COSE manifests with
  real Ed25519 (server-side key custody — client signing is forgeable); real
  SynthID = a robust PCM audio watermark surviving compression/pitch-shift.
  Provider verification (RIGHTS-11) needs proprietary detectors.
- **Seam:** the text-label provenance + own-watermark round-trip stay (honest,
  labelled); a `c2pa.sign()` interface throws `not_configured` without a signing
  key. **To finish:** a real C2PA lib (`c2pa-rs` wasm) + server-side key mgmt +
  a real PCM watermarker; this is a dedicated build, not lib-wiring.

### 4.7 `[~]` MASTER-11 / MASTER-8 / MASTER-N5 — Atmos authoring, stem-mastering, master dynamic/spectral EQ · P1/P2 · M-XL ✅ MASTER-N5 DYNAMIC-EQ INSERT DONE 2026-06-08 (Atmos/stem-mastering remain external)

- MASTER-11 Atmos = the surround engine (ENG-N3, bucket A-XL). MASTER-8
  stem-mastering needs stem separation (4.4, blocked).
- [x] **MASTER-N5 dynamic EQ (DONE 2026-06-08, real DSP — NOT the lib
      planner):** a from-scratch **per-band dynamic-EQ insert** built via the
      proven cross-stack effect recipe (so it works on ANY track incl. the
      master bus, not display-only). `dsp-core/dynamic_eq.rs` `DynamicEq` — a
      peaking `Biquad` EQ whose gain is pulled **down** when the band's own
      level (isolated by a sidechain band-pass + a peak-tracking envelope
      follower) exceeds `threshold_db`, by `ratio`, bounded by `range_db`
      (down-ward dynamic EQ / de-essing / resonance taming). RT-safe: detector +
      envelope per sample, the peaking biquad recomputed at most once per block
      (the AutoWah pattern); a 0 dB band that never triggers is a **bit-exact
      pass-through** (a 0 dB peaking biquad has transfer function 1).
      `dsp-graph` `DynamicEqNode` (AudioEffect + `set_param`); `dsp-wasm`
      `add_dynamiceq`/`set_dynamiceq_params`; worklet + `messages.ts` (add/set
      commands); `InsertKind 'dynamiceq'`; daw-session buildInsert +
      setInsertParams; session-rebuild; insert-rack "Dyn EQ" + PARAM_CONFIG
      (freq/q/gain/thr/ratio/ range). **Tests:** 5 cargo (below-threshold-0 dB
      pass-through; loud in-band tone cut > 6 dB + attenuated RMS; out-of-band
      tone untouched; reduction grows with level; reset) + 1 WASM-boundary (the
      rebuilt artifact's `add_dynamiceq` cuts a loud 440 Hz fundamental — bare
      RMS > dyneq RMS) + 1 reducer (add + live setInsertParams). dsp-core 185 /
      dsp-graph 109 / clippy clean; boundary 35; app 1485; tsc 0; wasm
      rebuilt+synced.
- MASTER-N5 was previously ≈ partly covered by the multiband (MIX-23); the lib's
  `dynamicEqResponse` is a PLANNER (wiring it as a realtime processor would be a
  display-only stub — NOT done). This is the **real** per-insert dynamic-EQ DSP
  node.
- **Seam / note:** multiband stays as the master glue; the dynamic-EQ insert is
  now the surgical per-band dynamics. Atmos (MASTER-11 = surround engine
  ENG-N3) + stem-mastering (MASTER-8 = needs neural stem sep, 4.4) remain
  external-blocked.

### 4.8 `[~]` MASTER-18(deliver) / RIGHTS-N1 / MASTER-12 / RIGHTS-14 — Distribution: DDEX delivery, ISRC/UPC assignment, DistroKid/Tracklib APIs · P1/P2/P3 · M ✅ ERN BUILDER + UPC VALIDATION + FAIL-LOUD SEAM DONE 2026-06-08

- The EXPORT METADATA (ISRC/ISWC/UPC fields, DDEX AI-disclosure flags) is
  local + partly done (MASTER-18 tags). The DELIVERY (DDEX ERN ingest to a
  distributor, ISRC auto-assignment, sample-clearance lookup) needs distributor
  API creds + DDEX endpoints.
- [x] **DDEX ERN builder + delivery seam (DONE 2026-06-08):**
      `apps/euterpe-studio-web/src/daw/distribution.ts` — the
      release-notification message a distributor ingests is **built locally +
      real**: `buildErnMessage(release, opts)` is a pure, deterministic
      DDEX-ERN-4.3-structured `NewReleaseMessage` serializer (MessageHeader,
      ResourceList of `<SoundRecording>`s with canonicalized ISRC + ISO-8601
      `<Duration>`, ReleaseList `<Release>` with the UPC as `<ICPN>` +
      display/P-line/C-line/ genre + resource refs, a worldwide
      `<ReleaseDeal>`), carrying the **DDEX AI-content disclosure**
      (`IsCreatedUsingArtificialIntelligence`, release default + per-track
      override). `validateUpc` is the **real GS1 GTIN-12 mod-10** check (a known
      code like `036000291452` passes, any flipped digit fails); `validateGtin`
      extends it to **UPC-A or EAN-13** (the global barcode;
      `4006381333931`/`5901234123457` pass) and a **UPC/EAN barcode field** in
      the tags panel (redden-on-invalid like ISRC) feeds the ERN `<ICPN>`;
      `formatDuration` → ISO-8601; `validateIsrc`/`formatIsrc` reused from
      `metadata-tags`. The **delivery** (uploading the ERN + audio to a
      distributor) is the fail-loud seam: `unconfiguredDistribution.deliver()`
      throws `NotConfiguredError` (reused from `plugin-host`) rather than
      fabricating a "delivered" success; `distributionCapability()` is surfaced
      in the build's capability report (`capabilities.ts` gains the
      `distribution` entry — the last missing bucket-C seam). **Wired:** a "⤓
      DDEX ERN" button in the transport-bar MASTER-18 tags panel builds a
      release-notification XML from the entered metadata + downloads it.
      **Honesty:** the serializer targets the ERN 4.3 structure but does not run
      XSD validation — documented to validate against the distributor's official
      DDEX XSD before a production upload. **Tests:** 15 pure/jsdom (UPC
      known-good/bad/length/non-digit; `formatDuration`; ERN well-formed +
      MessageId/recipient + SoundRecording/ISRC/duration + ICPN/UPC-stripped +
      AI-disclosure default+override + worldwide deal + XML escaping +
      determinism + minimal-omits-optional) + a capability assertion + a
      transport-bar ERN-button jsdom test. App tsc 0; full
      `src/daw`+`src/components/daw` suite 1531 green; stub scan clean.
- **To finish (external):** distributor (DistroKid/TuneCore) API creds + a DDEX
  ERN 4.3 SFTP/ingest endpoint for the actual upload; ISRC auto-assignment +
  sample-clearance lookup (registry APIs).

### 4.9 `[ ]` AI-4(deep) / AI-N2 / AI-N5(deep) / MIDI-13 — Melodyne-grade pitch correction, cross-channel masking EQ, deep AI session players · P1 · L-XL

- AI-4 polyphonic pitch correction shipped a real YIN+WSOLA monophonic version;
  Melodyne-grade POLYPHONIC DNA editing needs a much heavier algorithm
  (research-grade). AI-N2 cross-channel smart:EQ + AI-N5/MIDI-13 chord-reactive
  real-time session players need either ML models (bucket C) or substantial new
  DSP/heuristics.
- **Seam / note:** the monophonic scale-tune + the 55-LOC accompaniment helper
  stay; deeper versions are research-tier.

### 4.10 `[ ]` RIGHTS-7 / RIGHTS-8 / RIGHTS-10 / RIGHTS-15 — Blockchain rights registry, royalty AMM, GDPR console, copyright-similarity scan · P2/P3 · varies

- RIGHTS-7/8 (on-chain registration, royalty AMM) = bleeding-edge, not in any
  flagship DAW; out of the SOTA-DAW bar.
- RIGHTS-10 (GDPR/CCPA console) = platform/account-layer org tooling, not a
  per-project DAW surface.
- RIGHTS-15 (melody/lyric copyright-similarity scan) needs a known-melody CORPUS
  (licensed dataset) to be meaningful.
- **Seam:** these are platform/legal-layer or corpus-bound; document as
  out-of-scope-for-the-DAW-edit-surface or corpus-blocked.

---

## 5. Suggested sequencing (highest leverage first)

1. **ENG-3 PDC** (2.1) — unlocks correct latency for FX-9s/spectral + is a clean
   no-op-invariant engine win. **Do first.**
2. **ENG-2 sub-block automation** (2.2) — quality win, contained once the no-op
   invariant test is in place.
3. **Take comping + recording capture** (3.3 + 3.4) — a coherent recording
   subsystem; high user value.
4. **MusicXML interop** (3.1, pure half) and **codec export** (3.6) —
   standalone, testable, no engine risk.
5. **STFT engine → FX-9s + ENG-14** (2.5 + 2.3) — build the STFT framework once,
   two features fall out.
6. **MPE** (3.2) and **dockable panels** (3.7) — large but high-value UX; budget
   a full session each.
7. **i18n** (3.10) — mechanical breadth sweep, do in isolation.
8. **Surround/Atmos** (2.7, ENG-N3) — XL channel-count refactor; sequence last
   among engine work.
9. **Bucket C** — ship every fail-loud seam now (cheap, honest); finish each
   only when its external resource is provisioned.

## 6. Effort × risk matrix (quick reference)

| Item                 | Bucket | Effort | Hot-path risk | Verifiable here            | No-op invariant available  |
| -------------------- | ------ | ------ | ------------- | -------------------------- | -------------------------- |
| ENG-3 PDC            | A      | L      | Medium        | Yes (cargo align test)     | Yes (len==0 identical)     |
| ENG-2 sub-block auto | A      | M      | Med-High      | Yes                        | Yes (empty auto identical) |
| ENG-14 FFT window    | A      | M      | Low           | Yes (pure FFT)             | N/A (additive path)        |
| ENG-9/IO-N2 warp     | A      | L      | Medium        | Yes (transient proxies)    | Yes (WSOLA default)        |
| FX-9s spectral       | A      | L      | Med-High      | Yes (needs STFT)           | N/A                        |
| ENG-N2/11 SIMD       | A      | M-L    | Medium        | Yes (scalar-equiv)         | Yes (scalar fallback)      |
| ENG-N3 surround      | A      | XL     | High          | Partial                    | N/A (new bus path)         |
| ENG-7/N1 OPFS+SAB    | A/B    | L      | Low-Med       | Partial (ring pure)        | N/A                        |
| Notation/MusicXML    | B      | L-XL   | None          | Pure yes / UI browser      | N/A                        |
| MPE                  | B      | L-XL   | Medium        | Yes                        | Yes (channel mode default) |
| Comping+capture      | B      | L      | Low           | Pure yes / mic browser     | N/A                        |
| PWA                  | B      | L      | None          | Pure yes / offline browser | N/A                        |
| Codecs               | B      | M      | None          | Yes (round-trip)           | N/A                        |
| Dockable panels      | B      | L      | None          | jsdom / drag browser       | N/A                        |
| i18n                 | B      | L      | None          | Yes                        | N/A                        |
| Bucket C (all)       | C      | varies | None          | Seam only                  | N/A                        |

---

_Conventions: keep app `tsc -p tsconfig.tmpcheck.json` at 0; run the full
`cargo` + WASM-boundary + jsdom harness per item; preserve the no-op invariant;
ship fail-loud seams for external work; commit + push to BOTH refs; never wire
the provenance simulation as if real; never mark a box without the code + tests
behind it._
