Merge instrument own-project resolution: ReaSampler 9000 reads samples from its own project, not the focused tab

This commit is contained in:
2026-09-25 15:03:54 -04:00
11 changed files with 346 additions and 46 deletions
+3 -1
View File
@@ -45,7 +45,8 @@ subdirectories:
- **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float - **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float
WAVs — project-relative, travelling with the `.rpp`. The instrument resolves those WAVs — project-relative, travelling with the `.rpp`. The instrument resolves those
paths the same way `persist` does (a shared convention, not a re-implementation). paths the same way `persist` does (a shared convention, not a re-implementation),
against the project the instance lives in (`map/sample_roots`).
There is no live PCM stream across the bridge, by design. There is no live PCM stream across the bridge, by design.
- **Live-state seam (the mapping, via the bridge).** For everything that is not raw - **Live-state seam (the mapping, via the bridge).** For everything that is not raw
audio — the bank index, the mapping, which project is active — the instrument reads audio — the bank index, the mapping, which project is active — the instrument reads
@@ -367,6 +368,7 @@ anything for a trigger shape.
- `params_payload` — the PARAMS-PAYLOAD half of that codec, split from the envelope half on the axis the format already has: the payload carries its own version and grows independently, so the two version ladders are two responsibilities. An INTERNAL seam — the public entry points stay `serialize`/`deserializeComponentState`. The prose ladder and every version constant stay in `component_state_io.h`, their one home. - `params_payload` — the PARAMS-PAYLOAD half of that codec, split from the envelope half on the axis the format already has: the payload carries its own version and grows independently, so the two version ladders are two responsibilities. An INTERNAL seam — the public entry points stay `serialize`/`deserializeComponentState`. The prose ladder and every version constant stay in `component_state_io.h`, their one home.
- `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects. - `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects.
- `bridge_marshal` — pure marshalling helper for the REAPER VST-host bridge read: interprets the `GetProjExtState` int return against its filled buffer. - `bridge_marshal` — pure marshalling helper for the REAPER VST-host bridge read: interprets the `GetProjExtState` int return against its filled buffer.
- `sample_roots` — where a bank-relative sample path resolves: `ownProjectDir` picks the instance's own project out of the shell's enumeration BY IDENTITY (no fallback to any other project), `sampleRootCandidates` orders the roots to try, and `readFromFirstRoot` reads the first that yields bytes. The shell half and its rationale are `shell/instrument`'s.
- `trigger_seam` — the shared Trigger play-SPAN formula: how a %-length becomes the source-frame span the overlay draws over and the bake's window holds, threading `startFrame` correctly and clamping the fraction the same way `Voice::start` does (the engine evaluates the same formula inline rather than depending on `map/`). The spline fold every consumer must apply first — `effectiveLengthFraction` — is `play_params.h`'s, beside the rest of that rule family. (Its fade frames↔fraction converters retired with the fade pair itself.) - `trigger_seam` — the shared Trigger play-SPAN formula: how a %-length becomes the source-frame span the overlay draws over and the bake's window holds, threading `startFrame` correctly and clamping the fraction the same way `Voice::start` does (the engine evaluates the same formula inline rather than depending on `map/`). The spline fold every consumer must apply first — `effectiveLengthFraction` — is `play_params.h`'s, beside the rest of that rule family. (Its fade frames↔fraction converters retired with the fade pair itself.)
### `ui/` ### `ui/`
+3
View File
@@ -1,6 +1,9 @@
reasampler_pure_library(bridge_marshal SOURCES bridge_marshal.cpp) reasampler_pure_library(bridge_marshal SOURCES bridge_marshal.cpp)
reasampler_test(bridge_marshal LINK bridge_marshal) reasampler_test(bridge_marshal LINK bridge_marshal)
reasampler_pure_library(sample_roots SOURCES sample_roots.cpp LINK PRIVATE capture_paths)
reasampler_test(sample_roots LINK sample_roots)
reasampler_pure_library(trigger_seam SOURCES trigger_seam.cpp) reasampler_pure_library(trigger_seam SOURCES trigger_seam.cpp)
# Plain frame arithmetic over doubles: no engine, no value layer, no editor geometry. The # Plain frame arithmetic over doubles: no engine, no value layer, no editor geometry. The
# %-length fold it used to host lives with its siblings in play_params.h. # %-length fold it used to host lives with its siblings in play_params.h.
+36
View File
@@ -0,0 +1,36 @@
// sample_roots.cpp — see sample_roots.h.
#include "core/instrument/map/sample_roots.h"
#include "core/capture/capture_paths.h" // projectDirOfRpp / resolveBankFile (persist's convention)
namespace reasampler::instrument::map {
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open,
const void* activeFallback) {
const void* target = own ? own : activeFallback;
if (!target) return {};
for (const ProjectTab& tab : open) {
if (tab.project == target) return capture::projectDirOfRpp(tab.rppPath);
}
return {};
}
std::vector<std::string> sampleRootCandidates(const std::string& projectDir) {
if (projectDir.empty()) return {};
return {projectDir};
}
std::vector<std::uint8_t> readFromFirstRoot(const std::vector<std::string>& roots,
const std::string& relativePath,
const FileReader& read) {
for (const std::string& root : roots) {
const std::string abs = capture::resolveBankFile(root, relativePath);
if (abs.empty()) continue;
std::vector<std::uint8_t> bytes = read(abs);
if (!bytes.empty()) return bytes;
}
return {};
}
} // namespace reasampler::instrument::map
+47
View File
@@ -0,0 +1,47 @@
// sample_roots — where the instrument looks for a bank-relative sample: the directory of
// the project it LIVES IN, found by identity among the open projects, never the focused
// tab's. Pure: the shell enumerates REAPER's projects and reads files; this decides which
// project, which roots, and in what order.
#pragma once
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace reasampler::instrument::map {
// One enumerated project: its identity (an opaque ReaProject*, compared, never
// dereferenced) and its .rpp path, empty for a never-saved project.
struct ProjectTab {
const void* project = nullptr;
std::string rppPath;
};
// The directory of the project whose identity is `own`; empty when `own` is not among
// `open` or was never saved. Deliberately no fallback to any other project for a
// NON-NULL `own`: another project's bank folder can hold a same-named file, and playing
// it is the wrong-project bug this exists to close.
//
// `activeFallback` is used ONLY when `own` is null (REAPER may not expose
// getReaperParent(3) yet during plugin creation/setState) — then it stands in for `own`
// unchanged, so a still-null fallback (or one absent from `open`) resolves nothing exactly
// as a null `own` always did.
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open,
const void* activeFallback = nullptr);
// The roots a bank-relative path is tried against, in order. Only the project directory
// today — the ordered list is the seam a later root (tried after it) extends. Empty when
// there is no project directory, which every caller treats as nothing to play.
std::vector<std::string> sampleRootCandidates(const std::string& projectDir);
using FileReader = std::function<std::vector<std::uint8_t>(const std::string& path)>;
// The bytes of `relativePath` resolved under the first root whose read yields any (a read
// that comes back empty falls through to the next root); empty when none does.
std::vector<std::uint8_t> readFromFirstRoot(const std::vector<std::string>& roots,
const std::string& relativePath,
const FileReader& read);
} // namespace reasampler::instrument::map
+17 -1
View File
@@ -226,7 +226,7 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
## Modules ## Modules
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. Both PROVE the write by reading the key back (`wire::extStateWriteLanded`) — `SetProjExtState`'s own return cannot speak for one key, so testing it was a guard that could never fire, and the bake's "could not publish" refusal was consequently unreachable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`. - `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. Both PROVE the write by reading the key back (`wire::extStateWriteLanded`) — `SetProjExtState`'s own return cannot speak for one key, so testing it was a guard that could never fire, and the bake's "could not publish" refusal was consequently unreachable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`. And it owns `sampleRoots`, the ONE answer to where a bank-relative sample path resolves — see the Gotcha below.
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no cut to ringing tails. **Activation and decoding are separate lifetimes:** `setActive(false)` parks the decoded `SampleData` and destroys the voice state (a surviving `live_` would be displaced into the drain slot and resurrect stale sustained voices), and `setActive(true)` rebuilds the voices around the parked sample through that same swap — so a host-driven cycle costs no disk read and no decode. The resume still folds the live bank blob into the refs and republishes usage (a `GetProjExtState` plus a parse each, and with no editor open the activation is the only place either happens), and hands back to the full reload when that fold moved the loaded capture's decode source. A host-rate change while inactive does NOT re-decode: every stage time in the park (or in a publish that landed while inactive) and in the live block is in that rate's frames, so the resume re-stamps those in place (`restampHostRateLocked`) and falls back to the full reload only when the new rate refuses the file's (`sourceRateSupported`). Nothing parked means nothing was decoded, which routes the activation back through the full reload too; that is also where the pre-v10 legacy lift lives. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. **The master bus:** the summed output runs `voice mixer → master gain → limiter (core/instrument/engine/limiter) → output bus`, with the meter tapped at the bus output POST-limiter and published as relaxed atomics — per-channel peak and smallest limiter gain ACCUMULATED across every block since the UI last read, plus the latched clip (the meter Gotcha below owns why). The limiter's enable is persisted in the parameter set (params payload v15) and mirrored onto the audio thread by `setInstrumentParams`, the single funnel every writer already goes through. That mirror is also what `getLatencySamples()` answers from — the plugin's FIRST latency reporting: 0 bypassed, the lookahead engaged. The host's `restartComponent(kLatencyChanged)` is issued by `flushLatencyRestart` alone, UI thread only and never from `process()`; it is a LATENCY restart with the bus untouched, NOT the retired per-mode `kIoChanged` bus renegotiation the invariant above forbids. Why it is split from the commit is the Gotcha below. - `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no cut to ringing tails. **Activation and decoding are separate lifetimes:** `setActive(false)` parks the decoded `SampleData` and destroys the voice state (a surviving `live_` would be displaced into the drain slot and resurrect stale sustained voices), and `setActive(true)` rebuilds the voices around the parked sample through that same swap — so a host-driven cycle costs no disk read and no decode. The resume still folds the live bank blob into the refs and republishes usage (a `GetProjExtState` plus a parse each, and with no editor open the activation is the only place either happens), and hands back to the full reload when that fold moved the loaded capture's decode source. A host-rate change while inactive does NOT re-decode: every stage time in the park (or in a publish that landed while inactive) and in the live block is in that rate's frames, so the resume re-stamps those in place (`restampHostRateLocked`) and falls back to the full reload only when the new rate refuses the file's (`sourceRateSupported`). Nothing parked means nothing was decoded, which routes the activation back through the full reload too; that is also where the pre-v10 legacy lift lives. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. **The master bus:** the summed output runs `voice mixer → master gain → limiter (core/instrument/engine/limiter) → output bus`, with the meter tapped at the bus output POST-limiter and published as relaxed atomics — per-channel peak and smallest limiter gain ACCUMULATED across every block since the UI last read, plus the latched clip (the meter Gotcha below owns why). The limiter's enable is persisted in the parameter set (params payload v15) and mirrored onto the audio thread by `setInstrumentParams`, the single funnel every writer already goes through. That mirror is also what `getLatencySamples()` answers from — the plugin's FIRST latency reporting: 0 bypassed, the lookahead engaged. The host's `restartComponent(kLatencyChanged)` is issued by `flushLatencyRestart` alone, UI thread only and never from `process()`; it is a LATENCY restart with the bus untouched, NOT the retired per-mode `kIoChanged` bus renegotiation the invariant above forbids. Why it is split from the commit is the Gotcha below.
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred). - `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select). - `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
@@ -313,6 +313,22 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
meter's fixed right-anchored slot, at any resizable window size — so the fast path stays live meter's fixed right-anchored slot, at any resizable window size — so the fast path stays live
under it; `paintMeterField` reapplies the popup's own 0.50-alpha wash to the field alone so a under it; `paintMeterField` reapplies the popup's own 0.50-alpha wash to the field alone so a
meter-only frame doesn't flash through it at full brightness. meter-only frame doesn't flash through it at full brightness.
- **A sample resolves against the instance's OWN project, never the focused tab.**
`ReaperBridge::sampleRoots` matches `getReaperParent(3)` by identity among the projects
`EnumProjects(idx)` walks (plus the SDK's currently-rendering slot, `0x40000000`) and hands
the match's `.rpp` directory to the pure `core/instrument/map/sample_roots`, which returns an
ordered root list; every instrument-side sample read (`reloadInstrument`, `bakeSnapshot`,
the editor's `channelPcmFor`/`monoPcmFor`) goes through `readFromFirstRoot` over it. The old
`EnumProjects(-1)` lookup resolved a background tab's instance against the FOCUSED project's
folder — do not reintroduce it as the PRIMARY resolve. It survives ONE sanctioned use: when
`getReaperParent(3)` itself comes back null (REAPER may not expose the parent during plugin
creation/setState), `ownProjectDir` falls back to the focused tab — because at that point there
is no "own project" identity to prefer a wrong answer over. A NON-NULL parent that is an
unlisted project or an unsaved one still yields no roots, which plays nothing, exactly as an
unsaved project always did. The ordered list is one element today; a later root is added after
the project directory, in `sampleRootCandidates`. `[verify — DAW]`: that `getReaperParent(3)` is
non-null and enumerable during `setState` on project load, and that a background-tab instance
and two open projects each with an instance play their own project's sample.
- The bake's availability probe runs on the SAME tick that paints the button, so the - The bake's availability probe runs on the SAME tick that paints the button, so the
control can never be enabled on one tick and refuse on the next. The bake Hold control's control can never be enabled on one tick and refuse on the next. The bake Hold control's
applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and
+1 -1
View File
@@ -85,7 +85,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
# generated version header, so the instrument reads the SAME channel-derived ext-state # generated version header, so the instrument reads the SAME channel-derived ext-state
# namespace the extension writes. # namespace the extension writes.
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
sampler_core sample_map component_state_io capture_paths embed_strip app_version sampler_core sample_map sample_roots component_state_io capture_paths embed_strip app_version
capture_browser keyboard_strip sample_bands sample_chrome capture_browser keyboard_strip sample_bands sample_chrome
waveform_view loop_marks bank_sync browser_scroll param_slider tooltip waveform_view loop_marks bank_sync browser_scroll param_slider tooltip
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
+5 -8
View File
@@ -13,8 +13,8 @@
#include <vector> #include <vector>
#include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail) #include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail)
#include "core/capture/capture_paths.h" // resolveBankFile (shared path resolution)
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames #include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames
#include "core/instrument/map/sample_roots.h" // readFromFirstRoot (own-project resolution)
#include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (the pure key) #include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (the pure key)
#include "core/util/file_bytes.h" // shared whole-file loader #include "core/util/file_bytes.h" // shared whole-file loader
#include "ext_keys.h" #include "ext_keys.h"
@@ -33,7 +33,6 @@ using audio::computeEnvelope;
using capture::WavLayout; using capture::WavLayout;
using capture::extractFloatFrames; using capture::extractFloatFrames;
using capture::parseWavLayout; using capture::parseWavLayout;
using capture::resolveBankFile;
using instrument::ui::nameMatchesQuery; using instrument::ui::nameMatchesQuery;
using ui::ThumbnailKey; using ui::ThumbnailKey;
using ui::thumbnailKeyString; using ui::thumbnailKeyString;
@@ -425,9 +424,8 @@ const ReaSamplerEditor::ChannelPcm& ReaSamplerEditor::channelPcmFor(
const std::string relativePath = samplePathFor(sampleId); const std::string relativePath = samplePathFor(sampleId);
if (relativePath.empty()) return channelPcm_; if (relativePath.empty()) return channelPcm_;
const std::string projectDir = processor_->bridge().activeProjectDir(); const std::vector<std::uint8_t> bytes = readFromFirstRoot(
const std::vector<std::uint8_t> bytes = processor_->bridge().sampleRoots(), relativePath, readFileBytes); // empty on any failure
readFileBytes(resolveBankFile(projectDir, relativePath)); // empty on any failure
const WavLayout layout = parseWavLayout(bytes); const WavLayout layout = parseWavLayout(bytes);
if (layout.valid) { if (layout.valid) {
channelPcm_.interleaved = extractFloatFrames(bytes, layout, 0, layout.frameCount()); channelPcm_.interleaved = extractFloatFrames(bytes, layout, 0, layout.frameCount());
@@ -446,9 +444,8 @@ const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string&
MonoPcm mono; MonoPcm mono;
const std::string relativePath = samplePathFor(sampleId); const std::string relativePath = samplePathFor(sampleId);
if (!relativePath.empty()) { if (!relativePath.empty()) {
const std::string projectDir = processor_->bridge().activeProjectDir(); const std::vector<std::uint8_t> bytes = readFromFirstRoot(
const std::string abs = resolveBankFile(projectDir, relativePath); processor_->bridge().sampleRoots(), relativePath, readFileBytes); // empty on any failure
const std::vector<std::uint8_t> bytes = readFileBytes(abs); // empty on any failure
const WavLayout layout = parseWavLayout(bytes); const WavLayout layout = parseWavLayout(bytes);
if (layout.valid) { if (layout.valid) {
std::vector<AudioSample> interleaved = std::vector<AudioSample> interleaved =
+13 -15
View File
@@ -17,10 +17,10 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "core/capture/capture_paths.h" // resolveBankFile (shared path resolution)
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse) #include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
#include "core/instrument/map/bank_sync.h" // pure decisions: parseBankGeneration, consumeDecision #include "core/instrument/map/bank_sync.h" // pure decisions: parseBankGeneration, consumeDecision
#include "core/instrument/map/sample_map.h" // refs resolve, buildSampleData (self-contained) #include "core/instrument/map/sample_map.h" // refs resolve, buildSampleData (self-contained)
#include "core/instrument/map/sample_roots.h" // readFromFirstRoot (own-project resolution)
#include "core/util/file_bytes.h" // shared whole-file loader #include "core/util/file_bytes.h" // shared whole-file loader
#include "core/wire/assignment_request.h" // decodeAssignmentRequest (request wire parse) #include "core/wire/assignment_request.h" // decodeAssignmentRequest (request wire parse)
#include "core/wire/sample_usage.h" // usage publish plan + wire (prune-protection seam) #include "core/wire/sample_usage.h" // usage publish plan + wire (prune-protection seam)
@@ -32,7 +32,6 @@ using namespace instrument::map; // resolution + bank-sync vocabulary this TU
using namespace reasampler::wire; // assignment_request + sample_usage wire records using namespace reasampler::wire; // assignment_request + sample_usage wire records
using capture::extractFloatFrames; using capture::extractFloatFrames;
using capture::parseWavLayout; using capture::parseWavLayout;
using capture::resolveBankFile;
using capture::WavLayout; using capture::WavLayout;
using util::readFileBytes; using util::readFileBytes;
@@ -58,16 +57,15 @@ std::string mintUsageInstanceGuid() {
return std::string(buf); return std::string(buf);
} }
// Resolves a project-relative WAV path, reads + decodes it (file I/O, off-thread only), // Resolves a project-relative WAV path against the instance's sample roots, reads + decodes
// and applies the cross-mode channel policy for `mode` (mono downmix; stereo -> dual-mono // it (file I/O, off-thread only), and applies the cross-mode channel policy for `mode` (mono
// for a mono source, L/R for a stereo source — see decodeChannels). Returns nullopt on any // downmix; stereo -> dual-mono for a mono source, L/R for a stereo source — see
// resolve/read/decode failure — the caller plays silence. // decodeChannels). Returns nullopt on any resolve/read/decode failure — the caller plays
std::optional<DecodedPcm> decodeRelative(const std::string& projectDir, // silence.
std::optional<DecodedPcm> decodeRelative(const std::vector<std::string>& roots,
const std::string& relativePath, const std::string& relativePath,
ChannelMode mode) { ChannelMode mode) {
const std::string abs = resolveBankFile(projectDir, relativePath); const std::vector<std::uint8_t> bytes = readFromFirstRoot(roots, relativePath, readFileBytes);
if (abs.empty()) return std::nullopt;
const std::vector<std::uint8_t> bytes = readFileBytes(abs);
const WavLayout layout = parseWavLayout(bytes); const WavLayout layout = parseWavLayout(bytes);
if (!layout.valid) return std::nullopt; if (!layout.valid) return std::nullopt;
std::vector<AudioSample> interleaved = std::vector<AudioSample> interleaved =
@@ -82,9 +80,9 @@ std::optional<DecodedPcm> decodeRelative(const std::string& projectDir,
// never be built from a different fold than the one the user is hearing. // never be built from a different fold than the one the user is hearing.
BuiltCapture buildFromRef(const SelectedSample& sel, BuiltCapture buildFromRef(const SelectedSample& sel,
const InstrumentParams& params, const InstrumentParams& params,
const std::string& projectDir, const std::vector<std::string>& roots,
ChannelMode mode, int renderRate) { ChannelMode mode, int renderRate) {
std::optional<DecodedPcm> pcm = decodeRelative(projectDir, sel.relativePath, mode); std::optional<DecodedPcm> pcm = decodeRelative(roots, sel.relativePath, mode);
if (!pcm) return {}; if (!pcm) return {};
if (!sourceRateSupported(pcm->sampleRate, renderRate)) return {std::nullopt, true}; if (!sourceRateSupported(pcm->sampleRate, renderRate)) return {std::nullopt, true};
SampleData sample = SampleData sample =
@@ -125,7 +123,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
// (getState filters via retainRefs to what the instance plays). // (getState filters via retainRefs to what the instance plays).
refs = sampleRefs_; // snapshot for the decode below (outside the refs lock) refs = sampleRefs_; // snapshot for the decode below (outside the refs lock)
} }
const std::string projectDir = bridge_.activeProjectDir(); const std::vector<std::string> roots = bridge_.sampleRoots();
// Governs how the WAV decodes (mono downmix vs 2-channel); auto-defaulted from the // Governs how the WAV decodes (mono downmix vs 2-channel); auto-defaulted from the
// capture's own channel count below, before the decode. // capture's own channel count below, before the decode.
ChannelMode mode = channelMode(); ChannelMode mode = channelMode();
@@ -152,7 +150,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
channelModeExplicit_); channelModeExplicit_);
mode = channelMode_; mode = channelMode_;
} }
BuiltCapture decoded = buildFromRef(*sel, params, projectDir, mode, hostRenderRate()); BuiltCapture decoded = buildFromRef(*sel, params, roots, mode, hostRenderRate());
rateRefused = decoded.rateRefused; rateRefused = decoded.rateRefused;
if (decoded.sample) { if (decoded.sample) {
sample = std::move(*decoded.sample); sample = std::move(*decoded.sample);
@@ -220,7 +218,7 @@ BuiltCapture ReaSamplerProcessor::bakeSnapshot(int renderRate) {
if (!sel) return {}; if (!sel) return {};
// No live block is attached: the bake renders the stored parameter set, which is what // No live block is attached: the bake renders the stored parameter set, which is what
// every knob has already written, rather than whatever the audio thread is observing. // every knob has already written, rather than whatever the audio thread is observing.
return buildFromRef(*sel, params, bridge_.activeProjectDir(), channelMode(), renderRate); return buildFromRef(*sel, params, bridge_.sampleRoots(), channelMode(), renderRate);
} }
void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry, void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
+34 -12
View File
@@ -8,7 +8,7 @@
#include "core/instrument/map/bridge_marshal.h" #include "core/instrument/map/bridge_marshal.h"
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (grow-loop policy) #include "core/wire/ext_state_read.h" // readProjExtStateGrowing (grow-loop policy)
#include "core/capture/capture_paths.h" // projectDirOfRpp (shared project-dir derivation) #include "core/instrument/map/sample_roots.h" // ownProjectDir / sampleRootCandidates
#include "ext_keys.h" // kProjExtNamespace (shared wire contract) #include "ext_keys.h" // kProjExtNamespace (shared wire contract)
// VST3 base types must be included before REAPER's VST3 interface header, which uses // VST3 base types must be included before REAPER's VST3 interface header, which uses
@@ -34,8 +34,10 @@ DEF_CLASS_IID(Steinberg::IReaperHostApplication)
namespace reasampler::vst { namespace reasampler::vst {
using capture::projectDirOfRpp;
using instrument::map::decodeGetProjExtState; using instrument::map::decodeGetProjExtState;
using instrument::map::ownProjectDir;
using instrument::map::ProjectTab;
using instrument::map::sampleRootCandidates;
bool ReaperBridge::connect(Steinberg::FUnknown* context) { bool ReaperBridge::connect(Steinberg::FUnknown* context) {
getProjExtState_ = nullptr; getProjExtState_ = nullptr;
@@ -63,8 +65,7 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
reaper->getReaperApi("GetProjExtState")); reaper->getReaperApi("GetProjExtState"));
enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>( enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
reaper->getReaperApi("EnumProjExtState")); reaper->getReaperApi("EnumProjExtState"));
// EnumProjects(-1, ...) yields the active project + its .rpp path — same convention // Walked by sampleRoots to find this instance's own project among the open ones.
// the persist shell uses, so the instrument derives the project directory identically.
enumProjects_ = reinterpret_cast<EnumProjectsFn>( enumProjects_ = reinterpret_cast<EnumProjectsFn>(
reaper->getReaperApi("EnumProjects")); reaper->getReaperApi("EnumProjects"));
// The (prefix-guarded) usage publish write + the track-identity pair it stamps. All // The (prefix-guarded) usage publish write + the track-identity pair it stamps. All
@@ -93,8 +94,8 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) { std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
if (!getProjExtState_ || !hostApp_) return std::nullopt; if (!getProjExtState_ || !hostApp_) return std::nullopt;
// getReaperParent(3) reads the live "reasampler" ext-state against the active project // getReaperParent(3) reads the live "reasampler" ext-state against the project the
// the instrument was instantiated in, so it follows project switches for free. A null // instrument was instantiated in, so it follows project switches for free. A null
// project is legitimate (REAPER treats it as the current project) — pass it through // project is legitimate (REAPER treats it as the current project) — pass it through
// rather than bailing; a fruitless read still yields nullopt to the caller. // rather than bailing; a fruitless read still yields nullopt to the caller.
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_); auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
@@ -199,13 +200,34 @@ std::string ReaperBridge::currentTrackGuid() {
return std::string(buf); return std::string(buf);
} }
std::string ReaperBridge::activeProjectDir() { std::vector<std::string> ReaperBridge::sampleRoots() {
if (!enumProjects_) return {}; if (!enumProjects_ || !hostApp_) return {};
// idx=-1 is the current project tab; the out-buffer is empty for a never-saved auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
// project. projectDirOfRpp keeps that empty (no default-location fallback). const void* own = reaper->getReaperParent(3);
std::vector<ProjectTab> open;
std::vector<char> buf(4096, '\0'); std::vector<char> buf(4096, '\0');
enumProjects_(-1, buf.data(), static_cast<int>(buf.size())); const auto enumerate = [&](int idx) -> bool {
return projectDirOfRpp(std::string(buf.data())); buf[0] = '\0'; // so one project's path can never read as the next one's
void* proj = enumProjects_(idx, buf.data(), static_cast<int>(buf.size()));
if (proj) open.push_back(ProjectTab{proj, std::string(buf.data())});
return proj != nullptr;
};
for (int idx = 0; enumerate(idx); ++idx) {}
// The SDK's "currently rendering project" slot, appended after the tabs in case a
// render runs a project the tab walk does not list ([verify — DAW] whether one can).
// A duplicate of a listed tab is harmless: the first identity match wins.
constexpr int kRenderingProject = 0x40000000;
enumerate(kRenderingProject);
// getReaperParent(3) can come back null while REAPER has not yet exposed the parent
// (plugin creation/setState) — the focused tab is the one sanctioned fallback, used
// ONLY then; see ownProjectDir. A non-null own that isn't in `open`, or is unsaved,
// still resolves nothing.
const void* activeFallback =
own ? nullptr
: enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
return sampleRootCandidates(ownProjectDir(own, open, activeFallback));
} }
} // namespace reasampler::vst } // namespace reasampler::vst
+16 -8
View File
@@ -11,6 +11,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
#include "pluginterfaces/base/funknown.h" #include "pluginterfaces/base/funknown.h"
@@ -31,18 +32,23 @@ public:
bool isConnected() const { return getProjExtState_ != nullptr; } bool isConnected() const { return getProjExtState_ != nullptr; }
// Reads a "reasampler" ext-state value by key from the host's active project. // Reads a "reasampler" ext-state value by key from this instance's own project
// Returns nullopt when unconnected, unresolvable, or the key is absent. // (getReaperParent(3)). Returns nullopt when unconnected, unresolvable, or the key is
// absent.
// //
// NOT REAL-TIME SAFE (allocates + calls into REAPER): audio-thread callers MUST NOT // NOT REAL-TIME SAFE (allocates + calls into REAPER): audio-thread callers MUST NOT
// invoke this. The instrument reads on the main/UI thread and hands a snapshot to // invoke this. The instrument reads on the main/UI thread and hands a snapshot to
// the process path. // the process path.
std::optional<std::string> readReasamplerExtState(const std::string& key); std::optional<std::string> readReasamplerExtState(const std::string& key);
// The active project's directory (forward-slashed, no trailing slash) — the same // The ordered roots this instance's bank-relative sample paths resolve against — its OWN
// convention persist uses to place the bank alongside the .rpp. Empty for an unsaved // project's directory (getReaperParent(3) matched among EnumProjects' open projects),
// project or when unconnected. Not RT-safe. // never the focused tab's; see core/instrument/map/sample_roots. When getReaperParent(3)
std::string activeProjectDir(); // is null (REAPER may not expose it yet during plugin creation/setState), falls back to
// the focused tab (EnumProjects(-1)) — the one sanctioned use of it. Empty when
// unconnected, when neither resolves to an open project, or when it was never saved.
// Not RT-safe.
std::vector<std::string> sampleRoots();
// The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one // The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one
// key prefix and refusing every other key. That structural refusal is what keeps the // key prefix and refusing every other key. That structural refusal is what keeps the
@@ -107,8 +113,10 @@ private:
using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx, using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx,
char* keyOut, int keyOut_sz, char* valOut, char* keyOut, int keyOut_sz, char* valOut,
int valOut_sz); int valOut_sz);
// EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path. idx=-1 (current // EnumProjects(idx, projfnOut, sz) -> the idx'th open project + its .rpp path, null past
// tab) follows the active project, same convention as the persist shell. // the last. Walked by index for the open-tabs list; idx=-1 (the focused tab) was the
// wrong-project lookup sampleRoots replaced as the PRIMARY resolve — it survives only as
// the fallback used when getReaperParent(3) is null (see sampleRoots).
using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz); using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
// Used ONLY by the two prefix-guarded writers — see the read-only-bank note there. // Used ONLY by the two prefix-guarded writers — see the read-only-bank note there.
using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key, using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
+171
View File
@@ -0,0 +1,171 @@
// Standalone tests for reasampler::instrument::map::sample_roots — no VST3, no REAPER, no
// test framework. The project identities are addresses of local objects standing in for
// ReaProject*; paths are lowercase forward-slash so normalizeSlashes is the identity on
// every platform.
#include "../src/core/instrument/map/sample_roots.h"
#include <cstdint>
#include <cstdio>
#include <map>
#include <string>
#include <vector>
using namespace reasampler::instrument::map;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
namespace {
int projA = 0, projB = 0, projC = 0, stranger = 0;
// Tab 0 (projA) plays the focused tab — the one the old EnumProjects(-1) lookup answered.
std::vector<ProjectTab> threeTabs() {
return {
{&projA, "c:/projects/a/song_a.rpp"},
{&projB, "c:/projects/b/song_b.rpp"},
{&projC, ""}, // never saved
};
}
std::vector<std::uint8_t> bytesOf(const std::string& s) {
return std::vector<std::uint8_t>(s.begin(), s.end());
}
} // namespace
// --- ownProjectDir ------------------------------------------------------------
static void testBackgroundInstanceResolvesItsOwnProjectNotTheFirstTab() {
CHECK(ownProjectDir(&projB, threeTabs()) == "c:/projects/b");
}
static void testEachOfTwoProjectsResolvesItsOwnDirectory() {
const auto tabs = threeTabs();
CHECK(ownProjectDir(&projA, tabs) == "c:/projects/a");
CHECK(ownProjectDir(&projB, tabs) == "c:/projects/b");
}
static void testNullIdentityResolvesNothing() {
CHECK(ownProjectDir(nullptr, threeTabs()).empty());
}
static void testUnlistedProjectResolvesNothingRatherThanAnotherTab() {
CHECK(ownProjectDir(&stranger, threeTabs()).empty());
}
static void testUnsavedOwnProjectResolvesNothingEvenBesideSavedTabs() {
CHECK(ownProjectDir(&projC, threeTabs()).empty());
}
static void testNoOpenProjectsResolvesNothing() {
CHECK(ownProjectDir(&projA, {}).empty());
}
static void testNullOwnFallsBackToTheActiveTab() {
CHECK(ownProjectDir(nullptr, threeTabs(), &projB) == "c:/projects/b");
}
static void testNullOwnAndNullFallbackResolvesNothing() {
CHECK(ownProjectDir(nullptr, threeTabs(), nullptr).empty());
}
static void testNonNullOwnIgnoresTheFallback() {
CHECK(ownProjectDir(&stranger, threeTabs(), &projB).empty());
}
// --- sampleRootCandidates -----------------------------------------------------
static void testProjectDirIsTheOnlyCandidate() {
const auto roots = sampleRootCandidates("c:/projects/b");
CHECK(roots.size() == 1);
CHECK(!roots.empty() && roots[0] == "c:/projects/b");
}
static void testNoProjectDirMeansNoCandidates() {
CHECK(sampleRootCandidates("").empty());
}
// --- readFromFirstRoot --------------------------------------------------------
static void testReadsUnderTheFirstRootThatHasTheFile() {
const std::map<std::string, std::string> disk = {
{"c:/second/reasampler_bank/x.wav", "SECOND"},
};
std::vector<std::string> asked;
const auto bytes = readFromFirstRoot(
{"c:/first", "c:/second"}, "reasampler_bank/x.wav", [&](const std::string& p) {
asked.push_back(p);
const auto it = disk.find(p);
return it == disk.end() ? std::vector<std::uint8_t>{} : bytesOf(it->second);
});
CHECK(bytes == bytesOf("SECOND"));
CHECK(asked.size() == 2);
CHECK(asked.size() == 2 && asked[0] == "c:/first/reasampler_bank/x.wav");
}
static void testEarlierRootWinsWhenBothHaveTheFile() {
std::vector<std::string> asked;
const auto bytes = readFromFirstRoot(
{"c:/first", "c:/second"}, "reasampler_bank/x.wav", [&](const std::string& p) {
asked.push_back(p);
return bytesOf(p);
});
CHECK(bytes == bytesOf("c:/first/reasampler_bank/x.wav"));
CHECK(asked.size() == 1);
}
static void testNoRootsReadsNothing() {
int calls = 0;
const auto bytes = readFromFirstRoot({}, "reasampler_bank/x.wav",
[&](const std::string&) {
++calls;
return bytesOf("never");
});
CHECK(bytes.empty());
CHECK(calls == 0);
}
static void testEmptyRelativePathReadsNothing() {
int calls = 0;
const auto bytes = readFromFirstRoot({"c:/first"}, "", [&](const std::string&) {
++calls;
return bytesOf("never");
});
CHECK(bytes.empty());
CHECK(calls == 0);
}
static void testNoRootHoldingTheFileYieldsEmpty() {
const auto bytes = readFromFirstRoot({"c:/first", "c:/second"}, "reasampler_bank/x.wav",
[](const std::string&) {
return std::vector<std::uint8_t>{};
});
CHECK(bytes.empty());
}
int main() {
testBackgroundInstanceResolvesItsOwnProjectNotTheFirstTab();
testEachOfTwoProjectsResolvesItsOwnDirectory();
testNullIdentityResolvesNothing();
testUnlistedProjectResolvesNothingRatherThanAnotherTab();
testUnsavedOwnProjectResolvesNothingEvenBesideSavedTabs();
testNoOpenProjectsResolvesNothing();
testNullOwnFallsBackToTheActiveTab();
testNullOwnAndNullFallbackResolvesNothing();
testNonNullOwnIgnoresTheFallback();
testProjectDirIsTheOnlyCandidate();
testNoProjectDirMeansNoCandidates();
testReadsUnderTheFirstRootThatHasTheFile();
testEarlierRootWinsWhenBothHaveTheFile();
testNoRootsReadsNothing();
testEmptyRelativePathReadsNothing();
testNoRootHoldingTheFileYieldsEmpty();
if (g_fail == 0) std::printf("sample_roots: all tests passed\n");
return g_fail != 0;
}