diff --git a/src/instrument_drop.cpp b/src/instrument_drop.cpp index 6007620..0323a7e 100644 --- a/src/instrument_drop.cpp +++ b/src/instrument_drop.cpp @@ -26,10 +26,11 @@ int b64Value(unsigned char c) { std::vector instrumentDropStateBytes(const std::string& sampleId) { // The ONE fact the drop carries: this capture is the instance's selection. Everything - // else stays at the fresh-instance defaults (no zones, mono, generation 0) — the same - // ComponentState a browser click would produce. serializeComponentState is the - // instrument's own writer (the single source of truth for the byte layout), so this is - // NOT a parallel encoder — it IS the instrument's encoder. + // else stays at the fresh-instance defaults (no zones, implicit channel mode, generation + // 0) — the same ComponentState a browser click would produce. The implicit mode means + // the GA auto-default will follow the loaded capture's channel count on first reload. + // serializeComponentState is the instrument's own writer (the single source of truth for + // the byte layout), so this is NOT a parallel encoder — it IS the instrument's encoder. ComponentState cs; cs.selectionId = sampleId; return serializeComponentState(cs); diff --git a/src/vst/reasampler_processor.cpp b/src/vst/reasampler_processor.cpp index 9a7868e..d443d4a 100644 --- a/src/vst/reasampler_processor.cpp +++ b/src/vst/reasampler_processor.cpp @@ -505,16 +505,15 @@ std::string ReaSamplerProcessor::reloadFromBank() { std::optional sel = selectSample(*banksJson, selectedSampleId()); if (sel) { - // GA auto-default: while the channel mode is IMPLICIT (never user-toggled), - // follow the loaded capture's channel count — a stereo capture decodes (and - // shows) Stereo, a mono one Mono. An unknown count (0, an older bank entry) - // changes nothing; an explicit user choice is never fought. Decode-only: the - // output bus is fixed stereo, so no bus work follows a flip. - if (sel->channelCount > 0) { - const ChannelMode desired = sel->channelCount >= 2 ? ChannelMode::Stereo - : ChannelMode::Mono; + // GA auto-default: channelModeFor computes the mode from the loaded capture's + // REQUESTED channel count (always 2 for extension captures; mono only for + // ingest-imported mono files). An unknown count (0) or explicit user choice + // returns the current mode unchanged. Decode-only: the output bus is fixed + // stereo, so no bus work follows a flip. + { std::lock_guard lock(channelModeMutex_); - if (!channelModeExplicit_) channelMode_ = desired; + channelMode_ = channelModeFor(sel->channelCount, channelMode_, + channelModeExplicit_); mode = channelMode_; } std::optional pcm = @@ -888,9 +887,6 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { // render ADDS into a cleared buffer — RT-safe (no alloc/IO/lock). NEVER reads the mode here. float* ch0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr; float* ch1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr; - // The PLUG-IN owns output silenceFlags (VST3 contract). Claim non-silence on every rendered - // block — a stale host-side flag left unwritten could mute a channel downstream (GA). - out.silenceFlags = 0; if (ch0 && ch1) { // Stereo: clear both, render L/R. A mono sample plays dual-mono via the engine's stereo // path (both channels equal), so a mono capture in stereo mode is centered, not silent. diff --git a/src/vst/sample_map.cpp b/src/vst/sample_map.cpp index 384edf2..e693b09 100644 --- a/src/vst/sample_map.cpp +++ b/src/vst/sample_map.cpp @@ -63,6 +63,12 @@ std::optional selectSample(const std::string& banksJson, return std::nullopt; } +ChannelMode channelModeFor(int channelCount, ChannelMode current, bool isExplicit) { + if (isExplicit) return current; // user's explicit choice is never fought + if (channelCount <= 0) return current; // unknown (0) or pathological -> no change + return channelCount >= 2 ? ChannelMode::Stereo : ChannelMode::Mono; +} + std::vector listSamples(const std::string& banksJson) { std::vector out; if (banksJson.empty()) return out; @@ -753,8 +759,8 @@ ComponentState deserializeComponentState(const std::vector& bytes, // above-cap value (a corrupt blob) falls back to unity rather than silencing/blasting. if (version >= kSelectionZonesModeMarkerVelVoiceGainV8Version) { const double g = bitsToDouble(r.u64()); - if (!r.ok) return out; // truncated inside the gain double — unity holds (out already - // carries mode/marker/velocity/voice fields from above) + if (!r.ok) return out; // truncated inside the gain double — out already carries + // mode/marker/velocity/voice fields from above; unity holds out.masterGainLinear = (std::isfinite(g) && g >= 0.0 && g <= vst::masterGainMaxLinear() * (1.0 + 1e-9)) ? g @@ -763,7 +769,7 @@ ComponentState deserializeComponentState(const std::vector& bytes, // v9 (GA): the channel-mode-EXPLICIT flag. A v8-or-older blob skips it — the construction // default (false = implicit) holds, so an already-saved instance's mode is treated as the // un-touched default and the shell may auto-default it from the loaded capture. - if (version >= kComponentStateVersion) { + if (version >= kSelectionZonesModeMarkerVelVoiceGainExplicitV9Version) { const std::uint8_t explicitByte = r.u8(); if (!r.ok) return out; // truncated before the flag -> empty (implicit holds) out.channelModeExplicit = (explicitByte == 1); diff --git a/src/vst/sample_map.h b/src/vst/sample_map.h index 4214df6..483c44a 100644 --- a/src/vst/sample_map.h +++ b/src/vst/sample_map.h @@ -60,6 +60,16 @@ struct SelectedSample { std::optional selectSample(const std::string& banksJson, const std::string& sampleId); +// GA auto-default rule (pure, tested): given the capture's requested channel count, the +// instance's current mode, and whether the user has explicitly toggled the mode, return +// the mode to apply. Explicit choice is never overridden. An unknown channelCount (0) +// leaves the current mode unchanged. Used by reloadFromBank in the single-capture path. +// * isExplicit == true -> current (user's choice stands) +// * channelCount == 0 -> current (unknown, skip) +// * channelCount >= 2 -> Stereo +// * channelCount == 1 -> Mono +ChannelMode channelModeFor(int channelCount, ChannelMode current, bool isExplicit); + // One entry in the capture browser's card list: the stable id + display name plus the S2 // intrinsics + bank the browser draws as a card (peak thumbnail + name + root/key badge, // filterable by bank). Peaks are NOT here — they are computed shell-side from the decoded @@ -526,6 +536,10 @@ inline constexpr std::uint32_t kComponentStateVersion = 9; // explicit flag). Retained so deserializeComponentState can lift a v8 blob to implicit mode. inline constexpr std::uint32_t kSelectionZonesModeMarkerVelVoiceGainV8Version = 8; +// The GA combined-state version (v8 + the channel-mode-EXPLICIT flag). Mirrors the +// v8/v7/v6/… series so the v9-branch check in deserializeComponentState is self-describing. +inline constexpr std::uint32_t kSelectionZonesModeMarkerVelVoiceGainExplicitV9Version = 9; + // The pre-FB1 combined-state version (selection + zones + channel mode + consumed marker + // preview velocity + voice system, no master gain). Retained so deserializeComponentState can // lift a v7 blob to unity master gain. diff --git a/tests/test_sample_map.cpp b/tests/test_sample_map.cpp index ff77bd5..74ea740 100644 --- a/tests/test_sample_map.cpp +++ b/tests/test_sample_map.cpp @@ -146,6 +146,34 @@ static void testSelectChannelCountThreaded() { CHECK(selUn && selUn->channelCount == 0); // unstamped -> unknown, never a guess } +// --- channelModeFor (GA auto-default rule) ------------------------------------------- + +static void testChannelModeForExplicitIsNeverFought() { + // An explicit user choice is ALWAYS returned unchanged, regardless of channelCount. + CHECK(channelModeFor(2, ChannelMode::Mono, true) == ChannelMode::Mono); + CHECK(channelModeFor(1, ChannelMode::Stereo, true) == ChannelMode::Stereo); + CHECK(channelModeFor(0, ChannelMode::Stereo, true) == ChannelMode::Stereo); +} + +static void testChannelModeForUnknownCountIsNoOp() { + // An unknown channel count (0 — an older bank entry) leaves the current mode unchanged. + CHECK(channelModeFor(0, ChannelMode::Mono, false) == ChannelMode::Mono); + CHECK(channelModeFor(0, ChannelMode::Stereo, false) == ChannelMode::Stereo); +} + +static void testChannelModeForStereoCapture() { + // A capture with channelCount >= 2 selects Stereo (regardless of current mode). + CHECK(channelModeFor(2, ChannelMode::Mono, false) == ChannelMode::Stereo); + CHECK(channelModeFor(2, ChannelMode::Stereo, false) == ChannelMode::Stereo); + CHECK(channelModeFor(6, ChannelMode::Mono, false) == ChannelMode::Stereo); +} + +static void testChannelModeForMonoCapture() { + // A capture with channelCount == 1 selects Mono (ingest-imported mono files only). + CHECK(channelModeFor(1, ChannelMode::Stereo, false) == ChannelMode::Mono); + CHECK(channelModeFor(1, ChannelMode::Mono, false) == ChannelMode::Mono); +} + static void testSelectEmptyBlob() { CHECK(!selectSample("", "a").has_value()); } @@ -2100,6 +2128,10 @@ int main() { testSelectLoopThreaded(); testSelectNoLoopIsAbsent(); testSelectChannelCountThreaded(); + testChannelModeForExplicitIsNeverFought(); + testChannelModeForUnknownCountIsNoOp(); + testChannelModeForStereoCapture(); + testChannelModeForMonoCapture(); testSelectEmptyBlob(); testSelectMalformedBlob(); testSelectZeroSamples();