Files
daniel 1f24c4b095 docs: 1.0 documentation restructure
Split root CLAUDE.md into 19 per-directory files scoped to their source area.
Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md,
PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals
into docs/TODO.md.
2026-07-29 15:09:48 -04:00

221 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Q-W0 Track 3 — env-coupled-constant domain-modeling audit
Static analysis, 2026-07-28, branch `pq-w0-audit`. Scope: any value stored in an
environment-coupled domain — frames, sample rate, DPI, pixels, tick cadence — that should be
stored **rate-free / device-free and resolved at the point of use** (`docs/product/code-organization.md`
§2c.3 env-coupled bullet; the load-bearing `sample_map` seconds invariant, documented in
`src/core/instrument/CLAUDE.md`). Findings are domain-modeling calls, not "rescale by rate" patches. The judgment bar
applied: a finding requires (a) an env-coupled *stored* domain AND (b) an environment that can
actually change under it. Frame counts computed transiently from seconds at the use site are
correct and are not reported.
Waves referenced for disposition: Q-W1..Q-W6 open `bank_panel.cpp`, `main.cpp`, `actions.cpp`,
`persist.cpp`, and relocate the clean pure libs — **no downstream wave opens
`reasampler_processor.cpp` / `sampler_core.h` / `reasampler_editor.cpp` for logic change**, so
fix-now findings in those files must be remediated in Q-W0 itself.
---
## Findings
### T3-01 — master-gain ramp step is a frame-domain constant anchored to 48 kHz
- **Location:** `src/vst/reasampler_processor.cpp:46-51` (`kGainRampRate = 1.0f / 960.0f`,
`kGainRampSnap`), applied per-sample at `:1069-1085` (stereo) and `:1114-1126` (mono).
- **Stored vs. correct domain:** stored as a **per-sample linear step**`960` is literally
20 ms × 48 000 Hz, and the comment says so ("960 samples @ 48 kHz ≈ 20 ms"). The intended
quantity is a **wall-clock ramp time** (~20 ms); the correct model is a seconds/ms constant
with the per-sample step derived from `sampleRate_` at `setupProcessing` — exactly the
pattern the same file already uses two paragraphs away for `kPreserveWindowMs`
(`:618-623`, `:733-735`).
- **What breaks when the environment shifts:** the ramp's wall-clock length halves at 96 kHz
(~10 ms) and quarters at 192 kHz (~5 ms); at 44.1 kHz it stretches to ~21.8 ms. The FB1
"no zipper" contract degrades silently as the host rate rises. Not persisted, so no on-disk
breakage — but it is a live hardcoded-rate assumption in `src/`, which Daniel's standing
ruling forbids.
- **Severity:** Med.
- **Disposition:** **fix-now, remediated in Q-W0.** Rationale: trivial, isolated,
behavior-identical at 48 kHz; no downstream wave opens this file, so deferring means keeping
a named violation of the no-hardcoded-rate ruling through the whole reorg. Store
`kGainRampSeconds = 0.020`, derive the step from the live rate where `sampleRate_` is set.
### T3-02 — takeover-declick decay is a per-frame coefficient (documented deliberate)
- **Location:** `src/vst/sampler_core.h:409-410` (`kDeclickDecay = 0.95`,
`kDeclickFloor = 1e-4`), applied per output frame in `sampler_core.cpp:514-519`, `:643-645`.
- **Stored vs. correct domain:** a per-output-frame exponential coefficient; the implied
wall-clock decay-to-floor is ~4.2 ms at 44.1 kHz and ~1.9 ms at 96 kHz. The strict
domain-model form would be a time constant in seconds resolved to a coefficient at engine
build.
- **What breaks when the environment shifts:** the takeover-blend residue fades ~2× faster at
96 kHz. Audibly negligible for a declick micro-ramp — and the in-code comment
(`sampler_core.h:397-401`) **already documents this as a deliberate per-frame DSP micro-ramp,
"not a stored wall-clock quantity"**, with the 44.196 kHz variance stated and accepted.
- **Severity:** Low.
- **Disposition:** **document-and-defer.** Rationale: the coupling is already an explicit,
written, bounded design decision in the code; converting it buys no audible improvement.
Triage should ratify the in-code note as the record.
### T3-03 — Trigger-fade UI throw ceiling hardcodes 2 s × 44 100 as `88200.0` frames
- **Location:** `src/vst/reasampler_editor.cpp:395`
(`constexpr double kFadeMaxFrames = 88200.0`), used by `controlValue`/the commit path to
normalize the Trigger fade-in/out knobs.
- **Stored vs. correct domain:** the fade **storage** domain (int64 SOURCE frames, persisted in
the zones payload) is settled and correct — a source-timeline fact, invariant under project-
rate change (`docs/ARCHIVE.md` §S15). The *UI ceiling*, however, encodes a wall-clock intent ("2-second
max fade throw") as a frame count at an assumed 44.1 kHz source. `88200` is a rate-derived
literal in `src/`, brushing the no-hardcoded-rate ruling even though it never touches disk.
- **What breaks when the environment shifts:** the environment here is the **source file's
rate**: a 96 kHz capture's maximum fade throw is ~0.92 s; a 22.05 kHz file gets 4 s. The knob's
full-scale meaning silently varies per loaded sample.
- **Severity:** Low (UI-only, not persisted, comment flags it as a "build-time residual — one
place to retune").
- **Disposition:** **fix-now, remediated in Q-W0.** Rationale: small and contained — replace
with `kFadeMaxSeconds = 2.0` resolved against the loaded source's rate at the two normalize
sites (the editor already threads `frameCount + rate` through the pack/unpack path,
`envelope_edit.cpp:148`); storage domain unchanged. If triage prefers zero UI-feel change,
the fallback is document-and-defer with the comment amended to name the 44.1 k assumption.
### T3-04 — drop-hint banner duration stored in sync-timer ticks
- **Location:** `src/vst/reasampler_editor.cpp:2920-2923` (`dropHintTicks_ = 6`), decayed in
`onSyncTimer` (`:244-245`); field at `reasampler_editor.h:457`.
- **Stored vs. correct domain:** a wall-clock intent ("a few seconds of banner") stored as a
**count of `kSyncTimerIntervalMs` ticks** (6 × 500 ms). Correct model: a duration in ms,
ticks derived — or a `GetTickCount`-style deadline like the bank panel's tooltip already
uses.
- **What breaks when the environment shifts:** retuning the sync cadence (a plausible perf
tweak — the 500 ms value is itself a tuning constant) silently changes the banner duration.
The comment does state the coupling.
- **Severity:** Low.
- **Disposition:** **document-and-defer.** Rationale: cosmetic, self-documenting at the single
site, and the cadence and hint decay live three lines apart; a fix is fine to fold in
opportunistically if the file is ever opened, but does not justify a Q-W0 edit on its own.
### T3-05 — systemic: no DPI/content-scale support in either UI surface
- **Location:** systemic. VST3 editor: `reasampler_editor.cpp:150-153` (`ViewRect(0,0,840,620)`
default and the size floor at `:816`), all `editor_geometry` / `knob_deck` / `curve_popup` /
`envelope_overlay` px constants (e.g. the 8 px node min-separation, the 28×28 curve button),
cached font sizes in `draw_kit`. Extension side: the LICE-drawn `bank_panel` dock and its
geometry modules. No implementation of VST3's `IPlugViewContentScaleSupport` anywhere in
`src/vst/` (grep: zero hits for content-scale/DPI), no scale factor threaded through the
pure geometry modules.
- **Stored vs. correct domain:** every layout constant is a **physical device pixel** that
silently assumes ~96 DPI. Correct model: logical units × one scale factor resolved at draw
time (the pure geometry modules take widths/heights as parameters already, so a scale factor
threads through cleanly — the constants are centralized, which is the good news).
- **What breaks when the environment shifts:** on a 150200 % Windows display the editor and
dock render physically small (or get bitmap-stretched by the host, blurring text); hit
targets like the 8 px min node separation shrink below comfortable pointer accuracy.
Usability, not correctness — nothing mis-plays and nothing persisted is wrong.
- **Severity:** Med (usability on modern displays; Windows-only product makes high-DPI common).
- **Disposition:** **document-and-defer.** Rationale: a proper UI-scaling pass is a feature
wave of its own (scale plumbing through ~15 geometry modules + font cache + both shells),
far outside Q-W0's remediation budget; deferral should be recorded as a named future phase,
and Q-W1's relocation of the geometry modules should keep the constants centralized so the
eventual scale factor lands in one place.
### T3-06 — legacy v3 zone-payload lift divides by the *current* project rate
- **Location:** `src/vst/sample_map.cpp:601-605` (v3 lift inside `readZonesPayload`), format
note at `sample_map.h:439-456`, `:510-513`.
- **Stored vs. correct domain:** the v3 blobs (Daniel's beta projects) stored wall-clock times
as frames — **the prior incident itself**. The lift converts frames → seconds by dividing by
the live `projectRate` threaded in at read time. That is exact only if the project rate today
equals the rate in effect when the S15/S16 editor wrote the frames; the write-era rate was
never recorded, so a project whose rate changed since lifts skewed times (old/new ratio,
e.g. ~8.8 % for 44.1→48 k).
- **What breaks when the environment shifts:** already broken by construction for
rate-changed-since-write projects; a one-time lift residue, after which v5+ re-saves in
seconds and the skew is frozen in, silently.
- **Severity:** Low (legacy-only, beta-project blobs, envelope-time magnitudes; unrecoverable
in principle — the missing datum was never written).
- **Disposition:** **document-and-defer.** Rationale: no better conversion exists; this is the
documented residue of the incident that motivated the seconds invariant. Worth one sentence
in the code-quality-audit report so the skew is a recorded known, not a mystery bug later.
### T3-07 — SOLA correlation-segment cap of 512 frames (deliberate CPU bound; cross-ref T1)
- **Location:** `src/vst/pitch_shift.cpp:72`
(`corrFrames_ = max(1, min(dLow_ - 1, 512))`; rationale comment at `:64-67`).
- **Stored vs. correct domain:** borderline by design. The quantity being bounded is **work per
splice** (multiply-accumulates), which is genuinely frame-domain — a CPU bound *should* be in
frames. The side effect is that the correlation segment's wall-clock span halves at 96 kHz
(512 frames ≈ 11.6 ms at 44.1 k, ≈ 5.3 ms at 96 k), raising the lowest frequency the
alignment search can lock onto at high rates. All other shifter geometry correctly derives
from `kPreserveWindowMs` resolved at the live rate.
- **What breaks when the environment shifts:** alignment quality for low-frequency content
degrades somewhat at high host rates; no correctness or persistence impact.
- **Severity:** Low.
- **Disposition:** **document-and-defer**, and hand to the T1 DSP audit for the quality call.
Rationale: the frame domain is arguably correct for a compute bound; whether 512 is the right
*number* is an algorithm-quality question (T1's territory), not a domain-modeling one.
---
## Surfaces checked clean
- **`sample_map` v5+ persistence (the reference implementation):** AHDSR + pitch-env times as
SECONDS doubles; no rate constant anywhere in the read/write paths (`kLegacyV3NominalRate`
deliberately does not exist); legacy v3 lift takes the rate as a parameter. Clean.
- **ComponentState envelope v6v11 fields:** channel mode, assign generation, preview velocity,
voice count/mode/trigger, `masterGainLinear` (dimensionless linear), explicit flag,
`SampleRefs` (paths + root/loop/channels intrinsics), `instanceGuid` — all rate-free or
file-fact domains. Clean.
- **Trigger `fadeInFrames`/`fadeOutFrames`/`startPoint`/`SampleLoop.start/end` persisted as
int64 SOURCE frames:** deliberate, settled source-timeline facts (`docs/ARCHIVE.md` §S15;
`bank_model.h:66-72` documents the loop rationale) — frames *of the file* are invariant under
project-rate change; the file's own rate is stored alongside and resolved at decode. Correct
domain, not a finding.
- **`trigger_seam`:** frames↔fraction with `startFrame` threaded both directions; the overlay's
fraction domain is expressly rate-invariant. Clean.
- **`kPreserveWindowMs` (50 ms):** resolved to frames against the live host rate at both call
sites (`reasampler_processor.cpp:618-623`, `:733-735`) — the correct pattern, cited here as
the model T3-01 should copy.
- **`pitch_shift` internal geometry:** ring length, fade, lag band, delay band all derived from
the rate-resolved `window_`; ratio-scaled live fade length. Clean (T3-07 cap noted above).
- **Tail system:** `TailSetting.manualMs` persisted in **ms**; `kMaxTailSeconds`/`kMaxTailMs`
wall-clock; trim threshold in **dB** with the linear ratio derived
(`render_settings.h:59-86`); the realtime decay scan resolves frames against the **file's own
authoritative rate** (`capture_realtime.cpp:384-408`). Clean.
- **`wav_trim`:** frame counts are parsed file facts and transient truncate plans. Clean.
- **`bank_model` persisted metadata:** source bounds in seconds + PPQ (both stored, each for
its consumer); loudness in dB; `sampleRate`/`channelCount` are *recorded facts about the
file*, not assumptions; capture tempo + meter stamped at capture time deliberately so the
bars.beats read-out is stable under later project meter changes (`card_meta`). Clean.
- **Ext-state wires** (`banks` JSON, view-mode model, owned manifest, `assignment_request`,
`rsusage_*`): no frame-domain values; the rate field in the assign wire is a recorded fact.
Clean.
- **Envelope schematic (`envelope_overlay`/`envelope_edit`):** param-domain px↔seconds scale
derived from the live rect (`gatePxPerSecond`), sample-length-free; editor time-slider
ceiling is `kEnvTimeMaxSeconds = 2.0` (seconds). Clean (pixel constants themselves fall under
the systemic T3-05).
- **Timers:** bank_panel tooltip delay uses `GetTickCount()` ms against `kTooltipDelayMs = 500`
(wall-clock — the pattern T3-04 should copy); editor sync timer is a 500 ms `SetTimer`
interval (ms, not ticks); the new-content detector is an event diff per tick with no
wall-clock meaning encoded in tick counts; `retireIdleDrain` is idleness-driven, not
time-driven. Clean.
- **`peaks` / `waveform_view` / `master_gain` / `velocity_curve` / `keyboard_strip`:** bins and
columns derived from rects at use; dB↔linear taper and curve math dimensionless; key rects
from the passed strip rect. Clean.
## Summary
| ID | Location | Stored domain | Severity | Disposition |
|----|----------|---------------|----------|-------------|
| T3-01 | `reasampler_processor.cpp:46-51` gain-ramp step | per-sample step (20 ms @ 48 k baked in) | Med | **Fix-now (Q-W0)** — store seconds, derive step from `sampleRate_` |
| T3-02 | `sampler_core.h:409-410` declick decay | per-frame coefficient | Low | Document-and-defer — deliberate, already documented in-code |
| T3-03 | `reasampler_editor.cpp:395` fade throw ceiling | 88200 source frames (2 s @ 44.1 k) | Low | **Fix-now (Q-W0)** — seconds ceiling resolved vs. source rate at use |
| T3-04 | `reasampler_editor.cpp:2923` drop-hint duration | sync-timer ticks | Low | Document-and-defer — cosmetic, coupling stated in-code |
| T3-05 | systemic (both UI surfaces) | physical px, ~96 DPI assumed; no content-scale | Med | Document-and-defer — a UI-scaling phase of its own; keep geometry constants centralized through Q-W1 |
| T3-06 | `sample_map.cpp:601-605` v3 legacy lift | frames ÷ *current* project rate | Low | Document-and-defer — unrecoverable legacy residue; record as known skew |
| T3-07 | `pitch_shift.cpp:72` correlation cap | 512 frames (CPU bound) | Low | Document-and-defer — frame domain arguably correct for a compute bound; hand to T1 for the quality call |
Two fix-now findings (T3-01, T3-03), both assigned to **Q-W0 itself** — no downstream wave
opens those files for logic change. Five deferrals, each with a recorded rationale. The
persistence surfaces — the highest-stakes case — are clean: every wall-clock quantity written
to disk since the S12 remediation is in seconds or ms, and every frame-domain persisted value
is a source-file fact whose rate travels with it.