From 1573090454962fef452e1d854f228de3584431b5 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 1 Jun 2022 16:53:24 +0100 Subject: [PATCH] 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. --- examples/Plugins/MultiOutSynthPluginDemo.h | 3 +- .../VST3/juce_VST3_Wrapper.cpp | 90 +++- .../format_types/juce_VST3Common.h | 487 ++++++++++++------ .../format_types/juce_VST3PluginFormat.cpp | 9 +- .../juce_VST3PluginFormat_test.cpp | 436 +++++++++------- 5 files changed, 654 insertions(+), 371 deletions(-) diff --git a/examples/Plugins/MultiOutSynthPluginDemo.h b/examples/Plugins/MultiOutSynthPluginDemo.h index a8e9bf4d2e98..9a5c766128d8 100644 --- a/examples/Plugins/MultiOutSynthPluginDemo.h +++ b/examples/Plugins/MultiOutSynthPluginDemo.h @@ -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(); }); } diff --git a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp index 9cd29ecad454..f13ff7004145 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -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; @@ -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 @@ -3285,7 +3332,9 @@ class JuceVST3Component : public Vst::IComponent, template void processAudio (Vst::ProcessData& data) { - auto buffer = bufferMapper.getJuceLayoutForVst3Buffer (detail::Tag{}, data); + ClientRemappedBuffer remappedBuffer { bufferMapper, data }; + auto& buffer = remappedBuffer.buffer; + jassert ((int) buffer.getNumChannels() == jmax (pluginInstance->getTotalNumInputChannels(), pluginInstance->getTotalNumOutputChannels())); @@ -3358,7 +3407,8 @@ class JuceVST3Component : public Vst::IComponent, midiBuffer.ensureSize (2048); midiBuffer.clear(); - bufferMapper.prepare (p, bufferSize); + bufferMapper.updateFromProcessor (p); + bufferMapper.prepare (bufferSize); } //============================================================================== diff --git a/modules/juce_audio_processors/format_types/juce_VST3Common.h b/modules/juce_audio_processors/format_types/juce_VST3Common.h index 3f425c3cc7e2..e02fb559a228 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -495,27 +495,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: @@ -543,24 +536,104 @@ struct ChannelMapping 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, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers32; } inline auto& getAudioBusPointer (detail::Tag, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers64; } -static inline int countUsedChannels (const std::vector& inputMap, - const std::vector& outputMap) +static inline int countUsedClientChannels (const std::vector& inputMap, + const std::vector& outputMap) { - const auto countUsedChannelsInVector = [] (const std::vector& map) + const auto countUsedChannelsInVector = [] (const std::vector& 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 +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 buffer; + int channelCounter = 0; +}; + +template +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{}, buf) == nullptr && buf.numChannels > 0; + })); +} + +template +static bool validateLayouts (Iterator first, Iterator last, const std::vector& 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{}, *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. @@ -578,155 +651,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 getMappedBuffer (Steinberg::Vst::ProcessData& data, - const std::vector& inputMap, - const std::vector& outputMap) + const std::vector& inputMap, + const std::vector& 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{}, 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{}, 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 (data.inputs, data.numInputs); - if (auto* dest = channels[destIndex]) - FloatVectorOperations::copy (dest, busPtr[i], (int) data.numSamples); - else - channels[destIndex] = busPtr[i]; - } + if (! validateLayouts (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 clearOutputBuffersAndReturnBlankBuffer (Steinberg::Vst::ProcessData& data, int vstOutputs, int usedChannels) + static void setUpInputChannels (Steinberg::Vst::ProcessData& data, + size_t vstInputs, + ScratchBuffer& scratchBuffer, + const std::vector& map, + std::vector& 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{}, 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 }; - } - - std::vector getMappedOutputBus (Steinberg::Vst::ProcessData& data, - const std::vector& maps, - size_t index) const - { - const auto& map = maps[index]; + const auto mapping = map[busIndex]; - if (! map.isActive()) - return {}; + if (! mapping.isClientActive()) + continue; - auto** busPtr = getAudioBusPointer (detail::Tag{}, data.outputs[index]); + const auto originalSize = channels.size(); - std::vector result (map.size(), nullptr); + for (size_t channelIndex = 0; channelIndex < mapping.size(); ++channelIndex) + channels.push_back (scratchBuffer.getNextChannelBuffer()); - for (auto i = 0; i < (int) map.size(); ++i) - result[(size_t) map.getJuceChannelForVst3Channel (i)] = busPtr[i]; + if (mapping.isHostActive() && busIndex < vstInputs) + { + auto** busPtr = getAudioBusPointer (detail::Tag{}, data.inputs[busIndex]); - return result; + 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 - static bool validateLayouts (Iterator first, Iterator last, const std::vector& map) + static void setUpOutputChannels (ScratchBuffer& scratchBuffer, + const std::vector& map, + std::vector& 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{}, *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& inputMap, - int numOutputs, - const std::vector& outputMap) + AudioBuffer 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 channels; - AudioBuffer emptyBuffer; + ScratchBuffer scratchBuffer; }; //============================================================================== @@ -748,65 +773,213 @@ class ClientBufferMapperData class ClientBufferMapper { public: - void prepare (const AudioProcessor& processor, int blockSize) + void updateFromProcessor (const AudioProcessor& processor) { struct Pair { - std::vector& map; + std::vector& map; bool isInput; }; for (const auto& pair : { Pair { inputMap, true }, Pair { outputMap, false } }) { - pair.map.clear(); + 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 (auto i = 0; i < processor.getBusCount (pair.isInput); ++i) - pair.map.emplace_back (*processor.getBus (pair.isInput, i)); + 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) { return floatData; } + auto& getData (detail::Tag) { return doubleData; } + + AudioChannelSet getRequestedLayoutForInputBus (size_t bus) const { - if (bus < outputMap.size()) - outputMap[bus].setActive (state); + return getRequestedLayoutForBus (inputMap, bus); } - template - AudioBuffer getJuceLayoutForVst3Buffer (detail::Tag, Steinberg::Vst::ProcessData& data) + AudioChannelSet getRequestedLayoutForOutputBus (size_t bus) const { - return getData (detail::Tag{}).getMappedBuffer (data, inputMap, outputMap); + return getRequestedLayoutForBus (outputMap, bus); } + const std::vector& getInputMap() const { return inputMap; } + const std::vector& getOutputMap() const { return outputMap; } + private: - auto& getData (detail::Tag) { return floatData; } - auto& getData (detail::Tag) { return doubleData; } + static void setHostActive (std::vector& map, size_t bus, bool state) + { + if (bus < map.size()) + map[bus].setHostActive (state); + } + + static AudioChannelSet getRequestedLayoutForBus (const std::vector& map, size_t bus) + { + if (bus < map.size() && map[bus].isHostActive()) + return map[bus].getAudioChannelSet(); + + return AudioChannelSet::disabled(); + } ClientBufferMapperData floatData; ClientBufferMapperData doubleData; - std::vector inputMap; - std::vector outputMap; + std::vector inputMap; + std::vector 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 +class ClientRemappedBuffer +{ +public: + ClientRemappedBuffer (ClientBufferMapperData& mapperData, + const std::vector* inputMapIn, + const std::vector* 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{}), + &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 (data.outputs, data.numOutputs); + + if (validateLayouts (data.outputs, data.outputs + vstOutputs, *outputMap)) + copyToHostOutputBuses (vstOutputs); + else + clearHostOutputBuses (vstOutputs); + } + + AudioBuffer 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{}, 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{}, 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{}, bus); + std::for_each (busPtr, busPtr + bus.numChannels, [this] (auto* ptr) + { + if (ptr != nullptr) + FloatVectorOperations::clear (ptr, (int) data.numSamples); + }); + }); + } + + const std::vector* outputMap = nullptr; + Steinberg::Vst::ProcessData& data; + + JUCE_DECLARE_NON_COPYABLE (ClientRemappedBuffer) + JUCE_DECLARE_NON_MOVEABLE (ClientRemappedBuffer) }; //============================================================================== diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp index 605ca74f88d0..75e7bc47c179 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -2670,12 +2670,9 @@ class VST3PluginInstance final : public AudioPluginInstance // 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; } diff --git a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp index e287a8d839ae..c548b2513f4d 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat_test.cpp @@ -43,7 +43,6 @@ class VST3PluginFormatTests : public UnitTest { 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 @@ class VST3PluginFormatTests : public UnitTest { ChannelMapping map (AudioChannelSet::create9point1point6()); expect (map.size() == 16); - expect (map.isActive() == true); // VST3 order is: // L @@ -115,8 +113,8 @@ class VST3PluginFormatTests : public UnitTest ClientBufferMapperData remapper; remapper.prepare (2, blockSize * 2); - const std::vector emptyBuses; - const std::vector stereoBus { ChannelMapping { AudioChannelSet::stereo() } }; + const std::vector emptyBuses; + const std::vector stereoBus { DynamicChannelMapping { AudioChannelSet::stereo() } }; TestBuffers testBuffers { blockSize }; @@ -127,12 +125,17 @@ class VST3PluginFormatTests : public UnitTest 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 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 @@ class VST3PluginFormatTests : public UnitTest ClientBufferMapperData remapper; remapper.prepare (3, blockSize * 2); - const std::vector noBus; - const std::vector oneBus { ChannelMapping { AudioChannelSet::mono() } }; - const std::vector twoBuses { ChannelMapping { AudioChannelSet::mono() }, - ChannelMapping { AudioChannelSet::stereo() } }; + const std::vector noBus; + const std::vector oneBus { DynamicChannelMapping { AudioChannelSet::mono() } }; + const std::vector twoBuses { DynamicChannelMapping { AudioChannelSet::mono() }, + DynamicChannelMapping { AudioChannelSet::stereo() } }; TestBuffers testBuffers { blockSize }; @@ -166,12 +169,20 @@ class VST3PluginFormatTests : public UnitTest 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 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 @@ class VST3PluginFormatTests : public UnitTest ClientBufferMapperData remapper; remapper.prepare (3, blockSize * 2); - const std::vector monoBus { ChannelMapping { AudioChannelSet::mono() } }; - const std::vector stereoBus { ChannelMapping { AudioChannelSet::stereo() } }; + const std::vector monoBus { DynamicChannelMapping { AudioChannelSet::mono() } }; + const std::vector stereoBus { DynamicChannelMapping { AudioChannelSet::stereo() } }; TestBuffers testBuffers { blockSize }; @@ -197,12 +208,20 @@ class VST3PluginFormatTests : public UnitTest 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 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 @@ class VST3PluginFormatTests : public UnitTest ClientBufferMapperData 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 @@ class VST3PluginFormatTests : public UnitTest 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 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 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 @@ class VST3PluginFormatTests : public UnitTest 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 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 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 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 remapper; - remapper.prepare (4, blockSize * 2); + remapper.prepare (8, blockSize * 2); const std::vector emptyBuses; const std::vector 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 @@ class VST3PluginFormatTests : public UnitTest 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 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 @@ class VST3PluginFormatTests : public UnitTest //============================================================================== struct Config { - std::vector ins, outs; + Config (std::vector i, std::vector o) + : ins (std::move (i)), outs (std::move (o)) + { + for (auto container : { &ins, &outs }) + for (auto& x : *container) + x.setHostActive (true); + } + + std::vector ins, outs; - int getNumChannels() const { return countUsedChannels (ins, outs); } + int getNumChannels() const { return countUsedClientChannels (ins, outs); } }; struct TestBuffers @@ -546,6 +605,11 @@ class VST3PluginFormatTests : public UnitTest 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& buf, int index, float value) { const auto* ptr = buf.getReadPointer (index);