💥 Reimplement VST3 audio processing

In the same way as 50c25c1cf0 did it for
VST2 plugins. Input and output audio data is now stored in a shared
memory buffer instead of being sent over the sockets. This reduces the
bridging overhead to a minimum since copying data was the most expensive
operation we were doing and we now only need to copy the entire buffer
once per processing cycle.
This commit is contained in:
Robbert van der Helm
2021-06-11 13:56:42 +02:00
parent a7d8063db4
commit dec19dc12a
8 changed files with 444 additions and 363 deletions
+106 -166
View File
@@ -18,113 +18,6 @@
#include "src/common/utils.h"
YaAudioBusBuffers::YaAudioBusBuffers() noexcept {}
void YaAudioBusBuffers::clear(int32 sample_size,
size_t num_samples,
size_t num_channels) {
auto do_clear = [&]<typename T>(T) {
if (!std::holds_alternative<std::vector<std::vector<T>>>(buffers)) {
buffers.emplace<std::vector<std::vector<T>>>();
}
std::vector<std::vector<T>>& vector_buffers =
std::get<std::vector<std::vector<T>>>(buffers);
vector_buffers.resize(num_channels);
for (size_t i = 0; i < vector_buffers.size(); i++) {
vector_buffers[i].resize(num_samples);
}
};
if (sample_size == Steinberg::Vst::SymbolicSampleSizes::kSample64) {
// XXX: Clangd doesn't let you specify template parameters for templated
// lambdas. This argument should get optimized out
do_clear(double());
} else {
do_clear(float());
}
}
void YaAudioBusBuffers::repopulate(
int32 sample_size,
int32 num_samples,
const Steinberg::Vst::AudioBusBuffers& data) {
silence_flags = data.silenceFlags;
auto do_repopuldate = [&]<typename T>(T** original_buffer) {
if (!std::holds_alternative<std::vector<std::vector<T>>>(buffers)) {
buffers.emplace<std::vector<std::vector<T>>>();
}
std::vector<std::vector<T>>& vector_buffers =
std::get<std::vector<std::vector<T>>>(buffers);
vector_buffers.resize(data.numChannels);
for (int channel = 0; channel < data.numChannels; channel++) {
vector_buffers[channel].assign(
&original_buffer[channel][0],
&original_buffer[channel][num_samples]);
}
};
if (sample_size == Steinberg::Vst::kSample64) {
do_repopuldate(data.channelBuffers64);
} else {
// I don't think they'll add any other sample sizes any time soon
do_repopuldate(data.channelBuffers32);
}
}
void YaAudioBusBuffers::reconstruct(
Steinberg::Vst::AudioBusBuffers& reconstructed_buffers) {
// We'll update the `AudioBusBuffers` object in place to point to our new
// data
reconstructed_buffers.silenceFlags = silence_flags;
std::visit(
[&]<typename T>(std::vector<std::vector<T>>& buffers) {
buffer_pointers.resize(buffers.size());
for (size_t i = 0; i < buffers.size(); i++) {
buffer_pointers[i] = buffers[i].data();
}
reconstructed_buffers.numChannels =
static_cast<int32>(buffers.size());
if constexpr (std::is_same_v<T, double>) {
reconstructed_buffers.channelBuffers64 =
reinterpret_cast<T**>(buffer_pointers.data());
} else {
reconstructed_buffers.channelBuffers32 =
reinterpret_cast<T**>(buffer_pointers.data());
}
},
buffers);
}
size_t YaAudioBusBuffers::num_channels() const {
return std::visit([&](const auto& buffers) { return buffers.size(); },
buffers);
}
void YaAudioBusBuffers::write_back_outputs(
Steinberg::Vst::AudioBusBuffers& output_buffers) const {
output_buffers.silenceFlags = silence_flags;
std::visit(
[&]<typename T>(const std::vector<std::vector<T>>& buffers) {
for (int channel = 0; channel < output_buffers.numChannels;
channel++) {
if constexpr (std::is_same_v<T, double>) {
std::copy(buffers[channel].begin(), buffers[channel].end(),
output_buffers.channelBuffers64[channel]);
} else {
std::copy(buffers[channel].begin(), buffers[channel].end(),
output_buffers.channelBuffers32[channel]);
}
}
},
buffers);
}
YaProcessData::YaProcessData() noexcept
// This response object acts as an optimization. It stores pointers to the
// original fields in our objects, so we can both only serialize those
@@ -142,8 +35,8 @@ YaProcessData::YaProcessData() noexcept
// `create_response()` on the plugin side
reconstructed_process_data() {}
void YaProcessData::repopulate(
const Steinberg::Vst::ProcessData& process_data) {
void YaProcessData::repopulate(const Steinberg::Vst::ProcessData& process_data,
AudioShmBuffer& shared_audio_buffers) {
// In this function and in every function we call, we should be careful to
// not use `push_back`/`emplace_back` anywhere. Resizing vectors and
// modifying them in place performs much better because that avoids
@@ -152,19 +45,45 @@ void YaProcessData::repopulate(
symbolic_sample_size = process_data.symbolicSampleSize;
num_samples = process_data.numSamples;
// We'll make sure to not do any allocations here after the first processing
// cycle
// The actual audio is stored in an accompanying `AudioShmBuffer` object, so
// these inputs and outputs objects are only used to serialize metadata
// about the input and output audio bus buffers
inputs.resize(process_data.numInputs);
for (int i = 0; i < process_data.numInputs; i++) {
inputs[i].repopulate(symbolic_sample_size, num_samples,
process_data.inputs[i]);
for (int bus = 0; bus < process_data.numInputs; bus++) {
// NOTE: The host might provide more input channels than what the plugin
// asked for. Carla does this for some reason. We should just
// ignore these.
inputs[bus].numChannels = std::min(
static_cast<int32>(shared_audio_buffers.num_input_channels(bus)),
process_data.inputs[bus].numChannels);
inputs[bus].silenceFlags = process_data.inputs[bus].silenceFlags;
// We copy the actual input audio for every bus to the shared memory
// object
for (int channel = 0; channel < inputs[bus].numChannels; channel++) {
if (process_data.symbolicSampleSize == Steinberg::Vst::kSample64) {
std::copy_n(process_data.inputs[bus].channelBuffers64[channel],
process_data.numSamples,
shared_audio_buffers.input_channel_ptr<double>(
bus, channel));
} else {
std::copy_n(process_data.inputs[bus].channelBuffers32[channel],
process_data.numSamples,
shared_audio_buffers.input_channel_ptr<float>(
bus, channel));
}
}
}
// We only store how many channels ouch output has so we can recreate the
// objects on the Wine side
outputs_num_channels.resize(process_data.numOutputs);
for (int i = 0; i < process_data.numOutputs; i++) {
outputs_num_channels[i] = process_data.outputs[i].numChannels;
outputs.resize(process_data.numOutputs);
for (int bus = 0; bus < process_data.numOutputs; bus++) {
// NOTE: The host might provide more output channels than what the
// plugin asked for. Carla does this for some reason. We should
// just ignore these.
outputs[bus].numChannels = std::min(
static_cast<int32>(shared_audio_buffers.num_output_channels(bus)),
process_data.outputs[bus].numChannels);
outputs[bus].silenceFlags = process_data.outputs[bus].silenceFlags;
}
// Even though `ProcessData::inputParamterChanges` is mandatory, the VST3
@@ -175,7 +94,15 @@ void YaProcessData::repopulate(
input_parameter_changes.clear();
}
output_parameter_changes_supported = process_data.outputParameterChanges;
// The existence of the output parameter changes object indicates whether or
// not the host provides this for the plugin
if (process_data.outputParameterChanges) {
if (!output_parameter_changes) {
output_parameter_changes.emplace();
}
} else {
output_parameter_changes.reset();
}
if (process_data.inputEvents) {
if (!input_events) {
@@ -186,7 +113,14 @@ void YaProcessData::repopulate(
input_events.reset();
}
output_events_supported = process_data.outputEvents;
// Same for the output events
if (process_data.outputEvents) {
if (!output_events) {
output_events.emplace();
}
} else {
output_events.reset();
}
if (process_data.processContext) {
process_context.emplace(*process_data.processContext);
@@ -195,43 +129,40 @@ void YaProcessData::repopulate(
}
}
Steinberg::Vst::ProcessData& YaProcessData::reconstruct() {
Steinberg::Vst::ProcessData& YaProcessData::reconstruct(
std::vector<std::vector<void*>>& input_pointers,
std::vector<std::vector<void*>>& output_pointers) {
reconstructed_process_data.processMode = process_mode;
reconstructed_process_data.symbolicSampleSize = symbolic_sample_size;
reconstructed_process_data.numSamples = num_samples;
reconstructed_process_data.numInputs = static_cast<int32>(inputs.size());
reconstructed_process_data.numOutputs =
static_cast<int32>(outputs_num_channels.size());
reconstructed_process_data.numOutputs = static_cast<int32>(outputs.size());
// We'll have to transform our `YaAudioBusBuffers` objects into an array of
// `AudioBusBuffers` object so the plugin can deal with them. These objects
// contain pointers to those original objects and thus don't store any
// buffer data themselves.
inputs_audio_bus_buffers.resize(inputs.size());
for (size_t i = 0; i < inputs.size(); i++) {
inputs[i].reconstruct(inputs_audio_bus_buffers[i]);
// The actual audio data is contained within a shared memory object, and the
// input and output pointers point to regions in that object. These pointers
// are calculated while handling `IAudioProcessor::setupProcessing()`.
// NOTE: The 32-bit and 64-bit audio pointers are a union, and since this is
// a raw memory buffer we can set either `channelBuffers32` or
// `channelBuffers64` to point at that buffer as long as we do the
// same thing on both the native plugin side and on the Wine plugin
// host
assert(inputs.size() <= input_pointers.size() &&
outputs.size() <= output_pointers.size());
for (size_t bus = 0; bus < inputs.size(); bus++) {
inputs[bus].channelBuffers32 =
reinterpret_cast<float**>(input_pointers[bus].data());
}
for (size_t bus = 0; bus < outputs.size(); bus++) {
outputs[bus].channelBuffers32 =
reinterpret_cast<float**>(output_pointers[bus].data());
}
reconstructed_process_data.inputs = inputs_audio_bus_buffers.data();
reconstructed_process_data.inputs = inputs.data();
reconstructed_process_data.outputs = outputs.data();
// We'll do the same with with the outputs, but we'll first have to
// initialize zeroed out buffers for the plugin to work with since we didn't
// serialize those directly
outputs.resize(outputs_num_channels.size());
outputs_audio_bus_buffers.resize(outputs_num_channels.size());
for (size_t i = 0; i < outputs_num_channels.size(); i++) {
outputs[i].clear(symbolic_sample_size, num_samples,
outputs_num_channels[i]);
outputs[i].reconstruct(outputs_audio_bus_buffers[i]);
}
reconstructed_process_data.outputs = outputs_audio_bus_buffers.data();
reconstructed_process_data.inputParameterChanges = &input_parameter_changes;
if (output_parameter_changes_supported) {
if (!output_parameter_changes) {
output_parameter_changes.emplace();
}
if (output_parameter_changes) {
output_parameter_changes->clear();
reconstructed_process_data.outputParameterChanges =
&*output_parameter_changes;
@@ -245,10 +176,7 @@ Steinberg::Vst::ProcessData& YaProcessData::reconstruct() {
reconstructed_process_data.inputEvents = nullptr;
}
if (output_events_supported) {
if (!output_events) {
output_events.emplace();
}
if (output_events) {
output_events->clear();
reconstructed_process_data.outputEvents = &*output_events;
} else {
@@ -265,27 +193,39 @@ Steinberg::Vst::ProcessData& YaProcessData::reconstruct() {
}
YaProcessData::Response& YaProcessData::create_response() noexcept {
// NOTE: We _have_ to manually copy over the silence flags from the
// `ProcessData` object generated in `get()` here sicne these of
// course are not references or pointers like all other fields, so
// they're not implicitly copied like all of our other fields
//
// On the plugin side this is not necessary, but it also doesn't hurt
for (int i = 0; i < reconstructed_process_data.numOutputs; i++) {
outputs[i].silence_flags =
reconstructed_process_data.outputs[i].silenceFlags;
}
// NOTE: We return an object that only contains references to these original
// fields to avoid any copies or moves
return response_object;
}
void YaProcessData::write_back_outputs(
Steinberg::Vst::ProcessData& process_data) {
Steinberg::Vst::ProcessData& process_data,
const AudioShmBuffer& shared_audio_buffers) {
assert(static_cast<int32>(outputs.size()) == process_data.numOutputs);
for (int i = 0; i < process_data.numOutputs; i++) {
outputs[i].write_back_outputs(process_data.outputs[i]);
for (int bus = 0; bus < process_data.numOutputs; bus++) {
process_data.outputs[bus].silenceFlags = outputs[bus].silenceFlags;
// NOTE: Some hosts, like Carla, provide more output channels than what
// the plugin wants. We'll have already capped
// `outputs[bus].numChannels` to the number of channels requested
// by the plugin during `YaProcessData::repopulate()`.
for (int channel = 0; channel < outputs[bus].numChannels; channel++) {
// We copy the output audio for every bus from the shared memory
// object back to the buffer provided by the host
if (process_data.symbolicSampleSize == Steinberg::Vst::kSample64) {
std::copy_n(
shared_audio_buffers.output_channel_ptr<double>(bus,
channel),
process_data.numSamples,
process_data.outputs[bus].channelBuffers64[channel]);
} else {
std::copy_n(
shared_audio_buffers.output_channel_ptr<float>(bus,
channel),
process_data.numSamples,
process_data.outputs[bus].channelBuffers32[channel]);
}
}
}
if (output_parameter_changes && process_data.outputParameterChanges) {