Add support for double precision audio #34

So far I've only seen REAPER running iZotope Rx plugins utilize this.
This commit is contained in:
Robbert van der Helm
2020-08-24 16:12:24 +02:00
parent e14a5d4895
commit 8198a73742
9 changed files with 210 additions and 70 deletions
+22 -4
View File
@@ -551,12 +551,18 @@ struct ParameterResult {
/**
* A buffer of audio for the plugin to process, or the response of that
* processing. The number of samples is encoded in each audio buffer's length.
* This is used for both `process()/processReplacing()` and
* `processDoubleReplacing()`.
*/
struct AudioBuffers {
/**
* An audio buffer for each of the plugin's audio channels.
* An audio buffer for each of the plugin's audio channels. This uses floats
* or doubles depending on whether `process()/processReplacing()` or
* `processDoubleReplacing()` got called.
*/
std::vector<std::vector<float>> buffers;
std::variant<std::vector<std::vector<float>>,
std::vector<std::vector<double>>>
buffers;
/**
* The number of frames in a sample. If buffers is not empty, then
@@ -566,8 +572,20 @@ struct AudioBuffers {
template <typename S>
void serialize(S& s) {
s.container(buffers, max_audio_channels,
[](S& s, auto& v) { s.container4b(v, max_buffer_size); });
s.ext(
buffers,
bitsery::ext::StdVariant{
[](S& s, std::vector<std::vector<float>>& buffer) {
s.container(buffer, max_audio_channels, [](S& s, auto& v) {
s.container4b(v, max_buffer_size);
});
},
[](S& s, std::vector<std::vector<double>>& buffer) {
s.container(buffer, max_audio_channels, [](S& s, auto& v) {
s.container8b(v, max_buffer_size);
});
},
});
s.value4b(sample_frames);
}
};
+9
View File
@@ -298,6 +298,15 @@ class AEffect {
int version;
// processReplacing 50-53
void(VST_CALL_CONV* processReplacing)(AEffect*, float**, float**, int);
// Found at
// https://git.iem.at/zmoelnig/VeSTige/-/blob/b0e67183e155fec32dd85a2c7b5c2e4b58407323/vestige.h#L323
// The offset was also found based on a segfualt in REAPER's audio audio
// engine when it tried to call this function for the Rx7 plugins when
// yabridge did not yet implement it
void(VST_CALL_CONV* processDoubleReplacing)(AEffect*,
double**,
double**,
int);
};
class VstTimeInfo {
+43 -15
View File
@@ -33,8 +33,9 @@ namespace fs = boost::filesystem;
intptr_t dispatch_proxy(AEffect*, int, int, intptr_t, void*, float);
void process_proxy(AEffect*, float**, float**, int);
void process_replacing_proxy(AEffect*, float**, float**, int);
void setParameter_proxy(AEffect*, int, float);
float getParameter_proxy(AEffect*, int);
void process_double_replacing_proxy(AEffect*, double**, double**, int);
void set_parameter_proxy(AEffect*, int, float);
float get_parameter_proxy(AEffect*, int);
/**
* Fetch the bridge instance stored in an unused pointer from a VST plugin. This
@@ -130,9 +131,10 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
plugin.ptr3 = this;
plugin.dispatcher = dispatch_proxy;
plugin.process = process_proxy;
plugin.setParameter = setParameter_proxy;
plugin.getParameter = getParameter_proxy;
plugin.setParameter = set_parameter_proxy;
plugin.getParameter = get_parameter_proxy;
plugin.processReplacing = process_replacing_proxy;
plugin.processDoubleReplacing = process_double_replacing_proxy;
// For our communication we use simple threads and blocking operations
// instead of asynchronous IO since communication has to be handled in
@@ -512,14 +514,12 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
value, data, option);
}
void PluginBridge::process_replacing(AEffect* /*plugin*/,
float** inputs,
float** outputs,
int sample_frames) {
template <typename T>
void PluginBridge::do_process(T** inputs, T** outputs, int sample_frames) {
// The inputs and outputs arrays should be `[num_inputs][sample_frames]` and
// `[num_outputs][sample_frames]` floats large respectfully.
std::vector<std::vector<float>> input_buffers(
plugin.numInputs, std::vector<float>(sample_frames));
std::vector<std::vector<T>> input_buffers(plugin.numInputs,
std::vector<T>(sample_frames));
for (int channel = 0; channel < plugin.numInputs; channel++) {
std::copy(inputs[channel], inputs[channel] + sample_frames,
input_buffers[channel].begin());
@@ -531,11 +531,13 @@ void PluginBridge::process_replacing(AEffect* /*plugin*/,
// Write the results back to the `outputs` arrays
const auto response =
read_object<AudioBuffers>(host_vst_process_replacing, process_buffer);
const auto& response_buffers =
std::get<std::vector<std::vector<T>>>(response.buffers);
assert(response.buffers.size() == static_cast<size_t>(plugin.numOutputs));
assert(response_buffers.size() == static_cast<size_t>(plugin.numOutputs));
for (int channel = 0; channel < plugin.numOutputs; channel++) {
std::copy(response.buffers[channel].begin(),
response.buffers[channel].end(), outputs[channel]);
std::copy(response_buffers[channel].begin(),
response_buffers[channel].end(), outputs[channel]);
}
// Plugins are allowed to send MIDI events during processing using a host
@@ -553,6 +555,20 @@ void PluginBridge::process_replacing(AEffect* /*plugin*/,
incoming_midi_events.clear();
}
void PluginBridge::process_replacing(AEffect* /*plugin*/,
float** inputs,
float** outputs,
int sample_frames) {
do_process<float>(inputs, outputs, sample_frames);
}
void PluginBridge::process_double_replacing(AEffect* /*plugin*/,
double** inputs,
double** outputs,
int sample_frames) {
do_process<double>(inputs, outputs, sample_frames);
}
float PluginBridge::get_parameter(AEffect* /*plugin*/, int index) {
logger.log_get_parameter(index);
@@ -691,6 +707,10 @@ void process_proxy(AEffect* plugin,
float** inputs,
float** outputs,
int sample_frames) {
// FIXME: This is incorrect, and I only noticed just now. I'm 99% sure no
// hosts actually use this, but this will overwrite the buffer. On
// the plugin side we do properly handle plugins that only support
// the old cumulative process function.
return get_bridge_instance(*plugin).process_replacing(
plugin, inputs, outputs, sample_frames);
}
@@ -703,10 +723,18 @@ void process_replacing_proxy(AEffect* plugin,
plugin, inputs, outputs, sample_frames);
}
void setParameter_proxy(AEffect* plugin, int index, float value) {
void process_double_replacing_proxy(AEffect* plugin,
double** inputs,
double** outputs,
int sample_frames) {
return get_bridge_instance(*plugin).process_double_replacing(
plugin, inputs, outputs, sample_frames);
}
void set_parameter_proxy(AEffect* plugin, int index, float value) {
return get_bridge_instance(*plugin).set_parameter(plugin, index, value);
}
float getParameter_proxy(AEffect* plugin, int index) {
float get_parameter_proxy(AEffect* plugin, int index) {
return get_bridge_instance(*plugin).get_parameter(plugin, index);
}
+26 -2
View File
@@ -69,9 +69,33 @@ class PluginBridge {
float** inputs,
float** outputs,
int sample_frames);
/**
* The same as `PluginBridge::process_replacing`, but for double precision
* audio. Support for this on both the plugin and host side is pretty rare,
* but REAPER supports it. This reuses the same infrastructure as
* `process_replacing` is using since the host will only call one or the
* other.
*/
void process_double_replacing(AEffect* plugin,
double** inputs,
double** outputs,
int sample_frames);
float get_parameter(AEffect* plugin, int index);
void set_parameter(AEffect* plugin, int index, float value);
/**
* Process audio and handle plugin-generated MIDI events afterwards.
*
* @tparam T The sample type. Should be either `float` for single preceision
* audio processing called through `processReplacing`, or `double` for
* double precision audio through `processDoubleReplacing`.
*
* @see PluginBridge::process_replacing
* @see PluginBridge::process_double_replacing
*/
template <typename T>
void do_process(T** inputs, T** outputs, int sample_frames);
/**
* The configuration for this instance of yabridge. Set based on the values
* from a `yabridge.toml`, if it exists.
@@ -198,8 +222,8 @@ class PluginBridge {
std::jthread wine_io_handler;
/**
* A scratch buffer for sending and receiving data during `process` and
* `processReplacing` calls.
* A scratch buffer for sending and receiving data during `process`,
* `processReplacing` and `processDoubleReplacing` calls.
*/
std::vector<uint8_t> process_buffer;
+88 -41
View File
@@ -278,60 +278,107 @@ void Vst2Bridge::handle_parameters() {
}
void Vst2Bridge::handle_process_replacing() {
std::vector<std::vector<float>> output_buffers(plugin->numOutputs);
// These are used as scratch buffers to prevent unnecessary allocations.
// Since don't know in advance whether the host will call `processReplacing`
// or `processDoubleReplacing` we'll just create both.
std::vector<std::vector<float>> output_buffers_single_precision(
plugin->numOutputs);
std::vector<std::vector<double>> output_buffers_double_precision(
plugin->numOutputs);
while (true) {
try {
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
process_buffer);
// The process functions expect a `float**` for their inputs and
// their outputs
std::vector<float*> inputs;
for (auto& buffer : request.buffers) {
inputs.push_back(buffer.data());
}
// We reuse the buffers to avoid some unnecessary heap allocations,
// so we need to make sure the buffers are large enough since
// plugins can change their output configuration
std::vector<float*> outputs;
output_buffers.resize(plugin->numOutputs);
for (auto& buffer : output_buffers) {
buffer.resize(request.sample_frames);
outputs.push_back(buffer.data());
}
// Let the plugin process the MIDI events that were received since
// the last buffer, and then clean up those events. This approach
// should not be needed but Kontakt only stores pointers to rather
// than copies of the events.
{
std::lock_guard lock(next_buffer_midi_events_mutex);
std::lock_guard lock(next_buffer_midi_events_mutex);
// Any plugin made in the last fifteen years or so should
// support `processReplacing`. In the off chance it does not we
// can just emulate this behavior ourselves.
if (plugin->processReplacing) {
plugin->processReplacing(plugin, inputs.data(),
outputs.data(),
request.sample_frames);
} else {
// If we zero out this buffer then the behavior is the same
// as `processReplacing``
for (std::vector<float>& buffer : output_buffers) {
std::fill(buffer.begin(), buffer.end(), 0.0);
}
// Since the host should only be calling one of `process()`,
// processReplacing()` or `processDoubleReplacing()`, we can all
// handle them over the same socket. We pick which one to call
// depending on the type of data we got sent and the plugin's
// reported support for these functions.
std::visit(
overload{
[&](std::vector<std::vector<float>>& input_buffers) {
// The process functions expect a `float**` for their
// inputs and their outputs
std::vector<float*> inputs;
for (auto& buffer : input_buffers) {
inputs.push_back(buffer.data());
}
plugin->process(plugin, inputs.data(), outputs.data(),
request.sample_frames);
}
// We reuse the buffers to avoid some unnecessary heap
// allocations, so we need to make sure the buffers are
// large enough since plugins can change their output
// configuration. The type we're using here (single
// precision floats vs double precisioon doubles) should
// be the same as the one 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());
}
next_audio_buffer_midi_events.clear();
}
// Any plugin made in the last fifteen years or so
// should support `processReplacing`. In the off chance
// it does not we can just emulate this behavior
// ourselves.
if (plugin->processReplacing) {
plugin->processReplacing(plugin, inputs.data(),
outputs.data(),
request.sample_frames);
} else {
// If we zero out this buffer then the behavior is
// the same as `processReplacing``
for (std::vector<float>& buffer :
output_buffers_single_precision) {
std::fill(buffer.begin(), buffer.end(), 0.0);
}
AudioBuffers response{output_buffers, request.sample_frames};
write_object(host_vst_process_replacing, response, process_buffer);
plugin->process(plugin, inputs.data(),
outputs.data(),
request.sample_frames);
}
AudioBuffers response{output_buffers_single_precision,
request.sample_frames};
write_object(host_vst_process_replacing, response,
process_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, inputs.data(),
outputs.data(),
request.sample_frames);
AudioBuffers response{output_buffers_double_precision,
request.sample_frames};
write_object(host_vst_process_replacing, response,
process_buffer);
}},
request.buffers);
next_audio_buffer_midi_events.clear();
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this
// host application
+2 -1
View File
@@ -242,7 +242,8 @@ class Vst2Bridge {
*/
Win32Thread parameters_handler;
/**
* The thread that handles calls to `processReplacing` (and `process`).
* The thread that handles calls to `processReplacing` (and `process` as a
* fallback) and `processDoubleReplacing`.
*/
Win32Thread process_replacing_handler;