diff --git a/CHANGELOG.md b/CHANGELOG.md index 42380931..45d9d0b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 264c098f..97d71899 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md index 27f69cf9..580cf4c8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/src/common/serialization.h b/src/common/serialization.h index c82c0def..f915e766 100644 --- a/src/common/serialization.h +++ b/src/common/serialization.h @@ -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> buffers; + std::variant>, + std::vector>> + buffers; /** * The number of frames in a sample. If buffers is not empty, then @@ -566,8 +572,20 @@ struct AudioBuffers { template 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>& buffer) { + s.container(buffer, max_audio_channels, [](S& s, auto& v) { + s.container4b(v, max_buffer_size); + }); + }, + [](S& s, std::vector>& buffer) { + s.container(buffer, max_audio_channels, [](S& s, auto& v) { + s.container8b(v, max_buffer_size); + }); + }, + }); s.value4b(sample_frames); } }; diff --git a/src/include/vestige/aeffectx.h b/src/include/vestige/aeffectx.h index 0af00de2..5897c052 100644 --- a/src/include/vestige/aeffectx.h +++ b/src/include/vestige/aeffectx.h @@ -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 { diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/plugin-bridge.cpp index ca41315e..13e032a5 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/plugin-bridge.cpp @@ -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 +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> input_buffers( - plugin.numInputs, std::vector(sample_frames)); + std::vector> input_buffers(plugin.numInputs, + std::vector(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(host_vst_process_replacing, process_buffer); + const auto& response_buffers = + std::get>>(response.buffers); - assert(response.buffers.size() == static_cast(plugin.numOutputs)); + assert(response_buffers.size() == static_cast(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(inputs, outputs, sample_frames); +} + +void PluginBridge::process_double_replacing(AEffect* /*plugin*/, + double** inputs, + double** outputs, + int sample_frames) { + do_process(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); } diff --git a/src/plugin/plugin-bridge.h b/src/plugin/plugin-bridge.h index 77166ad8..fbac9cc2 100644 --- a/src/plugin/plugin-bridge.h +++ b/src/plugin/plugin-bridge.h @@ -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 + 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 process_buffer; diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index 44127ee6..e2647cb3 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -278,60 +278,107 @@ void Vst2Bridge::handle_parameters() { } void Vst2Bridge::handle_process_replacing() { - std::vector> 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> output_buffers_single_precision( + plugin->numOutputs); + std::vector> output_buffers_double_precision( + plugin->numOutputs); while (true) { try { auto request = read_object(host_vst_process_replacing, process_buffer); - - // The process functions expect a `float**` for their inputs and - // their outputs - std::vector 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 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& 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>& input_buffers) { + // The process functions expect a `float**` for their + // inputs and their outputs + std::vector 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 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& 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>& input_buffers) { + // Exactly the same as the above, but for double + // precision audio + std::vector inputs; + for (auto& buffer : input_buffers) { + inputs.push_back(buffer.data()); + } + + std::vector 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 diff --git a/src/wine-host/bridges/vst2.h b/src/wine-host/bridges/vst2.h index 3ecc98ed..3d0815af 100644 --- a/src/wine-host/bridges/vst2.h +++ b/src/wine-host/bridges/vst2.h @@ -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;