Reuse VST2 audio processing buffers on Wine side

Just like we made similar changes on the plugin side a few commits ago
to prevent allocations there.
This commit is contained in:
Robbert van der Helm
2021-05-23 16:36:41 +02:00
parent 206b528075
commit 29e0a0fd36
3 changed files with 130 additions and 131 deletions
+3 -2
View File
@@ -31,8 +31,9 @@ Versioning](https://semver.org/spec/v2.0.0.html).
objects on both sides. This greatly reduces the overhead of our VST3 bridging objects on both sides. This greatly reduces the overhead of our VST3 bridging
by getting rid of all memory allocations during audio processing. by getting rid of all memory allocations during audio processing.
- VST2 audio processing also received the same optimizations. In a few places - VST2 audio processing also received the same optimizations. In a few places
yabridge would still reallocate heap data during audio processing. We now make yabridge would still reallocate heap data during every audio processing cycle.
sure to always reuse all buffers used in the audio processing process. We now make sure to always reuse all buffers and heap data used in the audio
processing process.
- Considerably optimized both VST2 and VST3 audio processing by preventing - Considerably optimized both VST2 and VST3 audio processing by preventing
unnecessary memory operations. As it turns out, the underlying binary unnecessary memory operations. As it turns out, the underlying binary
serialization library used by yabridge would always reinitialize the type-safe serialization library used by yabridge would always reinitialize the type-safe
+90 -99
View File
@@ -206,38 +206,31 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context,
// they start producing denormals // they start producing denormals
ScopedFlushToZero ftz_guard; ScopedFlushToZero ftz_guard;
// These are used as scratch buffers to prevent unnecessary allocations. sockets.host_vst_process_replacing.receive_multi<
// Since don't know in advance whether the host will call AudioBuffers>([&](AudioBuffers& process_request,
// `processReplacing` or `processDoubleReplacing` we'll just create SerializationBufferBase& buffer) {
// both. // Since the value cannot change during this processing cycle, we'll
std::vector<std::vector<float>> output_buffers_single_precision( // send the current transport information as part of the request so
plugin->numOutputs); // we prefetch it to avoid unnecessary callbacks from the audio
std::vector<std::vector<double>> output_buffers_double_precision( // thread
plugin->numOutputs);
sockets.host_vst_process_replacing.receive_multi<AudioBuffers>(
[&](AudioBuffers& request, SerializationBufferBase& buffer) {
// Since the value cannot change during this processing cycle,
// we'll send the current transport information as part of the
// request so we prefetch it to avoid unnecessary callbacks from
// the audio thread
std::optional<decltype(time_info_cache)::Guard> std::optional<decltype(time_info_cache)::Guard>
time_info_cache_guard = time_info_cache_guard =
request.current_time_info process_request.current_time_info
? std::optional(time_info_cache.set( ? std::optional(time_info_cache.set(
*request.current_time_info)) *process_request.current_time_info))
: std::nullopt; : std::nullopt;
// We'll also prefetch the process level, since some plugins // We'll also prefetch the process level, since some plugins will
// will ask for this during every processing cycle // ask for this during every processing cycle
decltype(process_level_cache)::Guard process_level_cache_guard = decltype(process_level_cache)::Guard process_level_cache_guard =
process_level_cache.set(request.current_process_level); process_level_cache.set(process_request.current_process_level);
// As suggested by Jack Winter, we'll synchronize this thread's // As suggested by Jack Winter, we'll synchronize this thread's
// audio processing priority with that of the host's audio // audio processing priority with that of the host's audio thread
// thread every once in a while // every once in a while
if (request.new_realtime_priority) { if (process_request.new_realtime_priority) {
set_realtime_priority(true, *request.new_realtime_priority); set_realtime_priority(true,
*process_request.new_realtime_priority);
} }
// Let the plugin process the MIDI events that were received // Let the plugin process the MIDI events that were received
@@ -252,93 +245,91 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context,
// depending on the type of data we got sent and the plugin's // depending on the type of data we got sent and the plugin's
// reported support for these functions. // reported support for these functions.
std::visit( std::visit(
overload{ [&]<typename T>(
[&](std::vector<std::vector<float>>& input_buffers) { std::vector<std::vector<T>>& input_audio_buffers) {
// The process functions expect a `float**` for // The process functions expect a `T**` for their inputs
// their inputs and their outputs thread_local std::vector<T*> input_pointers{};
std::vector<float*> inputs; if (input_pointers.size() != input_audio_buffers.size()) {
for (auto& buffer : input_buffers) { input_pointers.resize(input_audio_buffers.size());
inputs.push_back(buffer.data()); for (size_t channel = 0;
channel < input_audio_buffers.size(); channel++) {
input_pointers[channel] =
input_audio_buffers[channel].data();
}
} }
// We reuse the buffers to avoid some unnecessary // We also reuse the output buffers to avoid some
// heap allocations, so we need to make sure the // unnecessary heap allocations
// buffers are large enough since plugins can change if (!std::holds_alternative<std::vector<std::vector<T>>>(
// their output configuration. The type we're using process_response.buffers)) {
// here (single precision floats vs double process_response.buffers
// precisioon doubles) should be the same as the one .emplace<std::vector<std::vector<T>>>();
// we're sending in our response.
std::vector<float*> outputs;
output_buffers_single_precision.resize(
plugin->numOutputs);
for (auto& buffer :
output_buffers_single_precision) {
buffer.resize(request.sample_frames);
outputs.push_back(buffer.data());
} }
std::vector<std::vector<T>>& output_audio_buffers =
std::get<std::vector<std::vector<T>>>(
process_response.buffers);
output_audio_buffers.resize(plugin->numOutputs);
for (size_t channel = 0;
channel < output_audio_buffers.size(); channel++) {
output_audio_buffers[channel].resize(
process_request.sample_frames);
}
// And the process functions also expect a `T**` for their
// outputs
thread_local std::vector<T*> output_pointers{};
if (output_pointers.size() != output_audio_buffers.size()) {
output_pointers.resize(output_audio_buffers.size());
for (size_t channel = 0;
channel < output_audio_buffers.size(); channel++) {
output_pointers[channel] =
output_audio_buffers[channel].data();
}
}
if constexpr (std::is_same_v<T, float>) {
// Any plugin made in the last fifteen years or so // Any plugin made in the last fifteen years or so
// should support `processReplacing`. In the off // should support `processReplacing`. In the off chance
// chance it does not we can just emulate this // it does not we can just emulate this behavior
// behavior ourselves. // ourselves.
if (plugin->processReplacing) { if (plugin->processReplacing) {
plugin->processReplacing(plugin, inputs.data(), plugin->processReplacing(
outputs.data(), plugin, input_pointers.data(),
request.sample_frames); output_pointers.data(),
process_request.sample_frames);
} else { } else {
// If we zero out this buffer then the behavior // If we zero out this buffer then the behavior is
// is the same as `processReplacing`` // the same as `processReplacing`
for (std::vector<float>& buffer : for (std::vector<T>& buffer :
output_buffers_single_precision) { output_audio_buffers) {
std::fill(buffer.begin(), buffer.end(), std::fill(buffer.begin(), buffer.end(), (T)0.0);
0.0);
} }
plugin->process(plugin, inputs.data(), plugin->process(plugin, input_pointers.data(),
outputs.data(), output_pointers.data(),
request.sample_frames); process_request.sample_frames);
} }
} else if (std::is_same_v<T, double>) {
AudioBuffers response{
.buffers = output_buffers_single_precision,
.sample_frames = request.sample_frames,
.current_time_info = std::nullopt,
.current_process_level = 0,
.new_realtime_priority = std::nullopt};
sockets.host_vst_process_replacing.send(response,
buffer);
},
[&](std::vector<std::vector<double>>& input_buffers) {
// Exactly the same as the above, but for double
// precision audio
std::vector<double*> inputs;
for (auto& buffer : input_buffers) {
inputs.push_back(buffer.data());
}
std::vector<double*> outputs;
output_buffers_double_precision.resize(
plugin->numOutputs);
for (auto& buffer :
output_buffers_double_precision) {
buffer.resize(request.sample_frames);
outputs.push_back(buffer.data());
}
plugin->processDoubleReplacing( plugin->processDoubleReplacing(
plugin, inputs.data(), outputs.data(), plugin, input_pointers.data(),
request.sample_frames); output_pointers.data(),
process_request.sample_frames);
} else {
static_assert(
std::is_same_v<T, float> ||
std::is_same_v<T, double>,
"Audio processing only works with single and "
"double precision floating point numbers");
}
},
process_request.buffers);
AudioBuffers response{ // We modified the buffers within the `process_response` object, so
.buffers = output_buffers_double_precision, // we can just send that object back. Like on the plugin side we
.sample_frames = request.sample_frames, // cannot reuse the request object because a plugin may have a
.current_time_info = std::nullopt, // different number of input and output channels
.current_process_level = 0, sockets.host_vst_process_replacing.send(process_response, buffer);
.new_realtime_priority = std::nullopt};
sockets.host_vst_process_replacing.send(response,
buffer);
}},
request.buffers);
// See the docstrong on `should_clear_midi_events` for why we // See the docstrong on `should_clear_midi_events` for why we
// don't just clear `next_buffer_midi_events` here // don't just clear `next_buffer_midi_events` here
+7
View File
@@ -107,6 +107,13 @@ class Vst2Bridge : public HostBridge {
*/ */
Configuration config; Configuration config;
/**
* The object we'll serialize the response into after the plugin has
* finished processing audio. We reuse this object to avoid reallocations
* since it contains pointers to heap data.
*/
AudioBuffers process_response;
/** /**
* We'll store the last transport information obtained from the host as a * We'll store the last transport information obtained from the host as a
* result of `audioMasterGetTime()` here so we can return a pointer to it if * result of `audioMasterGetTime()` here so we can return a pointer to it if