# Euterpe Deferrals — Actionability Analysis & Plan (2026-06-07)

During the 2026-06-07 session 16 backlog items shipped (the full S/M tier + all
five L/XL items: MIX-7, ENG-20, MIDI-19, MIX-11, ENG-5). Along the way several
pieces were **deferred** — each with a stated rationale, none stubbed. This
document re-examines every deferral against the actual code, classifies it, and
gives a concrete plan for the genuinely-actionable ones.

Legend — **Actionability:** ✅ clean local · ⚠️ local with caveats · ⛔ blocked
(external). **Effort:** S (≤½ day) · M (1 day) · L (multi-day).

---

## Tier 1 — Actionable now, high value (do these first)

### C. Insert-param automation lane re-map on insert move/remove — ✅ S

**Status:** a real correctness gap in shipped MIX-7. Lanes are keyed by
`insertId` (== chain index); `moveInsert` renumbers ids `0..n` by position
(daw-session.ts:1183), so a lane keeps its old index and then drives the _wrong_
insert after a reorder. `rebuildCommands` already guards on existence, so a
deleted insert's lane is dropped on rebuild, but a _moved_ one mis-targets in
live state. **Plan:**

1. `moveInsert` reducer: build the old→new id permutation from the reorder and
   remap each `track.insertParamAutomation` lane's `insertId` through it (same
   map already applied to insert ids). It already returns
   `rebuildCommands(next)`, so the engine re-syncs.
2. If/when a `removeInsert` action exists, drop lanes for the removed id and
   decrement higher ids (today inserts are only added/moved +
   whole-chain-replaced via `setInsertChain`, which already rebuilds; verify
   before adding).
3. Tests (daw-session.spec): move an insert that carries a lane → the lane
   follows it; rebuild emits `setInsertParamAutomation` on the new index. **Why
   first:** smallest, fixes a live bug in a just-shipped feature.

### A. EQ-band parameter automation — ✅ M

**Status:** MIX-7 automates any insert param via `AudioEffect::set_param`, but
EQ band params (`band{N}_freq/gain/q`) were excluded because `ParametricEq` only
had `set_band(i,freq,gain,q)` (no granular setter) — and the automation-lane
param list is sourced from insert-rack's `PARAM_CONFIG`, which has no EQ entries
(EQ uses its own band editor). **Both blockers are trivial:** `ParametricEq`
already stores per-band `{freq, gain_db, q}` (eq.rs:9-13), so a granular setter
just re-derives keeping the other two. **Plan:**

1. dsp-core `eq.rs`: add `set_band_gain(i, db)`, `set_band_freq(i, hz)`,
   `set_band_q(i, q)` (each calls `set_band` with the stored values for the
   unchanged two).
2. dsp-graph `effect.rs` `EqNode::set_param`: parse keys `band{N}_gain|freq|q`,
   route to the granular setter. (EQ was the one node left with the default
   no-op `set_param`.)
3. UI: surface EQ band params as automatable. Either add EQ rows to
   `PARAM_CONFIG` (`band2_gain` −24..24 dB, `band2_freq` 20..20k **log**,
   `band2_q` 0.1..18) or special-case `eq` in the automation-lane selector
   population. The lane editor + reducer + rebuild all work unchanged (generic
   `insert:<i>:<key>`).
4. Tests: cargo (`EqNode::set_param("band2_gain",6)` shifts the band's
   `magnitude_at`); DAW (automation-lane lists the EQ band params; emits
   `setInsertParamAutomation` key `band2_gain`). **Why high:** EQ is the
   most-automated insert in any DAW; this closes MIX-7's only real gap.

### B. MIDI-17 conditional conditions (`if-prev` / `if-not-prev`) — ✅ M

**Status:** ratchet + probability + ratio/`first` trig conditions shipped; the
Elektron _conditional_ family was deferred as "needs cross-note sequence state".
**It is deterministically computable** — `NoteClip.advance` already processes
start-sorted notes per loop iteration. **Plan:**

1. clip.rs: before building `desired`, compute `played: Vec<bool>` over the
   (already start-sorted) notes for this `iteration`:
   `played[i] = cond_seq(note[i].condition, iteration, prev=played[i-1]) && probability_gate(i,iteration)`.
   Add a `note_condition_passes_seq(code, iteration, prev_played)` that handles
   new codes `11=if-prev`, `12=if-not-prev` (delegating the stateless codes to
   the existing `note_condition_passes`). `desired` then uses `played[i]`. First
   note's `prev = false` (no predecessor). O(n)/block, pure fn of the playhead →
   still deterministic + offline-reproducible.
2. UI: append `if-prev`/`if-not-prev` to `NOTE_CONDITION_LABELS` + the
   Ctrl/Cmd-scroll cycle. **No new plumbing** — `condition` is already a wired
   u8 field.
3. Tests: cargo (a note with `if-prev` sounds iff its predecessor fired this
   pass; chains resolve through the recurrence) + the label/cycle helper. **Why
   high:** completes MIDI-17; reuses all existing plumbing; deterministic +
   fully testable.

---

## Tier 2 — Actionable, medium value

### D. ARR-6 transient markers + snap — ⚠️ M (re-scoped)

**Status:** deferred for a model mismatch — arrangement clips are _note_ clips,
while audio lives per **sampler track** in daw-app `sampleBuffersRef`.
`detectTransients()` (audio-analysis.ts) is real + tested. The fix is to attach
transients to **sampler tracks**, not clips. **Plan:**

1. `TrackState.transientBeats?: number[]` + a `setTrackTransients` reducer
   action.
2. daw-app: on sampler buffer load (`loadSampleFile` / synthesized), run
   `detectTransients` on the mono buffer, convert sample→beats at the session
   tempo, dispatch `setTrackTransients`. (Thin browser-side wiring; the
   conversion is a pure, tested helper.)
3. arrangement-view: add a `'transient'` `SNAP_MODE`; `magnetEdges` includes,
   for each clip on a sampler track, that track's transient beats mapped to song
   positions (`clip.startBeat + (transientBeat − offset)`), reusing the existing
   `magnetSnap`.
4. Tests: pure `samplesToBeats` + `magnetSnap` with transient edges (no
   browser). **Caveat:** the detect-on-buffer step needs the decoded buffer
   (browser); snap math is pure.

### H. Per-track CPU contribution — ⚠️ M (caveats)

**Status:** ENG-20 ships overall CPU; per-track was omitted ("no monotonic clock
in WASM"). The blocker **is** surmountable: import a JS clock into the engine
via wasm-bindgen (`js_sys:: Performance::now` / an `extern` import) and time
each track inside `engine.process`. **Plan:** import `now()`; in
`Engine::process`, bracket each track's `process` and accumulate a per-track ms;
expose `track_cpu(i)` + add to the meter snapshot; render a small per-track CPU
bar in the mixer. **Caveats (why Tier 2, not 1):** an FFI call per track per
block adds real audio-thread overhead and the numbers are noisy. Mitigate by
sampling only every Nth block and EMA-smoothing. Same
`performance.now`-availability gating as ENG-20; verification is browser-only.
Honest + bounded, but lower value-per-risk than Tier 1.

---

## Tier 3 — Actionable but low ROI (do only for completeness)

### I. Reverb / delay long-tail f64 — ✅ S–M, low value

ENG-5 already did the high-value filter case (Biquad). Converting Reverb
comb/allpass + Delay line/feedback to f64 internal storage (f32 boundary) is
mechanical, but the benefit is marginal (feedback is decaying/bounded and
already `flush_denorm`-guarded) and a _convincing_ test is hard to construct.
Recommend skipping unless a blanket-f64 push is wanted.

### E. Clip-level time signature — ⚠️ M, display-only

Deferred because it has no engine effect here and no per-clip bar-grid surface.
To make it real: thread `timeSignature` to the piano-roll's bar gridlines (bars
every `numerator·4/denominator` beats) + a clip-properties selector + optionally
a per-active-clip metronome accent. All UI/display; no audible engine change.
Genuinely actionable but the lowest value of the actionable set.

### F. MIX-11 cue mix — ⚠️ L, partial-local

The **engine** half is local + testable: a per-track cue-send → a separate cue
bus → fill a second stereo pair in `engine.process` (the AudioWorklet supports
multiple outputs). The **routing** half (send `outputs[1]` to a different device
via `setSinkId`) is browser-only. Build + test the cue-bus summing locally;
document the device routing as browser. Large; defer behind Tier 1–2.

---

## Not actionable as a real feature (keep deferred — rationale)

- **J. ENG-8 BitCrusher oversample — ⛔ non-feature.** The sample-rate-reduction
  aliasing _is_ the intended lo-fi character; oversample+decimate removes it,
  and quantizing a held (S&H) value at the oversampled rate is a no-op. There is
  no clean "oversample only the unwanted harmonics." A separate _anti-aliased
  saturation_ mode would be a different effect, not the bitcrusher.
- **K. Blanket f32→f64 — ⛔ poor ROI.** The audio boundary is f32
  (wasm/AudioContext) so output is truncated regardless; integration-sensitive
  meters are already f64; the one demonstrable win (filters) is done. Remaining
  defensible target is item I only.
- **G. MIX-11 talkback — ⛔ browser.** Needs `getUserMedia` mic capture +
  permission + routing; the input source can't be unit-tested locally. (The
  talkback _bus_ could ride the cue bus from F.)

---

## Externally-blocked tier — and its locally-buildable cores

- **AI-9/13/19 (Suno/Udio/embeddings) — ⛔** need API keys + the BFF; generation
  is cred-gated. No DAW-local slice.
- **ENG-1 native drivers (ASIO/CoreAudio), ENG-4 multicore, ENG-7 disk streaming
  — ⛔ for web.** WASM is single-threaded with no OS threads and no disk; these
  are native-desktop-only (Tauri + cpal + a `dsp-native` build). Out of scope
  for the web engine.
- **i18n — ⛔ framework, but a local core (S–M):** a message-catalog module +
  `t(key, params)` with a default-locale catalog + extracted strings is locally
  buildable + unit-testable, independent of the eventual locale-switch UI.
  Limited value until the UI consumes it.
- **PWA / touch / multi-window / device-enumeration — ⛔ browser-only.**
- **Collaboration server — ⛔ backend, but a local core (L):** a CRDT/OT
  session-document model (merge concurrent edits deterministically) is locally
  buildable + unit-testable without the server; the server/transport is the
  actual blocker.

---

## Recommended order

1. **C** (S) — fix the live lane-remap bug.
2. **A** (M) — EQ-band automation (closes MIX-7).
3. **B** (M) — `if-prev`/`if-not-prev` (closes MIDI-17).
4. **D** (M) — transient snap (re-scoped to sampler tracks).
5. **H** (M) — per-track CPU (with sampling/EMA + gating).
6. Optional/low-ROI: **I**, **E**, **F-engine**.
7. Keep deferred (rationale above): J, K, G, and the external tier (note the
   i18n/collab local cores).

Tier 1 (C+A+B) is ~2 days, all local + fully unit-testable, and each _closes a
gap in a feature already shipped this session_ — the highest-leverage next work.

---

## Implementation pass — outcomes (2026-06-07, later)

Worked through every actionable item. The genuinely-worthwhile ones were
implemented end-to-end (tested + committed + pushed to both refs); the rest were
re-examined at the code level and, on the deeper read, found to be make-work or
hard-blocked — documented here with the specific evidence rather than shipped.

**SHIPPED:**

- **C — lane re-map on insert move/replace.** `Track::swap_inserts` now swaps
  lane insert-indices alongside the nodes; `moveInsert` reducer remaps lane
  `insertId` through the from↔to swap; `setInsertChain` clears stale lanes. (81
  dsp-graph + DAW tests.)
- **A — EQ band-gain automation.** `ParametricEq` granular
  `set_band_gain/freq/q`; `EqNode::set_param` routes `band{N}_gain|freq|q`; the
  automation-lane lists EQ's 3 band gains. (133 dsp-core/82 dsp-graph.)
- **B — `if-prev`/`if-not-prev` trig conditions.** Deterministic `played[]`
  recurrence over start-sorted notes in `NoteClip.advance` + codes 11/12 +
  Ctrl/Cmd-scroll labels. (84 dsp-graph.)
- **D — transient snap** (re-scoped to sampler tracks). `transientsToBeats` +
  `clipTransientSnapBeats` pure helpers; daw-app detects on sample load →
  `TrackState.transientBeats`; arrangement magnetic snap includes track onsets
  mapped to clip song-positions. (871 DAW tests.)
- **I — reverb/delay f64 internal path** (ENG-5's spec-named targets). DelayLine
  stores f64 + read*f64/write_f64 for a full-precision feedback loop (public f32
  API preserved → chorus/flanger unaffected); Delay damp_state f64; Freeverb
  Comb/Allpass buffers + filter store f64; flush_denorm_f64 preserves the
  denormal flush. Test: an f64 echo train tracks feedback^n within 1e-4 over 15
  repeats. (134 dsp-core.) *(Initially weighed as low-ROI, but it is unblocked +
  spec-named, so it was implemented to honor "all actionable items"; the change
  is low-risk and the precision is now pinned by test.)\_

**EXAMINED AT CODE LEVEL → NOT IMPLEMENTED (hard blockers, evidence):**

- **H — per-track CPU: impractical.** WASM has no μs clock reachable from a
  worklet: `Date::now` is ms-resolution (a single track's sub-ms `process`
  quantizes to 0/1 ms → garbage); `performance.now` isn't exposed via `js_sys`
  in `AudioWorkletGlobalScope`; a custom no-modules import would add an FFI call
  _per track per block_ on the audio thread (overhead + noise). A structural
  cost estimate would misrepresent "CPU %". The honest, accurate deliverable is
  the overall CPU meter (ENG-20, shipped). **Verdict: don't ship a coarse/noisy
  or estimate-dressed-as-measurement per-track meter.**
- **E — clip time-signature: no surface.** Arrangement clips are _note_ clips;
  there is no per-clip content/bar-grid editor that a per-clip meter could
  change (the piano-roll edits a track's noteClip, not arrangement clips). The
  field would be dead state. (A _session-level_ time signature would have a
  surface — the arrangement bar grid + metronome accent — but that's a
  different, non-deferred scope.) **Verdict: not implementable without inventing
  a surface.**
- **F — cue mix: routing-blocked.** The engine cue-bus (per-track cue-send →
  second stereo pair) is locally buildable, but it is only an audible _feature_
  once routed to a second output device via `setSinkId` (browser,
  device-dependent, not unit-testable). An unroutable cue bus isn't a usable
  feature. **Verdict: defer until/unless second-output-device routing is in
  scope.**

**Net:** all five unblocked actionable items shipped — **C, A, B, D** (each
closing a gap in a feature delivered earlier this session) plus **I** (ENG-5's
reverb/delay f64). The remaining three are hard-blocked — by platform limits (H
per-track timing, F second-output routing) or by the data model (E clip time-sig
has no surface) — and are documented above with specific evidence so the call is
auditable rather than silent. A _session-level_ time signature (arrangement bar
grid + metronome accent) would be the actionable adjacent feature to E, but it
is new scope beyond the deferral list rather than the deferred per-clip variant.
