mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 12:10:09 +02:00
376 lines
15 KiB
C++
376 lines
15 KiB
C++
// yabridge: a Wine VST bridge
|
|
// Copyright (C) 2020 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 "plugin-bridge.h"
|
|
|
|
#include <boost/filesystem.hpp>
|
|
#include <iostream>
|
|
|
|
#include "../common/communication.h"
|
|
#include "../common/events.h"
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
/**
|
|
* A function pointer to what should be the entry point of a VST plugin.
|
|
*/
|
|
using VstEntryPoint = AEffect*(VST_CALL_CONV*)(audioMasterCallback);
|
|
|
|
/**
|
|
* This ugly global is needed so we can get the instance of a `Brdige` class
|
|
* from an `AEffect` when it performs a host callback during its initialization.
|
|
*/
|
|
PluginBridge* current_bridge_isntance = nullptr;
|
|
|
|
intptr_t VST_CALL_CONV
|
|
host_callback_proxy(AEffect*, int, int, intptr_t, void*, float);
|
|
|
|
/**
|
|
* Fetch the Pluginbridge instance stored in one of the two pointers reserved
|
|
* for the host of the hosted VST plugin. This is sadly needed as a workaround
|
|
* to avoid using globals since we need free function pointers to interface with
|
|
* the VST C API.
|
|
*/
|
|
PluginBridge& get_bridge_instance(const AEffect* plugin) {
|
|
// This is needed during the initialization of the plugin since we can only
|
|
// add our own pointer after it's done initializing
|
|
if (current_bridge_isntance != nullptr) {
|
|
// This should only be used during initialization
|
|
assert(plugin == nullptr || plugin->ptr1 == nullptr);
|
|
return *current_bridge_isntance;
|
|
}
|
|
|
|
return *static_cast<PluginBridge*>(plugin->ptr1);
|
|
}
|
|
|
|
PluginBridge::PluginBridge(std::string plugin_dll_path,
|
|
std::string socket_endpoint_path)
|
|
: plugin_handle(LoadLibrary(plugin_dll_path.c_str()), &FreeLibrary),
|
|
io_context(),
|
|
socket_endpoint(socket_endpoint_path),
|
|
host_vst_dispatch(io_context),
|
|
host_vst_dispatch_midi_events(io_context),
|
|
vst_host_callback(io_context),
|
|
host_vst_parameters(io_context),
|
|
host_vst_process_replacing(io_context),
|
|
vst_host_aeffect(io_context),
|
|
editor("yabridge plugin") {
|
|
// Got to love these C APIs
|
|
if (plugin_handle == nullptr) {
|
|
throw std::runtime_error("Could not load a shared library at '" +
|
|
plugin_dll_path + "'.");
|
|
}
|
|
|
|
// VST plugin entry point functions should be called `VSTPluginMain`, but
|
|
// there are some older deprecated names that legacy plugins may still use
|
|
VstEntryPoint vst_entry_point = nullptr;
|
|
for (auto name : {"VSTPluginMain", "MAIN", "main_plugin"}) {
|
|
vst_entry_point =
|
|
reinterpret_cast<VstEntryPoint>(reinterpret_cast<size_t>(
|
|
GetProcAddress(plugin_handle.get(), name)));
|
|
|
|
if (vst_entry_point != nullptr) {
|
|
break;
|
|
}
|
|
}
|
|
if (vst_entry_point == nullptr) {
|
|
throw std::runtime_error(
|
|
"Could not find a valid VST entry point for '" + plugin_dll_path +
|
|
"'.");
|
|
}
|
|
|
|
// It's very important that these sockets are accepted to in the same order
|
|
// in the Linux plugin
|
|
host_vst_dispatch.connect(socket_endpoint);
|
|
host_vst_dispatch_midi_events.connect(socket_endpoint);
|
|
vst_host_callback.connect(socket_endpoint);
|
|
host_vst_parameters.connect(socket_endpoint);
|
|
host_vst_process_replacing.connect(socket_endpoint);
|
|
vst_host_aeffect.connect(socket_endpoint);
|
|
|
|
// Initialize after communication has been set up
|
|
// We'll try to do the same `get_bridge_isntance` trick as in
|
|
// `plugin/plugin.cpp`, but since the plugin will probably call the host
|
|
// callback while it's initializing we sadly have to use a global here.
|
|
current_bridge_isntance = this;
|
|
plugin = vst_entry_point(host_callback_proxy);
|
|
if (plugin == nullptr) {
|
|
throw std::runtime_error("VST plugin at '" + plugin_dll_path +
|
|
"' failed to initialize.");
|
|
}
|
|
|
|
// We only needed this little hack during initialization
|
|
current_bridge_isntance = nullptr;
|
|
plugin->ptr1 = this;
|
|
|
|
// Send the plugin's information to the Linux VST plugin. Any updates during
|
|
// runtime are handled using the `audioMasterIOChanged` host callback.
|
|
write_object(vst_host_aeffect, *plugin);
|
|
|
|
// This works functionally identically to the `handle_dispatch()` function
|
|
// below, but this socket will only handle midi events. This is needed
|
|
// because of Win32 API limitations.
|
|
dispatch_midi_events_handler = std::thread([&]() {
|
|
while (true) {
|
|
passthrough_event(host_vst_dispatch_midi_events, std::nullopt,
|
|
plugin, plugin->dispatcher);
|
|
}
|
|
});
|
|
|
|
parameters_handler = std::thread([&]() {
|
|
while (true) {
|
|
// Both `getParameter` and `setParameter` functions are passed
|
|
// through on this socket since they have a lot of overlap. The
|
|
// presence of the `value` field tells us which one we're dealing
|
|
// with.
|
|
auto request = read_object<Parameter>(host_vst_parameters);
|
|
if (request.value.has_value()) {
|
|
// `setParameter`
|
|
plugin->setParameter(plugin, request.index,
|
|
request.value.value());
|
|
|
|
ParameterResult response{std::nullopt};
|
|
write_object(host_vst_parameters, response);
|
|
} else {
|
|
// `getParameter`
|
|
float value = plugin->getParameter(plugin, request.index);
|
|
|
|
ParameterResult response{value};
|
|
write_object(host_vst_parameters, response);
|
|
}
|
|
}
|
|
});
|
|
|
|
process_replacing_handler = std::thread([&]() {
|
|
while (true) {
|
|
AudioBuffers request;
|
|
request = read_object(host_vst_process_replacing, request,
|
|
process_buffer);
|
|
|
|
// TODO: Check if the plugin doesn't support `processReplacing` and
|
|
// call the legacy `process` function instead
|
|
std::vector<std::vector<float>> output_buffers(
|
|
plugin->numOutputs, std::vector<float>(request.sample_frames));
|
|
|
|
// The process functions expect a `float**` for their inputs and
|
|
// their outputs
|
|
std::vector<float*> inputs;
|
|
for (auto& buffer : request.buffers) {
|
|
inputs.push_back(buffer.data());
|
|
}
|
|
std::vector<float*> outputs;
|
|
for (auto& buffer : output_buffers) {
|
|
outputs.push_back(buffer.data());
|
|
}
|
|
|
|
plugin->processReplacing(plugin, inputs.data(), outputs.data(),
|
|
request.sample_frames);
|
|
|
|
AudioBuffers response{output_buffers, request.sample_frames};
|
|
write_object(host_vst_process_replacing, response, process_buffer);
|
|
}
|
|
});
|
|
|
|
std::cout << "Finished initializing '" << plugin_dll_path << "'"
|
|
<< std::endl;
|
|
}
|
|
|
|
void PluginBridge::handle_dispatch() {
|
|
using namespace std::placeholders;
|
|
|
|
// For our communication we use simple threads and blocking operations
|
|
// instead of asynchronous IO since communication has to be handled in
|
|
// lockstep anyway
|
|
try {
|
|
while (true) {
|
|
passthrough_event(host_vst_dispatch, std::nullopt, plugin,
|
|
std::bind(&PluginBridge::dispatch_wrapper, this,
|
|
_1, _2, _3, _4, _5, _6));
|
|
}
|
|
} catch (const boost::system::system_error&) {
|
|
// This happens when the sockets got closed because the plugin is being
|
|
// shut down. In that case we can just let the whole host terminate.
|
|
dispatch_midi_events_handler.detach();
|
|
parameters_handler.detach();
|
|
process_replacing_handler.detach();
|
|
}
|
|
}
|
|
|
|
intptr_t PluginBridge::dispatch_wrapper(AEffect* plugin,
|
|
int opcode,
|
|
int index,
|
|
intptr_t value,
|
|
void* data,
|
|
float option) {
|
|
// We have to intercept GUI open calls since we can't use
|
|
// the X11 window handle passed by the host
|
|
switch (opcode) {
|
|
case effEditIdle:
|
|
// Because of the way the Win32 API works we have to process events
|
|
// on the same thread the window was created, and that thread is the
|
|
// thread that's handling dispatcher calls
|
|
// To allow the GUI to update even when this thread gets blocked
|
|
// (e.g. when a dropdown is open), the actual `effEditIdle` event
|
|
// gets sent to the plugin on a timer.
|
|
editor.handle_events();
|
|
|
|
return 1;
|
|
break;
|
|
case effClose: {
|
|
// Closing the editor will also shut down the thread that's
|
|
// currently handling events
|
|
editor.close();
|
|
|
|
return plugin->dispatcher(plugin, opcode, index, value, data,
|
|
option);
|
|
} break;
|
|
case effEditOpen: {
|
|
const auto win32_handle = editor.open(plugin);
|
|
|
|
const auto return_value = plugin->dispatcher(
|
|
plugin, opcode, index, value, win32_handle, option);
|
|
if (return_value == 0) {
|
|
return 0;
|
|
}
|
|
|
|
// If opening the editor was succesful, reparent it to the window
|
|
// provided by the DAW
|
|
const auto x11_handle = reinterpret_cast<size_t>(data);
|
|
editor.embed_into(x11_handle);
|
|
|
|
return return_value;
|
|
} break;
|
|
case effEditClose: {
|
|
const intptr_t return_value =
|
|
plugin->dispatcher(plugin, opcode, index, value, data, option);
|
|
|
|
editor.close();
|
|
|
|
return return_value;
|
|
} break;
|
|
default:
|
|
return plugin->dispatcher(plugin, opcode, index, value, data,
|
|
option);
|
|
break;
|
|
}
|
|
}
|
|
|
|
class HostCallbackDataConverter : DefaultDataConverter {
|
|
public:
|
|
HostCallbackDataConverter(AEffect* plugin,
|
|
Editor& editor,
|
|
VstTimeInfo& time_info)
|
|
: plugin(plugin), editor(editor), time_info(time_info) {}
|
|
|
|
std::optional<EventPayload> read(const int opcode,
|
|
const int index,
|
|
const intptr_t value,
|
|
const void* data) {
|
|
switch (opcode) {
|
|
// Some hsots will outright crash if they receive this opcode, not
|
|
// sure why they don't just ignore it. Please let me know if there's
|
|
// a better way to handle this instead of just ignoring the event!
|
|
//
|
|
// TODO: Filtering these two events fixes crashes, but should this
|
|
// be needed? `audioMasterWantMidi` is deprecated though.
|
|
case audioMasterWantMidi:
|
|
case audioMasterUpdateDisplay:
|
|
std::cerr << "Got opcode "
|
|
<< opcode_to_string(false, opcode)
|
|
.value_or(std::to_string(opcode))
|
|
<< "), ignoring..." << std::endl;
|
|
|
|
return std::nullopt;
|
|
break;
|
|
case audioMasterGetTime:
|
|
return WantsVstTimeInfo{};
|
|
break;
|
|
case audioMasterSizeWindow:
|
|
// Plugins use this opcode to indicate that their editor should
|
|
// be resized. This is handled implicitly when handling the
|
|
// ConfigureNotify X11 events but handling this here as well
|
|
// makes the resizing look much smoother.
|
|
// TODO: Check if this actually makes drag resizing feel better,
|
|
// otherwise just remove this
|
|
editor.resize(value, index);
|
|
|
|
return DefaultDataConverter::read(opcode, index, value, data);
|
|
break;
|
|
case audioMasterIOChanged:
|
|
// This is a helpful event that indicates that the VST plugin's
|
|
// `AEffect` struct has changed. Writing these results back is
|
|
// done inside of `passthrough_event`.
|
|
return AEffect(*plugin);
|
|
break;
|
|
default:
|
|
return DefaultDataConverter::read(opcode, index, value, data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void write(const int opcode, void* data, const EventResult& response) {
|
|
switch (opcode) {
|
|
case audioMasterGetTime:
|
|
// Write the returned `VstTimeInfo` struct into a field and make
|
|
// the function return a poitner to it in the function below
|
|
time_info = std::get<VstTimeInfo>(response.payload);
|
|
break;
|
|
default:
|
|
DefaultDataConverter::write(opcode, data, response);
|
|
break;
|
|
}
|
|
}
|
|
|
|
intptr_t return_value(const int opcode, const intptr_t original) {
|
|
switch (opcode) {
|
|
case audioMasterGetTime:
|
|
// Return a pointer to the `VstTimeInfo` object written in the
|
|
// function above
|
|
return reinterpret_cast<intptr_t>(&time);
|
|
break;
|
|
default:
|
|
return DefaultDataConverter::return_value(opcode, original);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
AEffect* plugin;
|
|
Editor& editor;
|
|
VstTimeInfo& time_info;
|
|
};
|
|
|
|
intptr_t PluginBridge::host_callback(AEffect* effect,
|
|
int opcode,
|
|
int index,
|
|
intptr_t value,
|
|
void* data,
|
|
float option) {
|
|
HostCallbackDataConverter converter(effect, editor, time_info);
|
|
return send_event(vst_host_callback, host_callback_semaphore, converter,
|
|
std::nullopt, opcode, index, value, data, option);
|
|
}
|
|
|
|
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
|
|
int opcode,
|
|
int index,
|
|
intptr_t value,
|
|
void* data,
|
|
float option) {
|
|
return get_bridge_instance(effect).host_callback(effect, opcode, index,
|
|
value, data, option);
|
|
}
|