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.
16 KiB
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 —
960is 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 fromsampleRate_atsetupProcessing— exactly the pattern the same file already uses two paragraphs away forkPreserveWindowMs(: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 wheresampleRate_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 insampler_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.1–96 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 bycontrolValue/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.88200is a rate-derived literal insrc/, 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.0resolved against the loaded source's rate at the two normalize sites (the editor already threadsframeCount + ratethrough 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 inonSyncTimer(:244-245); field atreasampler_editor.h:457. - Stored vs. correct domain: a wall-clock intent ("a few seconds of banner") stored as a
count of
kSyncTimerIntervalMsticks (6 × 500 ms). Correct model: a duration in ms, ticks derived — or aGetTickCount-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), alleditor_geometry/knob_deck/curve_popup/envelope_overlaypx constants (e.g. the 8 px node min-separation, the 28×28 curve button), cached font sizes indraw_kit. Extension side: the LICE-drawnbank_paneldock and its geometry modules. No implementation of VST3'sIPlugViewContentScaleSupportanywhere insrc/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 150–200 % 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 insidereadZonesPayload), format note atsample_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
projectRatethreaded 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
kPreserveWindowMsresolved 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_mapv5+ persistence (the reference implementation): AHDSR + pitch-env times as SECONDS doubles; no rate constant anywhere in the read/write paths (kLegacyV3NominalRatedeliberately does not exist); legacy v3 lift takes the rate as a parameter. Clean.- ComponentState envelope v6–v11 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/endpersisted as int64 SOURCE frames: deliberate, settled source-timeline facts (docs/ARCHIVE.md§S15;bank_model.h:66-72documents 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 withstartFramethreaded 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_shiftinternal geometry: ring length, fade, lag band, delay band all derived from the rate-resolvedwindow_; ratio-scaled live fade length. Clean (T3-07 cap noted above).- Tail system:
TailSetting.manualMspersisted in ms;kMaxTailSeconds/kMaxTailMswall-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_modelpersisted metadata: source bounds in seconds + PPQ (both stored, each for its consumer); loudness in dB;sampleRate/channelCountare 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 (
banksJSON, 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 iskEnvTimeMaxSeconds = 2.0(seconds). Clean (pixel constants themselves fall under the systemic T3-05). - Timers: bank_panel tooltip delay uses
GetTickCount()ms againstkTooltipDelayMs = 500(wall-clock — the pattern T3-04 should copy); editor sync timer is a 500 msSetTimerinterval (ms, not ticks); the new-content detector is an event diff per tick with no wall-clock meaning encoded in tick counts;retireIdleDrainis 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.