mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-08 04:20:13 +02:00
💥 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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user