diff --git a/examples/Plugins/MultiOutSynthPluginDemo.h b/examples/Plugins/MultiOutSynthPluginDemo.h index 4701096670..cedc674d7d 100644 --- a/examples/Plugins/MultiOutSynthPluginDemo.h +++ b/examples/Plugins/MultiOutSynthPluginDemo.h @@ -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(); }); } 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 1f8cc6f294..bc753adb9b 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -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 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())); @@ -3488,7 +3537,8 @@ private: 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 dd245c1400..ea9a9ccac7 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -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, 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. @@ -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 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 }; - } + const auto mapping = map[busIndex]; - std::vector getMappedOutputBus (Steinberg::Vst::ProcessData& data, - const std::vector& maps, - size_t index) const - { - const auto& map = maps[index]; - - if (! map.isActive()) - return {}; - - auto** busPtr = getAudioBusPointer (detail::Tag{}, data.outputs[index]); + if (! mapping.isClientActive()) + continue; - std::vector 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{}, 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 - 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; }; //============================================================================== @@ -750,65 +775,213 @@ private: 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(); - - 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) { 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 9e5a53e577..1997448951 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_VST3PluginFormat.cpp @@ -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; } 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 53ec399d82..305a705cba 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 @@ 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 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 @@ 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 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 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 @@ 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 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 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 @@ 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 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 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 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 @@ 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 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 @@ 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 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 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 @@ 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& buf, int index, float value) { const auto* ptr = buf.getReadPointer (index);