diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index e50d580..827e5d5 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -887,3 +887,165 @@ wrong that refusal costs a working capture. Each track's DAW-verification obliga is recorded in `docs/PLAN.md`'s Phase Ψ section; `docs/verify-track-scope-multitrack.md` is a new standalone verification script on this branch, for Ψ-W3-T1's multi-track refusal specifically. No human has observed any of these seven behaviors in a DAW. + +### Γ-W1-T1 — knob-interaction-law + +One consistent interaction and taper law across every variable control in the instrument, +landed before any new control (Rate, Pitch) or VST3 parameter existed so both are authored +into it rather than retrofitted. The taper is extracted into its own pure module, +`core/instrument/ui/param_taper` — the norm↔value maps (ms knobs log-scaled, semitone knobs +log2/centre-expanded) and the modifier vocabulary (`DragModifiers`, `kFineDragScale`, the +four whole-unit Shift snaps), now the single home three consumers read: the knob's needle +(`deck_values`), the AHDSR overlay's schematic axis and its drag inverse +(`envelope_overlay`/`envelope_edit`), and — from a later wave — the VST3 host's +`toPlain`/`toNormalized`. Shift snaps to whole units in the control's displayed category +(ms, semitones, percent, curve-exponent, dB); Ctrl scales the drag by 0.05; Shift+Ctrl +resolves to Shift; a mid-drag modifier press or release re-anchors value and cursor +position so the rate changes without a value jump. `resetDeckParam`'s taper bypass — +writing the default directly rather than round-tripping through `norm → value` — is now +mandatory rather than merely convenient, since the 10 s ceiling is not a power of two the +way the retired 2 s one was. + +**The stage-time ceiling moves 2.0 s → 10.0 s** (`kGateStageMaxSeconds` / +`kEnvTimeMaxSeconds`, moved together so they cannot drift), reversing Γ-F3 on Daniel's +later ruling. The AHDSR overlay's schematic axis was re-derived against it: a stage's slot +width is now `slotPx × taperNorm(seconds)` rather than a linear fraction of the ceiling, so +a node's position within its slot is its knob's needle position and a short attack stays +legible at the raised ceiling instead of collapsing under a pixel. Every default gains an +exact normalized preimage under its own taper — the requirement Γ-W4-T1's +`defaultNormalizedValue` depends on, since a host's reset-to-default has no +`resetDeckParam` bypass to fall back on. The filter's four `*Norm` controls (cutoff, Q, +morph, drive) are untouched — their laws are wire-frozen in payload v9 — and the change is +persistence-neutral throughout: the payload stores raw engine doubles, so a project saved +at the old ceiling reloads with identical stored seconds and identical audio. + +### Γ-W1-T2 — master-bus-audio + +The master bus: a bypassable true-peak limiter, the meter's audio and publication half, and +the plugin's first latency report. New pure modules `core/instrument/engine/limiter` and +`core/instrument/engine/meter_ballistics`. Chain: voice mixer → master gain → limiter → +output bus, with the meter tapped post-limiter. The limiter is a single toggle with no +configurable controls — a baked −0.3 dBTP ceiling, default off, no makeup gain of any kind, +stereo-linked detection so the image never moves. True-peak detection is a 4x-oversampled +sidechain-only detector; the signal path itself is never oversampled. Its gain law is a +sliding minimum of the per-sample target over the lookahead window followed by a moving +average of the same width — every term of that average is a minimum whose own window +contains the sample being gained, so the ceiling holds structurally rather than by a tuned +attack, and the release only ever slows the rise. Switching is a mute, never a blend: the +unlimited signal is emitted at weight 1 or weight 0 and never in between, so the fade +always rides the limited path and the hard edge always lands on the bypassed side. + +**Dynamic reported latency** — `getLatencySamples()` returns 0 with the limiter off and the +lookahead in samples with it on, driving `restartComponent(kLatencyChanged)` on toggle — is +the plugin's first latency reporting of any kind; nothing in `src/` called +`restartComponent` before this track. Per block the processor publishes relaxed-atomic +peak, clip flag, and max gain reduction with no dB conversion or ballistics on the audio +thread; `meter_ballistics` (pure, unit-tested) does the conversion — instantaneous rise, +20 dB/s fall, a 1.5 s peak hold releasing at the same rate, linear-in-dB scale over +−60…+6 dBFS, and a clip latch cleared on request. **Spent the phase's first payload rung**: +`kParamsPayloadVersion` reaches **15**, appending the limiter enable flag as a strict +suffix; a pre-v15 blob lifts to bypassed. + +### Γ-W1-T3 — contour-trace-curves + +Staged envelope segments now draw as the curve their exponent defines, closing the defect +where the mid-segment knot floated off its own trace — the paint path dropped knots and +joined the remaining vertices with straight strokes even though the exponent was already +in scope, while knot *positioning* had honoured it since Θ-W3-T2. A new pure module, +`curve_tessellate`, joins the non-knot vertices along the same curve `envelopes.h`'s +evaluators use — one point per pixel column at `start + (end − start) × curveMap(phi)` — +so the drawn stage and the sound it makes cannot diverge; a neutral exponent or a +zero-level span still emits just the two endpoints, matching the straight stroke drawn +before curves existed. All three envelopes (amp, pitch, filter), both play modes, every +sloped stage, share the one fix. + +### Γ-W1-T4 — editor-floor-and-row-law + +Commits the editor's canvas — the window floor, the width budget it derives from, and +which row each deck group belongs to — so every later UI track in the phase is drawn and +judged at the final window size rather than a size a subsequent wave changes under it. +`kEditorMinWidth` moves 980 → 1190, staying in `sample_bands.h`; `kEditorMinHeight` stays +680 (Γ-F1). `kEditorCeilingWidth` (1280, the hard cap the floor may not exceed) relocates +from `knob_deck.h` into `sample_bands.h` alongside the min-width/min-height pair, since it +is a window fact rather than a deck one; the deck's own width-budget constants — the row +block (1020) and MASTER's reserved width (142) — stay in `knob_deck.h`. The floor is +derived rather than asserted as a literal: `1020 + 12 (gap) + 142 + 2×8 (pad) = 1190`, +leaving 90 px of headroom against the 1280 px ceiling. Row membership becomes a property of +the group id — `DeckRow { Sound, Contour, Spanning }` plus `deckRowFor(DeckGroupId)`, an +exhaustive switch (Sound = PITCH/RATE, FILTER, VELOCITY, VOICE; Contour = PITCH ENV, FILTER +ENV, AMP ENVELOPE; Spanning = MASTER) so a future group left unclassified is a compile +error. Nothing consumes the predicate yet — the two-row arrangement inside this canvas is +Γ-W3-T1's — so at the new floor the deck still packs by the unchanged greedy whole-group +wrap, landing on two rows rather than three; the composition is knowingly interim (PITCH +ENV sits with the sound decks, both rows left-packed with dead space) until Γ-W3-T1 lands +the reflow. No drawing code, descriptor, parameter, or audio changed. + +### Γ-W1-T5 — preserve-time-stretch + +A real pitch-preserving time-stretcher for Preserve mode, landed a wave ahead of the Rate +control that will drive it so Rate ships onto a finished engine instead of a disposable +stand-in — moved up from a later wave on Daniel's ruling that it was the phase's longest +pole and had no UI dependency. The write rate (duration) and the tap rate (pitch) are +independent, which is the whole mechanism: a new header-only pure module, `time_stretch`, +holds `StretchCursor` — the per-output-frame source-feed schedule, a fractional cursor +carrying its rate debt, loop-wrapped — plus the measured rate bounds and their clamp, +alongside `pitch_shift`'s existing shift-ratio control. Rate 1.0 is exactly one source +frame per output frame with no residue, which is what makes the unity-ratio Preserve read +bit-identical to the pre-stretch engine — the regression floor the track is gated on, since +nothing publishes a non-unity ratio until Γ-W2-T1's Rate knob exists. No new third-party +dependency, no allocation or lock in `process()`, no dispatch on the per-sample path, +buffers sized at voice allocation or reload on `pitch_shift`'s existing pre-warm precedent. + +### Γ-W1-T6 — exhaustive-switch gate on pure libraries + +Merged as `ee839cf`. **This track has no entry in `docs/PLAN.md`** — the plan's Γ-W1 wave +header states so directly ("the phase's track numbering runs to T7; T6 landed within this +wave but has no entry in this document") — so this record is reconstructed from +`cmake/reasampler_targets.cmake` and the enforcement comment at its confirmed call site, +`src/core/instrument/ui/deck_groups.cpp`, rather than from a spec section. + +`reasampler_pure_library()` (`cmake/reasampler_targets.cmake`) now promotes a default-less +`switch` missing an enumerator to a compile error on every pure library: `/we4062` on MSVC, +`-Werror=switch` on GCC/Clang. Neither fired before this track — MSVC's C4062 is off at the +repo's `/W1` default, and GCC/Clang's `-Wswitch` warns without `-Werror`, which this repo +sets nowhere else. The gate is deliberately **not** C4061, which fires even on a switch +that already has a `default:` clause — that would light up every defensive switch in the +tree instead of catching only the deliberately default-less ones, such as +`isLiveDeckParam` and `deck_groups.cpp`'s live-param routing, where a newly added +enumerator must be a compile error rather than a silent fall-through. Later Γ-W1 work +(T4's `deckRowFor`) relies on this gate being in place. + +### Γ-W1-T7 — psola-preserve + +Preserve's splices become pitch-synchronous. A new pure module, +`core/instrument/engine/period_detect` (two-pass YIN — a decimated cumulative-mean- +normalized difference picks the period, then the full-rate difference function refines it +to a fraction of a frame), estimates the source's fundamental period once at load; +`pitch_shift`'s splice jump becomes the multiple of that period nearest the fixed window +that still fits the ring's jump bound, so an aligned landing point sits at the centre of +the existing correlation search instead of possibly not existing inside it at all. +Detection runs off the audio thread by link graph — `sampler_core` does not link +`period_detect`, so no translation unit on the render path can name `detectPeriod` — and an +unknown period (noise, polyphony, percussion, a drifting source) restores the fixed-window +geometry byte for byte. A period is derived from the audio at load, so it is cache rather +than state: no `ComponentState` field, no payload rung. Merged as `7a162a5`. + +**Status, corrected against what `docs/PLAN.md` currently states — the closure is now +complete, not partial.** The **geometry** failure mode (no phase-aligned landing existing +inside the search window at all, for low material such as a 30 Hz tone) was closed and +asserted at the original merge and stands unchanged. The **cadence** failure mode — +splices recurring faster than the output period at rate/shift combinations where +`window/|rate − shift|` is short — was left "not closed, re-characterized rather than +fixed, no-regression asserted rather than improvement claimed" at that point, pending +remediation. Three remediation commits have since landed, including `f66b9bd` (correcting +the agreement denominator to count only probes that carried signal) and `83e7cca` — the +commit that closes the track, current tip of this merge — and a re-review confirmed the +earlier findings closed. The re-review found the original cadence analysis itself stale: +PSOLA made the splice interval follow `spliceJump()` rather than the fixed `window` the +module header still described, and the closeout supplied the missing measurement in the +collapse band. **Measured at P = 1470 frames (30 Hz at 44.1 kHz), rate 2.0 / −24 st, +against a known-answer metric floor of 55.86 % and a period-off control arm: fixed-window +excess 18.52 %, pitch-synchronous excess 0.00 %** — PSOLA eliminates that corner rather +than regressing it, with every splice at n = 1 landing exactly one source period away. +**These figures are a one-machine, Debug-build measurement against the named control arm, +not a general performance claim.**