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:
moveInsertreducer: build the old→new id permutation from the reorder and remap eachtrack.insertParamAutomationlane'sinsertIdthrough it (same map already applied to insert ids). It already returnsrebuildCommands(next), so the engine re-syncs.- If/when a
removeInsertaction exists, drop lanes for the removed id and decrement higher ids (today inserts are only added/moved + whole-chain-replaced viasetInsertChain, which already rebuilds; verify before adding). - Tests (daw-session.spec): move an insert that carries a lane → the lane
follows it; rebuild emits
setInsertParamAutomationon 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:
- dsp-core
eq.rs: addset_band_gain(i, db),set_band_freq(i, hz),set_band_q(i, q)(each callsset_bandwith the stored values for the unchanged two). - dsp-graph
effect.rsEqNode::set_param: parse keysband{N}_gain|freq|q, route to the granular setter. (EQ was the one node left with the default no-opset_param.) - UI: surface EQ band params as automatable. Either add EQ rows to
PARAM_CONFIG(band2_gain−24..24 dB,band2_freq20..20k log,band2_q0.1..18) or special-caseeqin the automation-lane selector population. The lane editor + reducer + rebuild all work unchanged (genericinsert:<i>:<key>). - Tests: cargo (
EqNode::set_param("band2_gain",6)shifts the band'smagnitude_at); DAW (automation-lane lists the EQ band params; emitssetInsertParamAutomationkeyband2_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:
- clip.rs: before building
desired, computeplayed: Vec<bool>over the (already start-sorted) notes for thisiteration:played[i] = cond_seq(note[i].condition, iteration, prev=played[i-1]) && probability_gate(i,iteration). Add anote_condition_passes_seq(code, iteration, prev_played)that handles new codes11=if-prev,12=if-not-prev(delegating the stateless codes to the existingnote_condition_passes).desiredthen usesplayed[i]. First note'sprev = false(no predecessor). O(n)/block, pure fn of the playhead → still deterministic + offline-reproducible. - UI: append
if-prev/if-not-prevtoNOTE_CONDITION_LABELS+ the Ctrl/Cmd-scroll cycle. No new plumbing —conditionis already a wired u8 field. - Tests: cargo (a note with
if-prevsounds 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:
TrackState.transientBeats?: number[]+ asetTrackTransientsreducer action.- daw-app: on sampler buffer load (
loadSampleFile/ synthesized), rundetectTransientson the mono buffer, convert sample→beats at the session tempo, dispatchsetTrackTransients. (Thin browser-side wiring; the conversion is a pure, tested helper.) - arrangement-view: add a
'transient'SNAP_MODE;magnetEdgesincludes, for each clip on a sampler track, that track's transient beats mapped to song positions (clip.startBeat + (transientBeat − offset)), reusing the existingmagnetSnap. - Tests: pure
samplesToBeats+magnetSnapwith 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
getUserMediamic 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-nativebuild). 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#
- C (S) — fix the live lane-remap bug.
- A (M) — EQ-band automation (closes MIX-7).
- B (M) —
if-prev/if-not-prev(closes MIDI-17). - D (M) — transient snap (re-scoped to sampler tracks).
- H (M) — per-track CPU (with sampling/EMA + gating).
- Optional/low-ROI: I, E, F-engine.
- 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_insertsnow swaps lane insert-indices alongside the nodes;moveInsertreducer remaps laneinsertIdthrough the from↔to swap;setInsertChainclears stale lanes. (81 dsp-graph + DAW tests.) - A — EQ band-gain automation.
ParametricEqgranularset_band_gain/freq/q;EqNode::set_paramroutesband{N}_gain|freq|q; the automation-lane lists EQ's 3 band gains. (133 dsp-core/82 dsp-graph.) - B —
if-prev/if-not-prevtrig conditions. Deterministicplayed[]recurrence over start-sorted notes inNoteClip.advance+ codes 11/12 + Ctrl/Cmd-scroll labels. (84 dsp-graph.) - D — transient snap (re-scoped to sampler tracks).
transientsToBeats+clipTransientSnapBeatspure 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::nowis ms-resolution (a single track's sub-msprocessquantizes to 0/1 ms → garbage);performance.nowisn't exposed viajs_sysinAudioWorkletGlobalScope; 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.