# Q-W0 Track 1 — DSP / audio algorithm-quality audit (findings) Static analysis only; no code changed. Surfaces per the track brief: `src/vst/pitch_shift.{h,cpp}` (highest priority — GA correlation-aligned SOLA rewrite + GA2 prime + GA3 freeze, never audited), `src/vst/sampler_core.{h,cpp}`, `src/peaks.*`, `src/wav_trim.*`, the capture/tail paths (`src/capture.cpp`, `src/capture_realtime.cpp`, `src/realtime_record.h`), `src/vst/master_gain.*`, `src/vst/velocity_curve.*`. Dispositions follow Q-11 (SETTLED): default document-and-defer; a bounded SOLA fix is weighed before any technique replacement; technique replacement is Daniel's decision at triage. Note on "assigned wave": **no structural wave Q-W1..Q-W6 opens the `src/vst/` DSP files** (they target `core/json`, `bank_panel`, `main`, `actions`, `persist`, registration tables) — so any DSP finding triaged fix-now is remediated in Q-W0 itself or folded in as a new point before Q-W1 begins. I cannot listen; every artifact below is stated as mechanism + predicted audible consequence. Perceptual materiality is Daniel's call. --- ## Overall verdict on the pitch engine (Q-11 framing) The correlation-aligned SOLA in `pitch_shift` is **not a reinvented wheel in the pejorative sense** — single-tap SOLA with normalized cross-correlation splice alignment, parabolic sub-sample peak refinement, and amplitude-complementary raised-cosine fades *is* an established time-domain technique family (SOLA/TD-PSOLA lineage), and the implementation is unusually well-defended: normalized (not raw) correlation, ratio-scaled fade lengths with a drain-headroom derivation, prime-with-real-content onset, frozen-writer tail, filled-span clamping, and double-before-int64 clamps at the overflow-prone spots. The RT discipline holds throughout: `process()` does no allocation, no locks; the splice burst is bounded and fires once per splice cadence, not per frame. **No technique replacement (phase-vocoder / WSOLA) is warranted on this evidence.** The findings below are bounded-fix candidates and documented limits within the existing approach, exactly the Q-11 escalation ladder's first rung. --- ## Findings ### T1-01 — Stereo Preserve: per-channel independent splice alignment decorrelates L/R - **Location:** `src/vst/sampler_core.cpp` `Voice::advanceFrame` (Preserve branch, ~577–589) + `src/vst/pitch_shift.cpp` `PitchShifter::splice`. - **Mechanism:** a stereo Preserve voice owns two `PitchShifter`s, each running its **own** correlation search on its own channel's PCM. `bestLag + frac` differ per channel at every splice, so after the first splice the two read taps sit at different ring positions — an inter-channel time offset of up to ±`maxLag` (= window/4 ≈ **12.5 ms** at the 50 ms window), re-drawn at every splice (cadence ≈ window/|ratio−1| frames). The splice *schedules* also diverge (delay drift depends on tap position), so L and R fade at different times. - **Predicted artifact:** on genuinely stereo captures played through Preserve off-root: stereo image wander / widening that changes at the splice cadence, and comb-filter coloration on any mono sum. Correlated stereo content (the common case for a captured bus) is the worst case. Dual-mono (mono sample in stereo bus) is unaffected — the code correctly mirrors one shifter. - **Severity:** **High** (Preserve is the product-default pitch engine and the output bus is permanently stereo with channel mode auto-defaulting from the capture — this hits the flagship path on stereo material). - **Disposition proposal:** this is the strongest candidate for a **bounded SOLA fix** (Q-11 rung 1): link the channels — run the correlation search once (on the L+R mid signal, or on L as master) and apply the same `bestLag + frac` and splice schedule to both channels. Standard practice for stereo SOLA. It reshapes the `PitchShifter` seam slightly (splice decision must be computable once and applied to two rings — e.g. a lag-provider hook or a two-channel shifter), but no technique change and no new dependency. Recommend **fix-now in Q-W0** pending Daniel's triage call; if deferred, record it as the known stereo-Preserve limitation. ### T1-02 — Ratio slew mid-fade can drain the outgoing tap past the writer - **Location:** `src/vst/pitch_shift.cpp` `splice()` fade-length cap (~301–311) + `process()` tap advance (~360–366). - **Mechanism:** `fadeLen_` is capped from the drain headroom **at splice time** using the then-current `ratio_`. The pitch envelope legitimately slews the ratio per frame (`setShiftRatio` mid-fade). A ratio that **rises** after the splice (pitch-env attack toward a positive peak, or attack from a negative dip back to base) drains tap B faster than the cap assumed; the code comment claims the 2-frame margin covers "any realistic per-frame bias", but the margin is absolute, not slew-proportional: e.g. a splice at ratio ≈ 1 sets `fadeLen_ = window/4` with no cap (drainRate ≈ 0), and a pitch-env attack ramping to +24 st (ratio 4) within those ~12 ms drains tap B ≈ 3·window/4 — far past the `dLow` ≈ window/4 headroom. Tap B laps the parked/advancing writer and reads ring-length-stale content at up to ~half fade gain. - **Predicted artifact:** a periodic click/garble burst at the splice cadence during fast upward pitch-envelope ramps on Preserve voices. Only reachable with the AD pitch envelope enabled and steep; base transpositions (constant ratio) are correctly covered by the existing cap. - **Severity:** Med. - **Disposition proposal:** document-and-defer (needs pitch-env + Preserve + steep attack to trigger), with a cheap bounded fix noted for whenever the file is opened: re-tighten `fadeLen_` when the ratio increases mid-fade (the `freezeTail()` re-anchor block is the exact pattern to reuse), or clamp tap B's delay to ≥ 2 during a fade. ### T1-03 — Preserve prime ignores the Trigger play-end bound (short-span rings hold cut content) - **Location:** `src/vst/sampler_core.cpp` `Voice::start` prime block (~375–400) vs. the GA3 `feedBound` in `advanceFrame` (~568–572). - **Mechanism:** the per-frame feed treats `playEnd_` (Trigger) / `frameCount` as source exhaustion and freezes the writer so "no padding ever enters the ring" (GA3). But `start()` primes a **full window** from `pcm[q]` bounded only by `frameCount` — not by `playEnd_` — and pads with zeros past the sample end while declaring the whole window `filled_`. Two consequences for spans shorter than the 50 ms window: (a) a Trigger zone's ring holds real PCM **past the user's chosen stop**, which an up-shifted tap can reach and play (transposed) before `readPos_ ≥ playEnd_` frees the voice; (b) a sample shorter than the window gets zero padding inside the ring as declared-valid history, so splices can land in silence — a bounded re-entry of exactly the burst/gap onset artifact GA2/GA3 eliminated, scoped to sub-50 ms material (short drum one-shots are realistic content). - **Severity:** Med (bounded to short spans / short samples in Preserve; inaudible for spans ≥ one window). - **Disposition proposal:** bounded fix candidate — prime `min(window, span-to-feedBound)` frames and call `freezeTail()` immediately after prime when the span is shorter than a window (the GA3 machinery then recycles the real short tail, which is its designed behavior). Small and contained in `Voice::start`. Recommend fix-now in Q-W0 if Daniel agrees the short-one-shot case matters; else document-and-defer with this note as the record. ### T1-04 — No sustain-loop crossfade (hard loop seam) - **Location:** `src/vst/sampler_core.cpp` `Voice::advanceFrame` loop wrap (~493–498, 610–612). - **Mechanism:** the sustain loop wraps by subtracting the loop length (phase-preserving) and the interpolation partner wraps `i1 → loop.start`, giving one-sample continuity only. There is no crossfade region: unless the user's loop points sit at amplitude/slope-matched positions, every loop pass produces a step discontinuity — a click at the loop rate. `waveform_view`'s zero-crossing snap on the loop markers mitigates but does not remove it (zero crossings with mismatched slopes still click). Established samplers crossfade the loop seam (equal-power over a user- or fixed-length region). - **Severity:** Med (musically prominent when it hits, fully user-avoidable with careful loop placement). - **Disposition proposal:** document-and-defer — a loop-crossfade is a *feature* (needs a crossfade-length parameter and UI surface), not a bug fix; wrong scope for a reorg phase. Record as a known limitation beside the zone-loop spec. ### T1-05 — Linear interpolation + no band-limiting on repitch (both engines) - **Location:** `src/vst/sampler_core.cpp` Varispeed read (~607–624); `src/vst/pitch_shift.cpp` `readTap` (~172–185). - **Mechanism:** all fractional reads are first-order (linear). Linear interpolation's frequency response rolls off highs and leaks imaging sidebands (the interpolation image spectrum is attenuated only ~12 dB/oct); Varispeed up-shifts additionally alias (reading faster than 1× with no pre-filter folds source content above the post-shift Nyquist back into band). This is classic hardware-sampler behavior — often accepted, sometimes desired — and both engines share it consistently. - **Severity:** Low (quality ceiling, not a defect; deterministic and stable). - **Disposition proposal:** document-and-defer as a recorded trade-off. If a quality bump is ever wanted, a 4-point cubic Hermite read is a drop-in bounded upgrade at both call sites (no structural change); band-limited varispeed is a much bigger lift and not recommended. ### T1-06 — Correlation search: coarse step 4 can mis-lock on very high fundamentals; maxLag bounds alignment to ≥ ~80 Hz - **Location:** `src/vst/pitch_shift.cpp` `splice()` search loops (~240–257) and `configure()` geometry (~68–72). - **Mechanism:** two documented-by-construction limits. (a) The coarse search samples the correlation every 4 lags and refines ±3 around the coarse best — full integer coverage only *near* the coarse winner. For content whose correlation oscillates with period < ~8 samples (fundamentals above ~5.5 kHz at 44.1k), the coarse grid can alias and lock a non-optimal region; the splice then lands up to half a period misaligned. (b) `maxLag = window/4` (~12.5 ms) cannot span a full period below ~80 Hz, so deep-bass fundamentals cannot be period-aligned and splices degrade toward unaligned OLA there. Both are inherent range/cost trades every SOLA makes; the in-code comment already states (b). - **Severity:** Low (edge content: pure tones > 5 kHz, fundamentals < 80 Hz). - **Disposition proposal:** document-and-defer; record both bounds as the engine's stated operating range. No change recommended — widening either costs splice-burst CPU linearly. ### T1-07 — `splice()` up-jump clamp comment contradicts the code (margin direction) - **Location:** `src/vst/pitch_shift.cpp` ~196–210. - **Mechanism:** the comment derives the "tight cap" as `filled_ - d - maxLag_ - 2`, then says the code's `- 1` is "one sample of conservative margin" — but `-1` permits a *larger* jump than `-2`, i.e. the code is *less* restrictive than the comment's own derivation; the sentence has the direction backwards. Re-deriving: the deepest probe is the parabola's outer lag at `d + jump + maxLag + 1` (the interpolator's `i1 = i0 + 1` read-ahead moves *younger*, not deeper), so the code's `-1` is exactly tight and the comment's `-2` double-counts the interpolator. No out-of-range read either way; the comment is wrong, not the code. - **Severity:** Low (doc-only; misleads the next maintainer of a safety-critical clamp). - **Disposition proposal:** fix-now (comment rewrite, zero behavior change) — fold into whichever Q-W0 remediation touches `pitch_shift`; if none does, a standalone one-line doc fix in Q-W0. ### T1-08 — Linear-in-amplitude ADSR decay/release segments - **Location:** `src/vst/sampler_core.cpp` `AdsrEnvelope::tick` (~131–170). - **Mechanism:** decay and release ramp linearly in amplitude. Constant-slope amplitude is constant-dB-rate nowhere: a long release spends most of its wall-clock at perceptually loud levels then collapses abruptly (in dB terms the curve is logarithmic-late). Classic samplers use exponential (constant-ratio) segments for decay/release. The evaluator itself is correct and well-tested (release-from-current-level, hold-0 byte-compat re-dispatch are both right). - **Severity:** Low (character, not correctness; the perceptual judgment is Daniel's). - **Disposition proposal:** document-and-defer. An exponential-segment option is a contained evaluator change but alters every existing instrument's envelope feel — a product decision, not a Q-W0 cleanup. ### T1-09 — Takeover-declick: `declickR_` is dead state - **Location:** `src/vst/sampler_core.cpp` (~598–599, 639–646, 515–518). - **Mechanism:** both channels deliberately share one blend weight (`declickL_` — commented), but `declickR_` is still seeded and decayed every frame and never read for output. Dead state that invites a future L/R-weight divergence bug. The blend itself audits **clean**: `out' = (1−w)·out + w·ref` is a convex combination for w ∈ [0,1], so `|out'| ≤ max(|out|,|ref|)` — the rev-2 boundedness claim is mathematically sound, the boundary-frame identity holds, and the ring-out path on voice end is handled (the peer-path symmetry is present). - **Severity:** Low (hygiene; no audio effect). - **Disposition proposal:** fix-now-trivial (delete the field or rename the shared weight) — fold into any Q-W0 edit of `sampler_core`; not worth its own change otherwise. ### T1-10 — `planWavTruncate` silently drops chunks located after `data` - **Location:** `src/wav_trim.cpp` `planWavTruncate` (~151), `capture_realtime.cpp` `trimAutoTailInPlace`. - **Mechanism:** the plan truncates the file at `dataByteOffset + keptDataBytes`. Any RIFF chunk REAPER wrote *after* the data chunk (bext/iXML/smpl orderings vary by writer) is discarded; the RIFF size is patched consistently so the result is a valid WAV, but metadata is lost without a trace. The PCM and the trim boundary math themselves audit clean (file-rate-authoritative frame math, scan confined to the tail region, -72 dB threshold single-sourced from `kAutoTrimThresholdDb`, one-frame-past-last-audible per spec, no-trim fallbacks total). - **Severity:** Low (metadata only; audio unaffected; trim is a convenience path). - **Disposition proposal:** document-and-defer — note the behavior in the header's FORMAT ASSUMPTION block when the file is next touched. Preserving trailing chunks would complicate the single-truncating-write design for no audio benefit. ### T1-11 — `makeUniqueTag` has one-second resolution (collision window) - **Location:** `src/capture.cpp` (~224–227) and `src/capture_realtime.cpp` (~120–123). - **Mechanism:** the uniqueness tag is `std::time(nullptr)` — 1 s resolution. Two captures of the same `baseName` within the same wall-clock second derive the same file stem: the offline path would overwrite the first render's file and mint two Samples with colliding ids. Reachable in practice via `batch_capture` driving several short renders back-to-back. The realtime path can't self-collide (transport exclusivity) but shares the pattern. DSP-adjacent rather than DSP; recorded here because the capture paths are this track's surface — Track 2 may claim it. - **Severity:** Low-Med (silent data loss on collision; narrow window). - **Disposition proposal:** fix-now candidate, trivial: append a per-session monotonic counter to the tag (both call sites). Belongs wherever Track 2/triage routes capture-path hygiene; Q-W3 (main/orchestration split) is the nearest wave that opens the extension capture flow, else Q-W0. --- ## Surfaces that came back clean - **`src/peaks.*` — clean.** The bin partition `[b·frames/binCount, (b+1)·frames/binCount)` is exact integer math, remainder-distributing, no dropped tail; overflow guarded; short-buffer clamped; per-channel with no fold (invariant honored). `columnMinMax` mirrors the partition with 64-bit products and the enclosing-bin fallback. `lastFrameAboveThreshold` scans backward with a correct strictly-greater test and no wrap hazard. - **`src/wav_trim.*` — clean** except T1-10 (metadata note). Chunk walk is bounds-checked and total; even-byte padding honored; extensible-format float discrimination via the SubFormat GUID leading tag is correct; LE reads via `memcpy` (no aliasing UB); truncate plan never grows. - **`src/capture.cpp` (offline) — clean** from the algorithm-quality lens except T1-11. Exact unrounded bounds, dither forced off (bit-identical repeats), float32-only with ground-truth format blob, surgical trim-end normalize only in Auto, full snapshot/restore RAII. The precision-invariant plumbing is disciplined. - **`src/capture_realtime.cpp` + `src/realtime_record.h` — clean** except T1-11 (shared) and the already-in-code DAW-verify flags (take/frame-0 alignment assumption for the trim; abort()'s best-effort finalize racing the flush — both explicitly documented in place, correctly scoped). The record state machine's decisions are pure and ceiling-bounded; the trim is best-effort and never eats the range body. - **`src/vst/master_gain.*` — clean.** Taper endpoints single-sourced; norm-0 true-zero detent with the finite −60 dB floor; unity at ≈ 0.714 as documented; inverse collapses sub-floor values to the detent (documented); non-finite input clamped. The dB↔linear math is correct. - **`src/vst/velocity_curve.*` — clean.** The Fritsch–Carlson tangent is the standard weighted-harmonic-mean form (w₁ = 2h₂ + h₁, w₂ = h₂ + 2h₁), which bounds m ≤ 3·min(d₁,d₂) — monotonicity and no-overshoot inside [0,1] hold as claimed; sign-change/flat neighbors pin to 0; zero-span steps and coincident-X knots are handled; deserialize repairs the invariant defensively. `eval` once per note-on keeps it off the per-frame path. - **`sampler_core` voice/steal/mono machinery — clean** (beyond the findings above): the steal policy is deterministic and as documented; the mono held-stack has correct range guards against uint8 aliasing, order-preserving removal, per-note velocity for retrigger fallback, and CC 123 as its only reset path; `soundingNote()` correctly excludes ring-out tails from the Preserve cap and legato predicates; the two-tier panic semantics are right; both render overloads share one summation discipline with no allocation; envelope/keymap resolution honors the rate-free-seconds invariant (frames resolved at keymap build against the live rate — the prior frame-domain incident is not repeated here). The per-frame `std::pow` when the pitch envelope is active is bounded and acceptable. - **`pitch_shift` core machinery — clean** (beyond the findings above): the safe-band geometry, filled-span clamps, normalized correlation, parabolic sub-sample refinement, complementary raised-cosine fade (correct for phase-aligned content; the −6 dB midpoint on uncorrelated content is a documented, benign trade), `freezeTail`'s fade re-anchor continuity, and the down-shift ring-lap margin all audit sound. Down-shift writer-lap is unreachable for any ratio above ≈ −109 st; up-shift fade drain is covered to +24 st and beyond by the ratio-scaled cap (T1-02 is the slew case only). --- ## Summary table | ID | Surface | Finding | Severity | Disposition proposal | |-------|----------------------------------|------------------------------------------------------------|----------|---------------------------------------------------| | T1-01 | pitch_shift + sampler_core | Stereo Preserve: independent L/R splice alignment | High | Bounded SOLA fix (linked lag) — recommend fix-now in Q-W0; Daniel's call | | T1-02 | pitch_shift | Ratio slew mid-fade can lap the outgoing tap | Med | Document-and-defer; bounded re-cap noted | | T1-03 | sampler_core (Preserve prime) | Prime ignores Trigger playEnd / pads short samples | Med | Bounded fix candidate in Q-W0; else defer w/ note | | T1-04 | sampler_core (loop) | No sustain-loop crossfade (hard seam) | Med | Document-and-defer (feature, not reorg scope) | | T1-05 | sampler_core + pitch_shift | Linear interp, no band-limiting on repitch | Low | Document-and-defer (recorded trade-off) | | T1-06 | pitch_shift | Coarse-search HF mis-lock; ≥ ~80 Hz alignment bound | Low | Document-and-defer (stated operating range) | | T1-07 | pitch_shift | maxJump clamp comment contradicts code | Low | Fix-now (comment-only), in Q-W0 | | T1-08 | sampler_core (ADSR) | Linear-amplitude decay/release segments | Low | Document-and-defer (product decision) | | T1-09 | sampler_core (declick) | `declickR_` dead state | Low | Fix-now-trivial, fold into any Q-W0 edit | | T1-10 | wav_trim | Truncate drops post-`data` chunks (metadata) | Low | Document-and-defer (header note) | | T1-11 | capture.cpp + capture_realtime | 1 s-resolution unique tag → batch collision window | Low-Med | Fix-now candidate (monotonic counter); route at triage | Clean surfaces: `peaks`, `master_gain`, `velocity_curve` (fully); `wav_trim`, offline + realtime capture paths, and the non-flagged machinery of `sampler_core` / `pitch_shift` (clean with the noted exceptions above).