diff --git a/src/common/serialization/clap.h b/src/common/serialization/clap.h index 6f5bc45e..0a6a59b2 100644 --- a/src/common/serialization/clap.h +++ b/src/common/serialization/clap.h @@ -32,6 +32,7 @@ #include "clap/ext/tail.h" #include "clap/host.h" #include "clap/plugin-factory.h" +#include "clap/process.h" #include "common.h" // The CLAP communication strategy is identical to what we do for VST3. diff --git a/src/common/serialization/clap/process.cpp b/src/common/serialization/clap/process.cpp new file mode 100644 index 00000000..e0268ef9 --- /dev/null +++ b/src/common/serialization/clap/process.cpp @@ -0,0 +1,242 @@ +// yabridge: a Wine plugin bridge +// Copyright (C) 2020-2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more destates. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "process.h" + +namespace clap { +namespace process { + +Process::Process() noexcept {} + +void Process::repopulate(const clap_process_t& process, + AudioShmBuffer& shared_audio_buffers) { + assert(process.in_events && process.out_events); + if (process.audio_inputs_count > 0) { + assert(process.audio_inputs); + } + if (process.audio_outputs_count > 0) { + assert(process.audio_outputs); + } + + // In this function and in every function we call, we should be careful to + // not use `push_back`/`emplace_back` anywhere. Resizing vectors and + // modifying them in place performs much better because that avoids + // destroying and creating objects most of the time. + steady_time_ = process.steady_time; + frames_count_ = process.frames_count; + + if (process.transport) { + transport_.emplace(*process.transport); + } else { + transport_.reset(); + } + + // The actual audio is stored in an accompanying `AudioShmBuffer` object, so + // these inputs and outputs objects are only used to serialize metadata + // about the input and output audio bus buffers + audio_inputs_.resize(process.audio_inputs_count); + audio_inputs_type_.resize(process.audio_inputs_count); + for (size_t port = 0; port < process.audio_inputs_count; port++) { + // NOTE: With VST3 plugins sometimes hosts provided more ports than the + // plugin asked for (or sometimes fewer, fun). So we'll account + // for both cases just to be safe. + audio_inputs_[port].channel_count = + std::min(static_cast( + shared_audio_buffers.num_input_channels(port)), + process.audio_inputs[port].channel_count); + audio_inputs_[port].latency = process.audio_inputs[port].latency; + audio_inputs_[port].constant_mask = + process.audio_inputs[port].constant_mask; + + // We'll encode the port type using a separate vector because we can't + // store it in place without creating dangling pointers + if (process.audio_inputs[port].data32) { + audio_inputs_type_[port] = + clap::audio_buffer::AudioBufferType::Float32; + + // We copy the actual input audio for every bus to the shared memory + // object + for (uint32_t channel = 0; + channel < process.audio_inputs[port].channel_count; + channel++) { + std::copy_n(process.audio_inputs[port].data32[channel], + frames_count_, + shared_audio_buffers.input_channel_ptr( + port, channel)); + } + } else if (process.audio_inputs[port].data64) { + audio_inputs_type_[port] = + clap::audio_buffer::AudioBufferType::Double64; + + for (uint32_t channel = 0; + channel < process.audio_inputs[port].channel_count; + channel++) { + std::copy_n(process.audio_inputs[port].data64[channel], + frames_count_, + shared_audio_buffers.input_channel_ptr( + port, channel)); + } + } else { + // Only reasonable-ish (it's still not reasonable) time where + // neither of the pointers is set + assert(process.audio_inputs[port].channel_count == 0); + } + } + + audio_outputs_.resize(process.audio_outputs_count); + audio_outputs_type_.resize(process.audio_outputs_count); + for (size_t port = 0; port < process.audio_outputs_count; port++) { + // The same notes apply to the outputs + audio_outputs_[port].channel_count = + std::min(static_cast( + shared_audio_buffers.num_output_channels(port)), + process.audio_outputs[port].channel_count); + audio_outputs_[port].latency = process.audio_outputs[port].latency; + // Shouldn't be any reason to bridge this, but who knows what will + // happen when we don't + audio_outputs_[port].constant_mask = + process.audio_outputs[port].constant_mask; + + if (process.audio_outputs[port].data32) { + audio_outputs_type_[port] = + clap::audio_buffer::AudioBufferType::Float32; + } else if (process.audio_outputs[port].data64) { + audio_outputs_type_[port] = + clap::audio_buffer::AudioBufferType::Double64; + } else { + // Only reasonable-ish (it's still not reasonable) time where + // neither of the pointers is set + assert(process.audio_outputs[port].channel_count == 0); + } + } + + in_events_.repopulate(*process.in_events); +} + +const clap_process_t& Process::reconstruct( + std::vector>& input_pointers, + std::vector>& output_pointers) { + reconstructed_process_data_.steady_time = steady_time_; + reconstructed_process_data_.frames_count = frames_count_; + reconstructed_process_data_.transport = transport_ ? &*transport_ : nullptr; + + // The actual audio data is contained within a shared memory object, and the + // input and output pointers point to regions in that object. These pointers + // are calculated during `clap_plugin::activate()`. + assert(audio_inputs_.size() <= input_pointers.size() && + audio_outputs_.size() <= output_pointers.size() && + audio_inputs_type_.size() == audio_inputs_.size() && + audio_outputs_type_.size() == audio_outputs_.size()); + for (size_t port = 0; port < audio_inputs_.size(); port++) { + // The sample depth depends on whether the plugin claimed to support + // 64-bit or not and whether the host ended up passing us 32-bit or + // 64-bit audio + switch (audio_inputs_type_[port]) { + case clap::audio_buffer::AudioBufferType::Float32: + default: + audio_inputs_[port].data32 = + reinterpret_cast(input_pointers[port].data()); + break; + case clap::audio_buffer::AudioBufferType::Double64: + audio_inputs_[port].data64 = + reinterpret_cast(input_pointers[port].data()); + break; + } + } + for (size_t port = 0; port < audio_outputs_.size(); port++) { + switch (audio_outputs_type_[port]) { + case clap::audio_buffer::AudioBufferType::Float32: + default: + audio_outputs_[port].data32 = + reinterpret_cast(output_pointers[port].data()); + break; + case clap::audio_buffer::AudioBufferType::Double64: + audio_outputs_[port].data64 = + reinterpret_cast(output_pointers[port].data()); + break; + } + } + + reconstructed_process_data_.audio_inputs = audio_inputs_.data(); + reconstructed_process_data_.audio_outputs = audio_outputs_.data(); + reconstructed_process_data_.audio_inputs_count = + static_cast(audio_inputs_.size()); + reconstructed_process_data_.audio_outputs_count = + static_cast(audio_outputs_.size()); + + out_events_.clear(); + reconstructed_process_data_.in_events = in_events_.input_events(); + reconstructed_process_data_.out_events = out_events_.output_events(); + + return reconstructed_process_data_; +} + +Process::Response& Process::create_response() noexcept { + // This response object acts as an optimization. It stores pointers to the + // original fields in our objects, so we can both only serialize those + // fields when sending the response from the Wine side. This lets us avoid + // allocations by not having to copy or move the data. On the plugin side we + // need to be careful to deserialize into an existing + // `clap::plugin::ProcessResponse` object with a response object that + // belongs to an actual process data object, because with these changes it's + // no longer possible to deserialize those results into a new ad-hoc created + // object. + response_object_.audio_outputs = &audio_outputs_; + response_object_.out_events = &out_events_; + + return response_object_; +} + +void Process::write_back_outputs(const clap_process_t& process, + const AudioShmBuffer& shared_audio_buffers) { + assert(process.audio_outputs && process.out_events); + + assert(audio_outputs_.size() == process.audio_outputs_count); + for (size_t port = 0; port < audio_outputs_.size(); port++) { + process.audio_outputs[port].constant_mask = + audio_outputs_[port].constant_mask; + // Don't think the plugin is supposed to change this, but uh may as well + process.audio_outputs[port].latency = audio_outputs_[port].latency; + + // `audio_outputs_[port].channel_count` is the minimum of the plugin's + // and the host's channel count + for (size_t channel = 0; channel < audio_outputs_[port].channel_count; + channel++) { + // We copy the output audio for every bus from the shared memory + // object back to the buffer provided by the host + switch (audio_outputs_type_[port]) { + case clap::audio_buffer::AudioBufferType::Float32: + default: + std::copy_n(shared_audio_buffers.output_channel_ptr( + port, channel), + process.frames_count, + process.audio_outputs[port].data32[channel]); + break; + case clap::audio_buffer::AudioBufferType::Double64: + std::copy_n(shared_audio_buffers.output_channel_ptr( + port, channel), + process.frames_count, + process.audio_outputs[port].data64[channel]); + break; + } + } + } + + out_events_.write_back_outputs(*process.out_events); +} + +} // namespace process +} // namespace clap diff --git a/src/common/serialization/clap/process.h b/src/common/serialization/clap/process.h new file mode 100644 index 00000000..ba4c0503 --- /dev/null +++ b/src/common/serialization/clap/process.h @@ -0,0 +1,247 @@ +// yabridge: a Wine plugin bridge +// Copyright (C) 2020-2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more destates. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +#include +#include + +#include "../../audio-shm.h" +#include "../../bitsery/ext/in-place-optional.h" +#include "audio-buffer.h" +#include "events.h" + +// Serialization messages for `clap/process.h` + +namespace clap { +namespace process { + +/** + * A serializable wrapper around `clap_process_t`. This works exactly the same + * as the process data wrapper for VST3. At the start of a process cycle all + * audio data is copied to the already set up shared memory buffers and the + * event and transport data is copied to this object. Then on the Wine side + * we'll reconstruct the original `clap_process_t` and pass that to the plugin. + * The output events are then sent back to the native plugin, where it can write + * those along with the audio outputs (which are also in that shared memory + * buffer) back to the host. The response is serialized in the + * `clap::process::Process::Response` object since only a small amount of + * information needs to be sent back. + * + * As mentioned earlier, the audio data is not stored in this object but it is + * instead stored in a shared memory object shared by a `clap_plugin_proxy` and + * `ClapPluginInstance` pair. Since the amount of audio data sent per processing + * call is fixed, this halves the number of required memory copies. + * + * Be sure to double check how `clap::process::Process::Response` is used. We do + * some pointer tricks there to avoid copies and moves when serializing the + * results of our audio processing. + */ +class Process { + public: + /** + * Initialize the process data. We only provide a default constructor here, + * because we need to fill the existing object with new data every + * processing cycle to avoid reallocating a new object every time. + */ + Process() noexcept; + + /** + * Copy data from a host provided `clap_process_T` object to this struct + * during a process call. This struct can then be serialized, and + * `reconstruct()` can then be used again to recreate the original + * `clap_process_t` object. This avoids allocating unless it's absolutely + * necessary (e.g. when we receive more events than we've received in + * previous calls). + * + * The input audio buffer will be copied to `shared_audio_buffers`. There's + * no direct link between this `Process` object and those buffers, but they + * should be treated as a pair. This is a bit ugly, but optimizations sadly + * never made code prettier. + */ + void repopulate(const clap_process_t& process, + AudioShmBuffer& shared_audio_buffers); + + /** + * Reconstruct the original `clap_process_t` object passed to `repopulate()` + * and return it. This is used in the Wine plugin host when handling a + * `clap_plugin::process()` call. + * + * Because the actual audio is stored in an `AudioShmBuffer` outside of this + * object, we need to make sure that the `AudioBusBuffers` objects we're + * using point to the correct buffer even after a resize. To make it more + * difficult for us to mess this up, we'll store those bus-channel pointers + * in `ClapBridge::ClapPluginInstance` and we'll point the pointers in our + * `inputs` and `outputs` fields directly to those pointers. They will have + * been set up during `clap_plugin::activate()`. + * + * CLAP allows mixed float and double precision audio if the plugin opts + * into it. The audio buffers thus always contain enough space for double + * precision if a port supports it. The actual sample format used is stored + * in our `clap::audio_buffer::AudioBuffer` serialization wrapper. + */ + const clap_process_t& reconstruct( + std::vector>& input_pointers, + std::vector>& output_pointers); + + /** + * A serializable wrapper around the output fields of `clap_process_t`, so + * we only have to copy the information back that's actually important. + * These fields are pointers to the corresponding fields in `Process`. On + * the plugin side this information can then be written back to the host. + * The actual output audio is stored in the shared memory object. + * + * HACK: All of this is an optimization to avoid unnecessarily copying or + * moving and reallocating. Directly serializing and deserializing + * from and to pointers does make all of this very error prone, hence + * all of the assertions. + * + * @see YaProcessData + */ + struct Response { + // We store raw pointers instead of references so we can default + // initialize this object during deserialization + llvm::SmallVectorImpl* audio_outputs = nullptr; + clap::events::EventList* out_events = nullptr; + + template + void serialize(S& s) { + assert(audio_outputs && out_events); + // Since these fields are references to the corresponding fields on + // the surrounding object, we're actually serializing those fields. + // This means that on the plugin side we can _only_ deserialize into + // an existing object, since our serializing code doesn't touch the + // actual pointers. + s.container(*audio_outputs, 1 << 14); + s.object(*out_events); + } + }; + + /** + * Create a `clap::process::Process::Response` object that refers to the + * output fields in this object. The object doesn't store any actual data, + * and may not outlive this object. We use this so we only have to copy the + * relevant fields back to the host. On the Wine side this function should + * only be called after the plugin's `clap_plugin::process()` function has + * been called with the reconstructed process data obtained from + * `Process::reconstruct()`. + * + * On the plugin side this should be used to create a response object that + * **must** be received into, since we're deserializing directly into some + * pointers. + */ + Response& create_response() noexcept; + + /** + * Write all of this output data back to the host's `clap_process_t` object. + * During this process we'll also write the output audio from the + * corresponding shared memory audio buffers back. + */ + void write_back_outputs(const clap_process_t& process, + const AudioShmBuffer& shared_audio_buffers); + + template + void serialize(S& s) { + s.value8b(steady_time_); + s.value4b(frames_count_); + + s.ext(transport_, bitsery::ext::InPlaceOptional{}); + + // Both `audio_inputs_` and `audio_outputs_` only store metadata. The + // actual audio is sent using an accompanying `AudioShmBuffer` object. + s.container(audio_inputs_, 1 << 14); + s.container1b(audio_inputs_type_, 1 << 14); + s.container(audio_outputs_, 1 << 14); + s.container1b(audio_outputs_type_, 1 << 14); + + // We don't need to serialize the output events because this will always + // be empty on the Wine side. The response is sent back through the + // separate `Response` object + s.object(in_events_); + } + + // These fields are input and context data read from the original + // `clap_process_t` object + + int64_t steady_time_ = 0; + uint32_t frames_count_ = 0; + + // This is an optional field + std::optional transport_; + + /** + * The audio input buffers for every port. We'll only serialize the metadata + * During `reconstruct()` the channel pointers pointers in these objects + * will be set to point to our shared memory surface that holds the actual + * audio data. + */ + llvm::SmallVector audio_inputs_; + /** + * The types corresponding to each buffer in `audio_inputs_`. This needs to + * be serialized separately since this information is encoded by setting one + * of the two pointers instead of through a flag. + */ + llvm::SmallVector + audio_inputs_type_; + + /** + * The audio output buffers for every port. We'll only serialize the + * metadata During `reconstruct()` the channel pointers pointers in these + * objects will be set to point to our shared memory surface that holds the + * actual audio data. + */ + llvm::SmallVector audio_outputs_; + /** + * The types corresponding to each buffer in `audio_outputs_`. This needs to + * be serialized separately since this information is encoded by setting one + * of the two pointers instead of through a flag. + */ + llvm::SmallVector + audio_outputs_type_; + + clap::events::EventList in_events_; + clap::events::EventList out_events_; + + private: + // These last few members are used on the Wine plugin host side to + // reconstruct the original `clap_process_t` object. Here we also initialize + // these output fields so the Windows CLAP plugin can write to them though a + // regular `ProcessData` object. Finally we can wrap these output fields + // back into a `clap::process::Process::Response` using `create_response()`. + // so they can be serialized and written back to the host's `clap_process_t` + // object. + + /** + * This is a `Response` object that contains pointers to other fields in + * this struct so we can serialize to and from them. + * + * NOTE: We use this on the plugin side as an optimization to be able to + * directly receive data into this object, avoiding the need for any + * allocations. + */ + Response response_object_; + + /** + * The process data we reconstruct from the other fields during + * `reconstruct()`. + */ + clap_process_t reconstructed_process_data_{}; +}; + +} // namespace process +} // namespace clap diff --git a/src/plugin/meson.build b/src/plugin/meson.build index 3dfac8a0..7a3fe7ee 100644 --- a/src/plugin/meson.build +++ b/src/plugin/meson.build @@ -83,6 +83,7 @@ if with_clap '../common/serialization/clap/events.cpp', '../common/serialization/clap/host.cpp', '../common/serialization/clap/plugin.cpp', + '../common/serialization/clap/process.cpp', '../common/serialization/clap/stream.cpp', '../common/utils.cpp', '../include/llvm/small-vector.cpp', diff --git a/src/wine-host/meson.build b/src/wine-host/meson.build index 5926f158..e4889a7a 100644 --- a/src/wine-host/meson.build +++ b/src/wine-host/meson.build @@ -86,6 +86,7 @@ if with_clap '../common/serialization/clap/events.cpp', '../common/serialization/clap/host.cpp', '../common/serialization/clap/plugin.cpp', + '../common/serialization/clap/process.cpp', '../common/serialization/clap/stream.cpp', 'bridges/clap-impls/host-proxy.cpp', 'bridges/clap.cpp',