fix(vst): pin output bus to stereo (mode is decode-only) killing the hard-right pan; auto-default channel mode from the loaded capture (state v9)

This commit is contained in:
2026-07-28 06:19:28 -04:00
parent 104a25f390
commit 3e6629a867
7 changed files with 212 additions and 74 deletions
+16 -15
View File
@@ -102,12 +102,11 @@ public:
Steinberg::tresult PLUGIN_API process(
Steinberg::Vst::ProcessData& data) override;
// S7 channel-mode bus negotiation. The instrument has ONE canonical output arrangement
// determined by its per-instance channel mode (mono -> kMono, stereo -> kStereo). We
// accept the host's proposal only when it matches that arrangement; otherwise we reject
// (kResultFalse) but keep the mode's arrangement, so getBusArrangement / getBusInfo always
// report the mode's channel count and REAPER routes accordingly. A runtime mode change
// updates the output bus + calls restartComponent(kIoChanged) to trigger re-negotiation.
// Output-bus negotiation. The instrument has ONE canonical output arrangement: a FIXED
// stereo bus (GA fix — the channel mode is a decode policy, never a bus fact; mono mode
// renders dual-mono through it). We accept the host's proposal only when it is a single
// stereo output; otherwise we reject (kResultFalse) but keep our stereo arrangement, so
// getBusArrangement / getBusInfo always report 2 channels and the host routes accordingly.
Steinberg::tresult PLUGIN_API setBusArrangements(
Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) override;
@@ -184,11 +183,14 @@ public:
// (the editor toggle) and read off-thread by getState/reloadFromBank; guarded by
// channelModeMutex_. NEVER read on the audio thread — process() renders against the host's
// negotiated output channel count, and reloadFromBank bakes the mode into the decode.
// GA fix: the mode is a DECODE policy only (downmix vs L/R split). The output bus is a
// FIXED stereo bus — mono mode renders dual-mono through it (centered) — so a mode change
// never renegotiates host I/O (the mono<->stereo bus flip's live pin remap was the
// hard-right-pan defect).
ChannelMode channelMode();
// Sets the mode. When it CHANGES, updates the output bus arrangement (mono->kMono /
// stereo->kStereo) and asks the host to re-negotiate I/O via restartComponent(kIoChanged),
// then reloads the instrument so the next block decodes the new channel count. A no-op set
// (same mode) does neither. UI thread only.
// Sets the mode from the EDITOR TOGGLE (a deliberate user choice): latches the mode
// EXPLICIT (the GA auto-default stops fighting it), and on a CHANGE reloads the instrument
// so the next block decodes the new channel count. UI thread only.
void setChannelMode(ChannelMode mode);
// The per-instance preview-trigger velocity (S-VIEW-4, MIDI 1..127). Read/written on the
@@ -234,11 +236,6 @@ public:
void previewNoteOff(int note);
private:
// Apply `mode` to the output audio bus's SpeakerArrangement (kMono / kStereo). Called from
// initialize (topology) and setChannelMode (runtime change). Does NOT re-negotiate — the
// caller drives restartComponent when appropriate.
void applyOutputArrangement(ChannelMode mode);
// Phase S drain retirement (FA1-review Major #2): if process() has published that the
// CURRENT drain instrument is fully idle (every engine voice + the preview card silent),
// move it out of the drain slot into the graveyard and prune — so an edited-away snapshot
@@ -330,8 +327,12 @@ private:
// The per-instance channel mode (S7). Off-thread only (UI + getState + reloadFromBank);
// guarded against a getState/editor race. Default Mono preserves pre-S7 behavior. NOT read
// on the audio thread — process renders against the host's negotiated output channel count.
// channelModeExplicit_ (GA, persisted v9): false = the mode is an un-touched default that
// reloadFromBank may auto-default from the loaded capture's channel count; true = the user
// deliberately toggled the mode (setChannelMode latches it) and it is never fought.
std::mutex channelModeMutex_;
ChannelMode channelMode_ = ChannelMode::Mono;
bool channelModeExplicit_ = false;
// The last assignment-request generation this instance CONSUMED (S8 reader). Persisted in
// component state (v5) so a re-open does not re-apply a request the user already got and