Browse Source

VST3 Client: Allow host to enable/disable buses at will

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.
pull/22/head
reuk 3 years ago
parent
commit
964a1aa870
No known key found for this signature in database GPG Key ID: 9ADCD339CFC98A11
5 changed files with 656 additions and 373 deletions
  1. +1
    -2
      examples/Plugins/MultiOutSynthPluginDemo.h
  2. +70
    -20
      modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
  3. +332
    -159
      modules/juce_audio_processors/format_types/juce_VST3Common.h
  4. +3
    -6
      modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp
  5. +250
    -186
      modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp

+ 1
- 2
examples/Plugins/MultiOutSynthPluginDemo.h View File

@@ -154,10 +154,9 @@ public:
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();
});
}


+ 70
- 20
modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp View File

@@ -3067,33 +3067,76 @@ public:
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;
@@ -3153,7 +3196,11 @@ public:
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
@@ -3413,7 +3460,9 @@ private:
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()));
@@ -3488,7 +3537,8 @@ private:
midiBuffer.ensureSize (2048);
midiBuffer.clear();
bufferMapper.prepare (p, bufferSize);
bufferMapper.updateFromProcessor (p);
bufferMapper.prepare (bufferSize);
}
//==============================================================================


+ 332
- 159
modules/juce_audio_processors/format_types/juce_VST3Common.h View File

@@ -497,27 +497,20 @@ inline AudioChannelSet getChannelSetForSpeakerArrangement (Steinberg::Vst::Speak
*/
struct ChannelMapping
{
explicit ChannelMapping (const AudioChannelSet& layout)
: ChannelMapping (layout, true)
{
}
ChannelMapping (const AudioChannelSet& layout, bool activeIn)
: indices (makeChannelIndices (layout)),
active (activeIn)
{
}
: indices (makeChannelIndices (layout)), active (activeIn) {}
explicit ChannelMapping (const AudioProcessor::Bus& juceBus)
: ChannelMapping (juceBus.getLastEnabledLayout(), juceBus.isEnabled())
{
}
explicit ChannelMapping (const AudioChannelSet& layout)
: ChannelMapping (layout, true) {}
explicit ChannelMapping (const AudioProcessor::Bus& bus)
: ChannelMapping (bus.getLastEnabledLayout(), bus.isEnabled()) {}
int getJuceChannelForVst3Channel (int vst3Channel) const { return indices[(size_t) vst3Channel]; }
size_t size() const { return indices.size(); }
void setActive (bool activeIn) { active = activeIn; }
void setActive (bool x) { active = x; }
bool isActive() const { return active; }
private:
@@ -545,24 +538,104 @@ private:
bool active = true;
};
class DynamicChannelMapping
{
public:
DynamicChannelMapping (const AudioChannelSet& channelSet, bool active)
: set (channelSet), map (channelSet, active) {}
explicit DynamicChannelMapping (const AudioChannelSet& channelSet)
: DynamicChannelMapping (channelSet, true) {}
explicit DynamicChannelMapping (const AudioProcessor::Bus& bus)
: DynamicChannelMapping (bus.getLastEnabledLayout(), bus.isEnabled()) {}
AudioChannelSet getAudioChannelSet() const { return set; }
int getJuceChannelForVst3Channel (int vst3Channel) const { return map.getJuceChannelForVst3Channel (vst3Channel); }
size_t size() const { return map.size(); }
/* Returns true if the host has activated this bus. */
bool isHostActive() const { return hostActive; }
/* Returns true if the AudioProcessor expects this bus to be active. */
bool isClientActive() const { return map.isActive(); }
void setHostActive (bool active) { hostActive = active; }
void setClientActive (bool active) { map.setActive (active); }
private:
AudioChannelSet set;
ChannelMapping map;
bool hostActive = false;
};
//==============================================================================
inline auto& getAudioBusPointer (detail::Tag<float>, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers32; }
inline auto& getAudioBusPointer (detail::Tag<double>, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers64; }
static inline int countUsedChannels (const std::vector<ChannelMapping>& inputMap,
const std::vector<ChannelMapping>& outputMap)
static inline int countUsedClientChannels (const std::vector<DynamicChannelMapping>& inputMap,
const std::vector<DynamicChannelMapping>& outputMap)
{
const auto countUsedChannelsInVector = [] (const std::vector<ChannelMapping>& map)
const auto countUsedChannelsInVector = [] (const std::vector<DynamicChannelMapping>& map)
{
return std::accumulate (map.begin(), map.end(), 0, [] (auto acc, const auto& item)
{
return acc + (item.isActive() ? (int) item.size() : 0);
return acc + (item.isClientActive() ? (int) item.size() : 0);
});
};
return jmax (countUsedChannelsInVector (inputMap), countUsedChannelsInVector (outputMap));
}
template <typename FloatType>
class ScratchBuffer
{
public:
void setSize (int numChannels, int blockSize)
{
buffer.setSize (numChannels, blockSize);
}
void clear() { channelCounter = 0; }
auto* getNextChannelBuffer() { return buffer.getWritePointer (channelCounter++); }
private:
AudioBuffer<FloatType> buffer;
int channelCounter = 0;
};
template <typename FloatType>
static int countValidBuses (Steinberg::Vst::AudioBusBuffers* buffers, int32 num)
{
return (int) std::distance (buffers, std::find_if (buffers, buffers + num, [] (auto& buf)
{
return getAudioBusPointer (detail::Tag<FloatType>{}, buf) == nullptr && buf.numChannels > 0;
}));
}
template <typename FloatType, typename Iterator>
static bool validateLayouts (Iterator first, Iterator last, const std::vector<DynamicChannelMapping>& map)
{
if ((size_t) std::distance (first, last) > map.size())
return false;
auto mapIterator = map.begin();
for (auto it = first; it != last; ++it, ++mapIterator)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, *it);
const auto anyChannelIsNull = std::any_of (busPtr, busPtr + it->numChannels, [] (auto* ptr) { return ptr == nullptr; });
// Null channels are allowed if the bus is inactive
if ((mapIterator->isHostActive() && anyChannelIsNull) || ((int) mapIterator->size() != it->numChannels))
return false;
}
// If the host didn't provide the full complement of buses, it must be because the other
// buses are all deactivated.
return std::none_of (mapIterator, map.end(), [] (const auto& item) { return item.isHostActive(); });
}
/*
The main purpose of this class is to remap a set of buffers provided by the VST3 host into an
equivalent JUCE AudioBuffer using the JUCE channel layout/order.
@@ -580,155 +653,107 @@ class ClientBufferMapperData
public:
void prepare (int numChannels, int blockSize)
{
emptyBuffer.setSize (numChannels, blockSize);
scratchBuffer.setSize (numChannels, blockSize);
channels.reserve ((size_t) jmin (128, numChannels));
}
AudioBuffer<FloatType> getMappedBuffer (Steinberg::Vst::ProcessData& data,
const std::vector<ChannelMapping>& inputMap,
const std::vector<ChannelMapping>& outputMap)
const std::vector<DynamicChannelMapping>& inputMap,
const std::vector<DynamicChannelMapping>& outputMap)
{
const auto usedChannels = countUsedChannels (inputMap, outputMap);
// WaveLab workaround: This host may report the wrong number of inputs/outputs so re-count here
const auto countValidBuses = [] (Steinberg::Vst::AudioBusBuffers* buffers, int32 num)
{
return int (std::distance (buffers, std::find_if (buffers, buffers + num, [] (Steinberg::Vst::AudioBusBuffers& buf)
{
return getAudioBusPointer (detail::Tag<FloatType>{}, buf) == nullptr && buf.numChannels > 0;
})));
};
const auto vstInputs = countValidBuses (data.inputs, data.numInputs);
const auto vstOutputs = countValidBuses (data.outputs, data.numOutputs);
if (! validateLayouts (data, vstInputs, inputMap, vstOutputs, outputMap))
return clearOutputBuffersAndReturnBlankBuffer (data, vstOutputs, usedChannels);
// If we're here, then we know that the host has given us a usable layout
scratchBuffer.clear();
channels.clear();
// Put the host-supplied output channel pointers into JUCE order
for (size_t i = 0; i < (size_t) vstOutputs; ++i)
{
const auto bus = getMappedOutputBus (data, outputMap, i);
channels.insert (channels.end(), bus.begin(), bus.end());
}
// For input channels that are < the total number of outputs channels, copy the input over
// the output buffer, at the appropriate JUCE channel index.
// For input channels that are >= the total number of output channels, add the input buffer
// pointer to the array of channel pointers.
for (size_t inputBus = 0, initialBusIndex = 0; inputBus < (size_t) vstInputs; ++inputBus)
{
const auto& map = inputMap[inputBus];
if (! map.isActive())
continue;
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, data.inputs[inputBus]);
const auto usedChannels = countUsedClientChannels (inputMap, outputMap);
for (auto i = 0; i < (int) map.size(); ++i)
{
const auto destIndex = initialBusIndex + (size_t) map.getJuceChannelForVst3Channel (i);
channels.resize (jmax (channels.size(), destIndex + 1), nullptr);
// WaveLab workaround: This host may report the wrong number of inputs/outputs so re-count here
const auto vstInputs = countValidBuses<FloatType> (data.inputs, data.numInputs);
if (auto* dest = channels[destIndex])
FloatVectorOperations::copy (dest, busPtr[i], (int) data.numSamples);
else
channels[destIndex] = busPtr[i];
}
if (! validateLayouts<FloatType> (data.inputs, data.inputs + vstInputs, inputMap))
return getBlankBuffer (usedChannels, (int) data.numSamples);
initialBusIndex += map.size();
}
setUpInputChannels (data, (size_t) vstInputs, scratchBuffer, inputMap, channels);
setUpOutputChannels (scratchBuffer, outputMap, channels);
return { channels.data(), (int) channels.size(), (int) data.numSamples };
}
private:
AudioBuffer<FloatType> clearOutputBuffersAndReturnBlankBuffer (Steinberg::Vst::ProcessData& data, int vstOutputs, int usedChannels)
static void setUpInputChannels (Steinberg::Vst::ProcessData& data,
size_t vstInputs,
ScratchBuffer<FloatType>& scratchBuffer,
const std::vector<DynamicChannelMapping>& map,
std::vector<FloatType*>& channels)
{
// The host is ignoring the bus layout we requested, so we can't process sensibly!
jassertfalse;
// Clear all output channels
std::for_each (data.outputs, data.outputs + vstOutputs, [&data] (auto& bus)
for (size_t busIndex = 0; busIndex < map.size(); ++busIndex)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, bus);
std::for_each (busPtr, busPtr + bus.numChannels, [&data] (auto* ptr)
{
if (ptr != nullptr)
FloatVectorOperations::clear (ptr, (int) data.numSamples);
});
});
// Return a silent buffer for the AudioProcessor to process
emptyBuffer.clear();
return { emptyBuffer.getArrayOfWritePointers(),
jmin (emptyBuffer.getNumChannels(), usedChannels),
data.numSamples };
}
const auto mapping = map[busIndex];
std::vector<FloatType*> getMappedOutputBus (Steinberg::Vst::ProcessData& data,
const std::vector<ChannelMapping>& maps,
size_t index) const
{
const auto& map = maps[index];
if (! map.isActive())
return {};
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, data.outputs[index]);
if (! mapping.isClientActive())
continue;
std::vector<FloatType*> result (map.size(), nullptr);
const auto originalSize = channels.size();
for (auto i = 0; i < (int) map.size(); ++i)
result[(size_t) map.getJuceChannelForVst3Channel (i)] = busPtr[i];
for (size_t channelIndex = 0; channelIndex < mapping.size(); ++channelIndex)
channels.push_back (scratchBuffer.getNextChannelBuffer());
return result;
if (mapping.isHostActive() && busIndex < vstInputs)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, data.inputs[busIndex]);
for (size_t channelIndex = 0; channelIndex < mapping.size(); ++channelIndex)
{
FloatVectorOperations::copy (channels[(size_t) mapping.getJuceChannelForVst3Channel ((int) channelIndex) + originalSize],
busPtr[channelIndex],
(size_t) data.numSamples);
}
}
else
{
for (size_t channelIndex = 0; channelIndex < mapping.size(); ++channelIndex)
FloatVectorOperations::clear (channels[originalSize + channelIndex], (size_t) data.numSamples);
}
}
}
template <typename Iterator>
static bool validateLayouts (Iterator first, Iterator last, const std::vector<ChannelMapping>& map)
static void setUpOutputChannels (ScratchBuffer<FloatType>& scratchBuffer,
const std::vector<DynamicChannelMapping>& map,
std::vector<FloatType*>& channels)
{
if ((size_t) std::distance (first, last) > map.size())
return false;
auto mapIterator = map.begin();
for (auto it = first; it != last; ++it, ++mapIterator)
for (size_t i = 0, initialBusIndex = 0; i < (size_t) map.size(); ++i)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, *it);
const auto anyChannelIsNull = std::any_of (busPtr, busPtr + it->numChannels, [] (auto* ptr) { return ptr == nullptr; });
const auto& mapping = map[i];
// Null channels are allowed if the bus is inactive
if ((mapIterator->isActive() && anyChannelIsNull) || ((int) mapIterator->size() != it->numChannels))
return false;
}
if (mapping.isClientActive())
{
for (size_t j = 0; j < mapping.size(); ++j)
{
if (channels.size() <= initialBusIndex + j)
channels.push_back (scratchBuffer.getNextChannelBuffer());
}
// If the host didn't provide the full complement of buses, it must be because the other
// buses are all deactivated.
return std::none_of (mapIterator, map.end(), [] (const auto& item) { return item.isActive(); });
initialBusIndex += mapping.size();
}
}
}
static bool validateLayouts (Steinberg::Vst::ProcessData& data,
int numInputs,
const std::vector<ChannelMapping>& inputMap,
int numOutputs,
const std::vector<ChannelMapping>& outputMap)
AudioBuffer<FloatType> getBlankBuffer (int usedChannels, int usedSamples)
{
// The host is ignoring the bus layout we requested, so we can't process sensibly!
jassertfalse;
// The plug-in should only process an activated bus.
// The host could provide fewer busses in the process call if the last busses are not activated.
// Return a silent buffer for the AudioProcessor to process
for (auto i = 0; i < usedChannels; ++i)
{
channels.push_back (scratchBuffer.getNextChannelBuffer());
FloatVectorOperations::clear (channels.back(), usedSamples);
}
return validateLayouts (data.inputs, data.inputs + numInputs, inputMap)
&& validateLayouts (data.outputs, data.outputs + numOutputs, outputMap);
return { channels.data(), (int) channels.size(), usedSamples };
}
std::vector<FloatType*> channels;
AudioBuffer<FloatType> emptyBuffer;
ScratchBuffer<FloatType> scratchBuffer;
};
//==============================================================================
@@ -750,65 +775,213 @@ private:
class ClientBufferMapper
{
public:
void prepare (const AudioProcessor& processor, int blockSize)
void updateFromProcessor (const AudioProcessor& processor)
{
struct Pair
{
std::vector<ChannelMapping>& map;
std::vector<DynamicChannelMapping>& map;
bool isInput;
};
for (const auto& pair : { Pair { inputMap, true }, Pair { outputMap, false } })
{
pair.map.clear();
for (auto i = 0; i < processor.getBusCount (pair.isInput); ++i)
pair.map.emplace_back (*processor.getBus (pair.isInput, i));
if (pair.map.empty())
{
for (auto i = 0; i < processor.getBusCount (pair.isInput); ++i)
pair.map.emplace_back (*processor.getBus (pair.isInput, i));
}
else
{
// The number of buses cannot change after creating a VST3 plugin!
jassert ((size_t) processor.getBusCount (pair.isInput) == pair.map.size());
for (size_t i = 0; i < (size_t) processor.getBusCount (pair.isInput); ++i)
{
pair.map[i] = [&]
{
DynamicChannelMapping replacement { *processor.getBus (pair.isInput, (int) i) };
replacement.setHostActive (pair.map[i].isHostActive());
return replacement;
}();
}
}
}
}
const auto findMaxNumChannels = [&] (bool isInput)
void prepare (int blockSize)
{
const auto findNumChannelsWhenAllBusesEnabled = [] (const auto& map)
{
auto sum = 0;
for (auto i = 0; i < processor.getBusCount (isInput); ++i)
sum += processor.getBus (isInput, i)->getLastEnabledLayout().size();
return sum;
return std::accumulate (map.cbegin(), map.cend(), 0, [] (auto acc, const auto& item)
{
return acc + (int) item.size();
});
};
const auto numChannels = jmax (findMaxNumChannels (true), findMaxNumChannels (false));
const auto numChannels = jmax (findNumChannelsWhenAllBusesEnabled (inputMap),
findNumChannelsWhenAllBusesEnabled (outputMap));
floatData .prepare (numChannels, blockSize);
doubleData.prepare (numChannels, blockSize);
}
void setInputBusActive (size_t bus, bool state)
void updateActiveClientBuses (const AudioProcessor::BusesLayout& clientBuses)
{
if (bus < inputMap.size())
inputMap[bus].setActive (state);
if ( (size_t) clientBuses.inputBuses .size() != inputMap .size()
|| (size_t) clientBuses.outputBuses.size() != outputMap.size())
{
jassertfalse;
return;
}
const auto sync = [] (auto& map, auto& client)
{
for (size_t i = 0; i < map.size(); ++i)
{
jassert (client[(int) i] == AudioChannelSet::disabled() || client[(int) i] == map[i].getAudioChannelSet());
map[i].setClientActive (client[(int) i] != AudioChannelSet::disabled());
}
};
sync (inputMap, clientBuses.inputBuses);
sync (outputMap, clientBuses.outputBuses);
}
void setOutputBusActive (size_t bus, bool state)
void setInputBusHostActive (size_t bus, bool state) { setHostActive (inputMap, bus, state); }
void setOutputBusHostActive (size_t bus, bool state) { setHostActive (outputMap, bus, state); }
auto& getData (detail::Tag<float>) { return floatData; }
auto& getData (detail::Tag<double>) { return doubleData; }
AudioChannelSet getRequestedLayoutForInputBus (size_t bus) const
{
if (bus < outputMap.size())
outputMap[bus].setActive (state);
return getRequestedLayoutForBus (inputMap, bus);
}
template <typename FloatType>
AudioBuffer<FloatType> getJuceLayoutForVst3Buffer (detail::Tag<FloatType>, Steinberg::Vst::ProcessData& data)
AudioChannelSet getRequestedLayoutForOutputBus (size_t bus) const
{
return getData (detail::Tag<FloatType>{}).getMappedBuffer (data, inputMap, outputMap);
return getRequestedLayoutForBus (outputMap, bus);
}
const std::vector<DynamicChannelMapping>& getInputMap() const { return inputMap; }
const std::vector<DynamicChannelMapping>& getOutputMap() const { return outputMap; }
private:
auto& getData (detail::Tag<float>) { return floatData; }
auto& getData (detail::Tag<double>) { return doubleData; }
static void setHostActive (std::vector<DynamicChannelMapping>& map, size_t bus, bool state)
{
if (bus < map.size())
map[bus].setHostActive (state);
}
static AudioChannelSet getRequestedLayoutForBus (const std::vector<DynamicChannelMapping>& map, size_t bus)
{
if (bus < map.size() && map[bus].isHostActive())
return map[bus].getAudioChannelSet();
return AudioChannelSet::disabled();
}
ClientBufferMapperData<float> floatData;
ClientBufferMapperData<double> doubleData;
std::vector<ChannelMapping> inputMap;
std::vector<ChannelMapping> outputMap;
std::vector<DynamicChannelMapping> inputMap;
std::vector<DynamicChannelMapping> outputMap;
};
//==============================================================================
/* Holds a buffer in the JUCE channel layout, and a reference to a Vst ProcessData struct, and
copies each JUCE channel to the appropriate host output channel when this object goes
out of scope.
*/
template <typename FloatType>
class ClientRemappedBuffer
{
public:
ClientRemappedBuffer (ClientBufferMapperData<FloatType>& mapperData,
const std::vector<DynamicChannelMapping>* inputMapIn,
const std::vector<DynamicChannelMapping>* outputMapIn,
Steinberg::Vst::ProcessData& hostData)
: buffer (mapperData.getMappedBuffer (hostData, *inputMapIn, *outputMapIn)),
outputMap (outputMapIn),
data (hostData)
{}
ClientRemappedBuffer (ClientBufferMapper& mapperIn, Steinberg::Vst::ProcessData& hostData)
: ClientRemappedBuffer (mapperIn.getData (detail::Tag<FloatType>{}),
&mapperIn.getInputMap(),
&mapperIn.getOutputMap(),
hostData)
{}
~ClientRemappedBuffer()
{
// WaveLab workaround: This host may report the wrong number of inputs/outputs so re-count here
const auto vstOutputs = (size_t) countValidBuses<FloatType> (data.outputs, data.numOutputs);
if (validateLayouts<FloatType> (data.outputs, data.outputs + vstOutputs, *outputMap))
copyToHostOutputBuses (vstOutputs);
else
clearHostOutputBuses (vstOutputs);
}
AudioBuffer<FloatType> buffer;
private:
void copyToHostOutputBuses (size_t vstOutputs) const
{
for (size_t i = 0, juceBusOffset = 0; i < outputMap->size(); ++i)
{
const auto& mapping = (*outputMap)[i];
if (mapping.isHostActive() && i < vstOutputs)
{
auto& bus = data.outputs[i];
if (mapping.isClientActive())
{
for (size_t j = 0; j < mapping.size(); ++j)
{
auto* hostChannel = getAudioBusPointer (detail::Tag<FloatType>{}, bus)[j];
const auto juceChannel = juceBusOffset + (size_t) mapping.getJuceChannelForVst3Channel ((int) j);
FloatVectorOperations::copy (hostChannel, buffer.getReadPointer ((int) juceChannel), (size_t) data.numSamples);
}
}
else
{
for (size_t j = 0; j < mapping.size(); ++j)
{
auto* hostChannel = getAudioBusPointer (detail::Tag<FloatType>{}, bus)[j];
FloatVectorOperations::clear (hostChannel, (size_t) data.numSamples);
}
}
}
if (mapping.isClientActive())
juceBusOffset += mapping.size();
}
}
void clearHostOutputBuses (size_t vstOutputs) const
{
// The host provided us with an unexpected bus layout.
jassertfalse;
std::for_each (data.outputs, data.outputs + vstOutputs, [this] (auto& bus)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, bus);
std::for_each (busPtr, busPtr + bus.numChannels, [this] (auto* ptr)
{
if (ptr != nullptr)
FloatVectorOperations::clear (ptr, (int) data.numSamples);
});
});
}
const std::vector<DynamicChannelMapping>* outputMap = nullptr;
Steinberg::Vst::ProcessData& data;
JUCE_DECLARE_NON_COPYABLE (ClientRemappedBuffer)
JUCE_DECLARE_NON_MOVEABLE (ClientRemappedBuffer)
};
//==============================================================================


+ 3
- 6
modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp View File

@@ -2781,12 +2781,9 @@ public:
// call releaseResources first!
jassert (! isActive);
bool result = syncBusLayouts (layouts);
// didn't succeed? Make sure it's back in its original state
if (! result)
syncBusLayouts (getBusesLayout());
const auto previousLayout = getBusesLayout();
const auto result = syncBusLayouts (layouts);
syncBusLayouts (previousLayout);
return result;
}


+ 250
- 186
modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp View File

@@ -43,7 +43,6 @@ public:
{
ChannelMapping map (AudioChannelSet::stereo());
expect (map.size() == 2);
expect (map.isActive() == true);
expect (map.getJuceChannelForVst3Channel (0) == 0); // L -> left
expect (map.getJuceChannelForVst3Channel (1) == 1); // R -> right
@@ -53,7 +52,6 @@ public:
{
ChannelMapping map (AudioChannelSet::create9point1point6());
expect (map.size() == 16);
expect (map.isActive() == true);
// VST3 order is:
// L
@@ -115,8 +113,8 @@ public:
ClientBufferMapperData<float> remapper;
remapper.prepare (2, blockSize * 2);
const std::vector<ChannelMapping> emptyBuses;
const std::vector<ChannelMapping> stereoBus { ChannelMapping { AudioChannelSet::stereo() } };
const std::vector<DynamicChannelMapping> emptyBuses;
const std::vector<DynamicChannelMapping> stereoBus { DynamicChannelMapping { AudioChannelSet::stereo() } };
TestBuffers testBuffers { blockSize };
@@ -127,12 +125,17 @@ public:
for (const auto& config : { Config { stereoBus, stereoBus }, Config { emptyBuses, stereoBus }, Config { stereoBus, emptyBuses } })
{
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
{
const ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
}
expect (! testBuffers.isClear (0));
expect (! testBuffers.isClear (1));
@@ -148,10 +151,10 @@ public:
ClientBufferMapperData<float> remapper;
remapper.prepare (3, blockSize * 2);
const std::vector<ChannelMapping> noBus;
const std::vector<ChannelMapping> oneBus { ChannelMapping { AudioChannelSet::mono() } };
const std::vector<ChannelMapping> twoBuses { ChannelMapping { AudioChannelSet::mono() },
ChannelMapping { AudioChannelSet::stereo() } };
const std::vector<DynamicChannelMapping> noBus;
const std::vector<DynamicChannelMapping> oneBus { DynamicChannelMapping { AudioChannelSet::mono() } };
const std::vector<DynamicChannelMapping> twoBuses { DynamicChannelMapping { AudioChannelSet::mono() },
DynamicChannelMapping { AudioChannelSet::stereo() } };
TestBuffers testBuffers { blockSize };
@@ -166,12 +169,20 @@ public:
Config { twoBuses, twoBuses } })
{
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
{
const ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
// The remapped buffer will only be cleared if the host's input layout does not
// match the client's input layout.
if (config.ins.size() != 1)
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
}
expect (! testBuffers.isClear (0));
expect (testBuffers.isClear (1));
@@ -183,8 +194,8 @@ public:
ClientBufferMapperData<float> remapper;
remapper.prepare (3, blockSize * 2);
const std::vector<ChannelMapping> monoBus { ChannelMapping { AudioChannelSet::mono() } };
const std::vector<ChannelMapping> stereoBus { ChannelMapping { AudioChannelSet::stereo() } };
const std::vector<DynamicChannelMapping> monoBus { DynamicChannelMapping { AudioChannelSet::mono() } };
const std::vector<DynamicChannelMapping> stereoBus { DynamicChannelMapping { AudioChannelSet::stereo() } };
TestBuffers testBuffers { blockSize };
@@ -197,12 +208,20 @@ public:
Config { monoBus, monoBus } })
{
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
{
const ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == config.getNumChannels());
expect (remapped.getNumSamples() == blockSize);
// The remapped buffer will only be cleared if the host's input layout does not
// match the client's input layout.
if (config.ins.front().size() != 1)
for (auto i = 0; i < remapped.getNumChannels(); ++i)
expect (allMatch (remapped, i, 0.0f));
}
expect (! testBuffers.isClear (0));
expect (testBuffers.isClear (1));
@@ -215,10 +234,10 @@ public:
ClientBufferMapperData<float> remapper;
remapper.prepare (20, blockSize * 2);
const Config config { { ChannelMapping { AudioChannelSet::mono() },
ChannelMapping { AudioChannelSet::create5point1() } },
{ ChannelMapping { AudioChannelSet::stereo() },
ChannelMapping { AudioChannelSet::create7point1() } } };
const Config config { { DynamicChannelMapping { AudioChannelSet::mono() },
DynamicChannelMapping { AudioChannelSet::create5point1() } },
{ DynamicChannelMapping { AudioChannelSet::stereo() },
DynamicChannelMapping { AudioChannelSet::create7point1() } } };
TestBuffers testBuffers { blockSize };
@@ -228,45 +247,54 @@ public:
auto data = makeProcessData (blockSize, ins, outs);
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == 10);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 5.0f));
expect (allMatch (remapped, 5, 6.0f));
expect (allMatch (remapped, 6, 7.0f));
// These channels are output-only, so they keep whatever data was previously on that output channel
expect (allMatch (remapped, 7, 17.0f));
expect (allMatch (remapped, 8, 14.0f));
expect (allMatch (remapped, 9, 15.0f));
// Channel pointers from the VST3 buffer are used
expect (remapped.getReadPointer (0) == testBuffers.get (7));
expect (remapped.getReadPointer (1) == testBuffers.get (8));
expect (remapped.getReadPointer (2) == testBuffers.get (9));
expect (remapped.getReadPointer (3) == testBuffers.get (10));
expect (remapped.getReadPointer (4) == testBuffers.get (11));
expect (remapped.getReadPointer (5) == testBuffers.get (12));
expect (remapped.getReadPointer (6) == testBuffers.get (15)); // JUCE surround side -> VST3 surround side
expect (remapped.getReadPointer (7) == testBuffers.get (16)); // JUCE surround side -> VST3 surround side
expect (remapped.getReadPointer (8) == testBuffers.get (13)); // JUCE surround rear -> VST3 surround rear
expect (remapped.getReadPointer (9) == testBuffers.get (14)); // JUCE surround rear -> VST3 surround rear
{
ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == 10);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 5.0f));
expect (allMatch (remapped, 5, 6.0f));
expect (allMatch (remapped, 6, 7.0f));
// The remaining channels are output-only, so they may contain any data
// Write some data to the buffer in JUCE layout
for (auto i = 0; i < remapped.getNumChannels(); ++i)
{
auto* ptr = remapped.getWritePointer (i);
std::fill (ptr, ptr + remapped.getNumSamples(), (float) i);
}
}
// Channels are copied back to the correct output buffer
expect (channelStartsWithValue (data.outputs[0], 0, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 1, 1.0f));
expect (channelStartsWithValue (data.outputs[1], 0, 2.0f));
expect (channelStartsWithValue (data.outputs[1], 1, 3.0f));
expect (channelStartsWithValue (data.outputs[1], 2, 4.0f));
expect (channelStartsWithValue (data.outputs[1], 3, 5.0f));
expect (channelStartsWithValue (data.outputs[1], 4, 8.0f)); // JUCE surround side -> VST3 surround side
expect (channelStartsWithValue (data.outputs[1], 5, 9.0f));
expect (channelStartsWithValue (data.outputs[1], 6, 6.0f)); // JUCE surround rear -> VST3 surround rear
expect (channelStartsWithValue (data.outputs[1], 7, 7.0f));
}
beginTest ("A layout with more input channels than output channels uses input channels directly in remapped buffer");
beginTest ("A layout with more input channels than output channels doesn't attempt to output any input channels");
{
ClientBufferMapperData<float> remapper;
remapper.prepare (15, blockSize * 2);
const Config config { { ChannelMapping { AudioChannelSet::create7point1point6() },
ChannelMapping { AudioChannelSet::mono() } },
{ ChannelMapping { AudioChannelSet::createLCRS() },
ChannelMapping { AudioChannelSet::stereo() } } };
const Config config { { DynamicChannelMapping { AudioChannelSet::create7point1point6() },
DynamicChannelMapping { AudioChannelSet::mono() } },
{ DynamicChannelMapping { AudioChannelSet::createLCRS() },
DynamicChannelMapping { AudioChannelSet::stereo() } } };
TestBuffers testBuffers { blockSize };
@@ -276,136 +304,148 @@ public:
auto data = makeProcessData (blockSize, ins, outs);
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == 15);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 7.0f));
expect (allMatch (remapped, 5, 8.0f));
expect (allMatch (remapped, 6, 9.0f));
expect (allMatch (remapped, 7, 10.0f));
expect (allMatch (remapped, 8, 11.0f));
expect (allMatch (remapped, 9, 12.0f));
expect (allMatch (remapped, 10, 5.0f));
expect (allMatch (remapped, 11, 6.0f));
expect (allMatch (remapped, 12, 13.0f));
expect (allMatch (remapped, 13, 14.0f));
expect (allMatch (remapped, 14, 15.0f));
// Use output channel pointers for output channels
expect (remapped.getReadPointer (0) == testBuffers.get (15));
expect (remapped.getReadPointer (1) == testBuffers.get (16));
expect (remapped.getReadPointer (2) == testBuffers.get (17));
expect (remapped.getReadPointer (3) == testBuffers.get (18));
expect (remapped.getReadPointer (4) == testBuffers.get (19));
expect (remapped.getReadPointer (5) == testBuffers.get (20));
// Use input channel pointers for channels with no corresponding output
expect (remapped.getReadPointer (6) == testBuffers.get (8));
expect (remapped.getReadPointer (7) == testBuffers.get (9));
expect (remapped.getReadPointer (8) == testBuffers.get (10));
expect (remapped.getReadPointer (9) == testBuffers.get (11));
expect (remapped.getReadPointer (10) == testBuffers.get (4));
expect (remapped.getReadPointer (11) == testBuffers.get (5));
expect (remapped.getReadPointer (12) == testBuffers.get (12));
expect (remapped.getReadPointer (13) == testBuffers.get (13));
expect (remapped.getReadPointer (14) == testBuffers.get (14));
{
ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == 15);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 7.0f));
expect (allMatch (remapped, 5, 8.0f));
expect (allMatch (remapped, 6, 9.0f));
expect (allMatch (remapped, 7, 10.0f));
expect (allMatch (remapped, 8, 11.0f));
expect (allMatch (remapped, 9, 12.0f));
expect (allMatch (remapped, 10, 5.0f));
expect (allMatch (remapped, 11, 6.0f));
expect (allMatch (remapped, 12, 13.0f));
expect (allMatch (remapped, 13, 14.0f));
expect (allMatch (remapped, 14, 15.0f));
// Write some data to the buffer in JUCE layout
for (auto i = 0; i < remapped.getNumChannels(); ++i)
{
auto* ptr = remapped.getWritePointer (i);
std::fill (ptr, ptr + remapped.getNumSamples(), (float) i);
}
}
// Channels are copied back to the correct output buffer
expect (channelStartsWithValue (data.outputs[0], 0, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 1, 1.0f));
expect (channelStartsWithValue (data.outputs[0], 2, 2.0f));
expect (channelStartsWithValue (data.outputs[0], 3, 3.0f));
expect (channelStartsWithValue (data.outputs[1], 0, 4.0f));
expect (channelStartsWithValue (data.outputs[1], 1, 5.0f));
}
beginTest ("Inactive buses are ignored");
{
ClientBufferMapperData<float> remapper;
remapper.prepare (15, blockSize * 2);
remapper.prepare (18, blockSize * 2);
Config config { { DynamicChannelMapping { AudioChannelSet::create7point1point6() },
DynamicChannelMapping { AudioChannelSet::mono(), false },
DynamicChannelMapping { AudioChannelSet::quadraphonic() },
DynamicChannelMapping { AudioChannelSet::mono(), false } },
{ DynamicChannelMapping { AudioChannelSet::create5point0(), false },
DynamicChannelMapping { AudioChannelSet::createLCRS() },
DynamicChannelMapping { AudioChannelSet::stereo() } } };
const Config config { { ChannelMapping { AudioChannelSet::create7point1point6() },
ChannelMapping { AudioChannelSet::mono(), false },
ChannelMapping { AudioChannelSet::quadraphonic() },
ChannelMapping { AudioChannelSet::mono(), false } },
{ ChannelMapping { AudioChannelSet::create5point0(), false },
ChannelMapping { AudioChannelSet::createLCRS() },
ChannelMapping { AudioChannelSet::stereo() } } };
config.ins[1].setHostActive (false);
config.ins[3].setHostActive (false);
TestBuffers testBuffers { blockSize };
// The host doesn't need to provide trailing buses that are inactive
// The host doesn't need to provide trailing buses that are inactive, as long as the
// client knows those buses are inactive.
auto ins = MultiBusBuffers{}.withBus (testBuffers, 14).withBus (testBuffers, 1).withBus (testBuffers, 4);
auto outs = MultiBusBuffers{}.withBus (testBuffers, 5) .withBus (testBuffers, 4).withBus (testBuffers, 2);
auto data = makeProcessData (blockSize, ins, outs);
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == 18);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 7.0f));
expect (allMatch (remapped, 5, 8.0f));
expect (allMatch (remapped, 6, 9.0f));
expect (allMatch (remapped, 7, 10.0f));
expect (allMatch (remapped, 8, 11.0f));
expect (allMatch (remapped, 9, 12.0f));
expect (allMatch (remapped, 10, 5.0f));
expect (allMatch (remapped, 11, 6.0f));
expect (allMatch (remapped, 12, 13.0f));
expect (allMatch (remapped, 13, 14.0f));
expect (allMatch (remapped, 14, 16.0f));
expect (allMatch (remapped, 15, 17.0f));
expect (allMatch (remapped, 16, 18.0f));
expect (allMatch (remapped, 17, 19.0f));
// Use output channel pointers for output channels
expect (remapped.getReadPointer (0) == testBuffers.get (24));
expect (remapped.getReadPointer (1) == testBuffers.get (25));
expect (remapped.getReadPointer (2) == testBuffers.get (26));
expect (remapped.getReadPointer (3) == testBuffers.get (27));
expect (remapped.getReadPointer (4) == testBuffers.get (28));
expect (remapped.getReadPointer (5) == testBuffers.get (29));
// Use input channel pointers for channels with no corresponding output
expect (remapped.getReadPointer (6) == testBuffers.get (8));
expect (remapped.getReadPointer (7) == testBuffers.get (9));
expect (remapped.getReadPointer (8) == testBuffers.get (10));
expect (remapped.getReadPointer (9) == testBuffers.get (11));
expect (remapped.getReadPointer (10) == testBuffers.get (4));
expect (remapped.getReadPointer (11) == testBuffers.get (5));
expect (remapped.getReadPointer (12) == testBuffers.get (12));
expect (remapped.getReadPointer (13) == testBuffers.get (13));
expect (remapped.getReadPointer (14) == testBuffers.get (15));
expect (remapped.getReadPointer (15) == testBuffers.get (16));
expect (remapped.getReadPointer (16) == testBuffers.get (17));
expect (remapped.getReadPointer (17) == testBuffers.get (18));
{
ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == 18);
// Data from the input channels is copied to the correct channels of the remapped buffer
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
expect (allMatch (remapped, 2, 3.0f));
expect (allMatch (remapped, 3, 4.0f));
expect (allMatch (remapped, 4, 7.0f));
expect (allMatch (remapped, 5, 8.0f));
expect (allMatch (remapped, 6, 9.0f));
expect (allMatch (remapped, 7, 10.0f));
expect (allMatch (remapped, 8, 11.0f));
expect (allMatch (remapped, 9, 12.0f));
expect (allMatch (remapped, 10, 5.0f));
expect (allMatch (remapped, 11, 6.0f));
expect (allMatch (remapped, 12, 13.0f));
expect (allMatch (remapped, 13, 14.0f));
expect (allMatch (remapped, 14, 16.0f));
expect (allMatch (remapped, 15, 17.0f));
expect (allMatch (remapped, 16, 18.0f));
expect (allMatch (remapped, 17, 19.0f));
// Write some data to the buffer in JUCE layout
for (auto i = 0; i < remapped.getNumChannels(); ++i)
{
auto* ptr = remapped.getWritePointer (i);
std::fill (ptr, ptr + remapped.getNumSamples(), (float) i);
}
}
// All channels on the first output bus should be cleared, because the plugin
// thinks that this bus is inactive.
expect (channelStartsWithValue (data.outputs[0], 0, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 1, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 2, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 3, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 4, 0.0f));
// Remaining channels should be copied back as normal
expect (channelStartsWithValue (data.outputs[1], 0, 0.0f));
expect (channelStartsWithValue (data.outputs[1], 1, 1.0f));
expect (channelStartsWithValue (data.outputs[1], 2, 2.0f));
expect (channelStartsWithValue (data.outputs[1], 3, 3.0f));
expect (channelStartsWithValue (data.outputs[2], 0, 4.0f));
expect (channelStartsWithValue (data.outputs[2], 1, 5.0f));
}
beginTest ("Null pointers are allowed on inactive buses provided to clients");
{
ClientBufferMapperData<float> remapper;
remapper.prepare (4, blockSize * 2);
remapper.prepare (8, blockSize * 2);
const std::vector<ChannelMapping> emptyBuses;
const std::vector<ChannelMapping> stereoBus { ChannelMapping { AudioChannelSet::stereo() } };
const Config config { { ChannelMapping { AudioChannelSet::stereo() },
ChannelMapping { AudioChannelSet::quadraphonic(), false },
ChannelMapping { AudioChannelSet::stereo() } },
{ ChannelMapping { AudioChannelSet::quadraphonic() },
ChannelMapping { AudioChannelSet::stereo(), false },
ChannelMapping { AudioChannelSet::quadraphonic() } } };
Config config { { DynamicChannelMapping { AudioChannelSet::stereo() },
DynamicChannelMapping { AudioChannelSet::quadraphonic(), false },
DynamicChannelMapping { AudioChannelSet::stereo() } },
{ DynamicChannelMapping { AudioChannelSet::quadraphonic() },
DynamicChannelMapping { AudioChannelSet::stereo(), false },
DynamicChannelMapping { AudioChannelSet::quadraphonic() } } };
config.ins[1].setHostActive (false);
config.outs[1].setHostActive (false);
TestBuffers testBuffers { blockSize };
// The host doesn't need to provide trailing buses that are inactive
auto ins = MultiBusBuffers{}.withBus (testBuffers, 2).withBus (testBuffers, 4).withBus (testBuffers, 2);
auto outs = MultiBusBuffers{}.withBus (testBuffers, 4).withBus (testBuffers, 2).withBus (testBuffers, 4);
@@ -418,25 +458,36 @@ public:
data.outputs[1].channelBuffers32[i] = nullptr;
testBuffers.init();
const auto remapped = remapper.getMappedBuffer (data, config.ins, config.outs);
expect (remapped.getNumChannels() == 8);
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
// skip 4 inactive channels
expect (allMatch (remapped, 2, 7.0f));
expect (allMatch (remapped, 3, 8.0f));
expect (remapped.getReadPointer (0) == testBuffers.get ( 8));
expect (remapped.getReadPointer (1) == testBuffers.get ( 9));
expect (remapped.getReadPointer (2) == testBuffers.get (10));
expect (remapped.getReadPointer (3) == testBuffers.get (11));
// skip 2 inactive channels
expect (remapped.getReadPointer (4) == testBuffers.get (14));
expect (remapped.getReadPointer (5) == testBuffers.get (15));
expect (remapped.getReadPointer (6) == testBuffers.get (16));
expect (remapped.getReadPointer (7) == testBuffers.get (17));
{
ClientRemappedBuffer<float> scopedBuffer { remapper, &config.ins, &config.outs, data };
auto& remapped = scopedBuffer.buffer;
expect (remapped.getNumChannels() == 8);
expect (allMatch (remapped, 0, 1.0f));
expect (allMatch (remapped, 1, 2.0f));
// skip 4 inactive channels
expect (allMatch (remapped, 2, 7.0f));
expect (allMatch (remapped, 3, 8.0f));
// Write some data to the buffer in JUCE layout
for (auto i = 0; i < remapped.getNumChannels(); ++i)
{
auto* ptr = remapped.getWritePointer (i);
std::fill (ptr, ptr + remapped.getNumSamples(), (float) i);
}
}
expect (channelStartsWithValue (data.outputs[0], 0, 0.0f));
expect (channelStartsWithValue (data.outputs[0], 1, 1.0f));
expect (channelStartsWithValue (data.outputs[0], 2, 2.0f));
expect (channelStartsWithValue (data.outputs[0], 3, 3.0f));
expect (channelStartsWithValue (data.outputs[2], 0, 4.0f));
expect (channelStartsWithValue (data.outputs[2], 1, 5.0f));
expect (channelStartsWithValue (data.outputs[2], 2, 6.0f));
expect (channelStartsWithValue (data.outputs[2], 3, 7.0f));
}
beginTest ("HostBufferMapper reorders channels correctly");
@@ -505,9 +556,17 @@ private:
//==============================================================================
struct Config
{
std::vector<ChannelMapping> ins, outs;
Config (std::vector<DynamicChannelMapping> i, std::vector<DynamicChannelMapping> o)
: ins (std::move (i)), outs (std::move (o))
{
for (auto container : { &ins, &outs })
for (auto& x : *container)
x.setHostActive (true);
}
std::vector<DynamicChannelMapping> ins, outs;
int getNumChannels() const { return countUsedChannels (ins, outs); }
int getNumChannels() const { return countUsedClientChannels (ins, outs); }
};
struct TestBuffers
@@ -546,6 +605,11 @@ private:
int numSamples = 0;
};
static bool channelStartsWithValue (Steinberg::Vst::AudioBusBuffers& bus, size_t index, float value)
{
return bus.channelBuffers32[index][0] == value;
}
static bool allMatch (const AudioBuffer<float>& buf, int index, float value)
{
const auto* ptr = buf.getReadPointer (index);


Loading…
Cancel
Save