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
+8
View File
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Added support for double precision audio processing. This is not very widely
used, but some plugins running under REAPER can make use of this. Without this
those plugins would cause REAPER's audio engine to segfault.
## [1.5.0] - 2020-08-21
### Added
+3 -1
View File
@@ -466,7 +466,9 @@ There are also some VST2.X extension features that have not been implemented yet
because I haven't needed them myself. Let me know if you need any of these
features for a certain plugin or VST host:
- Double precision audio (`processDoubleReplacing`).
- Double precision audio (`processDoubleReplacing`). **This is now implemented
on the master branch and will be part of the next release. Without this some
specific plugins will crash under REAPER.**
- SysEx messages. In addition to MIDI, VST 2.4 also supports SysEx. I don't know
of any hosts or plugins that use this, but please let me know if this is
needed for something.
+9 -6
View File
@@ -79,12 +79,15 @@ as the _Windows VST plugin_. The whole process works as follows:
plugin through the Wine VST host using a single socket because they're very
similar and don't need any complicated behaviour.
- Calls from the native VST host to the plugin's `processReplacing()`
function. This function gets forwarded to the Windows VST plugin through
the Wine VST. In the rare event that the plugin does not support
`processReplacing()` and only supports The deprecated commutative
`process()` function, then the Wine VST host will emulate the behavior of
`processReplacing()` instead.
- Calls from the native VST host to the plugin's `processReplacing()` and
`processDoubleReplacing()` functions. These functions get forwarded to the
Windows VST plugin through the Wine VST host. In the rare event that the
plugin does not support `processReplacing()` and only supports The
deprecated commutative `process()` function, then the Wine VST host will
emulate the behavior of `processReplacing()` instead. Single and double
precision audio go over the same socket since the host will only call one
or the other, and we just use a variant to determine which one should be
called on the Wine host side.
- And finally there's a separate socket for control messages. At the moment
this is only used to transfer the Windows VST plugin's `AEffect` object to
+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;