Retire the zone system: one capture = one parameter set, and re-seam the engine and Sample face into bands
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// processor_reload.cpp — ReaSamplerProcessor's off-audio-thread instrument lifecycle:
|
||||
// reloadInstrument (self-contained refs resolve + WAV decode + keymap build), the
|
||||
// reloadInstrument (self-contained refs resolve + WAV decode + SampleData build), the
|
||||
// safety-critical publishBuiltLocked drain-slot swap, the voice-param light rebuild,
|
||||
// idle-drain retirement, the pre-v10 legacy-lift gate, the bank-sync poll, and the
|
||||
// usage publish. Nothing here runs on the audio thread — process() only touches the
|
||||
@@ -19,7 +19,7 @@
|
||||
#include "core/capture/capture_paths.h" // resolveBankFile (shared path resolution)
|
||||
#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/sample_map.h" // refs resolve, buildZonedKeymap (self-contained)
|
||||
#include "core/instrument/map/sample_map.h" // refs resolve, buildSampleData (self-contained)
|
||||
#include "core/util/file_bytes.h" // shared whole-file loader
|
||||
#include "core/wire/assignment_request.h" // decodeAssignmentRequest (request wire parse)
|
||||
#include "core/wire/sample_usage.h" // usage publish plan + wire (prune-protection seam)
|
||||
@@ -60,11 +60,10 @@ std::string mintUsageInstanceGuid() {
|
||||
// Resolves a project-relative WAV path, reads + decodes it (file I/O, off-thread only),
|
||||
// and applies the cross-mode channel policy for `mode` (mono downmix; stereo -> dual-mono
|
||||
// for a mono source, L/R for a stereo source — see decodeChannels). Returns nullopt on any
|
||||
// resolve/read/decode failure — the caller drops the zone or plays silence. Shared by the
|
||||
// zoned build and the single-capture path.
|
||||
std::optional<DecodedZonePcm> decodeRelative(const std::string& projectDir,
|
||||
const std::string& relativePath,
|
||||
ChannelMode mode) {
|
||||
// resolve/read/decode failure — the caller plays silence.
|
||||
std::optional<DecodedPcm> decodeRelative(const std::string& projectDir,
|
||||
const std::string& relativePath,
|
||||
ChannelMode mode) {
|
||||
const std::string abs = resolveBankFile(projectDir, relativePath);
|
||||
if (abs.empty()) return std::nullopt;
|
||||
const std::vector<std::uint8_t> bytes = readFileBytes(abs);
|
||||
@@ -72,8 +71,8 @@ std::optional<DecodedZonePcm> decodeRelative(const std::string& projectDir,
|
||||
if (!layout.valid) return std::nullopt;
|
||||
std::vector<AudioSample> interleaved =
|
||||
extractFloatFrames(bytes, layout, 0, layout.frameCount());
|
||||
DecodedZonePcm out = decodeChannels(interleaved, layout.channelCount, mode,
|
||||
static_cast<int>(layout.sampleRate));
|
||||
DecodedPcm out = decodeChannels(interleaved, layout.channelCount, mode,
|
||||
static_cast<int>(layout.sampleRate));
|
||||
if (out.monoFrames.empty()) return std::nullopt;
|
||||
return out;
|
||||
}
|
||||
@@ -95,8 +94,8 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// nothing below — a project restored before PROJEXTSTATE parses (or with the
|
||||
// extension absent) resolves + plays from the persisted refs.
|
||||
const std::string selId = selectedSampleId();
|
||||
const PerformanceMap map = performanceMap();
|
||||
const std::vector<std::string> ids = referencedSampleIds(selId, map);
|
||||
const InstrumentParams params = instrumentParams();
|
||||
const std::vector<std::string> ids = referencedSampleIds(selId);
|
||||
SampleRefs refs;
|
||||
{
|
||||
std::optional<std::string> banksJson =
|
||||
@@ -110,8 +109,8 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
refs = sampleRefs_; // snapshot for the decode below (outside the refs lock)
|
||||
}
|
||||
const std::string projectDir = bridge_.activeProjectDir();
|
||||
// Governs how each WAV decodes (mono downmix vs 2-channel); the single-capture branch
|
||||
// below may auto-default it before its decode.
|
||||
// Governs how the WAV decodes (mono downmix vs 2-channel); auto-defaulted from the
|
||||
// capture's own channel count below, before the decode.
|
||||
ChannelMode mode = channelMode();
|
||||
// Snapshot the voice-system parameters once — baked into the built engine's
|
||||
// construction (immutable config; a later change rebuilds).
|
||||
@@ -127,60 +126,33 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
|
||||
std::string resolvedId;
|
||||
std::unique_ptr<LoadedInstrument> built;
|
||||
Keymap km;
|
||||
bool haveKeymap = false;
|
||||
SampleData sample;
|
||||
bool havePlayable = false;
|
||||
|
||||
// 2. Zoned build: if the performance map is non-empty, resolve its zones against the
|
||||
// owned refs (an id with no ref drops cleanly), decode each zone's WAV off-thread,
|
||||
// and build the keymap. A zone whose WAV fails to decode is dropped, not the whole
|
||||
// map — the defined no-play, no crash, no retry loop.
|
||||
if (!map.empty()) {
|
||||
const ResolvedPerformance resolved = resolvePerformanceFromRefs(refs, map);
|
||||
if (!resolved.zones.empty()) {
|
||||
std::vector<DecodedZonePcm> decoded;
|
||||
std::vector<ResolvedZone> kept;
|
||||
decoded.reserve(resolved.zones.size());
|
||||
kept.reserve(resolved.zones.size());
|
||||
for (const ResolvedZone& rz : resolved.zones) {
|
||||
std::optional<DecodedZonePcm> pcm =
|
||||
decodeRelative(projectDir, rz.relativePath, mode);
|
||||
if (!pcm) continue; // unreadable/missing WAV -> drop this zone
|
||||
kept.push_back(rz);
|
||||
decoded.push_back(std::move(*pcm));
|
||||
}
|
||||
km = buildZonedKeymap(kept, decoded);
|
||||
haveKeymap = !km.zones.empty();
|
||||
// 2. Resolve + decode the ONE loaded capture, which plays across the whole keyboard
|
||||
// repitched from its effective root. No first-sample fallback: an empty selection
|
||||
// (or one with no ref) resolves to nothing, so an un-picked instrument stays silent
|
||||
// rather than auto-playing sample #1. A missing/unreadable WAV is the same defined
|
||||
// no-play — no crash, no retry loop.
|
||||
if (const SelectedSample* sel = findRef(refs, selId)) {
|
||||
// Auto-default: channelModeFor computes the mode from the loaded capture's channel
|
||||
// count (always 2 for extension captures; mono only for ingest-imported mono files).
|
||||
// An unknown count (0) or explicit user choice keeps the mode.
|
||||
{
|
||||
std::lock_guard<std::mutex> cm(channelModeMutex_);
|
||||
channelMode_ = channelModeFor(sel->channelCount, channelMode_,
|
||||
channelModeExplicit_);
|
||||
mode = channelMode_;
|
||||
}
|
||||
std::optional<DecodedPcm> pcm = decodeRelative(projectDir, sel->relativePath, mode);
|
||||
if (pcm) {
|
||||
sample = buildSampleData(resolveCapture(*sel, params), std::move(*pcm));
|
||||
havePlayable = sample.playable();
|
||||
if (havePlayable) resolvedId = selId; // the concrete pick that resolved
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Single-capture fast path: an empty performance map plays the one selected capture
|
||||
// chromatically across the whole keyboard. No first-sample fallback: an empty
|
||||
// selection (or one with no ref) resolves to nothing, so an un-picked instrument
|
||||
// stays silent rather than auto-playing sample #1.
|
||||
if (!haveKeymap) {
|
||||
if (const SelectedSample* sel = findRef(refs, selId)) {
|
||||
// Auto-default: channelModeFor computes the mode from the loaded capture's
|
||||
// channel count (always 2 for extension captures; mono only for ingest-imported
|
||||
// mono files). An unknown count (0) or explicit user choice keeps the mode.
|
||||
{
|
||||
std::lock_guard<std::mutex> cm(channelModeMutex_);
|
||||
channelMode_ = channelModeFor(sel->channelCount, channelMode_,
|
||||
channelModeExplicit_);
|
||||
mode = channelMode_;
|
||||
}
|
||||
std::optional<DecodedZonePcm> pcm =
|
||||
decodeRelative(projectDir, sel->relativePath, mode);
|
||||
if (pcm) {
|
||||
km = buildTier0Keymap(std::move(pcm->monoFrames), pcm->sampleRate,
|
||||
sel->rootNote, sel->loop,
|
||||
std::move(pcm->framesR));
|
||||
haveKeymap = true;
|
||||
resolvedId = selId; // the concrete pick that resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (haveKeymap) {
|
||||
if (havePlayable) {
|
||||
// Preserve OLA window in output frames from the host rate (kPreserveWindowMs),
|
||||
// pre-sized here so process()-time note-on never allocates. Floored at 2 so a
|
||||
// valid window is always a real ring, covering a pathological host rate <= 0 too.
|
||||
@@ -188,16 +160,16 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
built = std::make_unique<LoadedInstrument>(
|
||||
std::move(km), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
}
|
||||
|
||||
// 4. Publish: atomically install the new instrument via the drain-slot swap (see the
|
||||
// 3. Publish: atomically install the new instrument via the drain-slot swap (see the
|
||||
// header). A null `built` (no ref / unreadable WAV) installs silence while any
|
||||
// displaced tails still ring out via the drain.
|
||||
publishBuiltLocked(std::move(built));
|
||||
|
||||
// 5. Publish this instance's held captures so the extension's prune can never reclaim
|
||||
// 4. Publish this instance's held captures so the extension's prune can never reclaim
|
||||
// them. Regardless of decode success: the holds are the refs the instance retains
|
||||
// (its play-set), not what decoded — a transiently unreadable WAV stays protected.
|
||||
publishUsage(refs, ids);
|
||||
@@ -282,12 +254,12 @@ void ReaSamplerProcessor::rebuildVoiceEngine() {
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
|
||||
// Deep-copy the decoded PCM + zones: safe to read concurrently with process() because
|
||||
// the keymap is immutable after construction and reloadMutex_ prevents `cur` from
|
||||
// being freed.
|
||||
Keymap km = cur->keymap;
|
||||
// Deep-copy the decoded sample: safe to read concurrently with process() because the
|
||||
// SampleData is immutable after construction and reloadMutex_ prevents `cur` from being
|
||||
// freed.
|
||||
SampleData sample = cur->sample;
|
||||
auto built = std::make_unique<LoadedInstrument>(
|
||||
std::move(km), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
publishBuiltLocked(std::move(built));
|
||||
}
|
||||
@@ -322,7 +294,7 @@ bool ReaSamplerProcessor::legacyLiftShouldRun() {
|
||||
if (legacyLiftConcluded_.load(std::memory_order_relaxed)) return false;
|
||||
const LegacyLiftDecision decision = legacyLiftDecision(
|
||||
bridge_.readReasamplerExtState(kProjExtBanksKey),
|
||||
referencedSampleIds(selectedSampleId(), performanceMap()));
|
||||
referencedSampleIds(selectedSampleId()));
|
||||
if (decision == LegacyLiftDecision::Stale) {
|
||||
// Provably stale: give up permanently. A later bank change that re-introduces an
|
||||
// id bumps the generation, and genChanged refreshes the refs without this latch.
|
||||
@@ -379,15 +351,10 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) {
|
||||
|
||||
if (decision.apply) {
|
||||
// Apply as this instance's own selection (the instrument updates its own state,
|
||||
// never the bank); reloadInstrument below rebuilds against it.
|
||||
// never the bank); reloadInstrument below rebuilds against it. The parameter set
|
||||
// carries over to the new capture — there is only one, and it governs whatever is
|
||||
// loaded (the peer of the editor's Browse Load).
|
||||
setSelectedSampleId(decision.sampleId);
|
||||
// Peer of the editor's Browse Load: a stale full-range zone from the previous
|
||||
// sample would shadow the assigned pick under first-match resolve. Authored maps
|
||||
// (narrow key ranges) are untouched.
|
||||
PerformanceMap reconciled = performanceMap();
|
||||
if (reconcileSingleCaptureZones(reconciled, decision.sampleId)) {
|
||||
setPerformanceMap(reconciled);
|
||||
}
|
||||
result.applied = true;
|
||||
}
|
||||
|
||||
@@ -412,8 +379,7 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) {
|
||||
// dependency (a v10 blob plays from its refs with no poll at all).
|
||||
bool legacyLift = false;
|
||||
if (!genChanged && !result.applied && sampleRefs().empty()) {
|
||||
const bool hasIntent = !selectedSampleId().empty() || !performanceMap().empty();
|
||||
legacyLift = hasIntent && legacyLiftShouldRun();
|
||||
legacyLift = !selectedSampleId().empty() && legacyLiftShouldRun();
|
||||
}
|
||||
|
||||
if (genChanged || result.applied || legacyLift) {
|
||||
|
||||
Reference in New Issue
Block a user