Files
reasampler/docs/COMPLETED.md
T
daniel 413967a205 docs: close out Θ-W2-T1 filter-voice-path into COMPLETED
Move the landed filter-voice-path track from PLAN.md into COMPLETED.md with full
narrative (quantizer removal, editor floor raise). Mark Θ-W2 fully landed, matching
W1's phrasing; fix a stale forward reference.
2026-07-30 18:23:42 -04:00

15 KiB
Raw Blame History

COMPLETED.md — ReaSampler landed milestones

Completed milestone entries removed from PLAN.md. Each entry preserves its original Goal, Verify, and checklist points with boxes marked done.

This file holds the current (1.x) cycle's landed milestones only. For all pre-1.0 (version-0) history, see docs/ARCHIVE.md.

Comment-reduction pass (tree-wide, twelve parallel tracks)

Cut source comment volume tree-wide: 209 files changed, net 6,493 lines. Comment-only lines went from 15,073 to ~8,671, a ~42% cut — before the pass, 37% of all source lines were comment-only. Zero code drift, verified across all 209 files by comparing comment-stripped hashes; the sole intentional exception (Daniel-approved) is two user-facing error strings in src/shell/capture/capture.cpp that lost internal milestone IDs (M8, M3, M7+). Build clean, 61/61 ctest pass. Code review surfaced 2 Major + 8 Minor findings — all content cut that should have survived — and all ten were remediated and re-gated before merge. Driver: Daniel's instruction — "Brief concise engineering comments. A little why, and maybe context, never WHAT."

CMake build-system split (ad-hoc, Daniel's request)

Split the 1423-line root CMakeLists.txt into a 91-line root plus 18 per-directory CMakeLists.txt files under src/, with two shared declaration helpers (reasampler_pure_library, reasampler_test) factored into a new cmake/reasampler_targets.cmake. The root now keeps only repo-global concerns: version/channel single-source-of-truth, configure_file, vendor path vars, LICE_SRC, enable_testing(), and the add_subdirectory calls. Comments throughout were rewritten to the project's comment conventions — phase/wave/ticket IDs removed, module semantics already owned by src/**/CLAUDE.md deleted, load-bearing build facts kept.

Also fixed two duplicate-object-code defects surfaced by the split: 18 core/ translation units were previously compiled directly into the reaper_reasampler module while also being linked in as static libraries — those 18 source entries were removed from the module's source list and 3 missing link edges (view_tree, guid_diff, lane_keys) added so every core/ TU now enters through exactly one static-library link edge. A dead bridge_marshal link edge was also dropped from reaper_reasampler, and two inaccurate comments in src/app/CMakeLists.txt were corrected.

No .cpp, .h, tests/, or vendor/ file was touched. Behaviour is unchanged and was verified mechanically: same 130 targets, same 65 tests all passing, reaper_reasampler.dll byte-identical at 3,477,504 bytes, both channels (stable/beta) building to the same artifact names and locations as before.

Θ-W1-T1 — zone-retirement

ReaSampler 9000's zone-mapping system is retired: one loaded capture, one parameter set, playing across the full keyboard repitched from root with key-tracking — no zones, no per-zone divergence, no keymap of captures. The dedicated zone-editing face and its authoring affordances (add/delete zone, per-zone parameter panel, Low/High/Root zone legend) are gone; the root note survives as a first-class parameter. sampler_core split along the note-routing/per-voice-render responsibility seam (no virtual tick() on the per-voice path), and the Sample face split into chrome/waveform/decks bands with a shared band-stack allocator, discharging the wave's two structural deliverables. Migration adopts a saved multi-zone instance's first zone; single-zone instances lift losslessly.

Deviations from spec:

  • The key-range open question ([propose]) is answered outright rather than left open: no key-range concept survives at all — KeyZone/lowNote/highNote are gone from the engine, the write format, and the strip. A low/high pair remains re-addable later as two ordinary parameters. Θ-W2-T3 consumes this decision.
  • "Which zone is first" ([verify]) confirmed: PerformanceMap::zones was an ordered vector and Keymap::resolve was first-match-in-order, so index 0 was the audible zone. Migration adopts index 0, and that zone's sampleId supersedes the envelope's stored selectionId.
  • The control row (root strip, preview, velocity knob, curve button, Mono|Stereo) moved into the chrome band, directly under the title rather than above the deck — user-visible and deliberate; it's what makes Θ-W2's band-disjointness real.
  • Loading a capture now clears the three capture-anchored overrides (root, loop span, start frame) while keeping the shaping parameters — strictly less destructive than the previous whole-zone drop.
  • The embed strip became a read-only readout; it lost its click handler since with no zones there is nothing to select.
  • Migration side-effect: a previously-zoned instance with implicit channel mode and a stereo capture persisted as Mono will reopen as Stereo. It converges on the documented rule and is reachable only for old blobs, but "sounds identical" was an acceptance bar, so it's a real deviation.
  • sampler_core.{h,cpp}'s 956-line documented hot-path exception is retired, not relocated — the tree now carries no over-ceiling exception at all.
  • note_entry was deleted as dead code (its only consumer was the removed zone editor).
  • Still unverified: a real pre-change project reopening through REAPER's setState has not been exercised in the DAW; migration is proven only in the pure domain against hand-laid legacy bytes.

Θ-W1-T2 — capture-handoff-bugs

Fixed both extension-side capture-handoff defects: drag-out now delivers the capture's audio at the drop target every time (previously intermittent, retry-fixable); dropping a capture onto an FX container now loads the instrument with the capture, matching the FX-button drop path.

Deviations from spec:

  • Item 5's root cause was three defects, not one: non-atomic COM refcounts racing a drop target's background copy; an unverified assumption that REAPER had already called OleInitialize on the calling thread; and a teardown-before-payload-check ordering bug that let an unresolvable payload consume the gesture.
  • Item 6's fix rests on an unconfirmed hypothesis — that a bare instantiate = -1 left placement to REAPER's ambient FX-chain insert point, which a container-focused chain window moves. It is now pinned to an explicit top-level position; nobody could confirm the mechanism without the DAW.
  • The opportunistic rider was partly taken: src/ingest.{h,cpp} re-homed to src/shell/actions/. ext_keys.h was declined (most consumers sit in another track's exclusive surface); resource.h was declined (it's a build input paired with src/resource.rc and the SWELL resgen step).
  • Neither acceptance criterion has actually been met yet. Item 5's gate is explicitly a soak ("a single pass is not a gate") and item 6 needs a live container drop; both require REAPER and are outstanding. The code is merged; the acceptance gates are not closed.

Θ-W1-T3 — filter-dsp-port

Lands the per-voice resonant filter as a standalone pure module (core/instrument/engine/filter/, five files: filter_params, filter_coeffs, filter_morph, filter_saturate, voice_filter) — concrete VoiceFilter type, no vtable, no allocation in process(). No call site; Θ-W2-T1 wires it into the voice path.

Deviations from spec — the spec itself changed mid-flight, headline first:

  • The Cortex-M4 biquad port was superseded entirely by a TPT/SVF topology, Daniel's call. Measurement found the firmware's high-pass resonance feedback tap vestigial: its stated rationale was inverted (the HP numerator approaches 1 as cutoff falls, not zero — it's the LP numerator that collapses), it reduced HP resonance everywhere it ran, and it carried unwanted sample-rate and input-level dependence. It was a Q15 fixed-point workaround for a ~17-bit cancellation float32 doesn't suffer. Daniel's ruling on the resulting level-dependent resonance bloom: "was a feature on the hardware (one knob colorful HP for master FX), wrong choice for this approach."
  • Two discrete modes (HP/LP) became a continuous morph, with two selectable morph laws — HP→BP→LP (default) and HP→notch→LP (Oberheim SEM) — chosen at prepare() via a MorphLaw enum on FilterSettings. Zero per-sample cost, verified by diffing emitted assembly (byte-identical between laws).
  • A configurable drive stage was added: an in-loop soft limiter on the band-pass integrator state, normalized 0..1, with a radial dial planned for Θ-W2. Drive at 0 is bit-exact linear.
  • Biquad1PoleLP was struck (Daniel's call) and never ported.
  • Q spans the full 0.110 with √2 at the control centre, replacing the firmware's 0.707-floored mapping — as originally specified.
  • No reference sample rate exists anywhere in the module — rate reaches the DSP only via g = tan(π·fc/sr). An interim fix that anchored a feedback tap to a 1/48000 constant was superseded by the rewrite.
  • The rewrite fixed a float32 conditioning defect the biquad carried: Direct Form I measured 27% peak error at 20 Hz / 192 kHz; TPT measures +0.034%.
  • Still open, deliberately: drive's maximum depth (4.0, set by measurement — at 64 the resonant peak inverted below passband) and the absence of makeup gain both await an ear pass against the real dial.
  • The module has no call site — integration is Θ-W2-T1, which also carries three recorded decisions of its own: the envelope-order choice (drive is level-dependent, so pre- vs post-envelope placement is sound-defining), the drive dial's calibration, and the fact that drive authority varies ~11 dB across the morph sweep.

Θ-W2-T1 — filter-voice-path

Wires the pure filter module into ReaSampler 9000's per-voice signal path as a new fixed processing point between the pitch envelope and the amp stage, gives it its own knob-deck group, and relays the deck row in signal-flow order (pitch → filter → amp). Each Voice owns its own VoiceFilter and a second AdsrEnvelope instance — per-voice, never shared — and neither adds allocation or virtual dispatch to the per-sample path. Parameters — morph position, cutoff, Q, drive, mod amount (bipolar ±100%, targeting cutoff), velocity and key-tracking modulation, then AHDSR — live in the one parameter set; FilterParams stores the filter module's own FilterSettings by value rather than a parallel copy of the normalized positions. The filter is off by default and bit-exact off — a project saved before the change reopens sounding identical, pinned by a bit-equality test. ComponentState's params payload moves v8 → v9, appending the filter tail; a v8 blob is a strict prefix and lifts to the off/neutral filter default, with non-finite filter fields falling back to neutral, pinned by a golden byte-literal fixture. Deck composition was extracted into a new pure module core/instrument/ui/deck_groups, with the pitch → filter → amp row order pinned by test; the filter's AHDSR ships as its own FILTER ENV group sibling to FILTER, mirroring the existing PITCH / PITCH ENV split, and a morph-law toggle (Band | Notch) ships as the FILTER group's row toggle.

Deviations from spec:

  • An initial mod-quantizer was added, then rejected and removed. A kFilterModSteps = 2048 step gate on the coefficient re-solve stair-stepped the corner (~5.8 cents per step); Daniel rejected it. Replaced by a cutoff-only re-solve (VoiceFilter::setCutoffNorm) that re-derives only g = tan(π·fc/sr) — Q's parabola, the morph's cos/sin, and the folded mix are all cutoff-independent and stay cached from prepare(); filter_params hoists its constant logs. Measured at 48 kHz, Release, net of the sweep generator: kernel alone 2.8 ns/frame, full prepare() 56.9 ns, cutoff-only re-solve 15.5 ns — about 1.2% of a core for 16 continuously-swept voices. The corner now sweeps continuously rather than stair-stepping.
  • The editor floor was raised to 840×620, which is also its new default size, up from a 560×460 floor at which the grown deck wrapped to four rows and pushed FILTER ENV / AMP ENVELOPE / VOICE / MASTER off-screen with no scroll. kEditorMinWidth/kEditorMinHeight now live in sample_bands, read by both the shell's checkSizeConstraint and the opening ViewRect. Consequence worth recording: a host with a saved editor rect below 840×620 is clamped up on reopen.

Θ-W2-T2 — stereo-waveform-lanes

Delivered as specified: two stacked lanes, L above R, in stereo mode; one lane in mono; overlays draw once at full stacked height.

Deviations from spec:

  • A mono source under stereo mode draws one lane, not two — lane count keys off stereoMode && sourceChannels >= 2, not channel mode alone, since a mono capture in stereo mode is dual-mono and a second lane would be the redundant duplicate the spec forbids. Review confirmed this is the only reading consistent with the decode path.
  • The full-height overlay contract Θ-W3 and Θ-W4 consume is type-enforced, not merely documented: an OverlayArea wrapper type that lane rects cannot satisfy.
  • A single-slot per-channel PCM cache was added to the editor session, invalidating alongside the existing PCM cache.

Θ-W2-T3 — toolbar-and-piano-strip

Delivered as specified: one toolbar font, no zone-count label, full-width piano strip, uniform key widths, note-name tooltips, root displayed and settable.

Deviations from spec:

  • The non-uniform key widths were integer quantisation, not aliasing — the old keyEdgeToX truncated an exact rational, alternating 6px/7px. Θ-W6's general antialiasing audit inherits nothing on key widths as a result, though key edges are still drawn unantialiased.
  • Uniform integer key widths and gap-free edge-to-edge tiling are mutually exclusive — 75 white keys do not divide an arbitrary width. The residue now lands in symmetric end gutters, 37px each side at the shipped 840px default — the maximum of a sawtooth with period 75px of window width. Daniel accepted this provisionally, pending how it looks in REAPER.
  • The whole control run moved into the toolbar row, not just preview and mono/stereo — the full-width strip left the velocity knob and curve button nowhere else to go.
  • Root drag became absolute-tracking rather than pixel-delta, since black keys overlaying white give no single pixels-per-semitone rate.
  • Width uniformity is guaranteed in client pixels only. Nothing in the instrument consumes a DPI scale factor, so host-side scaling is unverified — Θ-W6 already carries a "confirm the fix survives DPI scaling" item and now genuinely inherits it.
  • A stale-hover latch was fixed across all drag kinds and both drag-termination paths, wider than the strip work that surfaced it.