mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-09 20:29:10 +02:00
Add CLAP audio thread callback sockets
The initialization here is a bit funky and happens in lockstep to ensure both sides are synchronized.
This commit is contained in:
+144
-49
@@ -23,6 +23,73 @@
|
|||||||
#include "../serialization/clap.h"
|
#include "../serialization/clap.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every CLAP plugin instance gets its own audio thread along with host->plugin
|
||||||
|
* control and a plugin->host callback sockets. This feels like a bit much, but
|
||||||
|
* some CLAP extensions require plugins to make audio thread callbacks and those
|
||||||
|
* should not have to wait for other callbacks (or spin up a new thread).
|
||||||
|
*/
|
||||||
|
template <typename Thread>
|
||||||
|
class ClapAudioThreadSockets {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Sets up the audio thread sockets for a specific plugin instance. The
|
||||||
|
* sockets won't be active until `connect()` gets called. This cannot be
|
||||||
|
* initialized inline in the `ClapSockets::add_audio_thread_and_listen_*()`
|
||||||
|
* functions as that would require the sockets to be moved, which is not
|
||||||
|
* possible they contain atomics.
|
||||||
|
*
|
||||||
|
* @param io_context The IO context the sockets should be bound to. Relevant
|
||||||
|
* when doing asynchronous operations.
|
||||||
|
* @param endpoint_base_dir The base directory that will be used for the
|
||||||
|
* Unix domain sockets.
|
||||||
|
* @param instance_id The CLAP plugin instance ID these sockets belong to.
|
||||||
|
* @param listen If `true`, start listening on the sockets. Incoming
|
||||||
|
* connections will be accepted when `connect()` gets called. This should
|
||||||
|
* be set to `true` on the plugin side, and `false` on the Wine host side.
|
||||||
|
*
|
||||||
|
* @see ClapSockets::connect
|
||||||
|
*/
|
||||||
|
ClapAudioThreadSockets(asio::io_context& io_context,
|
||||||
|
const ghc::filesystem::path& endpoint_base_dir,
|
||||||
|
size_t instance_id,
|
||||||
|
bool listen)
|
||||||
|
: control_(io_context,
|
||||||
|
(endpoint_base_dir / ("host_plugin_audio_thread_control_" +
|
||||||
|
std::to_string(instance_id) + ".sock"))
|
||||||
|
.string(),
|
||||||
|
// The Wine side will end up listening for control messages
|
||||||
|
!listen),
|
||||||
|
callback_(
|
||||||
|
io_context,
|
||||||
|
(endpoint_base_dir / ("plugin_host_audio_thread_callback_" +
|
||||||
|
std::to_string(instance_id) + ".sock"))
|
||||||
|
.string(),
|
||||||
|
// And the plugin side for callbacks
|
||||||
|
listen) {}
|
||||||
|
|
||||||
|
void connect() {
|
||||||
|
control_.connect();
|
||||||
|
callback_.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
control_.close();
|
||||||
|
callback_.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for host->plugin audio thread function calls.
|
||||||
|
*/
|
||||||
|
TypedMessageHandler<Thread, ClapLogger, ClapAudioThreadControlRequest>
|
||||||
|
control_;
|
||||||
|
/**
|
||||||
|
* Used for plugin->host audio thread callbacks.
|
||||||
|
*/
|
||||||
|
TypedMessageHandler<Thread, ClapLogger, ClapAudioThreadCallbackRequest>
|
||||||
|
callback_;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages all the sockets used for communicating between the plugin and the
|
* Manages all the sockets used for communicating between the plugin and the
|
||||||
* Wine host when hosting a CLAP plugin.
|
* Wine host when hosting a CLAP plugin.
|
||||||
@@ -91,35 +158,17 @@ class ClapSockets final : public Sockets {
|
|||||||
|
|
||||||
// This map should be empty at this point, but who knows
|
// This map should be empty at this point, but who knows
|
||||||
std::lock_guard lock(audio_thread_sockets_mutex_);
|
std::lock_guard lock(audio_thread_sockets_mutex_);
|
||||||
for (auto& [instance_id, socket] : audio_thread_sockets_) {
|
for (auto& [instance_id, sockets] : audio_thread_sockets_) {
|
||||||
socket.close();
|
sockets.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the dedicated audio thread socket socket for a plugin
|
* Create and listen on a dedicated audio thread socket for host->plugin
|
||||||
* instance. This should be called on the plugin side after instantiating
|
* audio thread messages, and connect to the corresponding socket for
|
||||||
* the plugin.
|
* plugin->host audio thread callbacks. The thread will blocked until the
|
||||||
*
|
* socket has been closed. This should be called from the Wine plugin host
|
||||||
* @param instance_id The object instance identifier of the socket.
|
* side after instantiating the plugin.
|
||||||
*/
|
|
||||||
void add_audio_thread_and_connect(size_t instance_id) {
|
|
||||||
std::lock_guard lock(audio_thread_sockets_mutex_);
|
|
||||||
audio_thread_sockets_.try_emplace(
|
|
||||||
instance_id, io_context_,
|
|
||||||
(base_dir_ / ("host_plugin_audio_thread_" +
|
|
||||||
std::to_string(instance_id) + ".sock"))
|
|
||||||
.string(),
|
|
||||||
false);
|
|
||||||
|
|
||||||
audio_thread_sockets_.at(instance_id).connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and listen on a dedicated audio thread socket for a plugin object
|
|
||||||
* instance. The calling thread will block until the socket has been closed.
|
|
||||||
* This should be called from the Wine plugin host side after instantiating
|
|
||||||
* the plugin.
|
|
||||||
*
|
*
|
||||||
* @param instance_id The object instance identifier of the socket.
|
* @param instance_id The object instance identifier of the socket.
|
||||||
* @param socket_listening_latch A promise we'll set a value for once the
|
* @param socket_listening_latch A promise we'll set a value for once the
|
||||||
@@ -133,19 +182,23 @@ class ClapSockets final : public Sockets {
|
|||||||
* in `ClapAudioThreadControlRequest::Payload`.
|
* in `ClapAudioThreadControlRequest::Payload`.
|
||||||
*/
|
*/
|
||||||
template <typename F>
|
template <typename F>
|
||||||
void add_audio_thread_and_listen(size_t instance_id,
|
void add_audio_thread_and_listen_control(
|
||||||
std::promise<void>& socket_listening_latch,
|
size_t instance_id,
|
||||||
F&& callback) {
|
std::promise<void>& socket_listening_latch,
|
||||||
|
F&& callback) {
|
||||||
{
|
{
|
||||||
std::lock_guard lock(audio_thread_sockets_mutex_);
|
std::lock_guard lock(audio_thread_sockets_mutex_);
|
||||||
audio_thread_sockets_.try_emplace(
|
// This is called on the Wine side when creating the plugin
|
||||||
instance_id, io_context_,
|
// instance. Once the sockets have been created we'll unlock the
|
||||||
(base_dir_ / ("host_plugin_audio_thread_" +
|
// latch and send the result to the native plugin. At that point the
|
||||||
std::to_string(instance_id) + ".sock"))
|
// native plugin will connect to the sockets and everything will
|
||||||
.string(),
|
// continue.
|
||||||
true);
|
audio_thread_sockets_.try_emplace(instance_id, io_context_,
|
||||||
|
base_dir_, instance_id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're blocking for a connection here, so the latch must be unlocked
|
||||||
|
// before doing so
|
||||||
socket_listening_latch.set_value();
|
socket_listening_latch.set_value();
|
||||||
audio_thread_sockets_.at(instance_id).connect();
|
audio_thread_sockets_.at(instance_id).connect();
|
||||||
|
|
||||||
@@ -153,8 +206,56 @@ class ClapSockets final : public Sockets {
|
|||||||
// receiving buffers for all calls. This slightly reduces the amount of
|
// receiving buffers for all calls. This slightly reduces the amount of
|
||||||
// allocations in the audio processing loop.
|
// allocations in the audio processing loop.
|
||||||
audio_thread_sockets_.at(instance_id)
|
audio_thread_sockets_.at(instance_id)
|
||||||
.template receive_messages<true>(std::nullopt,
|
.control_.template receive_messages<true>(
|
||||||
std::forward<F>(callback));
|
std::nullopt, std::forward<F>(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and listen on a dedicated audio thread socket for plugin->host
|
||||||
|
* audio thread callbacks, and connect to the corresponding socket for
|
||||||
|
* host->plugin audio thread messages. The thread will blocked until the
|
||||||
|
* socket has been closed. This should be called from the native plugin side
|
||||||
|
* after instantiating the plugin.
|
||||||
|
*
|
||||||
|
* @param instance_id The object instance identifier of the socket.
|
||||||
|
* @param logger The native plugin's logger instance.
|
||||||
|
* @param socket_listening_latch A promise we'll set a value for once the
|
||||||
|
* socket is being listened on so we can wait for it. Otherwise it can be
|
||||||
|
* that the native plugin already tries to connect to the socket before
|
||||||
|
* Wine plugin host is even listening on it.
|
||||||
|
* @param cb An overloaded function that can take every type `T` in the
|
||||||
|
* `ClapAudioThreadCallbackRequest` variant and then returns
|
||||||
|
* `T::Response`.
|
||||||
|
*
|
||||||
|
* @tparam F A function type in the form of `T::Response(T)` for every `T`
|
||||||
|
* in `ClapAudioThreadCallbackRequest::Payload`.
|
||||||
|
*/
|
||||||
|
template <typename F>
|
||||||
|
void add_audio_thread_and_listen_callback(
|
||||||
|
size_t instance_id,
|
||||||
|
ClapLogger& logger,
|
||||||
|
std::promise<void>& socket_listening_latch,
|
||||||
|
F&& callback) {
|
||||||
|
{
|
||||||
|
std::lock_guard lock(audio_thread_sockets_mutex_);
|
||||||
|
audio_thread_sockets_.try_emplace(instance_id, io_context_,
|
||||||
|
base_dir_, instance_id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is called on the native plugin side after the Wine side is
|
||||||
|
// already listening on the sockets. We'll connect here, and once the
|
||||||
|
// connection has been made we unlock the latch to finalize the plugin
|
||||||
|
// instance creation.
|
||||||
|
audio_thread_sockets_.at(instance_id).connect();
|
||||||
|
socket_listening_latch.set_value();
|
||||||
|
|
||||||
|
// This `true` indicates that we want to reuse our serialization and
|
||||||
|
// receiving buffers for all calls. This slightly reduces the amount of
|
||||||
|
// allocations in the audio processing loop.
|
||||||
|
audio_thread_sockets_.at(instance_id)
|
||||||
|
.callback_.template receive_messages<true>(
|
||||||
|
std::pair<ClapLogger&, bool>(logger, false),
|
||||||
|
std::forward<F>(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,25 +362,19 @@ class ClapSockets final : public Sockets {
|
|||||||
thread_local SerializationBuffer<256> audio_thread_buffer{};
|
thread_local SerializationBuffer<256> audio_thread_buffer{};
|
||||||
|
|
||||||
return audio_thread_sockets_.at(instance_id)
|
return audio_thread_sockets_.at(instance_id)
|
||||||
.receive_into(object, response_object, logging,
|
.control_.receive_into(object, response_object, logging,
|
||||||
audio_thread_buffer);
|
audio_thread_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
asio::io_context& io_context_;
|
asio::io_context& io_context_;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every plugin instance gets a dedicated audio thread socket. These
|
* Every plugin instance gets dedicated audio thread sockets for plugin
|
||||||
* functions are always called in a hot loop, so there should not be any
|
* function calls and callbacks. These functions are always called in a hot
|
||||||
* waiting or additional thread or socket creation happening there.
|
* loop, so there should not be any waiting or additional thread or socket
|
||||||
*
|
* creation happening there.
|
||||||
* TODO: All plugin instances have a socket for re-entrant audio thread
|
|
||||||
* function calls. This is probably not needed. It's probably useful
|
|
||||||
* to investgate why this socket was added back to the VST3 audio
|
|
||||||
* threads.
|
|
||||||
*/
|
*/
|
||||||
std::unordered_map<
|
std::unordered_map<size_t, ClapAudioThreadSockets<Thread>>
|
||||||
size_t,
|
|
||||||
TypedMessageHandler<Thread, ClapLogger, ClapAudioThreadControlRequest>>
|
|
||||||
audio_thread_sockets_;
|
audio_thread_sockets_;
|
||||||
std::mutex audio_thread_sockets_mutex_;
|
std::mutex audio_thread_sockets_mutex_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -160,7 +160,29 @@ using ClapMainThreadCallbackRequest =
|
|||||||
template <typename S>
|
template <typename S>
|
||||||
void serialize(S& s, ClapMainThreadCallbackRequest& payload) {
|
void serialize(S& s, ClapMainThreadCallbackRequest& payload) {
|
||||||
// All of the objects in `ClapMainThreadCallbackRequest` should have their
|
// All of the objects in `ClapMainThreadCallbackRequest` should have their
|
||||||
// own serialization function.
|
// own serialization function
|
||||||
|
s.ext(payload, bitsery::ext::InPlaceVariant{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as `ClapMainThreadCallbackRequest`, but for callbacks that can be
|
||||||
|
* made from the audio therad. This uses a separate per-instance socket to avoid
|
||||||
|
* blocking or spawning up a new thread when multiple plugin instances make
|
||||||
|
* callbacks at the same time, or when they made simultaneous GUI and audio
|
||||||
|
* thread callbacks. A request of type `ClapAudioThreadCallbackRequest(T)`
|
||||||
|
* should send back a `T::Response`.
|
||||||
|
*
|
||||||
|
* TODO: I still have absolutely no idea why you enter template deduction hell
|
||||||
|
* if you remove the `WantsConfiguration` entry. This is not actually
|
||||||
|
* used.
|
||||||
|
*/
|
||||||
|
using ClapAudioThreadCallbackRequest =
|
||||||
|
std::variant<WantsConfiguration, clap::ext::tail::host::Changed>;
|
||||||
|
|
||||||
|
template <typename S>
|
||||||
|
void serialize(S& s, ClapAudioThreadCallbackRequest& payload) {
|
||||||
|
// All of the objects in `ClapAudioThreadCallbackRequest` should have their
|
||||||
|
// own serialization function
|
||||||
s.ext(payload, bitsery::ext::InPlaceVariant{});
|
s.ext(payload, bitsery::ext::InPlaceVariant{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <clap/ext/audio-ports.h>
|
#include <clap/ext/audio-ports.h>
|
||||||
@@ -201,6 +202,13 @@ class clap_plugin_proxy {
|
|||||||
*/
|
*/
|
||||||
ClapHostExtensions host_extensions_;
|
ClapHostExtensions host_extensions_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler for receiving audio thread callbacks. Set when initializing the
|
||||||
|
* plugin. This is needed to minimize blocking during those callbacks, as
|
||||||
|
* certain CLAP extensions allow callbacks on the audio thread.
|
||||||
|
*/
|
||||||
|
std::jthread audio_thread_handler_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ClapPluginBridge& bridge_;
|
ClapPluginBridge& bridge_;
|
||||||
size_t instance_id_;
|
size_t instance_id_;
|
||||||
|
|||||||
@@ -251,8 +251,48 @@ void ClapPluginBridge::register_plugin_proxy(
|
|||||||
plugin_proxies_.emplace(instance_id, std::move(plugin_proxy));
|
plugin_proxies_.emplace(instance_id, std::move(plugin_proxy));
|
||||||
|
|
||||||
// For optimization reaons we use dedicated sockets for functions that will
|
// For optimization reaons we use dedicated sockets for functions that will
|
||||||
// be run in the audio processing loop
|
// be run in the audio processing loop.
|
||||||
sockets_.add_audio_thread_and_connect(instance_id);
|
// Every plugin instance gets its own audio thread along with sockets for
|
||||||
|
// host->plugin control messages and plugin->host callbacks
|
||||||
|
std::promise<void> socket_listening_latch;
|
||||||
|
plugin_proxies_.at(instance_id)
|
||||||
|
->audio_thread_handler_ = std::jthread([&, instance_id]() {
|
||||||
|
set_realtime_priority(true);
|
||||||
|
|
||||||
|
// XXX: Like with VST2 worker threads, when using plugin groups the
|
||||||
|
// thread names from different plugins will clash. Not a huge
|
||||||
|
// deal probably, since duplicate thread names are still more
|
||||||
|
// useful than no thread names.
|
||||||
|
const std::string thread_name = "audio-" + std::to_string(instance_id);
|
||||||
|
pthread_setname_np(pthread_self(), thread_name.c_str());
|
||||||
|
|
||||||
|
// Certain CLAP extensions allow audio thread callbacks, so we need
|
||||||
|
// a dedicated per-instance socket for that
|
||||||
|
sockets_.add_audio_thread_and_listen_callback(
|
||||||
|
instance_id, logger_, socket_listening_latch,
|
||||||
|
overload{
|
||||||
|
[&](const WantsConfiguration&) -> WantsConfiguration::Response {
|
||||||
|
// FIXME: Without starting the variant with
|
||||||
|
// `WantsConfiguration` you enter template deduction
|
||||||
|
// hell. I haven't been able to figure out why.
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
[&](const clap::ext::tail::host::Changed& request)
|
||||||
|
-> clap::ext::tail::host::Changed::Response {
|
||||||
|
// FIXME:
|
||||||
|
// const auto& [instance, _] =
|
||||||
|
// get_instance(request.instance_id);
|
||||||
|
|
||||||
|
// return instance.plugin->start_processing(
|
||||||
|
// instance.plugin.get());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the new socket to be listening on before continuing. Otherwise
|
||||||
|
// the native plugin may try to connect to it before our thread is up and
|
||||||
|
// running.
|
||||||
|
socket_listening_latch.get_future().wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClapPluginBridge::unregister_plugin_proxy(size_t instance_id) {
|
void ClapPluginBridge::unregister_plugin_proxy(size_t instance_id) {
|
||||||
|
|||||||
@@ -605,7 +605,8 @@ void ClapBridge::register_plugin_instance(
|
|||||||
object_instances_.emplace(
|
object_instances_.emplace(
|
||||||
instance_id, ClapPluginInstance(plugin, std::move(host_proxy)));
|
instance_id, ClapPluginInstance(plugin, std::move(host_proxy)));
|
||||||
|
|
||||||
// Every plugin instance gets its own audio thread
|
// Every plugin instance gets its own audio thread along with sockets for
|
||||||
|
// host->plugin control messages and plugin->host callbacks
|
||||||
std::promise<void> socket_listening_latch;
|
std::promise<void> socket_listening_latch;
|
||||||
object_instances_.at(instance_id).audio_thread_handler =
|
object_instances_.at(instance_id).audio_thread_handler =
|
||||||
Win32Thread([&, instance_id]() {
|
Win32Thread([&, instance_id]() {
|
||||||
@@ -619,7 +620,7 @@ void ClapBridge::register_plugin_instance(
|
|||||||
"audio-" + std::to_string(instance_id);
|
"audio-" + std::to_string(instance_id);
|
||||||
pthread_setname_np(pthread_self(), thread_name.c_str());
|
pthread_setname_np(pthread_self(), thread_name.c_str());
|
||||||
|
|
||||||
sockets_.add_audio_thread_and_listen(
|
sockets_.add_audio_thread_and_listen_control(
|
||||||
instance_id, socket_listening_latch,
|
instance_id, socket_listening_latch,
|
||||||
overload{
|
overload{
|
||||||
[&](const clap::plugin::StartProcessing& request)
|
[&](const clap::plugin::StartProcessing& request)
|
||||||
|
|||||||
Reference in New Issue
Block a user