From bb2b36a253ad8b2f94da116860c4720fd12bb3c3 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 2 Mar 2022 21:26:25 +0000 Subject: [PATCH] VST3 Client: Properly map between VST3 and JUCE layouts --- .../VST3/juce_VST3_Wrapper.cpp | 225 ++------------ .../format_types/juce_VST3Common.h | 285 +++++++++++++++++- 2 files changed, 307 insertions(+), 203 deletions(-) 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 b30ccb8251..e1624fb090 100644 --- a/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp +++ b/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp @@ -2186,18 +2186,6 @@ private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceVST3EditController) }; -namespace -{ - template struct AudioBusPointerHelper {}; - template <> struct AudioBusPointerHelper { static float** impl (Vst::AudioBusBuffers& data) noexcept { return data.channelBuffers32; } }; - template <> struct AudioBusPointerHelper { static double** impl (Vst::AudioBusBuffers& data) noexcept { return data.channelBuffers64; } }; - - template struct ChooseBufferHelper {}; - template <> struct ChooseBufferHelper { static AudioBuffer& impl (AudioBuffer& f, AudioBuffer& ) noexcept { return f; } }; - template <> struct ChooseBufferHelper { static AudioBuffer& impl (AudioBuffer& , AudioBuffer& d) noexcept { return d; } }; -} - - //============================================================================== class JuceVST3Component : public Vst::IComponent, public Vst::IAudioProcessor, @@ -2343,9 +2331,6 @@ public: if (! state) { getPluginInstance().releaseResources(); - - deallocateChannelListAndBuffers (channelListFloat, emptyBufferFloat); - deallocateChannelListAndBuffers (channelListDouble, emptyBufferDouble); } else { @@ -2360,9 +2345,6 @@ public: ? (int) processSetup.maxSamplesPerBlock : bufferSize; - allocateChannelListAndBuffers (channelListFloat, emptyBufferFloat); - allocateChannelListAndBuffers (channelListDouble, emptyBufferDouble); - preparePlugin (sampleRate, bufferSize); } @@ -2823,6 +2805,7 @@ public: info.mediaType = Vst::kAudio; info.direction = dir; info.channelCount = bus->getLastEnabledLayout().size(); + jassert (info.channelCount == Steinberg::Vst::SpeakerArr::getChannelCount (getVst3SpeakerArrangement (bus->getLastEnabledLayout()))); toString128 (info.name, bus->getName()); info.busType = [&] @@ -2904,7 +2887,10 @@ public: return kResultFalse; } - tresult PLUGIN_API activateBus (Vst::MediaType type, Vst::BusDirection dir, Steinberg::int32 index, TBool state) override + tresult PLUGIN_API activateBus (Vst::MediaType type, + Vst::BusDirection dir, + Steinberg::int32 index, + TBool state) override { // The host is misbehaving! The plugin must be deactivated before setting new arrangements. jassert (! active); @@ -2935,6 +2921,13 @@ public: if (index < 0 || index >= getNumAudioBuses (dir == Vst::kInput)) 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); + else + bufferMapper.setOutputBusActive ((size_t) index, state != 0); + if (auto* bus = pluginInstance->getBus (dir == Vst::kInput, index)) { #ifdef JucePlugin_PreferredChannelConfigurations @@ -3179,8 +3172,8 @@ public: // If all of these are zero, the host is attempting to flush parameters without processing audio. if (data.numSamples != 0 || data.numInputs != 0 || data.numOutputs != 0) { - if (processSetup.symbolicSampleSize == Vst::kSample32) processAudio (data, channelListFloat); - else if (processSetup.symbolicSampleSize == Vst::kSample64) processAudio (data, channelListDouble); + if (processSetup.symbolicSampleSize == Vst::kSample32) processAudio (data); + else if (processSetup.symbolicSampleSize == Vst::kSample64) processAudio (data); else jassertfalse; } @@ -3251,131 +3244,11 @@ private: //============================================================================== template - void processAudio (Vst::ProcessData& data, Array& channelList) + void processAudio (Vst::ProcessData& data) { - int totalInputChans = 0, totalOutputChans = 0; - bool tmpBufferNeedsClearing = false; - - auto plugInInputChannels = pluginInstance->getTotalNumInputChannels(); - auto plugInOutputChannels = pluginInstance->getTotalNumOutputChannels(); - - // Wavelab workaround: wave-lab lies on the number of inputs/outputs so re-count here - const auto countValidChannels = [] (Vst::AudioBusBuffers* buffers, int32 num) - { - return int (std::distance (buffers, std::find_if (buffers, buffers + num, [] (Vst::AudioBusBuffers& buf) - { - return getPointerForAudioBus (buf) == nullptr && buf.numChannels > 0; - }))); - }; - - const auto vstInputs = countValidChannels (data.inputs, data.numInputs); - const auto vstOutputs = countValidChannels (data.outputs, data.numOutputs); - - { - auto n = jmax (vstOutputs, getNumAudioBuses (false)); - - for (int bus = 0; bus < n && totalOutputChans < plugInOutputChannels; ++bus) - { - if (auto* busObject = pluginInstance->getBus (false, bus)) - if (! busObject->isEnabled()) - continue; - - if (bus < vstOutputs) - { - if (auto** const busChannels = getPointerForAudioBus (data.outputs[bus])) - { - auto numChans = jmin ((int) data.outputs[bus].numChannels, plugInOutputChannels - totalOutputChans); - - for (int i = 0; i < numChans; ++i) - { - if (auto dst = busChannels[i]) - { - if (totalOutputChans >= plugInInputChannels) - FloatVectorOperations::clear (dst, (int) data.numSamples); - - channelList.set (totalOutputChans++, busChannels[i]); - } - } - } - } - else - { - const int numChans = jmin (pluginInstance->getChannelCountOfBus (false, bus), plugInOutputChannels - totalOutputChans); - - for (int i = 0; i < numChans; ++i) - { - if (auto* tmpBuffer = getTmpBufferForChannel (totalOutputChans, data.numSamples))\ - { - tmpBufferNeedsClearing = true; - channelList.set (totalOutputChans++, tmpBuffer); - } - else - return; - } - } - } - } - - { - auto n = jmax (vstInputs, getNumAudioBuses (true)); - - for (int bus = 0; bus < n && totalInputChans < plugInInputChannels; ++bus) - { - if (auto* busObject = pluginInstance->getBus (true, bus)) - if (! busObject->isEnabled()) - continue; - - if (bus < vstInputs) - { - if (auto** const busChannels = getPointerForAudioBus (data.inputs[bus])) - { - const int numChans = jmin ((int) data.inputs[bus].numChannels, plugInInputChannels - totalInputChans); - - for (int i = 0; i < numChans; ++i) - { - if (busChannels[i] != nullptr) - { - if (totalInputChans >= totalOutputChans) - channelList.set (totalInputChans, busChannels[i]); - else - { - auto* dst = channelList.getReference (totalInputChans); - auto* src = busChannels[i]; - - if (dst != src) - FloatVectorOperations::copy (dst, src, (int) data.numSamples); - } - } - - ++totalInputChans; - } - } - } - else - { - auto numChans = jmin (pluginInstance->getChannelCountOfBus (true, bus), plugInInputChannels - totalInputChans); - - for (int i = 0; i < numChans; ++i) - { - if (auto* tmpBuffer = getTmpBufferForChannel (totalInputChans, data.numSamples)) - { - tmpBufferNeedsClearing = true; - channelList.set (totalInputChans++, tmpBuffer); - } - else - return; - } - } - } - } - - if (tmpBufferNeedsClearing) - ChooseBufferHelper::impl (emptyBufferFloat, emptyBufferDouble).clear(); - - AudioBuffer buffer; - - if (int totalChans = jmax (totalOutputChans, totalInputChans)) - buffer.setDataToReferTo (channelList.getRawDataPointer(), totalChans, (int) data.numSamples); + auto buffer = bufferMapper.getJuceLayoutForVst3Buffer (detail::Tag{}, data); + jassert ((int) buffer.getNumChannels() == jmax (pluginInstance->getTotalNumInputChannels(), + pluginInstance->getTotalNumOutputChannels())); { const ScopedLock sl (pluginInstance->getCallbackLock()); @@ -3392,16 +3265,12 @@ private: } else { - if (totalInputChans == pluginInstance->getTotalNumInputChannels() - && totalOutputChans == pluginInstance->getTotalNumOutputChannels()) - { - // processBlockBypassed should only ever be called if the AudioProcessor doesn't - // return a valid parameter from getBypassParameter - if (pluginInstance->getBypassParameter() == nullptr && comPluginInstance->getBypassParameter()->getValue() >= 0.5f) - pluginInstance->processBlockBypassed (buffer, midiBuffer); - else - pluginInstance->processBlock (buffer, midiBuffer); - } + // processBlockBypassed should only ever be called if the AudioProcessor doesn't + // return a valid parameter from getBypassParameter + if (pluginInstance->getBypassParameter() == nullptr && comPluginInstance->getBypassParameter()->getValue() >= 0.5f) + pluginInstance->processBlockBypassed (buffer, midiBuffer); + else + pluginInstance->processBlock (buffer, midiBuffer); } #if JUCE_DEBUG && (! JucePlugin_ProducesMidiOutput) @@ -3425,44 +3294,6 @@ private: } //============================================================================== - template - void allocateChannelListAndBuffers (Array& channelList, AudioBuffer& buffer) - { - channelList.clearQuick(); - channelList.insertMultiple (0, nullptr, 128); - - auto& p = getPluginInstance(); - buffer.setSize (jmax (p.getTotalNumInputChannels(), p.getTotalNumOutputChannels()), p.getBlockSize() * 4); - buffer.clear(); - } - - template - void deallocateChannelListAndBuffers (Array& channelList, AudioBuffer& buffer) - { - channelList.clearQuick(); - channelList.resize (0); - buffer.setSize (0, 0); - } - - template - static FloatType** getPointerForAudioBus (Vst::AudioBusBuffers& data) noexcept - { - return AudioBusPointerHelper::impl (data); - } - - template - FloatType* getTmpBufferForChannel (int channel, int numSamples) noexcept - { - auto& buffer = ChooseBufferHelper::impl (emptyBufferFloat, emptyBufferDouble); - - // we can't do anything if the host requests to render many more samples than the - // block size, we need to bail out - if (numSamples > buffer.getNumSamples() || channel >= buffer.getNumChannels()) - return nullptr; - - return buffer.getWritePointer (channel); - } - Steinberg::uint32 PLUGIN_API getProcessContextRequirements() override { return kNeedSystemTime @@ -3487,6 +3318,8 @@ private: midiBuffer.ensureSize (2048); midiBuffer.clear(); + + bufferMapper.prepare (p, bufferSize); } //============================================================================== @@ -3542,11 +3375,7 @@ private: Vst::ProcessSetup processSetup; MidiBuffer midiBuffer; - Array channelListFloat; - Array channelListDouble; - - AudioBuffer emptyBufferFloat; - AudioBuffer emptyBufferDouble; + ClientBufferMapper bufferMapper; bool active = false; diff --git a/modules/juce_audio_processors/format_types/juce_VST3Common.h b/modules/juce_audio_processors/format_types/juce_VST3Common.h index 6060206774..8f3a52cf35 100644 --- a/modules/juce_audio_processors/format_types/juce_VST3Common.h +++ b/modules/juce_audio_processors/format_types/juce_VST3Common.h @@ -23,6 +23,8 @@ ============================================================================== */ +#pragma once + #ifndef DOXYGEN namespace juce @@ -43,7 +45,7 @@ JUCE_BEGIN_NO_SANITIZE ("vptr") return Steinberg::kNotImplemented; \ } -static bool doUIDsMatch (const Steinberg::TUID a, const Steinberg::TUID b) noexcept +inline bool doUIDsMatch (const Steinberg::TUID a, const Steinberg::TUID b) noexcept { return std::memcmp (a, b, sizeof (Steinberg::TUID)) == 0; } @@ -473,7 +475,7 @@ static Steinberg::Vst::SpeakerArrangement getVst3SpeakerArrangement (const Audio return result; } -static AudioChannelSet getChannelSetForSpeakerArrangement (Steinberg::Vst::SpeakerArrangement arr) noexcept +inline AudioChannelSet getChannelSetForSpeakerArrangement (Steinberg::Vst::SpeakerArrangement arr) noexcept { using namespace Steinberg::Vst::SpeakerArr; @@ -486,17 +488,26 @@ static AudioChannelSet getChannelSetForSpeakerArrangement (Steinberg::Vst::Speak } //============================================================================== +/* + Provides fast remapping of the channels on a single bus, from VST3 order to JUCE order. + + For multi-bus plugins, you'll need several instances of this, one per bus. +*/ struct ChannelMapping { explicit ChannelMapping (const AudioChannelSet& layout) + : ChannelMapping (layout, true) + { + } + + ChannelMapping (const AudioChannelSet& layout, bool activeIn) : indices (makeChannelIndices (layout)), - active (true) + active (activeIn) { } explicit ChannelMapping (const AudioProcessor::Bus& juceBus) - : indices (makeChannelIndices (juceBus.getLastEnabledLayout())), - active (juceBus.isEnabled()) + : ChannelMapping (juceBus.getLastEnabledLayout(), juceBus.isEnabled()) { } @@ -532,6 +543,270 @@ private: bool active = true; }; +//============================================================================== +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) +{ + 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 jmax (countUsedChannelsInVector (inputMap), countUsedChannelsInVector (outputMap)); +} + +/* + 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. + + An instance of this class handles input and output remapping for a single data type (float or + double), matching the FloatType template parameter. + + This is in VST3Common.h, rather than in the VST3_Wrapper.cpp, so that we can test it. + + @see ClientBufferMapper +*/ +template +class ClientBufferMapperData +{ +public: + void prepare (int numChannels, int blockSize) + { + emptyBuffer.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 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 + 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]); + + 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); + + if (auto* dest = channels[destIndex]) + FloatVectorOperations::copy (dest, busPtr[i], (int) data.numSamples); + else + channels[destIndex] = busPtr[i]; + } + + initialBusIndex += map.size(); + } + + return { channels.data(), (int) channels.size(), (int) data.numSamples }; + } + +private: + AudioBuffer clearOutputBuffersAndReturnBlankBuffer (Steinberg::Vst::ProcessData& data, int vstOutputs, int usedChannels) + { + // 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) + { + 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]; + + if (! map.isActive()) + return {}; + + auto** busPtr = getAudioBusPointer (detail::Tag{}, data.outputs[index]); + + std::vector result (map.size(), nullptr); + + for (auto i = 0; i < (int) map.size(); ++i) + result[(size_t) map.getJuceChannelForVst3Channel (i)] = busPtr[i]; + + return result; + } + + 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; }); + + if (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.isActive(); }); + } + + static bool validateLayouts (Steinberg::Vst::ProcessData& data, + int numInputs, + const std::vector& inputMap, + int numOutputs, + const std::vector& outputMap) + { + + // 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 validateLayouts (data.inputs, data.inputs + numInputs, inputMap) + && validateLayouts (data.outputs, data.outputs + numOutputs, outputMap); + } + + std::vector channels; + AudioBuffer emptyBuffer; +}; + +//============================================================================== +/* + Remaps a set of buffers provided by the VST3 host into an equivalent JUCE AudioBuffer using the + JUCE channel layout/order. + + An instance of this class can remap to either a float or double JUCE buffer, as necessary. + + Although the VST3 spec requires that the bus layout does not change while the plugin is + activated and processing, some hosts get this wrong and try to enable/disable buses during + playback. This class attempts to be resilient, and should cope with buses being switched on and + off during processing. + + This is in VST3Common.h, rather than in the VST3_Wrapper.cpp, so that we can test it. + + @see ClientBufferMapper +*/ +class ClientBufferMapper +{ +public: + void prepare (const AudioProcessor& processor, int blockSize) + { + struct Pair + { + 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)); + } + + const auto findMaxNumChannels = [&] (bool isInput) + { + auto sum = 0; + + for (auto i = 0; i < processor.getBusCount (isInput); ++i) + sum += processor.getBus (isInput, i)->getLastEnabledLayout().size(); + + return sum; + }; + + const auto numChannels = jmax (findMaxNumChannels (true), findMaxNumChannels (false)); + + floatData .prepare (numChannels, blockSize); + doubleData.prepare (numChannels, blockSize); + } + + void setInputBusActive (size_t bus, bool state) + { + if (bus < inputMap.size()) + inputMap[bus].setActive (state); + } + + void setOutputBusActive (size_t bus, bool state) + { + if (bus < outputMap.size()) + outputMap[bus].setActive (state); + } + + template + AudioBuffer getJuceLayoutForVst3Buffer (detail::Tag, Steinberg::Vst::ProcessData& data) + { + return getData (detail::Tag{}).getMappedBuffer (data, inputMap, outputMap); + } + +private: + auto& getData (detail::Tag) { return floatData; } + auto& getData (detail::Tag) { return doubleData; } + + ClientBufferMapperData floatData; + ClientBufferMapperData doubleData; + + std::vector inputMap; + std::vector outputMap; +}; //============================================================================== /*