Files
reasampler/docs/VERIFICATION.md
T

27 KiB
Raw Blame History

DAW verification — post-1.0 work on dev

Checks for Θ, Ξ, Ψ, Ε, Ρ, Γ, and Ω work that no unit test can close. Build Release, install into UserPlugins/, restart REAPER. Panel tail toggle = None, project rate 48000, unless a check says otherwise.

Precision invariants

  • Dry offline item capture of a 2 s range, re-inserted at its source position, inverted against the source — reads silence (CLAUDE.md §"Precision invariants" — "Null test")
  • Run the identical offline capture request twice — the two files are byte-identical on disk (CLAUDE.md §"Precision invariants" — "Bit-identical repeats")
  • After any capture, source items and tracks are unchanged: fader, pan, mute, FX bypass, selection (CLAUDE.md §"Precision invariants" — "Non-destructive")
  • After a realtime capture, the temp track is gone and every source track's routing is back as it was (CLAUDE.md §"Precision invariants" — "Non-destructive")
  • Capture 10.00012.000 s — card reads 2.000 s / 96000 frames, no leading or trailing silence (CLAUDE.md §"Precision invariants" — "Exact bounds")
  • With an FX on the source track: item scope does NOT carry it, track scope does (CLAUDE.md §"Precision invariants" — "Capture FX scope")
  • Track scope on a child track with FX, gain, and pan set on the parent and master — neither colors the capture (CLAUDE.md §"Precision invariants" — "Capture FX scope")
  • Save, move the whole project folder elsewhere, reopen — every card still resolves and auditions (CLAUDE.md §"Precision invariants" — "Relative paths only")

The decisive observation

  • Run first. docs/verify-track-scope-multitrack.md §3 by hand, and count the files REAPER writes (docs/TODO.md §"The &128 multi-track output shape is still DAW-unobserved", docs/COMPLETED.md §"Ψ-W3-T1", docs/COMPLETED.md §"None of the seven is DAW-verified")
  • Two files confirms Ψ-W1-T1 and Ψ-W3-T1 at once; one summed file invalidates both — stop and report, the refusal is costing a capture 1.0.0 accepted (docs/COMPLETED.md §"None of the seven is DAW-verified")
  • Then walk the rest of docs/verify-track-scope-multitrack.md (§1–§2, §4–§7) for the multi-track refusal itself (docs/COMPLETED.md §"Ψ-W3-T1")

Capture range and bounds

  • Over an item much longer than the selection: item scope × time selection, and item scope × razor — each lands exactly the window, not the whole item (docs/COMPLETED.md §"Ψ-W1-T1")
  • Same source: track scope × time selection, and track scope × razor — same exact window (docs/COMPLETED.md §"Ψ-W1-T1")
  • One razor-union case (two disjoint areas, one track) — lands the requested window, no ReaSampler capture failed: line (docs/COMPLETED.md §"Ψ-W1-T1")
  • Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (docs/COMPLETED.md §"Ψ-W1-T1")
  • The millisecond floor — SETTLED, nothing to re-run for TailMode::None. The floor lives in the custom-time-bounds field (RENDER_BOUNDSFLAG=0), not in the render engine. Two live 48 kHz TailMode::None renders on RENDER_BOUNDSFLAG=2 (time selection, handed over via GetSet_LoopTimeRange) came back exact — 97627 frames against 97627 — the second over a window whose START carried a sub-millisecond remainder, with no floored model of that window able to reproduce the count. Time selection is now the only bounds mode a capture can reach; the console verdict line and the RENDER_STARTPOS/ENDPOS read-back probe that answered this are gone. Full observation: src/core/capture/render_settings.h's kRenderBoundsTimeSelection
  • Still open — Auto and Manual tail. checkRenderedBounds judges TailMode::None only (Auto/Manual add frames by design), so the settled result covers those two by INFERENCE, not observation, and the inference rests on an unverified PREMISE too: that the (retired) floor applied to the bounds identically across all three tail modes, and that all three now hand the window over the same way. Neither is measured — both live short renders that settled the bounds mode were TailMode::None; no Auto or Manual capture has been observed at all. On Auto/Manual, the ONLY automatic check left is the 0-byte gate (checkRenderedFileNotEmpty) — there is no automatic bounds signal for those two modes at all until this bullet is closed by hand. What would establish it: repeat an off-grid-start capture at Manual over a source that is loud right to the window's end, and check the landed file's frames against window + tailMs — a floored edge shows up in that count. Auto cannot be checked by count (it trims trailing silence), so it needs the null test by ear/inversion against the source instead
  • [verify — DAW] A tail is assumed to render PAST the window end — the SDK header (:3048) confirms only that RENDER_TAILMS is a length in ms, not that it extends past the end. If that assumption is wrong, a tail capture is silently SHORTER than its window with no detector at all. Report whether either tail capture comes up short against the source
  • A refused render is kept for diagnosis at <project folder>/reasampler_refused/ (the refusal line names the path; a failed move leaves it unindexed in the bank folder and says so). Delete the folder when done — nothing in the bank references it
  • If a capture is refused for a short render, report the refusal line verbatim. A message naming floored to the millisecond means the floor is back on a mode measured escaping it; a shortfall of one or two frames with no such sentence may be the gate's own edge-convention tolerance rather than the render (render_window.h's renderHonoredBounds)

Names and channels

  • Capture from a named track — the card reads <Track> MM-DD HHMM; capture again the same minute and the second carries an ordinal (docs/COMPLETED.md §"Ψ-W2-T1")
  • Capture from an unnamed track, and from a multi-item selection — both readable, +N present on the multi (docs/COMPLETED.md §"Ψ-W2-T1")
  • Load a named capture into ReaSampler 9000 — the same name shows there (docs/COMPLETED.md §"Ψ-W2-T1")
  • The card label stays legible over its scrim at every card size (docs/COMPLETED.md §"Ψ-W2-T1")
  • Capture a dead-center mono source — the .wav is roughly half the size of the equivalent stereo capture (docs/COMPLETED.md §"Ψ-W2-T2")
  • Insert that collapsed file on a stereo track and null it against the source — confirms REAPER sums a 1-channel item at unity (docs/COMPLETED.md §"Ψ-W2-T2")
  • Capture a true-stereo source — stays 2-channel, and both it and the collapsed file load into the instrument correctly (docs/COMPLETED.md §"Ψ-W2-T2")

Mode switching

  • Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (docs/COMPLETED.md §"Ψ-W1-T2")
  • Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (docs/COMPLETED.md §"Ψ-W1-T2")
  • Click the footer mode segment, save, reopen the project — the mode persisted (docs/COMPLETED.md §"Ψ-W1-T2")
  • [verify — DAW] Reproduce the strand: project with FX in both modes, switch to Design, save, close, reopen, toggle to Arrange. Toggle back and forth several more times — the arrangement's FX must NOT converge on permanently offline/hidden. A track the tool refuses to park is NAMED in the console, and the same refusal is not reprinted on a later reapply unless the refused set changed (src/shell/view/view_fx_park.h's decidePark / reportRefusedParks)
  • [verify — DAW] ONE switch, ONE point. In a project with several FX across several tracks, switch modes once and count the undo points REAPER shows (Ctrl-Z tooltip / undo history): expect exactly ONE, ReaSampler: activate <mode> view — never two, and never one per FX. Then press Ctrl-Z once: the flags unpark and EVERY FX comes back online, in that single step. Verify the EFFECT, not merely that one entry exists — a mask that failed to pick up UNDO_STATE_FX would still show one correct-looking entry while leaving every plugin offline, which reads as a pass and isn't. Then Ctrl-Y: the redo re-parks all of it, also in one step (src/shell/view/CLAUDE.md §Invariants — "ONE mode switch is ONE undo point")
  • [verify — DAW] Where the view_state lands on undo AND redo, and whether the redo strands a track. persistViewState writes the ext state AFTER applyMode's block closes, so the point never saw the new view_state — the undo direction is expected to come out right by position, the redo direction is the open one. Use a project with FX on the tracks that park. After the Ctrl-Z above, read the footer segment: it must show the mode the user came FROM, and a further switch must bank solos under that mode id. Then Ctrl-Y and read it again: if the tracks re-park while the footer still shows the pre-switch mode, the redo restored the stale ext state and model-vs-project is out of step. Then switch modes ONCE MORE and read the console — the hypothesised consequence is a left unparked refusal line naming those tracks, because the redo-restored model carries no snapshot for a chain that reads parked. A clean switch with no refusal line kills the hypothesis; the line appearing confirms a NEW refusal route and the persist position has to be reopened. Report which, and the footer state at each step. Do NOT fix by moving persistViewState inside the block — it can open a modal Save-As (src/shell/view/CLAUDE.md §Gotchas)
  • [verify — DAW] No point on open. Open a project saved in Design mode with every inactive leaf already fully parked (flags AND FX), and check the undo history: the load-tick reapply writes nothing, so it must add NO undo point at all — the ("", 0) discard form. This only holds when the saved state really was fully parked; a first open after tagging a new leaf, an FX added to a parked track, or a plugin hand-onlined since the last save all have real state to write and legitimately mint one correct point. An entry appearing on a project that WAS fully parked means the discard is not working — and since mintManagedLanes' no-op path rests on the same idiom, check that alongside it (src/shell/view/view.h's applyMintsUndoPoint)
  • [verify — DAW] The hitch now sits on the switch. On a project with heavy plugins (convolution, a loaded sampler) across several parked tracks, time the mode toggle: by design the action does not return until every plugin has unloaded/reloaded. Report roughly how long, and whether the UI recovers cleanly afterwards — the whole write phase runs under one PreventUIRefresh(1) hold (src/shell/view/CLAUDE.md §Invariants — the documented caveat)
  • [verify — DAW] Nested blocks around the FX writes. Two directions, both undocumented in the SDK header. (a) render_in_place calls applyMode inside its OWN undo block, so its single ReaSampler: render selected track to a new track point should now absorb the reapply's FX moves — run that verb from Design mode over a track with FX and confirm ONE point, not two, and that Ctrl-Z reverses all of it. (b) The inverse: fire a mode-switch action WHILE a switch's plugin loads are in flight (a heavy chain widens the window) — applyMode is latched, so the expected outcome is that the second switch is DROPPED entirely and silently; the history must show one point, and the footer must still read the first switch's target. Re-firing it afterwards must work normally. (c) The same window, but let OnTimer -> bankPanelRefresh -> mintManagedLanes run into it: that path is NOT latched and opens a block of its own, so check the history for a split or misordered point (src/shell/view/CLAUDE.md §Gotchas)
  • [verify — DAW] The load reapply is never lost to the latch. Open a project saved in Design mode with heavy plugins on parked tracks (so an apply is long) while another apply can overlap — e.g. trigger a project open from an action fired during a switch. The saved mode must still be applied: the load glue tests modeApplyInProgress before spending its one-shot signal and retries on the next timer tick, so a delayed-by-a-tick reapply is a pass and a never-applied one is a failure (src/app/main.cpp's load glue, src/shell/view/view.h's modeApplyInProgress)
  • [verify — DAW] A tab switch mid-apply cannot split the block. applyMode pins the project once at entry and threads it through Undo_BeginBlock2/Undo_EndBlock2/ValidatePtr2. With a long switch in flight, switch project tabs (the pump premise makes this reachable) and confirm the undo point lands on the project the switch started in, with nothing written into the other tab (src/shell/view/view.cpp's applyMode)
  • [verify — DAW] A→B→A costs two full cycles. Toggle Arrange→Design→Arrange quickly on a project with heavy plugins: every plugin unloads and reloads twice, where the retired intent queue made a fast double-toggle free. Confirm the END STATE is still correct (every FX online, every driven flag back at its captured value) and report whether the doubled cost is tolerable in practice (src/shell/view/CLAUDE.md §Invariants — the A→B→A bullet)
  • [verify — DAW] For a track the console names as left unparked, follow the message's recovery IN FULL: re-enable its TCP/mixer visibility, main send and FX enable, and set every FX in its chain online (FX enable is the chain bypass — it does not bring an individually offlined FX back). Then switch modes twice: it parks and restores normally, with no further console line. Do the flag half ONLY on a second such track and confirm it is still refused — the FX half of the trust test, and the reason the message spells the step out (unit-tested only as a decision, never against a live chain)

Actions and drops

  • Add the import action to a Media Explorer toolbar and fire it from there — it imports (docs/COMPLETED.md §"Ψ-W1-T3")
  • Fire the existing Main-section import binding — still works (docs/COMPLETED.md §"Ψ-W1-T3")
  • Unload/reload (restart REAPER) — no duplicate Media Explorer entry in the action list (docs/COMPLETED.md §"Ψ-W1-T3")
  • Drag one card across the arrange, over an FX window, over the TCP/MCP and back — cue changes per surface, every transition reverses (docs/COMPLETED.md §"Ψ-W1-T4")
  • Drag fast, and drag onto a narrow TCP — target class still resolves; no release anywhere in REAPER is a silent no-op (docs/COMPLETED.md §"Ψ-W1-T4")
  • Drop a single card into the arrange — an item lands at the pointer's track and time (docs/COMPLETED.md §"Ψ-W1-T4")
  • Drag-out to an external app twenty-plus times in a row — audio arrives every time; this is a soak, a single pass is not a gate (docs/COMPLETED.md §"Θ-W1-T2" — "Neither acceptance criterion has actually been met yet")
  • Drop a capture onto an FX container — the instrument loads with that capture (docs/COMPLETED.md §"Θ-W1-T2" — "Neither acceptance criterion has actually been met yet")

Bank packages

  • Run in full. docs/verify-package-transfer.md — the whole cross-machine export/import round trip: writes-one-file, the transfer itself, re-importing the same file never overwrites, the round trip back to the source, the too-new / truncated / mid-payload-corruption refusals (each an exact string), the unsaved-project refusals, and drag-and-drop (docs/COMPLETED.md §"Ε-W3-T1")
  • Force a degraded tracking ledger and confirm the import refuses before the file picker opens: save a project with a bank, close REAPER, edit the saved .rpp's owned_files ext-state value inside its <REASAMPLER ...> block — corrupt the JSON for the Unreadable case, or bump "v":2 to "v":3 for the FutureVersion case — reopen the project, then run ReaSampler: import bank package (.rsbank). Read off: the console prints the ledger-refusal block and no file dialog ever appears (origin_ledger.h's LedgerStatus and ledgerDegraded, package_import_action.cpp's ledgerPermits)
  • Export dialog: type a destination name with no extension, then again over a name that already carries a different one (e.g. mybank.bak) — read off whether GetUserFileName appended .rsbank itself or ReaSampler's own re-append produced the double-extension result (mybank.bak.rsbank) the code expects (src/shell/package/CLAUDE.md §"Gotchas" — "The re-append is suffix-blind")
  • Both the export and the import file dialogs open in front of REAPER's main window, not behind it — GetUserFileName takes no owner window (src/shell/package/CLAUDE.md §"Gotchas" — "GetUserFileName also takes no owner window")
  • With a ReaSampler 9000 instance's editor open on the destination project (Browse view visible), import a .rsbank from the docked panel — the browser reflects the new bank without closing or reopening the editor (the bank-generation bump, session.h's bumpBankGeneration, polled by the instrument at processor_reload.cpp's pollBankSync)
  • Drag two or more .rsbank files onto the docked panel in one drop — each lands as its OWN new bank, never merged into one, and if the tracking ledger is degraded the refusal prints ONCE for the whole drop rather than once per file (panel_window.cpp's handleDropFiles)
  • Kill REAPER (or the process) partway through an import so a partial bank file is stranded under its real name in the bank folder, then re-run the same import into the same project — read off what happens. Whether the import verb should pre-clean that stale debris is an open question, not yet decided (src/shell/package/CLAUDE.md §"Gotchas" — "A crash mid-export strands the .rsbanktmp sibling")

The resample bake

  • Bake a dialed sound — the banked file sounds like what the editor was playing (docs/COMPLETED.md §"Ξ-W2-T1" — "Not demonstrated")
  • Bake the result twice more — iteration composes, nothing is lost per pass (docs/COMPLETED.md §"Ξ-W2-T1" — "Not demonstrated")
  • Save and reopen after a bake — the instance still points at the baked capture (docs/COMPLETED.md §"Ξ-W2-T1" — "Not demonstrated")
  • Confirm no bake put an item in the arrange, and the superseded file is still on disk (docs/COMPLETED.md §"Ξ-W2-T1" — "Not demonstrated")
  • Bake from an instance in a background project tab — refuses rather than writing into the wrong bank (docs/COMPLETED.md §"Ξ-W2-T1" — "Two behaviors worth recording")
  • Load the VST with the extension not installed — the resample affordance reads unavailable, not silently lossy (docs/COMPLETED.md §"Ξ-W2-T1" — "Two behaviors worth recording")
  • After a bake: instance is in Trigger with start point reset, channel mode and preview velocity survived (docs/COMPLETED.md §"Ξ-W2-T1" — "Reset-scope classifications made at review")
  • Gate mode + active sustain loop — "Bake Hold" appears within ~500 ms, its label fits its cell, its travel is duration-ordered (docs/COMPLETED.md §"Ξ-W3-T1")

Render in place

  • Fire Render track in place over a track with a range selected — solo the source and the new sibling track, invert one track's polarity, and confirm silence. This is Ρ's own trust anchor: the placement is the null test performed automatically (docs/product/render-in-place.md §"DAW-verification obligations", docs/COMPLETED.md §"Phase Ρ — Render in place")
  • Render in place from three source positions in turn — a normal mid-folder track, a track that is last in its folder, and a folder-parent track — each time confirm the new sibling track lands at the same nesting level as the source and that the folder bus feeds (or bypasses) it correctly. Then, to settle whether TrackList_AdjustWindows(false) is actually needed: comment out that call in render_in_place.cpp's RunRenderTrackInPlace (it was added preemptively, answering a question the spec had left open rather than one the code confirmed), rebuild, and repeat the folder-parent case — if nesting still displays correctly with the call removed, it can be dropped in a follow-up (docs/product/render-in-place.md §"DAW-verification obligations")
  • Render in place from a dead-centre (channel-identical) source — confirm the placed item is mono, and confirm it plays back at the same perceived level the stereo source did before the render. This is root CLAUDE.md's existing mono-summing [verify — DAW], promoted to load-bearing because Ρ is the first path that places a collapsed render into the mix automatically (docs/product/render-in-place.md §"DAW-verification obligations")
  • Fire Render track in place once from Arrange and once from Design. From Arrange: confirm the source track parks and the result track is visible and in the mix. From Design: confirm the source stays on the bench, the result track is parked too, then switch to Arrange and confirm the result track appears in the source's place. In both cases wait out at least one panel timer tick before checking membership — that is the check that catches a missing explicit-tag-wins filter or an untagged item, either of which silently reverses the ruling that the result track is always an Arrange member (docs/product/render-in-place.md §"Mode transitions — the source parks, the result goes to Arrange")
  • Render in place, then press Ctrl-Z once — confirm the new track and its item are both gone, the source track's folder depth is restored, the rendered file itself is still on disk, and the source track is still tagged Design (docs/product/render-in-place.md §"DAW-verification obligations")
  • Render in place from a named source, then run it again over the resulting (already Capture -prefixed) track — confirm the name does not stack a second prefix, and confirm the new sibling's colour matches the source's (the colour clone has no unit coverage at all). Repeat once from an unnamed source and confirm the result reads Capture Track N (docs/product/render-in-place.md §"DAW-verification obligations"; the name-composition logic itself — apart from the live colour clone and the real GetTrackName/P_NAME round trip — is unit-tested in tests/test_capture_name.cpp)
  • Save a project into a folder whose recording path is set away from the default (Project Settings → Media → Path), then Render track in place — confirm the rendered file lands in that configured recording path, not the project folder itself (render_in_place.cpp's RunRenderTrackInPlace, GetProjectPathEx)
  • Render in place, save the project, and reopen it — confirm the result track (which carries an explicit kArrangeModeId membership record, unlike the shipped tag-selected-tracks action which never writes one) behaves identically, in every mode-switch and visibility check, to an ordinary untagged Arrange track. The JSON round-trip itself is unit-tested (tests/test_view_mode_model.cpp); this is the live-view half that isn't (docs/product/render-in-place.md §"Mode transitions — the source parks, the result goes to Arrange")

Instrument migration

  • Open a project saved before the zone retirement — the instance reopens on its first zone and sounds the same (docs/COMPLETED.md §"Θ-W1-T1")
  • Such an instance with implicit channel mode + a stereo capture reopens Stereo — confirm that is acceptable by ear (docs/COMPLETED.md §"Θ-W1-T1")

Waveform marker zero-crossing snap (Ω-W2-T5)

  • [verify — DAW] Drag a Gate loop mark on a single-cycle capture — the felt radius the mark may be pulled to reach a crossing genuinely reads as imperceptible, i.e. a displacement inside the pointer's own grab neighbourhood is not one the eye catches (src/core/instrument/ui/waveform_view.h's kZeroCrossingSnapPx comment)
  • [verify — DAW] Hold Ctrl, then drag a waveform marker (start/loop start/loop end) off a zero crossing — confirm it lands exactly where dropped, with no snap, in a live editor window (editor_input_waveform.cpp's dragWaveform, GetKeyState-driven, no CI test target)
  • [verify — DAW] Press and hold Ctrl BEFORE mouse-down on a mark that coincides with a contour node or another mark's cap — confirm the mark is grabbed (not the node dragged, not a hard/smooth toggle fired), matching the fix in resolveWaveformClaim (src/core/instrument/ui/spline_edit.cpp)

Look and feel

  • Sign off by eye in a live editor window: knob arcs, needles, envelope splines, waveform outline (docs/COMPLETED.md §"Θ-W7-T1", docs/COMPLETED.md §"Θ-W6-T1" — "Antialiasing pass")
  • Same pass for legibility: text sizes, arc weight, and whether the waveform stroke thickens the docked panel (docs/COMPLETED.md §"Θ-W6-T1", docs/COMPLETED.md §"Θ-W6-T1" — "Sizing")
  • Piano strip at the 840 px default — keys tile uniformly, the 37 px end gutters read as acceptable (docs/COMPLETED.md §"Θ-W2-T3")
  • Resize the editor across several widths — gutters stay symmetric, no key width jumps (docs/COMPLETED.md §"Θ-W2-T3")
  • Set host/OS scaling to 150% then 200% — record how the strip and the AA strokes actually look (docs/TODO.md §"High-DPI host scaling is unverified (distinct from the antialiasing audit)", docs/COMPLETED.md §"Θ-W2-T3" — "Width uniformity is guaranteed in client pixels only")

The VST3 parameter surface

  • [verify — DAW] Whether REAPER renders ParameterInfo::units beside the string getParamStringByValue returns, or shows the string alone. We ship the SDK's own convention (digits in the string, unit carried separately); if REAPER shows no unit at all, the fallback is a one-line change to the single formatter, touching neither the frozen id table nor the editor (docs/COMPLETED.md §"Γ-W4-T1")
  • Whether REAPER's own per-parameter MIDI learn covers what a shipped IMidiMapping CC table would have — the decision to ship no default map rests on it; if learn does not reach these parameters, a CC table is additive and frozen by nothing (docs/COMPLETED.md §"Γ-W4-T1")
  • The three migration round trips: a pre-parameter project opens with every parameter reading the blob's value and sounds identical; a project saved by this build restores fully in an older binary; a project with automation drawn, saved and reopened, replays against the same plain values (docs/COMPLETED.md §"Γ-W4-T1")
  • [verify — DAW] Whether an offline render replays automation at all — the sharpest case for the audio-side drain, because the host drives process() and may never touch the controller (docs/COMPLETED.md §"Γ-W4-T1")
  • [verify — DAW] Whether REAPER restores instance state through setState, not setComponentState — the entry-point half of the original bundled [verify, FIRST] that an earlier pass closed only the delivery-channel half of. The evidence short of a DAW is strong but is inference: vstsinglecomponenteffect.h:41-47 collapses the two names on a single-component plug-in, and the blob has round-tripped through payload v1…v16 in real projects. Exercising it costs one save/reopen (docs/COMPLETED.md §"Γ-W4-T1")