Skip to content

Commit

Permalink
VST3 Client: Allow host to enable/disable buses at will
Browse files Browse the repository at this point in the history
Previously, activateBus would fail if the new BusesLayout wasn't
supported, as reported by isBusesLayoutSupported. However, according to
the VST3 docs, a host is allowed to enable and disable buses in any
combination, and the plugin should be able to handle this gracefully.

The ability to enable/disable individual buses without failure is
particularly important because there's no VST3 API to set a complete bus
layout in one go. That is, the only way to set all buses active or all
buses inactive is to set the appropriate state on each bus individually,
which in turn means that at some point, some buses will be active and
some will be inactive. Disallowing such 'intermediate' states may
prevent the host from putting the plugin into other (valid) states.

To ensure that the VST3 wrapper always accepts activateBus calls, it now
keeps track of the activation state of each bus as requested by the
host. When the host tries to change the activation state, the wrapper
will try to set the host's "ideal" bus layout on the AudioProcessor. If
this fails, the AudioProcessor will retain its previous bus layout.

The buffer remapping inside the process callback has been made more
robust, to handle cases where the host and the AudioProcessor disagree
about the activation state of each bus:

For input buses:
- If the host has activated the bus, but the AudioProcessor decided to
  keep the bus inactive, the host's input will be ignored.
- If the host deactivated the bus, but the AudioProcessor wanted to keep
  the bus active, the AudioProcessor will be provided with silence on
  that bus.

For output buses:
- If the host has activated the bus, but the AudioProcessor decided to
  keep the bus inactive, the wrapper will clear the host's output
  bus buffers.
- If the host deactivated the bus, but the AudioProcessor wanted to keep
  the bus active, the AudioProcessor's output on that bus will be
  ignored.

The AudioBuffer passed to the wrapped AudioProcessor will no longer
contain any pointers from the host's ProcessData. Instead, the host's
inputs will be copied (in JUCE channel order) to a temporary buffer,
and this temporary buffer will be passed to
AudioProcessor::processBlock. After processBlock, the buffer contents
will be copied to the host's output buffers.

This change is intended to avoid a potential issue when reordering
channels into JUCE order, which may necessitate copying a host input
channel to a different host output channel. In the case that the host is
using the same buffers for both inputs and outputs, copying an input to
an output channel may end up overwriting another input channel, breaking
the plugin's inputs.
  • Loading branch information
reuk authored and modosc committed Jun 23, 2022
1 parent d941192 commit 1573090
Show file tree
Hide file tree
Showing 5 changed files with 654 additions and 371 deletions.
3 changes: 1 addition & 2 deletions examples/Plugins/MultiOutSynthPluginDemo.h
Expand Up @@ -154,10 +154,9 @@ class MultiOutSynth : public AudioProcessor

return layout.inputBuses.isEmpty()
&& 1 <= outputs.size()
&& outputs.getFirst() != AudioChannelSet::disabled()
&& std::all_of (outputs.begin(), outputs.end(), [] (const auto& bus)
{
return bus == AudioChannelSet::stereo() || bus == AudioChannelSet::disabled();
return bus == AudioChannelSet::stereo();
});
}

Expand Down
90 changes: 70 additions & 20 deletions modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
Expand Up @@ -2957,33 +2957,76 @@ class JuceVST3Component : public Vst::IComponent,

if (type == Vst::kAudio)
{
if (index < 0 || index >= getNumAudioBuses (dir == Vst::kInput))
const auto numInputBuses = getNumAudioBuses (true);
const auto numOutputBuses = getNumAudioBuses (false);

if (! isPositiveAndBelow (index, dir == Vst::kInput ? numInputBuses : numOutputBuses))
return kResultFalse;

// Some hosts (old cakewalk, bitwig studio) might call this function without
// deactivating the plugin, so we update the channel mapping here.
if (dir == Vst::BusDirections::kInput)
bufferMapper.setInputBusActive ((size_t) index, state != 0);
// The host is allowed to enable/disable buses as it sees fit, so the plugin needs to be
// able to handle any set of enabled/disabled buses, including layouts for which
// AudioProcessor::isBusesLayoutSupported would return false.
// Our strategy is to keep track of the layout that the host last requested, and to
// attempt to apply that layout directly.
// If the layout isn't supported by the processor, we'll try enabling all the buses
// instead.
// If the host enables a bus that the processor refused to enable, then we'll ignore
// that bus (and return silence for output buses). If the host disables a bus that the
// processor refuses to disable, the wrapper will provide the processor with silence for
// input buses, and ignore the contents of output buses.
// Note that some hosts (old bitwig and cakewalk) may incorrectly call this function
// when the plugin is in an activated state.
if (dir == Vst::kInput)
bufferMapper.setInputBusHostActive ((size_t) index, state != 0);
else
bufferMapper.setOutputBusActive ((size_t) index, state != 0);
bufferMapper.setOutputBusHostActive ((size_t) index, state != 0);

if (auto* bus = pluginInstance->getBus (dir == Vst::kInput, index))
AudioProcessor::BusesLayout desiredLayout;

for (auto i = 0; i < numInputBuses; ++i)
desiredLayout.inputBuses.add (bufferMapper.getRequestedLayoutForInputBus ((size_t) i));

for (auto i = 0; i < numOutputBuses; ++i)
desiredLayout.outputBuses.add (bufferMapper.getRequestedLayoutForOutputBus ((size_t) i));

const auto prev = pluginInstance->getBusesLayout();

const auto busesLayoutSupported = [&]
{
#ifdef JucePlugin_PreferredChannelConfigurations
auto newLayout = pluginInstance->getBusesLayout();
auto targetLayout = (state != 0 ? bus->getLastEnabledLayout() : AudioChannelSet::disabled());

(dir == Vst::kInput ? newLayout.inputBuses : newLayout.outputBuses).getReference (index) = targetLayout;
struct ChannelPair
{
short ins, outs;

short configs[][2] = { JucePlugin_PreferredChannelConfigurations };
auto compLayout = pluginInstance->getNextBestLayoutInLayoutList (newLayout, configs);
auto tie() const { return std::tie (ins, outs); }
bool operator== (ChannelPair x) const { return tie() == x.tie(); }
};

if ((dir == Vst::kInput ? compLayout.inputBuses : compLayout.outputBuses).getReference (index) != targetLayout)
return kResultFalse;
const auto countChannels = [] (auto& range)
{
return std::accumulate (range.begin(), range.end(), (short) 0, [] (auto acc, auto set)
{
return acc + set.size();
});
};

const ChannelPair requested { countChannels (desiredLayout.inputBuses),
countChannels (desiredLayout.outputBuses) };
const ChannelPair configs[] = { JucePlugin_PreferredChannelConfigurations };
return std::find (std::begin (configs), std::end (configs), requested) != std::end (configs);
#else
return pluginInstance->checkBusesLayoutSupported (desiredLayout);
#endif
}();

return bus->enable (state != 0) ? kResultTrue : kResultFalse;
}
if (busesLayoutSupported)
pluginInstance->setBusesLayout (desiredLayout);
else
pluginInstance->enableAllBuses();

bufferMapper.updateActiveClientBuses (pluginInstance->getBusesLayout());

return kResultTrue;
}

return kResultFalse;
Expand Down Expand Up @@ -3043,7 +3086,11 @@ class JuceVST3Component : public Vst::IComponent,
return kResultFalse;
#endif

return pluginInstance->setBusesLayoutWithoutEnabling (requested) ? kResultTrue : kResultFalse;
if (! pluginInstance->setBusesLayoutWithoutEnabling (requested))
return kResultFalse;

bufferMapper.updateFromProcessor (*pluginInstance);
return kResultTrue;
}

tresult PLUGIN_API getBusArrangement (Vst::BusDirection dir, Steinberg::int32 index, Vst::SpeakerArrangement& arr) override
Expand Down Expand Up @@ -3285,7 +3332,9 @@ class JuceVST3Component : public Vst::IComponent,
template <typename FloatType>
void processAudio (Vst::ProcessData& data)
{
auto buffer = bufferMapper.getJuceLayoutForVst3Buffer (detail::Tag<FloatType>{}, data);
ClientRemappedBuffer<FloatType> remappedBuffer { bufferMapper, data };
auto& buffer = remappedBuffer.buffer;

jassert ((int) buffer.getNumChannels() == jmax (pluginInstance->getTotalNumInputChannels(),
pluginInstance->getTotalNumOutputChannels()));

Expand Down Expand Up @@ -3358,7 +3407,8 @@ class JuceVST3Component : public Vst::IComponent,
midiBuffer.ensureSize (2048);
midiBuffer.clear();

bufferMapper.prepare (p, bufferSize);
bufferMapper.updateFromProcessor (p);
bufferMapper.prepare (bufferSize);
}

//==============================================================================
Expand Down

0 comments on commit 1573090

Please sign in to comment.