Browse Source

VST3 Client: Properly map between VST3 and JUCE layouts

pull/22/head
reuk 3 years ago
parent
commit
bb2b36a253
No known key found for this signature in database GPG Key ID: FCB43929F012EE5C
2 changed files with 307 additions and 203 deletions
  1. +27
    -198
      modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
  2. +280
    -5
      modules/juce_audio_processors/format_types/juce_VST3Common.h

+ 27
- 198
modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp View File

@@ -2186,18 +2186,6 @@ private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceVST3EditController)
};
namespace
{
template <typename FloatType> struct AudioBusPointerHelper {};
template <> struct AudioBusPointerHelper<float> { static float** impl (Vst::AudioBusBuffers& data) noexcept { return data.channelBuffers32; } };
template <> struct AudioBusPointerHelper<double> { static double** impl (Vst::AudioBusBuffers& data) noexcept { return data.channelBuffers64; } };
template <typename FloatType> struct ChooseBufferHelper {};
template <> struct ChooseBufferHelper<float> { static AudioBuffer<float>& impl (AudioBuffer<float>& f, AudioBuffer<double>& ) noexcept { return f; } };
template <> struct ChooseBufferHelper<double> { static AudioBuffer<double>& impl (AudioBuffer<float>& , AudioBuffer<double>& 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<float> (data, channelListFloat);
else if (processSetup.symbolicSampleSize == Vst::kSample64) processAudio<double> (data, channelListDouble);
if (processSetup.symbolicSampleSize == Vst::kSample32) processAudio<float> (data);
else if (processSetup.symbolicSampleSize == Vst::kSample64) processAudio<double> (data);
else jassertfalse;
}
@@ -3251,131 +3244,11 @@ private:
//==============================================================================
template <typename FloatType>
void processAudio (Vst::ProcessData& data, Array<FloatType*>& 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<FloatType> (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<FloatType> (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<FloatType> (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<FloatType> (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<FloatType> (totalInputChans, data.numSamples))
{
tmpBufferNeedsClearing = true;
channelList.set (totalInputChans++, tmpBuffer);
}
else
return;
}
}
}
}
if (tmpBufferNeedsClearing)
ChooseBufferHelper<FloatType>::impl (emptyBufferFloat, emptyBufferDouble).clear();
AudioBuffer<FloatType> buffer;
if (int totalChans = jmax (totalOutputChans, totalInputChans))
buffer.setDataToReferTo (channelList.getRawDataPointer(), totalChans, (int) data.numSamples);
auto buffer = bufferMapper.getJuceLayoutForVst3Buffer (detail::Tag<FloatType>{}, 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 <typename FloatType>
void allocateChannelListAndBuffers (Array<FloatType*>& channelList, AudioBuffer<FloatType>& buffer)
{
channelList.clearQuick();
channelList.insertMultiple (0, nullptr, 128);
auto& p = getPluginInstance();
buffer.setSize (jmax (p.getTotalNumInputChannels(), p.getTotalNumOutputChannels()), p.getBlockSize() * 4);
buffer.clear();
}
template <typename FloatType>
void deallocateChannelListAndBuffers (Array<FloatType*>& channelList, AudioBuffer<FloatType>& buffer)
{
channelList.clearQuick();
channelList.resize (0);
buffer.setSize (0, 0);
}
template <typename FloatType>
static FloatType** getPointerForAudioBus (Vst::AudioBusBuffers& data) noexcept
{
return AudioBusPointerHelper<FloatType>::impl (data);
}
template <typename FloatType>
FloatType* getTmpBufferForChannel (int channel, int numSamples) noexcept
{
auto& buffer = ChooseBufferHelper<FloatType>::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<float*> channelListFloat;
Array<double*> channelListDouble;
AudioBuffer<float> emptyBufferFloat;
AudioBuffer<double> emptyBufferDouble;
ClientBufferMapper bufferMapper;
bool active = false;


+ 280
- 5
modules/juce_audio_processors/format_types/juce_VST3Common.h View File

@@ -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<float>, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers32; }
inline auto& getAudioBusPointer (detail::Tag<double>, Steinberg::Vst::AudioBusBuffers& data) { return data.channelBuffers64; }
static inline int countUsedChannels (const std::vector<ChannelMapping>& inputMap,
const std::vector<ChannelMapping>& outputMap)
{
const auto countUsedChannelsInVector = [] (const std::vector<ChannelMapping>& 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 <typename FloatType>
class ClientBufferMapperData
{
public:
void prepare (int numChannels, int blockSize)
{
emptyBuffer.setSize (numChannels, blockSize);
channels.reserve ((size_t) jmin (128, numChannels));
}
AudioBuffer<FloatType> getMappedBuffer (Steinberg::Vst::ProcessData& data,
const std::vector<ChannelMapping>& inputMap,
const std::vector<ChannelMapping>& outputMap)
{
const auto usedChannels = countUsedChannels (inputMap, outputMap);
// WaveLab workaround: This host may report the wrong number of inputs/outputs so re-count here
const auto countValidBuses = [] (Steinberg::Vst::AudioBusBuffers* buffers, int32 num)
{
return int (std::distance (buffers, std::find_if (buffers, buffers + num, [] (Steinberg::Vst::AudioBusBuffers& buf)
{
return getAudioBusPointer (detail::Tag<FloatType>{}, buf) == nullptr && buf.numChannels > 0;
})));
};
const auto vstInputs = countValidBuses (data.inputs, data.numInputs);
const auto vstOutputs = countValidBuses (data.outputs, data.numOutputs);
if (! validateLayouts (data, vstInputs, inputMap, vstOutputs, outputMap))
return clearOutputBuffersAndReturnBlankBuffer (data, vstOutputs, usedChannels);
// If we're here, then we know that the host has given us a usable layout
channels.clear();
// Put the host-supplied output channel pointers into JUCE order
for (size_t i = 0; i < (size_t) vstOutputs; ++i)
{
const auto bus = getMappedOutputBus (data, outputMap, i);
channels.insert (channels.end(), bus.begin(), bus.end());
}
// For input channels that are < the total number of outputs channels, copy the input over
// the output buffer, at the appropriate JUCE channel index.
// For input channels that are >= the total number of output channels, add the input buffer
// pointer to the array of channel pointers.
for (size_t inputBus = 0, initialBusIndex = 0; inputBus < (size_t) vstInputs; ++inputBus)
{
const auto& map = inputMap[inputBus];
if (! map.isActive())
continue;
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, data.inputs[inputBus]);
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<FloatType> 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<FloatType>{}, bus);
std::for_each (busPtr, busPtr + bus.numChannels, [&data] (auto* ptr)
{
if (ptr != nullptr)
FloatVectorOperations::clear (ptr, (int) data.numSamples);
});
});
// Return a silent buffer for the AudioProcessor to process
emptyBuffer.clear();
return { emptyBuffer.getArrayOfWritePointers(),
jmin (emptyBuffer.getNumChannels(), usedChannels),
data.numSamples };
}
std::vector<FloatType*> getMappedOutputBus (Steinberg::Vst::ProcessData& data,
const std::vector<ChannelMapping>& maps,
size_t index) const
{
const auto& map = maps[index];
if (! map.isActive())
return {};
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, data.outputs[index]);
std::vector<FloatType*> result (map.size(), nullptr);
for (auto i = 0; i < (int) map.size(); ++i)
result[(size_t) map.getJuceChannelForVst3Channel (i)] = busPtr[i];
return result;
}
template <typename Iterator>
static bool validateLayouts (Iterator first, Iterator last, const std::vector<ChannelMapping>& map)
{
if ((size_t) std::distance (first, last) > map.size())
return false;
auto mapIterator = map.begin();
for (auto it = first; it != last; ++it, ++mapIterator)
{
auto** busPtr = getAudioBusPointer (detail::Tag<FloatType>{}, *it);
const auto anyChannelIsNull = std::any_of (busPtr, busPtr + it->numChannels, [] (auto* ptr) { return ptr == nullptr; });
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<ChannelMapping>& inputMap,
int numOutputs,
const std::vector<ChannelMapping>& 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<FloatType*> channels;
AudioBuffer<FloatType> 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<ChannelMapping>& 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 <typename FloatType>
AudioBuffer<FloatType> getJuceLayoutForVst3Buffer (detail::Tag<FloatType>, Steinberg::Vst::ProcessData& data)
{
return getData (detail::Tag<FloatType>{}).getMappedBuffer (data, inputMap, outputMap);
}
private:
auto& getData (detail::Tag<float>) { return floatData; }
auto& getData (detail::Tag<double>) { return doubleData; }
ClientBufferMapperData<float> floatData;
ClientBufferMapperData<double> doubleData;
std::vector<ChannelMapping> inputMap;
std::vector<ChannelMapping> outputMap;
};
//==============================================================================
/*


Loading…
Cancel
Save