// 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 . #include "clap.h" #include #include #include // Generated inside of the build directory #include namespace fs = ghc::filesystem; ClapPluginExtensions::ClapPluginExtensions(const clap_plugin& plugin) noexcept : audio_ports(static_cast( plugin.get_extension(&plugin, CLAP_EXT_AUDIO_PORTS))), note_ports(static_cast( plugin.get_extension(&plugin, CLAP_EXT_NOTE_PORTS))) {} ClapPluginExtensions::ClapPluginExtensions() noexcept {} clap::plugin::SupportedPluginExtensions ClapPluginExtensions::supported() const noexcept { return clap::plugin::SupportedPluginExtensions{ .supports_audio_ports = audio_ports != nullptr, .supports_note_ports = note_ports != nullptr}; } ClapPluginInstance::ClapPluginInstance( const clap_plugin* plugin, std::unique_ptr host_proxy) noexcept : host_proxy(std::move(host_proxy)), plugin((assert(plugin), plugin), plugin->destroy), // We may only query the supported extensions after initializing the // plugin extensions() {} 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_), plugin_handle_(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary), entry_(plugin_handle_ ? reinterpret_cast( GetProcAddress(plugin_handle_.get(), "clap_entry")) : nullptr, [](clap_plugin_entry_t* entry) { entry->deinit(); }), sockets_(main_context.context_, endpoint_base_dir, false) { if (!plugin_handle_) { throw std::runtime_error( "Could not load the Windows .clap (.dll) file at '" + plugin_dll_path + "'"); } if (!entry_) { throw std::runtime_error( "" + plugin_dll_path + "' does not export the 'clap_entry' entry point."); } if (!clap_version_is_compatible(entry_->clap_version)) { throw std::runtime_error( "" + plugin_dll_path + "' has an incompatible CLAP version (" + std::to_string(entry_->clap_version.major) + "." + std::to_string(entry_->clap_version.minor) + "." + std::to_string(entry_->clap_version.revision) + ")."); } // CLAP plugins receive the library path in their init function. The problem // is that `plugin_dll_path` is a Linux path. This should be fine as all // Wine syscalls can work with both Windows and Linux style paths, but if // the plugin wants to manipulate the path then this may result in // unexpected behavior. Wine can convert these paths for us, but we'd get a // `WCHAR*` back which we must first convert back to UTF-8. bool init_success; WCHAR* dos_plugin_dll_path(wine_get_dos_file_name(plugin_dll_path.c_str())); if (dos_plugin_dll_path) { static_assert(sizeof(WCHAR) == sizeof(char16_t)); std::wstring_convert, char16_t> converter; const std::string converted_plugin_path = std::string(converter.to_bytes(std::u16string( reinterpret_cast(dos_plugin_dll_path)))); // This function is not optional, but if the plugin somehow does not // provide it and we'll call it anyways then the error will be less than // obvious assert(entry_->init); init_success = entry_->init(converted_plugin_path.c_str()); // Can't use regular `free()` or `unique_ptr` here HeapFree(GetProcessHeap(), 0, dos_plugin_dll_path); } else { // This should never be hit, but just in case init_success = entry_->init(plugin_dll_path.c_str()); } if (!init_success) { // `clap_entry->deinit()` is normally called when `entry_` is dropped, // but taht shouldn't happen if the entry point was never initialized. [[maybe_unused]] auto _ = entry_.release(); throw std::runtime_error("'clap_entry->init()' returned false."); } 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); sockets_.host_plugin_main_thread_control_.receive_messages( std::nullopt, overload{ [&](const WantsConfiguration&) -> WantsConfiguration::Response { // FIXME: This overload shouldn't be here, but // bitsery simply won't allow us to serialize the // variant without it. return {}; }, [&](const clap::plugin_factory::List&) -> clap::plugin_factory::List::Response { return main_context_ .run_in_context([&]() { plugin_factory_ = static_cast( entry_->get_factory(CLAP_PLUGIN_FACTORY_ID)); if (!plugin_factory_) { return clap::plugin_factory::ListResponse{ .descriptors = std::nullopt}; } std::vector descriptors; const uint32_t num_plugins = plugin_factory_->get_plugin_count(plugin_factory_); for (uint32_t i = 0; i < num_plugins; i++) { const clap_plugin_descriptor_t* descriptor = plugin_factory_->get_plugin_descriptor( plugin_factory_, i); if (!descriptor) { std::cerr << "Plugin returned a null pointer " "for plugin index " << i << "(" << num_plugins << " total), skipping..." << std::endl; continue; } descriptors.push_back(*descriptor); } return clap::plugin_factory::ListResponse{ .descriptors = descriptors}; }) .get(); }, [&](clap::plugin_factory::Create& request) -> clap::plugin_factory::Create::Response { return main_context_ .run_in_context([&]() { // This assertion should never be hit, but you can never // be too sure! assert(plugin_factory_); // We need the instance ID before the instance exists. // If creating the plugin fails then that's no problem // since we're using sparse hash maps anyways. const size_t instance_id = generate_instance_id(); auto host_proxy = std::make_unique( *this, instance_id, std::move(request.host)); const clap_plugin_t* plugin = plugin_factory_->create_plugin( plugin_factory_, host_proxy->host_vtable(), request.plugin_id.c_str()); if (plugin) { register_plugin_instance(plugin, std::move(host_proxy)); return clap::plugin_factory::CreateResponse{ .instance_id = instance_id}; } else { return clap::plugin_factory::CreateResponse{ .instance_id = std::nullopt}; } }) .get(); }, [&](clap::plugin::Init& request) -> clap::plugin::Init::Response { const auto& [instance, _] = get_instance(request.instance_id); return main_context_ .run_in_context([&, plugin = instance.plugin.get(), &instance = instance]() { // The plugin is allowed to query the same set of // extensions from our host proxy that the native host // supports instance.host_proxy->supported_extensions_ = request.supported_host_extensions; const bool result = plugin->init(plugin); if (result) { // This mimics the same behavior we had to implement // for VST2 and VST3. The Win32 message loop is // completely blocked while a plugin instance has // been created but not yet initialized. instance.is_initialized = true; // At this point we should also get the extension // pointers for the plugin's supported extensions. // In addition we'll send whether or not the plugin // supports these extensions as booleans to the // native plugin side so we can expose these same // extensions to the host. instance.extensions = ClapPluginExtensions(*plugin); return clap::plugin::InitResponse{ .result = result, // Similarly, we'll make the plugin's supported // extensions available to the host .supported_plugin_extensions = instance.extensions.supported()}; } else { return clap::plugin::InitResponse{ .result = result, .supported_plugin_extensions = {}}; } }) .get(); }, [&](clap::plugin::Destroy& request) -> clap::plugin::Destroy::Response { return main_context_ .run_in_context([&]() { // This calls `clap_plugin::destroy()` as part of // cleaning up the `unique_ptr` holding the plugin // instance pointer unregister_plugin_instance(request.instance_id); return Ack{}; }) .get(); }, [&](clap::plugin::Activate& request) -> clap::plugin::Activate::Response { const auto& [instance, _] = get_instance(request.instance_id); return main_context_ .run_in_context([&, plugin = instance.plugin.get()]() { const bool result = plugin->activate( plugin, request.sample_rate, request.min_frames_count, request.max_frames_count); const std::optional updated_audio_buffers_config = setup_shared_audio_buffers(request.instance_id, request); return clap::plugin::ActivateResponse{ .result = result, .updated_audio_buffers_config = std::move(updated_audio_buffers_config)}; }) .get(); }, [&](clap::plugin::Deactivate& request) -> clap::plugin::Deactivate::Response { const auto& [instance, _] = get_instance(request.instance_id); return main_context_ .run_in_context([&, plugin = instance.plugin.get()]() { plugin->deactivate(plugin); return Ack{}; }) .get(); }, [&](const clap::ext::audio_ports::plugin::Count& request) -> clap::ext::audio_ports::plugin::Count::Response { const auto& [instance, _] = get_instance(request.instance_id); // We'll ignore the main thread requirement for simple array // lookups to avoid the synchronisation costs in code code paths return instance.extensions.audio_ports->count( instance.plugin.get(), request.is_input); }, [&](const clap::ext::audio_ports::plugin::Get& request) -> clap::ext::audio_ports::plugin::Get::Response { const auto& [instance, _] = get_instance(request.instance_id); // We'll ignore the main thread requirement for simple array // lookups to avoid the synchronisation costs in code code paths clap_audio_port_info_t info{}; if (instance.extensions.audio_ports->get( instance.plugin.get(), request.index, request.is_input, &info)) { return clap::ext::audio_ports::plugin::GetResponse{ .result = info}; } else { return clap::ext::audio_ports::plugin::GetResponse{ .result = std::nullopt}; } }, [&](const clap::ext::note_ports::plugin::Count& request) -> clap::ext::note_ports::plugin::Count::Response { const auto& [instance, _] = get_instance(request.instance_id); // We'll ignore the main thread requirement for simple array // lookups to avoid the synchronisation costs in code code paths return instance.extensions.note_ports->count( instance.plugin.get(), request.is_input); }, [&](const clap::ext::note_ports::plugin::Get& request) -> clap::ext::note_ports::plugin::Get::Response { const auto& [instance, _] = get_instance(request.instance_id); // We'll ignore the main thread requirement for simple array // lookups to avoid the synchronisation costs in code code paths clap_note_port_info_t info{}; if (instance.extensions.note_ports->get( instance.plugin.get(), request.index, request.is_input, &info)) { return clap::ext::note_ports::plugin::GetResponse{.result = info}; } else { return clap::ext::note_ports::plugin::GetResponse{ .result = std::nullopt}; } }, }); } // 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(); } std::pair> ClapBridge::get_instance(size_t instance_id) noexcept { std::shared_lock lock(object_instances_mutex_); return std::pair>( object_instances_.at(instance_id), std::move(lock)); } size_t ClapBridge::generate_instance_id() noexcept { return current_instance_id_.fetch_add(1); } std::optional ClapBridge::setup_shared_audio_buffers( size_t instance_id, const clap::plugin::Activate& activate_request) { const auto& [instance, _] = get_instance(instance_id); const clap_plugin_t* plugin = instance.plugin.get(); const clap_plugin_audio_ports_t* audio_ports = instance.extensions.audio_ports; if (!audio_ports) { return std::nullopt; } // We'll query the plugin for its audio port 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 bytes because CLAP // allows the host to send mixed 32-bit and 64-bit audio if the plugin // advertises supporting 64-bit audio. Because of that we'll allocate enough // space for double precision audio when the port supports it, and then // we'll simply only use the first half of that space if the host sends // 32-bit audio. uint32_t current_offset = 0; auto create_bus_offsets = [&](bool is_input) { const uint32_t num_ports = audio_ports->count(plugin, is_input); std::vector> offsets(num_ports); for (uint32_t port = 0; port < num_ports; port++) { clap_audio_port_info_t info{}; assert(audio_ports->get(plugin, port, is_input, &info)); // If the audio port supports 64-bit audio, then we should allocate // enough memory for that const size_t sample_size = (info.flags & CLAP_AUDIO_PORT_SUPPORTS_64BITS) != 0 ? sizeof(double) : sizeof(float); offsets[port].resize(info.channel_count); for (size_t channel = 0; channel < info.channel_count; channel++) { offsets[port][channel] = current_offset; current_offset += activate_request.max_frames_count * sample_size; } } return offsets; }; // Creating the audio buffer offsets for every channel in every bus will // advance `current_offset` to keep pointing to the starting position for // the next channel const auto input_bus_offsets = create_bus_offsets(true); const auto output_bus_offsets = create_bus_offsets(false); const uint32_t buffer_size = current_offset; // 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; } // 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), .output_offsets = std::move(output_bus_offsets)}; 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 `ClapProcess` struct in // `ClapProcess::reconstruct()` before passing the reconstructed process // data to `clap_plugin::process()`. auto set_port_pointers = [&, &process_buffers = instance.process_buffers] F>( std::vector>& port_pointers, const std::vector>& offsets, F&& get_channel_pointer) { port_pointers.resize(offsets.size()); for (size_t port = 0; port < offsets.size(); port++) { port_pointers[port].resize(offsets[port].size()); for (size_t channel = 0; channel < offsets[port].size(); channel++) { port_pointers[port][channel] = get_channel_pointer(port, channel); } } }; set_port_pointers(instance.process_buffers_input_pointers, instance.process_buffers->config_.input_offsets, [&process_buffers = instance.process_buffers]( uint32_t port, uint32_t channel) { // This can be treated as either a `double*` or a // `float*` depending on what the port supports and // what the host gives us return process_buffers->input_channel_ptr( port, channel); }); set_port_pointers(instance.process_buffers_output_pointers, instance.process_buffers->config_.output_offsets, [&process_buffers = instance.process_buffers]( uint32_t port, uint32_t channel) { return process_buffers->output_channel_ptr( port, channel); }); return buffer_config; } void ClapBridge::register_plugin_instance( const clap_plugin* plugin, std::unique_ptr host_proxy) { std::unique_lock lock(object_instances_mutex_); assert(plugin); assert(host_proxy); // This instance ID has already been generated because the host proxy has to // be created before the plugin instance const size_t instance_id = host_proxy->owner_instance_id(); object_instances_.emplace( instance_id, ClapPluginInstance(plugin, std::move(host_proxy))); // Every plugin instance gets its own audio thread std::promise 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()); sockets_.add_audio_thread_and_listen( instance_id, socket_listening_latch, overload{ [&](const clap::plugin::StartProcessing& request) -> clap::plugin::StartProcessing::Response { const auto& [instance, _] = get_instance(request.instance_id); return instance.plugin->start_processing( instance.plugin.get()); }, [&](const clap::plugin::StopProcessing& request) -> clap::plugin::StopProcessing::Response { const auto& [instance, _] = get_instance(request.instance_id); instance.plugin->stop_processing(instance.plugin.get()); return Ack{}; }, [&](const clap::plugin::Reset& request) -> clap::plugin::Reset::Response { const auto& [instance, _] = get_instance(request.instance_id); instance.plugin->reset(instance.plugin.get()); return Ack{}; }, }); }); // 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 ClapBridge::unregister_plugin_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 // NOTE: This will implicitly run `clap_plugin::destroy()` as part of the // `unique_ptr`'s cleanup main_context_ .run_in_context([&, instance_id]() -> void { std::unique_lock lock(object_instances_mutex_); object_instances_.erase(instance_id); }) .wait(); }