mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-06 19:40:10 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
// 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.
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
plugin->process(plugin, inputs.data(), outputs.data(),
|
||||
plugin->process(plugin, inputs.data(),
|
||||
outputs.data(),
|
||||
request.sample_frames);
|
||||
}
|
||||
|
||||
next_audio_buffer_midi_events.clear();
|
||||
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());
|
||||
}
|
||||
|
||||
AudioBuffers response{output_buffers, request.sample_frames};
|
||||
write_object(host_vst_process_replacing, response, process_buffer);
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user