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:
@@ -11,13 +11,10 @@
|
||||
|
||||
#include "pluginterfaces/base/ibstream.h"
|
||||
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
||||
#include "pluginterfaces/vst/ivsteditcontroller.h" // RestartFlags::kIoChanged (S7 re-negotiate)
|
||||
#include "pluginterfaces/vst/ivstevents.h"
|
||||
#include "pluginterfaces/vst/ivstmidicontrollers.h" // kCtrlAllNotesOff / kCtrlAllSoundsOff (panic)
|
||||
#include "pluginterfaces/vst/vstspeaker.h"
|
||||
|
||||
#include "public.sdk/source/vst/vstbus.h" // Vst::AudioBus::setArrangement (S7 output arr)
|
||||
|
||||
#include "assignment_request.h" // decodeAssignmentRequest (S8 request wire parse)
|
||||
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
|
||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||
@@ -121,14 +118,18 @@ tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
||||
bridge_.connect(context);
|
||||
|
||||
// Instrument bus topology: one event input (MIDI in, 16 channels), one audio output, no
|
||||
// audio input. The output arrangement follows the instance's channel mode (S7) — mono by
|
||||
// default (kMono), stereo (kStereo) when the mode is stereo. addAudioOutput needs an initial
|
||||
// arrangement; seed it at the mode's arrangement so getBusInfo is correct from the first
|
||||
// query. (setState may later flip the mode and re-negotiate via setChannelMode.)
|
||||
// audio input. GA fix (hard-right pan): the output bus is a FIXED STEREO bus regardless of
|
||||
// the channel mode. The mode is a DECODE policy (downmix vs L/R split) — mono mode renders
|
||||
// dual-mono through the stereo bus (both channels equal, centered), which is audibly
|
||||
// identical to a mono bus but never asks the host to re-map a live instance's pins. The
|
||||
// prior design flipped the bus kMono<->kStereo via restartComponent(kIoChanged) on every
|
||||
// mode change/restore; in the DAW that flip panned a dual-mono capture hard RIGHT. The
|
||||
// in-plugin path is provably symmetric (decode, per-voice stereo render, engine sum, buffer
|
||||
// write — see testDualMonoStereoSampleRendersCentered), so the asymmetry sat in the host's
|
||||
// re-routing of the live instance's pins across the arrangement change. A fixed arrangement
|
||||
// is the maximally-standard VSTi shape and removes that whole negotiation surface.
|
||||
addEventInput(STR16("MIDI In"), 16);
|
||||
const ChannelMode mode = channelMode();
|
||||
addAudioOutput(STR16("Audio Out"),
|
||||
mode == ChannelMode::Stereo ? SpeakerArr::kStereo : SpeakerArr::kMono);
|
||||
addAudioOutput(STR16("Audio Out"), SpeakerArr::kStereo);
|
||||
|
||||
return kResultOk;
|
||||
}
|
||||
@@ -209,14 +210,13 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
||||
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
|
||||
lastConsumedAssignGeneration_ = cs.lastConsumedAssignGeneration;
|
||||
}
|
||||
// Restore the S7 channel mode and point the output bus at its arrangement so a reopened
|
||||
// project comes back in the saved mode. setState runs before the host queries bus info, so
|
||||
// seeding the arrangement here (rather than re-negotiating) is enough — no restartComponent.
|
||||
// Restore the S7 channel mode + the GA explicit flag. The output bus is FIXED stereo (see
|
||||
// initialize) — the mode only governs how the reload below decodes, so no bus work here.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
||||
channelMode_ = cs.channelMode;
|
||||
channelModeExplicit_ = cs.channelModeExplicit;
|
||||
}
|
||||
applyOutputArrangement(cs.channelMode);
|
||||
// S-VIEW-4: restore the per-instance preview velocity. Guarded by previewMutex_ — since Wave 2
|
||||
// the editor's velocity knob is a concurrent UI-thread writer.
|
||||
{
|
||||
@@ -251,7 +251,12 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
||||
ComponentState state_out;
|
||||
state_out.selectionId = selectedSampleId();
|
||||
state_out.map = performanceMap();
|
||||
state_out.channelMode = channelMode(); // S7: persist the per-instance mono/stereo mode
|
||||
{
|
||||
// S7: persist the per-instance mono/stereo decode mode + the GA explicit flag (v9).
|
||||
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
||||
state_out.channelMode = channelMode_;
|
||||
state_out.channelModeExplicit = channelModeExplicit_;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
|
||||
state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; // S8 reader marker
|
||||
@@ -397,49 +402,32 @@ void ReaSamplerProcessor::previewNoteOff(int note) {
|
||||
previewOffRequest_.store(packed, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::applyOutputArrangement(ChannelMode mode) {
|
||||
// Set the single output bus's SpeakerArrangement to the mode's arrangement so getBusInfo /
|
||||
// getBusArrangement report the right channel count. The default getBusArrangement (from the
|
||||
// base) reads back exactly what we store here. No re-negotiation — the caller drives that.
|
||||
BusList* outs = getBusList(kAudio, kOutput);
|
||||
if (!outs || outs->empty()) return;
|
||||
if (auto* bus = FCast<AudioBus>(outs->at(0))) {
|
||||
bus->setArrangement(mode == ChannelMode::Stereo ? SpeakerArr::kStereo
|
||||
: SpeakerArr::kMono);
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::setChannelMode(ChannelMode mode) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
||||
if (channelMode_ == mode) return; // no-op: don't churn the bus / re-negotiate
|
||||
// The editor toggle is a DELIBERATE choice either way: latch explicit even on a
|
||||
// same-mode click (the user confirmed the mode; the GA auto-default stops fighting it).
|
||||
channelModeExplicit_ = true;
|
||||
if (channelMode_ == mode) return; // no decode change: don't churn a reload
|
||||
channelMode_ = mode;
|
||||
}
|
||||
// The mode changed: repoint the output bus and ask the host to re-negotiate I/O so REAPER's
|
||||
// routing follows (mono<->stereo). restartComponent is a main/UI-thread call; setChannelMode
|
||||
// is driven from the editor, so this is safe. Then reload so the next block decodes the new
|
||||
// channel count into the LoadedInstrument (off-thread, RT path untouched).
|
||||
applyOutputArrangement(mode);
|
||||
if (componentHandler) componentHandler->restartComponent(kIoChanged);
|
||||
// The DECODE policy changed. The output bus is FIXED stereo (GA fix — no bus repoint, no
|
||||
// restartComponent): reloading re-decodes the loaded WAV(s) under the new mode off-thread
|
||||
// (mono = downmix, stereo = L/R split) and the RT path just keeps rendering.
|
||||
reloadFromBank();
|
||||
}
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::setBusArrangements(
|
||||
SpeakerArrangement* inputs, int32 numIns,
|
||||
SpeakerArrangement* outputs, int32 numOuts) {
|
||||
// The instrument has ONE canonical arrangement per its channel mode (S7). We take NO audio
|
||||
// input, so any inputs are rejected. For the single output bus: accept (kResultTrue) only
|
||||
// when the host proposes exactly the mode's arrangement; otherwise reject (kResultFalse) but
|
||||
// KEEP the mode's arrangement (per the VST3 contract, a plug-in that can't honor a proposal
|
||||
// keeps a valid arrangement of its own). getBusArrangement then still reports the mode's
|
||||
// channel count, so the host adapts its routing to us rather than forcing our channel count.
|
||||
// ONE canonical arrangement: the fixed stereo output bus (GA fix — the channel mode is a
|
||||
// decode policy, never a bus fact). We take NO audio input, so any inputs are rejected.
|
||||
// Accept (kResultTrue) only a single stereo output proposal; otherwise reject (kResultFalse)
|
||||
// and keep our stereo arrangement (per the VST3 contract, a plug-in that can't honor a
|
||||
// proposal keeps a valid arrangement of its own) — the host adapts its routing to us.
|
||||
if (numIns < 0 || numOuts < 0) return kInvalidArgument;
|
||||
if (numIns > 0) return kResultFalse; // no audio input bus to arrange
|
||||
|
||||
const SpeakerArrangement want =
|
||||
channelMode() == ChannelMode::Stereo ? SpeakerArr::kStereo : SpeakerArr::kMono;
|
||||
applyOutputArrangement(channelMode()); // keep the bus pinned to the mode's arrangement
|
||||
if (numOuts == 1 && outputs && outputs[0] == want) return kResultTrue;
|
||||
if (numOuts == 1 && outputs && outputs[0] == SpeakerArr::kStereo) return kResultTrue;
|
||||
return kResultFalse;
|
||||
}
|
||||
|
||||
@@ -459,8 +447,9 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
bridge_.readReasamplerExtState(kProjExtBanksKey);
|
||||
const std::string projectDir = bridge_.activeProjectDir();
|
||||
// The active channel mode (S7) governs how each WAV decodes (mono downmix vs 2-channel).
|
||||
// Read once under its mutex, off the audio thread, before the decode loop.
|
||||
const ChannelMode mode = channelMode();
|
||||
// Read once under its mutex, off the audio thread, before the decode loop. The single-
|
||||
// capture branch below may auto-default it (GA) before its decode.
|
||||
ChannelMode mode = channelMode();
|
||||
// Phase S: snapshot the voice-system parameters once — they are baked into the built
|
||||
// engine's construction (the engine's config is immutable; a later change rebuilds).
|
||||
int builtVoiceCount = kDefaultVoiceCount;
|
||||
@@ -516,6 +505,18 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
std::optional<SelectedSample> 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;
|
||||
std::lock_guard<std::mutex> lock(channelModeMutex_);
|
||||
if (!channelModeExplicit_) channelMode_ = desired;
|
||||
mode = channelMode_;
|
||||
}
|
||||
std::optional<DecodedZonePcm> pcm =
|
||||
decodeRelative(projectDir, sel->relativePath, mode);
|
||||
if (pcm) {
|
||||
@@ -887,6 +888,9 @@ 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.
|
||||
|
||||
Reference in New Issue
Block a user