mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-06 19:40:10 +02:00
Add boilerplate for a CLAP bridge
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
// 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 details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "clap.h"
|
||||
|
||||
#include <bitset>
|
||||
|
||||
// Generated inside of the build directory
|
||||
#include <version.h>
|
||||
|
||||
// TODO: Query extensions in the initializer list
|
||||
ClapPluginExtensions::ClapPluginExtensions(const clap_plugin& plugin) noexcept {
|
||||
}
|
||||
|
||||
ClapPluginInstance::ClapPluginInstance(const clap_plugin* plugin) noexcept
|
||||
: plugin((assert(plugin), plugin), plugin->destroy), extensions(*plugin) {}
|
||||
|
||||
ClapBridge::ClapBridge(MainContext& main_context,
|
||||
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
|
||||
std::string plugin_dll_path,
|
||||
std::string endpoint_base_dir,
|
||||
pid_t parent_pid)
|
||||
: HostBridge(main_context, plugin_dll_path, parent_pid),
|
||||
logger_(generic_logger_),
|
||||
sockets_(main_context.context_, endpoint_base_dir, false) {
|
||||
// TODO: Load the CLAP module
|
||||
// std::string error;
|
||||
// module_ = CLAP::Hosting::Win32Module::create(plugin_dll_path, error);
|
||||
// if (!module_) {
|
||||
// throw std::runtime_error("Could not load the CLAP module for '" +
|
||||
// plugin_dll_path + "': " + error);
|
||||
// }
|
||||
|
||||
sockets_.connect();
|
||||
|
||||
// Fetch this instance's configuration from the plugin to finish the setup
|
||||
// process
|
||||
config_ = sockets_.plugin_host_main_thread_callback_.send_message(
|
||||
WantsConfiguration{.host_version = yabridge_git_version}, std::nullopt);
|
||||
|
||||
// Allow this plugin to configure the main context's tick rate
|
||||
main_context.update_timer_interval(config_.event_loop_interval());
|
||||
}
|
||||
|
||||
bool ClapBridge::inhibits_event_loop() noexcept {
|
||||
std::shared_lock lock(object_instances_mutex_);
|
||||
|
||||
for (const auto& [instance_id, instance] : object_instances_) {
|
||||
if (!instance.is_initialized) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClapBridge::run() {
|
||||
set_realtime_priority(true);
|
||||
|
||||
// TODO: Listen on the socket
|
||||
// sockets_.host_plugin_control_.receive_messages(
|
||||
// std::nullopt,
|
||||
// overload{
|
||||
// [&](const ClapPluginFactoryProxy::Construct&)
|
||||
// -> ClapPluginFactoryProxy::Construct::Response {
|
||||
// return ClapPluginFactoryProxy::ConstructArgs(
|
||||
// module_->getFactory().get());
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
// bool ClapBridge::maybe_resize_editor(size_t instance_id,
|
||||
// const Steinberg::ViewRect& new_size) {
|
||||
// const auto& [instance, _] = get_instance(instance_id);
|
||||
|
||||
// if (instance.editor) {
|
||||
// instance.editor->resize(new_size.getWidth(), new_size.getHeight());
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
void ClapBridge::close_sockets() {
|
||||
sockets_.close();
|
||||
}
|
||||
|
||||
size_t ClapBridge::generate_instance_id() noexcept {
|
||||
return current_instance_id_.fetch_add(1);
|
||||
}
|
||||
|
||||
std::pair<ClapPluginInstance&, std::shared_lock<std::shared_mutex>>
|
||||
ClapBridge::get_instance(size_t instance_id) noexcept {
|
||||
std::shared_lock lock(object_instances_mutex_);
|
||||
|
||||
return std::pair<ClapPluginInstance&, std::shared_lock<std::shared_mutex>>(
|
||||
object_instances_.at(instance_id), std::move(lock));
|
||||
}
|
||||
|
||||
// TODO: Implement audio processing
|
||||
// std::optional<AudioShmBuffer::Config> ClapBridge::setup_shared_audio_buffers(
|
||||
// size_t instance_id) {
|
||||
// const auto& [instance, _] = get_instance(instance_id);
|
||||
|
||||
// const Steinberg::IPtr<Steinberg::Vst::IComponent> component =
|
||||
// instance.interfaces.component;
|
||||
// const Steinberg::IPtr<Steinberg::Vst::IAudioProcessor> audio_processor =
|
||||
// instance.interfaces.audio_processor;
|
||||
|
||||
// if (!instance.process_setup || !component || !audio_processor) {
|
||||
// return std::nullopt;
|
||||
// }
|
||||
|
||||
// // We'll query the plugin for its audio bus layouts, and then create
|
||||
// // calculate the offsets in a large memory buffer for the different audio
|
||||
// // channels. The offsets for each audio channel are in samples (since
|
||||
// // they'll be used with pointer arithmetic in `AudioShmBuffer`).
|
||||
// uint32_t current_offset = 0;
|
||||
|
||||
// auto create_bus_offsets = [&, &setup = instance.process_setup](
|
||||
// Steinberg::Vst::BusDirection direction) {
|
||||
// const auto num_busses =
|
||||
// component->getBusCount(Steinberg::Vst::kAudio, direction);
|
||||
|
||||
// // This function is also run from `IAudioProcessor::setActive()`.
|
||||
// // According to the docs this does not need to be realtime-safe, but we
|
||||
// // should at least still try to not do anything expensive when no work
|
||||
// // needs to be done.
|
||||
// llvm::SmallVector<llvm::SmallVector<uint32_t, 32>, 16> bus_offsets(
|
||||
// num_busses);
|
||||
// for (int bus = 0; bus < num_busses; bus++) {
|
||||
// Steinberg::Vst::SpeakerArrangement speaker_arrangement{};
|
||||
// audio_processor->getBusArrangement(direction, bus,
|
||||
// speaker_arrangement);
|
||||
|
||||
// const size_t num_channels =
|
||||
// std::bitset<sizeof(Steinberg::Vst::SpeakerArrangement) * 8>(
|
||||
// speaker_arrangement)
|
||||
// .count();
|
||||
// bus_offsets[bus].resize(num_channels);
|
||||
|
||||
// for (size_t channel = 0; channel < num_channels; channel++) {
|
||||
// bus_offsets[bus][channel] = current_offset;
|
||||
// current_offset += setup->maxSamplesPerBlock;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return bus_offsets;
|
||||
// };
|
||||
|
||||
// // Creating the audio buffer offsets for every channel in every bus will
|
||||
// // advacne `current_offset` to keep pointing to the starting position for
|
||||
// // the next channel
|
||||
// const auto input_bus_offsets =
|
||||
// create_bus_offsets(Steinberg::Vst::kInput); const auto output_bus_offsets
|
||||
// = create_bus_offsets(Steinberg::Vst::kOutput);
|
||||
|
||||
// // The size of the buffer is in bytes, and it will depend on whether the
|
||||
// // host is going to pass 32-bit or 64-bit audio to the plugin
|
||||
// const bool double_precision =
|
||||
// instance.process_setup->symbolicSampleSize ==
|
||||
// Steinberg::Vst::kSample64;
|
||||
// const uint32_t buffer_size =
|
||||
// current_offset * (double_precision ? sizeof(double) : sizeof(float));
|
||||
|
||||
// // If this function has been called previously and the size did not change,
|
||||
// // then we should not do any work
|
||||
// if (instance.process_buffers &&
|
||||
// instance.process_buffers->config_.size == buffer_size) {
|
||||
// return std::nullopt;
|
||||
// }
|
||||
|
||||
// // Because the above check should be super cheap, we'll now need to convert
|
||||
// // the stack allocated SmallVectors to regular heap vectors
|
||||
// std::vector<std::vector<uint32_t>> input_bus_offsets_vector;
|
||||
// input_bus_offsets_vector.reserve(input_bus_offsets.size());
|
||||
// for (const auto& channel_offsets : input_bus_offsets) {
|
||||
// input_bus_offsets_vector.push_back(
|
||||
// std::vector(channel_offsets.begin(), channel_offsets.end()));
|
||||
// }
|
||||
|
||||
// std::vector<std::vector<uint32_t>> output_bus_offsets_vector;
|
||||
// output_bus_offsets_vector.reserve(output_bus_offsets.size());
|
||||
// for (const auto& channel_offsets : output_bus_offsets) {
|
||||
// output_bus_offsets_vector.push_back(
|
||||
// std::vector(channel_offsets.begin(), channel_offsets.end()));
|
||||
// }
|
||||
|
||||
// // We'll set up these shared memory buffers on the Wine side first, and then
|
||||
// // when this request returns we'll do the same thing on the native plugin
|
||||
// // side
|
||||
// AudioShmBuffer::Config buffer_config{
|
||||
// .name = sockets_.base_dir_.filename().string() + "-" +
|
||||
// std::to_string(instance_id),
|
||||
// .size = buffer_size,
|
||||
// .input_offsets = std::move(input_bus_offsets_vector),
|
||||
// .output_offsets = std::move(output_bus_offsets_vector)};
|
||||
// if (!instance.process_buffers) {
|
||||
// instance.process_buffers.emplace(buffer_config);
|
||||
// } else {
|
||||
// instance.process_buffers->resize(buffer_config);
|
||||
// }
|
||||
|
||||
// // After setting up the shared memory buffer, we need to create a vector of
|
||||
// // channel audio pointers for every bus. These will then be assigned to the
|
||||
// // `AudioBusBuffers` objects in the `ProcessData` struct in
|
||||
// // `YaProcessData::reconstruct()` before passing the reconstructed process
|
||||
// // data to `IAudioProcessor::process()`.
|
||||
// auto set_bus_pointers =
|
||||
// [&]<std::invocable<uint32_t, uint32_t> F>(
|
||||
// std::vector<std::vector<void*>>& bus_pointers,
|
||||
// const std::vector<std::vector<uint32_t>>& bus_offsets,
|
||||
// F&& get_channel_pointer) {
|
||||
// bus_pointers.resize(bus_offsets.size());
|
||||
|
||||
// for (size_t bus = 0; bus < bus_offsets.size(); bus++) {
|
||||
// bus_pointers[bus].resize(bus_offsets[bus].size());
|
||||
|
||||
// for (size_t channel = 0; channel < bus_offsets[bus].size();
|
||||
// channel++) {
|
||||
// bus_pointers[bus][channel] =
|
||||
// get_channel_pointer(bus, channel);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// set_bus_pointers(
|
||||
// instance.process_buffers_input_pointers,
|
||||
// instance.process_buffers->config_.input_offsets,
|
||||
// [&, &instance = instance](uint32_t bus, uint32_t channel) -> void* {
|
||||
// if (double_precision) {
|
||||
// return instance.process_buffers->input_channel_ptr<double>(
|
||||
// bus, channel);
|
||||
// } else {
|
||||
// return instance.process_buffers->input_channel_ptr<float>(
|
||||
// bus, channel);
|
||||
// }
|
||||
// });
|
||||
// set_bus_pointers(
|
||||
// instance.process_buffers_output_pointers,
|
||||
// instance.process_buffers->config_.output_offsets,
|
||||
// [&, &instance = instance](uint32_t bus, uint32_t channel) -> void* {
|
||||
// if (double_precision) {
|
||||
// return instance.process_buffers->output_channel_ptr<double>(
|
||||
// bus, channel);
|
||||
// } else {
|
||||
// return instance.process_buffers->output_channel_ptr<float>(
|
||||
// bus, channel);
|
||||
// }
|
||||
// });
|
||||
|
||||
// return buffer_config;
|
||||
// }
|
||||
|
||||
size_t ClapBridge::register_plugin_instance(const clap_plugin* plugin) {
|
||||
std::unique_lock lock(object_instances_mutex_);
|
||||
|
||||
if (!plugin) {
|
||||
throw std::invalid_argument("The plugin pointer cannot be null");
|
||||
}
|
||||
|
||||
const size_t instance_id = generate_instance_id();
|
||||
object_instances_.emplace(instance_id, plugin);
|
||||
|
||||
// Every plugin instance gets its own audio thread
|
||||
std::promise<void> socket_listening_latch;
|
||||
object_instances_.at(instance_id)
|
||||
.audio_thread_handler = Win32Thread([&, 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());
|
||||
|
||||
// TODO: Listen on the socket
|
||||
// sockets_.add_audio_processor_and_listen(
|
||||
// instance_id, socket_listening_latch,
|
||||
// overload{
|
||||
// [&](YaAudioProcessor::SetBusArrangements& request)
|
||||
// -> YaAudioProcessor::SetBusArrangements::Response {
|
||||
// const auto& [instance, _] =
|
||||
// get_instance(request.instance_id);
|
||||
|
||||
// // HACK: WA Production Imperfect CLAP somehow requires
|
||||
// // `inputs` to be a valid pointer, even if there
|
||||
// // are no inputs.
|
||||
// Steinberg::Vst::SpeakerArrangement empty_arrangement =
|
||||
// 0b00000000;
|
||||
|
||||
// return instance.interfaces.audio_processor
|
||||
// ->setBusArrangements(
|
||||
// request.num_ins > 0 ? request.inputs.data()
|
||||
// : &empty_arrangement,
|
||||
// request.num_ins,
|
||||
// request.num_outs > 0 ? request.outputs.data()
|
||||
// : &empty_arrangement,
|
||||
// request.num_outs);
|
||||
// },
|
||||
// });
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
return instance_id;
|
||||
}
|
||||
|
||||
void ClapBridge::unregister_object_instance(size_t instance_id) {
|
||||
sockets_.remove_audio_thread(instance_id);
|
||||
|
||||
// Remove the instance from within the main IO context so
|
||||
// removing it doesn't interfere with the Win32 message loop
|
||||
// XXX: I don't think we have to wait for the object to be
|
||||
// deleted most of the time, but I can imagine a situation
|
||||
// where the plugin does a host callback triggered by a
|
||||
// Win32 timer in between where the above closure is being
|
||||
// executed and when the actual host application context on
|
||||
// the plugin side gets deallocated.
|
||||
main_context_
|
||||
.run_in_context([&, instance_id]() -> void {
|
||||
std::unique_lock lock(object_instances_mutex_);
|
||||
object_instances_.erase(instance_id);
|
||||
})
|
||||
.wait();
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
// 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 details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
|
||||
#include <clap/plugin.h>
|
||||
|
||||
#include "../../common/audio-shm.h"
|
||||
#include "../../common/communication/clap.h"
|
||||
#include "../../common/configuration.h"
|
||||
#include "../../common/mutual-recursion.h"
|
||||
#include "../editor.h"
|
||||
#include "common.h"
|
||||
|
||||
/**
|
||||
* Pointers to all of a CLAP plugin's extension structs. These will be null if
|
||||
* the plugin doesn't support the extensions.
|
||||
*
|
||||
* @relates ClapPluginInstance
|
||||
*/
|
||||
struct ClapPluginExtensions {
|
||||
/**
|
||||
* Query all of the plugin's extensions. This can only be done after the
|
||||
* call to init.
|
||||
*
|
||||
* TODO: Return the supported extensions along with the init call.
|
||||
*/
|
||||
ClapPluginExtensions(const clap_plugin& plugin) noexcept;
|
||||
|
||||
// TODO: Supported extensions
|
||||
};
|
||||
|
||||
/**
|
||||
* A CLAP plugin instance. This is created when the plugin is created from the
|
||||
* plugin factory. Dropping this object will also destroy the plugin instance,
|
||||
* but it will still need to be manually unregistered from the `ClapBridge`'s
|
||||
* object instance map. The extensions object is queried after the host calls
|
||||
* the init function. Before that time all extension pointers will be null
|
||||
* pointers.
|
||||
*/
|
||||
struct ClapPluginInstance {
|
||||
/**
|
||||
* Bind a CLAP plugin pointer to this plugin instance object. This can only
|
||||
* be done once per plugin pointer. The pointer must be non-null.
|
||||
*/
|
||||
ClapPluginInstance(const clap_plugin* plugin) noexcept;
|
||||
|
||||
public:
|
||||
/**
|
||||
* A dedicated thread for handling incoming audio thread function calls.
|
||||
*/
|
||||
Win32Thread audio_thread_handler;
|
||||
|
||||
// TODO: Proxies for host extension objects
|
||||
|
||||
/**
|
||||
* A shared memory object we'll write the input audio buffers to on the
|
||||
* native plugin side. We'll then let the plugin write its outputs here on
|
||||
* the Wine side. The buffer will be configured during
|
||||
* `clap_plugin::activate()`. At that point we'll build the configuration
|
||||
* for the object here, on the Wine side, and then we'll initialize the
|
||||
* buffers using that configuration. This same configuration is then used on
|
||||
* the native plugin side to connect to this same shared memory object for
|
||||
* the matching plugin instance.
|
||||
*/
|
||||
std::optional<AudioShmBuffer> process_buffers;
|
||||
|
||||
/**
|
||||
* Pointers to the per-bus input channels in process_buffers so we can pass
|
||||
* them to the plugin after a call to `YaProcessData::reconstruct()`. These
|
||||
* can be either `float*` or `double*`, so we sadly have to use void
|
||||
* pointers here.
|
||||
*
|
||||
* FIXME: Update docstring for CLAP
|
||||
*/
|
||||
std::vector<std::vector<void*>> process_buffers_input_pointers;
|
||||
|
||||
/**
|
||||
* Pointers to the per-bus output channels in process_buffers so we can pass
|
||||
* them to the plugin after a call to `YaProcessData::reconstruct()`. These
|
||||
* can be either `float*` or `double*`, so we sadly have to use void
|
||||
* pointers here.
|
||||
*
|
||||
* FIXME: Update docstring for CLAP
|
||||
*/
|
||||
std::vector<std::vector<void*>> process_buffers_output_pointers;
|
||||
|
||||
/**
|
||||
* This instance's editor, if it has an open editor. Embedding here works
|
||||
* exactly the same as how it works for VST2 plugins.
|
||||
*/
|
||||
std::optional<Editor> editor;
|
||||
|
||||
/**
|
||||
* The plugin object. The plugin gets destroyed together with this struct.
|
||||
*/
|
||||
std::unique_ptr<const clap_plugin, decltype(clap_plugin::destroy)> plugin;
|
||||
|
||||
/**
|
||||
* Contains the plugin's supported extensions. Initialized after the host
|
||||
* calls `clap_plugin::init()`.
|
||||
*/
|
||||
ClapPluginExtensions extensions;
|
||||
|
||||
/**
|
||||
* Whether `clap_plugin::init()` has already been called for this object
|
||||
* instance. Some VST2 and VST3 plugins would have memory errors if the
|
||||
* Win32 message loop is run in between creating the plugin and initializing
|
||||
* it, so we're also preventing this for CLAP as a precaution.
|
||||
*/
|
||||
bool is_initialized = false;
|
||||
|
||||
// TODO: Add this when we add support for audio processing
|
||||
// /**
|
||||
// * The plugin's current process setup, containing information about the
|
||||
// * buffer sizes, sample rate, and processing mode. Used for setting up
|
||||
// * shared memory audio buffers, and to know whether the plugin instance
|
||||
// is
|
||||
// * currently in offline processing mode or not. The latter is needed as a
|
||||
// * HACK for IK Multimedia's T-RackS 5 because those plugins will deadlock
|
||||
// if
|
||||
// * they don't process audio from the GUI thread while doing offline
|
||||
// * processing.
|
||||
// */
|
||||
// std::optional<Steinberg::Vst::ProcessSetup> process_setup;
|
||||
};
|
||||
|
||||
/**
|
||||
* This hosts a Windows CLAP plugin, forwards messages sent by the Linux CLAP
|
||||
* plugin and provides host callback function for the plugin to talk back.
|
||||
*/
|
||||
class ClapBridge : public HostBridge {
|
||||
public:
|
||||
/**
|
||||
* Initializes the Windows CLAP plugin and set up communication with the
|
||||
* native Linux CLAP plugin.
|
||||
*
|
||||
* @param main_context The main IO context for this application. Most events
|
||||
* will be dispatched to this context, and the event handling loop should
|
||||
* also be run from this context.
|
||||
* @param plugin_dll_path A (Unix style) path to the Windows .clap file to
|
||||
* load. In yabridgectl we'll create symlinks to these using a `.clap-win`
|
||||
* file extension as CLAP uses the same file extension on Windows and
|
||||
* Linux.
|
||||
* @param endpoint_base_dir The base directory used for the socket
|
||||
* endpoints. See `Sockets` for more information.
|
||||
* @param parent_pid The process ID of the native plugin host this bridge is
|
||||
* supposed to communicate with. Used as part of our watchdog to prevent
|
||||
* dangling Wine processes.
|
||||
*
|
||||
* @note The object has to be constructed from the same thread that calls
|
||||
* `main_context.run()`.
|
||||
*
|
||||
* @throw std::runtime_error Thrown when the CLAP plugin could not be
|
||||
* loaded, or if communication could not be set up.
|
||||
*/
|
||||
ClapBridge(MainContext& main_context,
|
||||
std::string plugin_dll_path,
|
||||
std::string endpoint_base_dir,
|
||||
pid_t parent_pid);
|
||||
|
||||
/**
|
||||
* This returns `true` if `clap_plugin::init()` has not yet been called for
|
||||
* any of the registered plugins. Some VST2 and VST3 plugins have memory
|
||||
* errors if the Win32 message loop is pumped before init is called, so
|
||||
* we'll just keep the same behaviour for CLAP just in case.
|
||||
*/
|
||||
bool inhibits_event_loop() noexcept override;
|
||||
|
||||
/**
|
||||
* Here we'll listen for and handle incoming control messages until the
|
||||
* sockets get closed.
|
||||
*/
|
||||
void run() override;
|
||||
|
||||
// TODO: Editor resizing
|
||||
// /**
|
||||
// * If the plugin instance has an editor, resize the wrapper window to
|
||||
// match
|
||||
// * the new size. This is called from `IPlugFrame::resizeView()` to make
|
||||
// sure
|
||||
// * we do the resize before the request gets sent to the host.
|
||||
// */
|
||||
// bool maybe_resize_editor(size_t instance_id,
|
||||
// const Steinberg::ViewRect& new_size);
|
||||
|
||||
protected:
|
||||
void close_sockets() override;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Send a callback message to the host return the response. This is a
|
||||
* shorthand for `sockets.plugin_host_callback_.send_message` for use in
|
||||
* CLAP interface implementations.
|
||||
*/
|
||||
template <typename T>
|
||||
typename T::Response send_message(const T& object) {
|
||||
return sockets_.plugin_host_main_thread_callback_.send_message(
|
||||
object, std::nullopt);
|
||||
}
|
||||
|
||||
/**
|
||||
* When called from the GUI thread, spawn a new thread and call
|
||||
* `send_message()` from there, and then handle functions passed by calls to
|
||||
* `do_mutual_recursion_on_gui_thread()` and
|
||||
* `do_mutual_recursion_on_off_thread()` on this thread until we get a
|
||||
* response back. See the function in `Vst3Bridge` for a much more in-depth
|
||||
* explanatio nof why this is neede.d
|
||||
*
|
||||
* TODO: Is this needed for CLAP?
|
||||
*/
|
||||
template <typename T>
|
||||
typename T::Response send_mutually_recursive_message(const T& object) {
|
||||
if (main_context_.is_gui_thread()) {
|
||||
return mutual_recursion_.fork(
|
||||
[&]() { return send_message(object); });
|
||||
} else {
|
||||
// TODO: Remove if this isn't needed
|
||||
logger_.log_trace([]() {
|
||||
return "'ClapBridge::send_mutually_recursive_message()' called "
|
||||
"from a non-GUI thread, sending the message directly";
|
||||
});
|
||||
send_message(object);
|
||||
|
||||
// return audio_thread_mutual_recursion_.fork(
|
||||
// [&]() { return send_message(object); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crazy functions ask for crazy naming. This is the other part of
|
||||
* `send_mutually_recursive_message()`, for executing mutually recursive
|
||||
* functions on the GUI thread. If another thread is currently calling that
|
||||
* function (from the UI thread), then we'll execute `fn` from the UI thread
|
||||
* using the IO context started in the above function. Otherwise `f` will be
|
||||
* run on the UI thread through `main_context_` as usual.
|
||||
*
|
||||
* @see ClapBridge::send_mutually_recursive_message
|
||||
*/
|
||||
template <std::invocable F>
|
||||
std::invoke_result_t<F> do_mutual_recursion_on_gui_thread(F&& fn) {
|
||||
// If the above function is currently being called from some thread,
|
||||
// then we'll call `fn` from that same thread. Otherwise we'll just
|
||||
// submit it to the main IO context.
|
||||
if (const auto result =
|
||||
mutual_recursion_.maybe_handle(std::forward<F>(fn))) {
|
||||
return *result;
|
||||
} else {
|
||||
return main_context_.run_in_context(std::forward<F>(fn)).get();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check if this is needed, remove if it isn't
|
||||
|
||||
// /**
|
||||
// * The same as the above function, but we'll just execute the function on
|
||||
// * this thread when the mutual recursion context is not active.
|
||||
// *
|
||||
// * @see ClapBridge::do_mutual_recursion_on_gui_thread
|
||||
// */
|
||||
// template <std::invocable F>
|
||||
// std::invoke_result_t<F> do_mutual_recursion_on_off_thread(F&& fn) {
|
||||
// if (const auto result = audio_thread_mutual_recursion_.maybe_handle(
|
||||
// std::forward<F>(fn))) {
|
||||
// return *result;
|
||||
// } else {
|
||||
// return mutual_recursion_.handle(std::forward<F>(fn));
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* A logger instance we'll use to log about failed
|
||||
* `clap_host::get_extension()` calls, so they can be hidden on verbosity
|
||||
* level 0.
|
||||
*
|
||||
* This only has to be used instead of directly writing to `std::cerr` when
|
||||
* the message should be hidden on lower verbosity levels.
|
||||
*
|
||||
* TODO: Actually use this
|
||||
*/
|
||||
ClapLogger logger_;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Generate a nique instance identifier using an atomic fetch-and-add. This
|
||||
* is used to be able to refer to specific plugin instances in the messages.
|
||||
*/
|
||||
size_t generate_instance_id() noexcept;
|
||||
|
||||
/**
|
||||
* Fetch the plugin instance along with a lock valid for the instance's
|
||||
* lifetime. This is mostly just to save some boilerplate everywhere. Use
|
||||
* C++17's structured binding as syntactic sugar to not have to deal with
|
||||
* the lock handle.
|
||||
*/
|
||||
std::pair<ClapPluginInstance&, std::shared_lock<std::shared_mutex>>
|
||||
get_instance(size_t instance_id) noexcept;
|
||||
|
||||
/**
|
||||
* Sets up the shared memory audio buffers for a plugin instance plugin
|
||||
* instance and return the configuration so the native plugin can connect to
|
||||
* it as well.
|
||||
*
|
||||
* This returns a nullopt when the buffer size parameters passed to
|
||||
* `clap_plugin::activate()` have not yet been set in the
|
||||
* `ClapPluginInstance`.
|
||||
*
|
||||
* A nullopt will also be returned if this is called again after shared
|
||||
* audio buffers have been set up and the audio buffer size has not changed.
|
||||
*/
|
||||
std::optional<AudioShmBuffer::Config> setup_shared_audio_buffers(
|
||||
size_t instance_id);
|
||||
|
||||
/**
|
||||
* Assign a unique identifier to an object and add it to
|
||||
* `object_instances_`. This will also set up an audio thread socket
|
||||
* listener for the plugin instance.
|
||||
*/
|
||||
size_t register_plugin_instance(const clap_plugin* plugin);
|
||||
|
||||
/**
|
||||
* Remove an object from `object_instances_`. Will also tear down the
|
||||
* instance's audio thread.
|
||||
*/
|
||||
void unregister_object_instance(size_t instance_id);
|
||||
|
||||
/**
|
||||
* The configuration for this instance of yabridge based on the path to the
|
||||
* `.so` (or well `.clap`) file that got loaded by the host. This
|
||||
* configuration gets loaded on the plugin side, and then sent over to the
|
||||
* Wine host as part of the startup process.
|
||||
*/
|
||||
Configuration config_;
|
||||
|
||||
// TODO: Grab the entry point and factory
|
||||
// std::shared_ptr<CLAP::Hosting::Module> module_;
|
||||
|
||||
/**
|
||||
* All sockets used for communicating with this specific plugin.
|
||||
*
|
||||
* NOTE: This is defined **after** the threads on purpose. This way the
|
||||
* sockets will be closed first, and we can then safely wait for the
|
||||
* threads to exit.
|
||||
*/
|
||||
ClapSockets<Win32Thread> sockets_;
|
||||
|
||||
/**
|
||||
* Used to assign a unique identifier to created plugin instances so they
|
||||
* can be referred to later.
|
||||
*
|
||||
* @related generate_instance_id
|
||||
*/
|
||||
std::atomic_size_t current_instance_id_;
|
||||
|
||||
/**
|
||||
* These are all the objects we have created through the Windows CLAP
|
||||
* plugins' plugin factory. The keys in all of these maps are the unique
|
||||
* identifiers we generated for them so we can identify specific instances.
|
||||
* During the proxy object's destructor (on the plugin side), we'll get a
|
||||
* request to remove the corresponding plugin object from this map. This
|
||||
* will cause all pointers to it to get dropped and the object to be cleaned
|
||||
* up.
|
||||
*/
|
||||
std::unordered_map<size_t, ClapPluginInstance> object_instances_;
|
||||
/**
|
||||
* In theory all object handling is safe iff the host also doesn't do
|
||||
* anything weird even without locks. The only time a data race can occur is
|
||||
* when the host removes or inserts a plugin while also interacting with
|
||||
* other plugins on different threads. Since the lock should never be
|
||||
* contested, we should also not get a measurable performance penalty from
|
||||
* making double sure nothing can go wrong.
|
||||
*
|
||||
* TODO: At some point replace this with a multiple reader single writer
|
||||
* lock based by a spinlock. Because this lock is rarely contested
|
||||
* `get_instance()` never yields to the scheduler during audio
|
||||
* processing, but it's still something we should avoid at all costs.
|
||||
*/
|
||||
std::shared_mutex object_instances_mutex_;
|
||||
|
||||
/**
|
||||
* Used in `send_mutually_recursive_message()` to be able to execute
|
||||
* functions from that same calling thread (through
|
||||
* `do_mutual_recursion_on_gui_thread()` and
|
||||
* `do_mutual_recursion_on_off_thread()`) while we're waiting for a
|
||||
* response.
|
||||
*/
|
||||
MutualRecursionHelper<Win32Thread> mutual_recursion_;
|
||||
|
||||
// TODO: Check if this is needed, remove it if it isn't
|
||||
// /**
|
||||
// * The same thing as above, but just for the pair of
|
||||
// * `IEditController::setParamNormalized()` and
|
||||
// * `IComponentHandler::performEdit()`, when
|
||||
// * `IComponentHandler::performEdit()` is called from an audio thread.
|
||||
// *
|
||||
// * HACK: This is sadly needed to work around an interaction between a bug
|
||||
// in
|
||||
// * JUCE with a bug in Ardour/Mixbus. JUCE calls
|
||||
// * `IComponentHandler::performEdit()` from the audio thread instead
|
||||
// of
|
||||
// * using the output parameters, and Ardour/Mixbus immediately call
|
||||
// * `IEditController::setParamNormalized()` with the same value
|
||||
// after
|
||||
// * the plugin calls `IComponentHandler::performEdit()`. Both of
|
||||
// these
|
||||
// * functions need to be run on the same thread (because of
|
||||
// recursive
|
||||
// * mutexes), but they may not interfere with the GUI thread if
|
||||
// * `IComponentHandler::performEdit()` wasn't called from there.
|
||||
// */
|
||||
// MutualRecursionHelper<Win32Thread> audio_thread_mutual_recursion_;
|
||||
};
|
||||
@@ -22,6 +22,9 @@
|
||||
#include <version.h>
|
||||
|
||||
#include "../common/utils.h"
|
||||
#ifdef WITH_CLAP
|
||||
#include "bridges/clap.h"
|
||||
#endif
|
||||
#include "bridges/group.h"
|
||||
#include "bridges/vst2.h"
|
||||
#ifdef WITH_VST3
|
||||
@@ -138,6 +141,19 @@ int YABRIDGE_EXPORT
|
||||
std::unique_ptr<HostBridge> bridge;
|
||||
try {
|
||||
switch (plugin_type) {
|
||||
case PluginType::clap:
|
||||
#ifdef WITH_CLAP
|
||||
bridge = std::make_unique<ClapBridge>(
|
||||
main_context, plugin_location, socket_endpoint_path,
|
||||
parent_pid);
|
||||
#else
|
||||
std::cerr
|
||||
<< "This version of yabridge has not been compiled "
|
||||
"with CLAP support"
|
||||
<< std::endl;
|
||||
return 1;
|
||||
#endif
|
||||
break;
|
||||
case PluginType::vst2:
|
||||
bridge = std::make_unique<Vst2Bridge>(
|
||||
main_context, plugin_location, socket_endpoint_path,
|
||||
|
||||
@@ -16,6 +16,9 @@ if is_64bit_system
|
||||
wine_threads_dep,
|
||||
xcb_64bit_dep,
|
||||
]
|
||||
if with_clap
|
||||
host_64bit_deps += [clap_dep]
|
||||
endif
|
||||
if with_vst3
|
||||
host_64bit_deps += [
|
||||
vst3_sdk_hosting_wine_64bit_dep,
|
||||
@@ -41,6 +44,9 @@ if with_bitbridge
|
||||
wine_threads_dep,
|
||||
xcb_32bit_dep,
|
||||
]
|
||||
if with_clap
|
||||
host_32bit_deps += [clap_dep]
|
||||
endif
|
||||
if with_vst3
|
||||
host_32bit_deps += [
|
||||
vst3_sdk_hosting_wine_32bit_dep,
|
||||
@@ -71,6 +77,13 @@ host_sources = files(
|
||||
'xdnd-proxy.cpp',
|
||||
)
|
||||
|
||||
if with_clap
|
||||
host_sources += files(
|
||||
'../common/logging/clap.cpp',
|
||||
'bridges/clap.cpp',
|
||||
)
|
||||
endif
|
||||
|
||||
if with_vst3
|
||||
host_sources += files(
|
||||
'../common/logging/vst3.cpp',
|
||||
@@ -135,5 +148,5 @@ if with_vst3
|
||||
'bridges/vst3-impls/host-context-proxy.cpp',
|
||||
'bridges/vst3-impls/plug-frame-proxy.cpp',
|
||||
'bridges/vst3.cpp',
|
||||
)
|
||||
)
|
||||
endif
|
||||
|
||||
Reference in New Issue
Block a user