Files
yabridge/src/plugin/bridges/vst3-impls/plugin-proxy.cpp
T
Robbert van der Helm a1c9d0fbf5 Report a rolling minimum and maximum instead
Over the last 2048 processing cycles. Should give a better idea of the
spread.
2021-06-24 11:38:30 +02:00

1436 lines
55 KiB
C++

// yabridge: a Wine VST bridge
// Copyright (C) 2020-2021 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-proxy.h"
#include "plug-view-proxy.h"
using namespace std::literals::chrono_literals;
/**
* When the host tries to connect two plugin instances with connection proxies,
* we'll first try to bypass that proxy. This goes against the idea of yabridge,
* but these proxies can make things very difficult when plugins start sending
* messages from the GUI thread. If we cannot figure out what we're connected
* to, we'll still proxy the host's connection proxy.
*/
constexpr char other_instance_message_id[] = "yabridge_other_instance";
/**
* In the message described above we'll use this attribute to pass through a
* pointer to the sender of the message. This will the other side set the
* `connected_instance_id` field on the other object to the instance ID of that
* connected object. This will let us bypass the connection proxy since we can
* then just connect the two objects directly.
*/
constexpr char other_instance_pointer_attribute[] = "other_proxy_ptr";
/**
* The time between reports for the mean processing time.
*/
constexpr std::chrono::high_resolution_clock::duration report_interval = 5s;
Vst3PluginProxyImpl::ContextMenu::ContextMenu(
Steinberg::IPtr<Steinberg::Vst::IContextMenu> menu)
: menu(menu) {}
Vst3PluginProxyImpl::Vst3PluginProxyImpl(Vst3PluginBridge& bridge,
Vst3PluginProxy::ConstructArgs&& args)
: Vst3PluginProxy(std::move(args)),
bridge(bridge),
last_report(std::chrono::high_resolution_clock::now()) {
bridge.register_plugin_proxy(*this);
}
Vst3PluginProxyImpl::~Vst3PluginProxyImpl() noexcept {
// NOTE: This can actually throw (e.g. out of memory or the socket got
// closed). But if that were to happen, then we wouldn't be able to
// recover from it anyways.
bridge.send_message(
Vst3PluginProxy::Destruct{.instance_id = instance_id()});
bridge.unregister_plugin_proxy(*this);
}
tresult PLUGIN_API
Vst3PluginProxyImpl::queryInterface(const Steinberg::TUID _iid, void** obj) {
const tresult result = Vst3PluginProxy::queryInterface(_iid, obj);
bridge.logger.log_query_interface("In FUnknown::queryInterface()", result,
Steinberg::FUID::fromTUID(_iid));
return result;
}
size_t Vst3PluginProxyImpl::register_context_menu(
Steinberg::IPtr<Steinberg::Vst::IContextMenu> menu) {
std::lock_guard lock(context_menus_mutex);
const size_t context_menu_id = current_context_menu_id.fetch_add(1);
context_menus.emplace(context_menu_id, menu);
return context_menu_id;
}
/**
* Unregister a context menu using the ID generated by a previous call to
* `register_context_menu()`. This will release the context menu object
* returned by the host.
*/
bool Vst3PluginProxyImpl::unregister_context_menu(size_t context_menu_id) {
std::lock_guard lock(context_menus_mutex);
return context_menus.erase(context_menu_id);
}
void Vst3PluginProxyImpl::clear_caches() noexcept {
clear_bus_cache();
std::lock_guard lock(function_result_cache_mutex);
function_result_cache = FunctionResultCache{};
}
tresult PLUGIN_API Vst3PluginProxyImpl::setAudioPresentationLatencySamples(
Steinberg::Vst::BusDirection dir,
int32 busIndex,
uint32 latencyInSamples) {
return bridge.send_message(
YaAudioPresentationLatency::SetAudioPresentationLatencySamples{
.instance_id = instance_id(),
.dir = dir,
.bus_index = busIndex,
.latency_in_samples = latencyInSamples});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setBusArrangements(
Steinberg::Vst::SpeakerArrangement* inputs,
int32 numIns,
Steinberg::Vst::SpeakerArrangement* outputs,
int32 numOuts) {
clear_bus_cache();
// NOTE: Ardour passes a null pointer when `numIns` or `numOuts` is 0, so we
// need to work around that
return bridge.send_audio_processor_message(
YaAudioProcessor::SetBusArrangements{
.instance_id = instance_id(),
.inputs =
(inputs ? std::vector<Steinberg::Vst::SpeakerArrangement>(
inputs, &inputs[numIns])
: std::vector<Steinberg::Vst::SpeakerArrangement>()),
.num_ins = numIns,
.outputs =
(outputs ? std::vector<Steinberg::Vst::SpeakerArrangement>(
outputs, &outputs[numOuts])
: std::vector<Steinberg::Vst::SpeakerArrangement>()),
.num_outs = numOuts,
});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getBusArrangement(
Steinberg::Vst::BusDirection dir,
int32 index,
Steinberg::Vst::SpeakerArrangement& arr) {
const GetBusArrangementResponse response =
bridge.send_audio_processor_message(YaAudioProcessor::GetBusArrangement{
.instance_id = instance_id(), .dir = dir, .index = index});
arr = response.arr;
return response.result;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::canProcessSampleSize(int32 symbolicSampleSize) {
const auto request = YaAudioProcessor::CanProcessSampleSize{
.instance_id = instance_id(),
.symbolic_sample_size = symbolicSampleSize};
{
std::lock_guard lock(function_result_cache_mutex);
if (auto it = function_result_cache.can_process_sample_size.find(
symbolicSampleSize);
it != function_result_cache.can_process_sample_size.end()) {
const bool log_response = bridge.logger.log_request(true, request);
if (log_response) {
bridge.logger.log_response(
true,
YaAudioProcessor::CanProcessSampleSize::Response(
it->second),
true);
}
return it->second;
}
}
const tresult result = bridge.send_audio_processor_message(request);
{
std::lock_guard lock(function_result_cache_mutex);
function_result_cache.can_process_sample_size[symbolicSampleSize] =
result;
}
return result;
}
uint32 PLUGIN_API Vst3PluginProxyImpl::getLatencySamples() {
return bridge.send_audio_processor_message(
YaAudioProcessor::GetLatencySamples{.instance_id = instance_id()});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setupProcessing(Steinberg::Vst::ProcessSetup& setup) {
const YaAudioProcessor::SetupProcessingResponse response =
bridge.send_audio_processor_message(YaAudioProcessor::SetupProcessing{
.instance_id = instance_id(), .setup = setup});
// We have now set up the shared audio buffers on the Wine side, and we'll
// be able to able to connect to them by using the same audio configuration
if (!process_buffers) {
process_buffers.emplace(response.audio_buffers_config);
} else {
process_buffers->resize(response.audio_buffers_config);
}
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::setProcessing(TBool state) {
// REAPER used to repeatedly query the plugin for its bus information on
// every processing cycle. Because this really adds up in terms of latency
// we sadly have to deviate from yabridge's principles and implement a
// cache. We keep this in because it can still help performance a little in
// some DAWs.
{
std::lock_guard lock(processing_bus_cache_mutex);
if (state) {
processing_bus_cache.emplace();
} else {
processing_bus_cache.reset();
}
}
return bridge.send_audio_processor_message(YaAudioProcessor::SetProcessing{
.instance_id = instance_id(), .state = state});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::process(Steinberg::Vst::ProcessData& data) {
if (const auto& now = std::chrono::high_resolution_clock::now();
now - last_report >= report_interval) {
bridge.logger.log(
"Mean processing time: " +
std::to_string(
std::chrono::duration_cast<
std::chrono::duration<float, std::micro>>(mean_process_time)
.count()) +
" us");
if (process_time_ring_buffer_wrapped_around) {
// We only report these values every few seconds, so we don't need
// to be clever with keeping rolling minima and maxima and we can
// just linearly iterate over everything every now and then
std::chrono::high_resolution_clock::duration min_process_time =
process_time_ring_buffer[0];
std::chrono::high_resolution_clock::duration max_process_time =
process_time_ring_buffer[0];
for (size_t i = 1; i < process_time_ring_buffer.size(); i++) {
if (process_time_ring_buffer[i] < min_process_time) {
min_process_time = process_time_ring_buffer[i];
} else if (process_time_ring_buffer[i] > max_process_time) {
max_process_time = process_time_ring_buffer[i];
}
}
bridge.logger.log(
"Min processing time: " +
std::to_string(std::chrono::duration_cast<
std::chrono::duration<float, std::micro>>(
min_process_time)
.count()) +
" us");
bridge.logger.log(
"Max processing time: " +
std::to_string(std::chrono::duration_cast<
std::chrono::duration<float, std::micro>>(
max_process_time)
.count()) +
" us");
} else {
bridge.logger.log("Min processing time: <still warming up>");
bridge.logger.log("Max processing time: <still warming up>");
}
last_report = now;
}
// Doing this twice is a bit of a waste, but we don't want to measure IO
const auto& process_start = std::chrono::high_resolution_clock::now();
// We'll synchronize the scheduling priority of the audio thread on the Wine
// plugin host with that of the host's audio thread every once in a while
std::optional<int> new_realtime_priority = std::nullopt;
time_t now = time(nullptr);
if (now > last_audio_thread_priority_synchronization +
audio_thread_priority_synchronization_interval) {
new_realtime_priority = get_realtime_priority();
last_audio_thread_priority_synchronization = now;
}
// We reuse this existing object to avoid allocations.
// `YaProcessData::repopulate()` will write the input audio to the shared
// audio buffers, so they're not stored within the request object itself.
assert(process_buffers);
process_request.instance_id = instance_id();
process_request.data.repopulate(data, *process_buffers);
process_request.new_realtime_priority = new_realtime_priority;
// HACK: This is a bit ugly. This `YaProcessData::Response` object actually
// contains pointers to the corresponding `YaProcessData` fields in
// this object, so we can only send back the fields that are actually
// relevant. This is necessary to avoid allocating copies or moves on
// the Wine side. This `create_response()` function creates a response
// object that points to the fields in `process_request.data`, so when
// we deserialize into `process_response` we end up actually writing
// to the actual `process_request.data` object. Thus we can also call
// `process_request.data.write_back_outputs()` later.
//
// `YaProcessData::Response::serialize()` should make this a lot
// clearer.
process_response.output_data = process_request.data.create_response();
// We'll also receive the response into an existing object so we can also
// avoid heap allocations there
bridge.receive_audio_processor_message_into(
MessageReference<YaAudioProcessor::Process>(process_request),
process_response);
// At this point the shared audio buffers should contain the output audio,
// so we'll write that back to the host along with any metadata (which in
// practice are only the silence flags), as well as any output parameter
// changes and events
process_request.data.write_back_outputs(data, *process_buffers);
const auto& process_end = std::chrono::high_resolution_clock::now();
const auto& process_time = process_end - process_start;
mean_process_time = std::chrono::duration_cast<
std::chrono::high_resolution_clock::duration>(
(process_time * 0.05) + (mean_process_time * 0.95));
// For the minma and maxima we keep the last `process_time_ring_buffer_size`
// timings around so we can compute these during the report
process_time_ring_buffer[process_time_ring_buffer_pos] = process_time;
process_time_ring_buffer_pos += 1;
if (process_time_ring_buffer_pos >= process_time_ring_buffer.size()) {
process_time_ring_buffer_pos = 0;
process_time_ring_buffer_wrapped_around = true;
}
return process_response.result;
}
uint32 PLUGIN_API Vst3PluginProxyImpl::getTailSamples() {
return bridge.send_audio_processor_message(
YaAudioProcessor::GetTailSamples{.instance_id = instance_id()});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setAutomationState(int32 state) {
return bridge.send_message(YaAutomationState::SetAutomationState{
.instance_id = instance_id(), .state = state});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getControllerClassId(Steinberg::TUID classId) {
if (classId) {
const GetControllerClassIdResponse response =
bridge.send_audio_processor_message(
YaComponent::GetControllerClassId{.instance_id =
instance_id()});
ArrayUID native_uid = response.editor_cid.get_native_uid();
std::copy(native_uid.begin(), native_uid.end(), classId);
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IComponent::getControllerClassId()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::setIoMode(Steinberg::Vst::IoMode mode) {
return bridge.send_audio_processor_message(
YaComponent::SetIoMode{.instance_id = instance_id(), .mode = mode});
}
int32 PLUGIN_API
Vst3PluginProxyImpl::getBusCount(Steinberg::Vst::MediaType type,
Steinberg::Vst::BusDirection dir) {
const auto request = YaComponent::GetBusCount{
.instance_id = instance_id(), .type = type, .dir = dir};
std::tuple<Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection> args{
type, dir};
{
std::lock_guard lock(processing_bus_cache_mutex);
if (processing_bus_cache) {
if (auto it = processing_bus_cache->bus_count.find(args);
it != processing_bus_cache->bus_count.end()) {
const bool log_response =
bridge.logger.log_request(true, request);
if (log_response) {
bridge.logger.log_response(
true, YaComponent::GetBusCount::Response(it->second),
true);
}
return it->second;
}
}
}
const int32 result = bridge.send_audio_processor_message(request);
{
std::lock_guard lock(processing_bus_cache_mutex);
if (processing_bus_cache) {
processing_bus_cache->bus_count[args] = result;
}
}
return result;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getBusInfo(Steinberg::Vst::MediaType type,
Steinberg::Vst::BusDirection dir,
int32 index,
Steinberg::Vst::BusInfo& bus /*out*/) {
const auto request = YaComponent::GetBusInfo{
.instance_id = instance_id(), .type = type, .dir = dir, .index = index};
std::tuple<Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection, int32>
args{type, dir, index};
{
std::lock_guard lock(processing_bus_cache_mutex);
if (processing_bus_cache) {
if (auto it = processing_bus_cache->bus_info.find(args);
it != processing_bus_cache->bus_info.end()) {
const bool log_response =
bridge.logger.log_request(true, request);
if (log_response) {
bridge.logger.log_response(
false,
YaComponent::GetBusInfo::Response{
.result = Steinberg::kResultOk, .bus = it->second},
true);
}
bus = it->second;
return Steinberg::kResultOk;
}
}
}
const GetBusInfoResponse response =
bridge.send_audio_processor_message(request);
bus = response.bus;
{
std::lock_guard lock(processing_bus_cache_mutex);
if (processing_bus_cache) {
processing_bus_cache->bus_info[args] = response.bus;
}
}
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::getRoutingInfo(
Steinberg::Vst::RoutingInfo& inInfo,
Steinberg::Vst::RoutingInfo& outInfo /*out*/) {
const GetRoutingInfoResponse response =
bridge.send_audio_processor_message(YaComponent::GetRoutingInfo{
.instance_id = instance_id(), .in_info = inInfo});
outInfo = response.out_info;
return response.result;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::activateBus(Steinberg::Vst::MediaType type,
Steinberg::Vst::BusDirection dir,
int32 index,
TBool state) {
return bridge.send_audio_processor_message(
YaComponent::ActivateBus{.instance_id = instance_id(),
.type = type,
.dir = dir,
.index = index,
.state = state});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setActive(TBool state) {
// HACK: Even though we initially implemented this cache specifically for
// REAPER, REAPER doesn't use `IComponent::setProcessing` properly and
// calls it before doing setting up input and output busses. So now
// our workaround to get acceptable performance in REAPER needs a
// workaround of its ownn. Great!
clear_bus_cache();
return bridge.send_audio_processor_message(
YaComponent::SetActive{.instance_id = instance_id(), .state = state});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setState(Steinberg::IBStream* state) {
if (state) {
// Since both interfaces contain this function, this is used for both
// `IComponent::setState()` as well as `IEditController::setState()`
// NOTE: This will likely be called from the GUI thread, and some
// plugins will try to resize as part of setting their new state.
// That `IPlugFrame::resizeView()` _also_ has to be handled on the
// GUI thread. So if the GUI is active, we'll use the mutual
// recursion mechanism to allow this resize call to also be
// performed from the GUI thread.
return bridge.send_mutually_recursive_message(Vst3PluginProxy::SetState{
.instance_id = instance_id(), .state = state});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'I{Component,EditController}::setState()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getState(Steinberg::IBStream* state) {
if (state) {
// Since both interfaces contain this function, this is used for both
// `IComponent::getState()` as well as `IEditController::getState()`
// NOTE: This will likely be called from the GUI thread, if the plugin
// somehow ends up sending a resize while this happens, we should
// end up in a deadlock. Normally this wouldn't be an issue, but
// REAPER will fetch the complete plugin state from time to time,
// so when changing a parameter also resizes the GUI we can run
// into a situation where we need mutually recursive function
// calls.
const GetStateResponse response =
bridge.send_mutually_recursive_message(Vst3PluginProxy::GetState{
.instance_id = instance_id(), .state = state});
assert(response.state.write_back(state) == Steinberg::kResultOk);
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'I{Component,EditController}::getState()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::connect(IConnectionPoint* other) {
if (!other) {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IConnectionPointProxy::connect()'");
return Steinberg::kInvalidArgument;
}
// When the host is trying to connect two plugin proxy objects, we can just
// identify the other object by its instance IDs and then connect the
// objects in the Wine plugin host directly. If this is not possible, we'll
// still try to bypass the proxy and connect the object directly. That goes
// against the principles of yabridge, but the alternative is nearly
// impossible to pull off correctly because FabFilter VST3 plugins will call
// `IConnectionPoint::notify()` from the GUI thread to communicate between
// the processor and the edit controller. If we try to handle those mutually
// recursive function calls from the GUI thread, then we'll still run into
// issues when using multiple instances of the plugin. If we cannot figure
// out which object the plugins are connected to, we'll still proxy the
// host's connection proxy.
if (auto other_instance = dynamic_cast<Vst3PluginProxy*>(other)) {
connected_instance_id = other_instance->instance_id();
return bridge.send_message(
YaConnectionPoint::Connect{.instance_id = instance_id(),
.other = other_instance->instance_id()});
}
// As mentioned above, we'll first try to bypass the connection point proxy
// and connect the objects directly
if (host_application) {
Steinberg::IPtr<Steinberg::Vst::IMessage> message =
Steinberg::owned(Steinberg::Vst::allocateMessage(host_application));
if (message) {
message->setMessageID(other_instance_message_id);
Steinberg::IPtr<Steinberg::Vst::IAttributeList> attributes =
message->getAttributes();
if (attributes) {
attributes->setInt(
other_instance_pointer_attribute,
static_cast<int64>(reinterpret_cast<size_t>(this)));
}
// If we are connected with another object instance from this
// plugin, `connected_instance_id` should now be set
other->notify(message);
if (connected_instance_id) {
return bridge.send_message(YaConnectionPoint::Connect{
.instance_id = instance_id(),
.other = *connected_instance_id});
}
}
}
// If we cannot bypass the proxy, we'll just proxy the host's proxy
connection_point_proxy = other;
return bridge.send_message(YaConnectionPoint::Connect{
.instance_id = instance_id(),
.other =
Vst3ConnectionPointProxy::ConstructArgs(other, instance_id())});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::disconnect(IConnectionPoint* /*other*/) {
// See `Vst3PluginProxyImpl::connect()`, if we directly connected two
// instances we'll also disconnect them again
if (connected_instance_id) {
return bridge.send_message(YaConnectionPoint::Disconnect{
.instance_id = instance_id(),
.other_instance_id = *connected_instance_id});
} else {
const tresult result = bridge.send_message(
YaConnectionPoint::Disconnect{.instance_id = instance_id(),
.other_instance_id = std::nullopt});
connection_point_proxy.reset();
return result;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::notify(Steinberg::Vst::IMessage* message) {
// Since there is no way to enumerate over all values in an
// `IAttributeList`, we can only support relaying messages that were sent by
// our own objects. Additionally, the `IMessage*` we end up passing to the
// plugin needs to have the same lifetime as the original object, because
// some plugins are being a bit naughty. That's why we pass around a pointer
// to the original message object.
// All of this is only needed to support hosts that place a connection proxy
// between two objects instead of connecting them directly. If the objects
// are connected directly we also connected them directly on the Wine side,
// so we don't have to do any additional when those objects pass through
// messages.
if (auto message_ptr = dynamic_cast<YaMessagePtr*>(message)) {
return bridge.send_message(YaConnectionPoint::Notify{
.instance_id = instance_id(), .message_ptr = *message_ptr});
}
// NOTE: As mentioned above, when the host (or specifically, Ardour or
// Mixbus) calls `IConnectionPoint::connect()`, we'll try to bypass
// the connection proxy since this creates some difficult situations
// when plugins start calling `IConnectionPoint::notify()` from the
// GUI thread
if (message &&
strcmp(message->getMessageID(), other_instance_message_id) == 0) {
Steinberg::IPtr<Steinberg::Vst::IAttributeList> attributes =
message->getAttributes();
if (attributes) {
int64 other_object_ptr;
if (attributes->getInt(other_instance_pointer_attribute,
other_object_ptr) == Steinberg::kResultOk &&
other_object_ptr != 0) {
Vst3PluginProxyImpl& other_object =
*reinterpret_cast<Vst3PluginProxyImpl*>(
static_cast<size_t>(other_object_ptr));
other_object.connected_instance_id = instance_id();
return Steinberg::kResultOk;
}
}
}
bridge.logger.log(
"WARNING: Unknown message type passed to "
"'IConnectionPoint::notify()', ignoring");
return Steinberg::kNotImplemented;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setComponentState(Steinberg::IBStream* state) {
if (state) {
return bridge.send_message(YaEditController::SetComponentState{
.instance_id = instance_id(), .state = state});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IEditController::setComponentState()'");
return Steinberg::kInvalidArgument;
}
}
int32 PLUGIN_API Vst3PluginProxyImpl::getParameterCount() {
const auto request =
YaEditController::GetParameterCount{.instance_id = instance_id()};
{
std::lock_guard lock(function_result_cache_mutex);
if (function_result_cache.parameter_count) {
const bool log_response = bridge.logger.log_request(true, request);
if (log_response) {
bridge.logger.log_response(
true,
YaEditController::GetParameterCount::Response(
*function_result_cache.parameter_count),
true);
}
return *function_result_cache.parameter_count;
}
}
const int32 result = bridge.send_message(request);
{
std::lock_guard lock(function_result_cache_mutex);
function_result_cache.parameter_count = result;
}
return result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::getParameterInfo(
int32 paramIndex,
Steinberg::Vst::ParameterInfo& info /*out*/) {
const auto request = YaEditController::GetParameterInfo{
.instance_id = instance_id(), .param_index = paramIndex};
{
std::lock_guard lock(function_result_cache_mutex);
if (auto it = function_result_cache.parameter_info.find(paramIndex);
it != function_result_cache.parameter_info.end()) {
const bool log_response = bridge.logger.log_request(true, request);
if (log_response) {
bridge.logger.log_response(
true,
YaEditController::GetParameterInfo::Response{
.result = Steinberg::kResultOk, .info = it->second},
true);
}
info = it->second;
return Steinberg::kResultOk;
}
}
const GetParameterInfoResponse response = bridge.send_message(request);
info = response.info;
{
std::lock_guard lock(function_result_cache_mutex);
function_result_cache.parameter_info[paramIndex] = response.info;
}
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::getParamStringByValue(
Steinberg::Vst::ParamID id,
Steinberg::Vst::ParamValue valueNormalized /*in*/,
Steinberg::Vst::String128 string /*out*/) {
if (string) {
const GetParamStringByValueResponse response =
bridge.send_message(YaEditController::GetParamStringByValue{
.instance_id = instance_id(),
.id = id,
.value_normalized = valueNormalized});
std::copy(response.string.begin(), response.string.end(), string);
string[response.string.size()] = 0;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IEditController::getParamStringByValue()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getParamValueByString(
Steinberg::Vst::ParamID id,
Steinberg::Vst::TChar* string /*in*/,
Steinberg::Vst::ParamValue& valueNormalized /*out*/) {
if (string) {
const GetParamValueByStringResponse response =
bridge.send_message(YaEditController::GetParamValueByString{
.instance_id = instance_id(), .id = id, .string = string});
valueNormalized = response.value_normalized;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IEditController::getParamValueByString()'");
return Steinberg::kInvalidArgument;
}
}
Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::normalizedParamToPlain(
Steinberg::Vst::ParamID id,
Steinberg::Vst::ParamValue valueNormalized) {
return bridge.send_message(YaEditController::NormalizedParamToPlain{
.instance_id = instance_id(),
.id = id,
.value_normalized = valueNormalized});
}
Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::plainParamToNormalized(
Steinberg::Vst::ParamID id,
Steinberg::Vst::ParamValue plainValue) {
return bridge.send_message(YaEditController::PlainParamToNormalized{
.instance_id = instance_id(), .id = id, .plain_value = plainValue});
}
Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::getParamNormalized(Steinberg::Vst::ParamID id) {
return bridge.send_message(YaEditController::GetParamNormalized{
.instance_id = instance_id(), .id = id});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setParamNormalized(Steinberg::Vst::ParamID id,
Steinberg::Vst::ParamValue value) {
return bridge.send_message(YaEditController::SetParamNormalized{
.instance_id = instance_id(), .id = id, .value = value});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setComponentHandler(
Steinberg::Vst::IComponentHandler* handler) {
// Null pointers are valid here going from the reference implementations in
// the SDK
if (handler) {
// We'll store the pointer for when the plugin later makes a callback to
// this component handler
component_handler = handler;
// Automatically converted smart pointers for when the plugin performs a
// callback later
component_handler_2 = component_handler;
component_handler_3 = component_handler;
component_handler_bus_activation = component_handler;
progress = component_handler;
unit_handler = component_handler;
unit_handler_2 = component_handler;
return bridge.send_message(YaEditController::SetComponentHandler{
.instance_id = instance_id(),
.component_handler_proxy_args =
Vst3ComponentHandlerProxy::ConstructArgs(component_handler,
instance_id())});
} else {
component_handler = nullptr;
component_handler_2 = nullptr;
component_handler_3 = nullptr;
component_handler_bus_activation = nullptr;
progress = nullptr;
unit_handler = nullptr;
unit_handler_2 = nullptr;
return bridge.send_message(YaEditController::SetComponentHandler{
.instance_id = instance_id(),
.component_handler_proxy_args = std::nullopt});
}
}
Steinberg::IPlugView* PLUGIN_API
Vst3PluginProxyImpl::createView(Steinberg::FIDString name) {
if (name) {
CreateViewResponse response =
bridge.send_message(YaEditController::CreateView{
.instance_id = instance_id(), .name = name});
if (response.plug_view_args) {
// The host should manage this. Returning raw pointers feels scary.
auto plug_view_proxy = new Vst3PlugViewProxyImpl(
bridge, std::move(*response.plug_view_args));
// We also need to store an (unmanaged, since we don't want to
// affect the reference counting) pointer to this to be able to
// handle calls to `IPlugFrame::resizeView()` in the future
last_created_plug_view = plug_view_proxy;
return plug_view_proxy;
} else {
return nullptr;
}
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IEditController::createView()'");
return nullptr;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setKnobMode(Steinberg::Vst::KnobMode mode) {
return bridge.send_message(YaEditController2::SetKnobMode{
.instance_id = instance_id(), .mode = mode});
}
tresult PLUGIN_API Vst3PluginProxyImpl::openHelp(TBool onlyCheck) {
return bridge.send_message(YaEditController2::OpenHelp{
.instance_id = instance_id(), .only_check = onlyCheck});
}
tresult PLUGIN_API Vst3PluginProxyImpl::openAboutBox(TBool onlyCheck) {
return bridge.send_message(YaEditController2::OpenAboutBox{
.instance_id = instance_id(), .only_check = onlyCheck});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::beginEditFromHost(Steinberg::Vst::ParamID paramID) {
return bridge.send_message(YaEditControllerHostEditing::BeginEditFromHost{
.instance_id = instance_id(), .param_id = paramID});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::endEditFromHost(Steinberg::Vst::ParamID paramID) {
return bridge.send_message(YaEditControllerHostEditing::EndEditFromHost{
.instance_id = instance_id(), .param_id = paramID});
}
tresult PLUGIN_API Vst3PluginProxyImpl::setChannelContextInfos(
Steinberg::Vst::IAttributeList* list) {
if (list) {
// NOTE: After getting and setting state, the plugin may want to resize
// to match its old size. The DMG plugins tend to delay this
// request until after the state has been set, but at that point
// in time REAPER will also send channel context info. Both of
// these things need to be handled on the GUI thread on their
// receiving sides, resulting in a deadlock without this mutual
// recursion.
return bridge.send_mutually_recursive_message(
YaInfoListener::SetChannelContextInfos{
.instance_id = instance_id(),
.list = YaAttributeList::read_channel_context(list)});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IInfoListener::setChannelContextInfos()'");
return Steinberg::kInvalidArgument;
}
}
int32 PLUGIN_API Vst3PluginProxyImpl::getKeyswitchCount(int32 busIndex,
int16 channel) {
return bridge.send_message(
YaKeyswitchController::GetKeyswitchCount{.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getKeyswitchInfo(
int32 busIndex,
int16 channel,
int32 keySwitchIndex,
Steinberg::Vst::KeyswitchInfo& info /*out*/) {
const GetKeyswitchInfoResponse response =
bridge.send_message(YaKeyswitchController::GetKeyswitchInfo{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.key_switch_index = keySwitchIndex});
info = response.info;
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::onLiveMIDIControllerInput(
int32 busIndex,
int16 channel,
Steinberg::Vst::CtrlNumber midiCC) {
return bridge.send_message(
YaMidiLearn::OnLiveMIDIControllerInput{.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.midi_cc = midiCC});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getMidiControllerAssignment(
int32 busIndex,
int16 channel,
Steinberg::Vst::CtrlNumber midiControllerNumber,
Steinberg::Vst::ParamID& id /*out*/) {
const GetMidiControllerAssignmentResponse response =
bridge.send_message(YaMidiMapping::GetMidiControllerAssignment{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.midi_controller_number = midiControllerNumber});
id = response.id;
return response.result;
}
int32 PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionCount(int32 busIndex,
int16 channel) {
return bridge.send_message(
YaNoteExpressionController::GetNoteExpressionCount{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionInfo(
int32 busIndex,
int16 channel,
int32 noteExpressionIndex,
Steinberg::Vst::NoteExpressionTypeInfo& info /*out*/) {
const GetNoteExpressionInfoResponse response =
bridge.send_message(YaNoteExpressionController::GetNoteExpressionInfo{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.note_expression_index = noteExpressionIndex});
info = response.info;
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionStringByValue(
int32 busIndex,
int16 channel,
Steinberg::Vst::NoteExpressionTypeID id,
Steinberg::Vst::NoteExpressionValue valueNormalized /*in*/,
Steinberg::Vst::String128 string /*out*/) {
if (string) {
const GetNoteExpressionStringByValueResponse response =
bridge.send_message(
YaNoteExpressionController::GetNoteExpressionStringByValue{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.id = id,
.value_normalized = valueNormalized});
std::copy(response.string.begin(), response.string.end(), string);
string[response.string.size()] = 0;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'INoteExpressionController::getNoteExpressionStringByValue()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionValueByString(
int32 busIndex,
int16 channel,
Steinberg::Vst::NoteExpressionTypeID id,
const Steinberg::Vst::TChar* string /*in*/,
Steinberg::Vst::NoteExpressionValue& valueNormalized /*out*/) {
if (string) {
const GetNoteExpressionValueByStringResponse response =
bridge.send_message(
YaNoteExpressionController::GetNoteExpressionValueByString{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.id = id,
.string = string});
valueNormalized = response.value_normalized;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'INoteExpressionController::getNoteExpressionValueByString()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getPhysicalUIMapping(
int32 busIndex,
int16 channel,
Steinberg::Vst::PhysicalUIMapList& list) {
const GetNotePhysicalUIMappingResponse response = bridge.send_message(
YaNoteExpressionPhysicalUIMapping::GetNotePhysicalUIMapping{
.instance_id = instance_id(),
.bus_index = busIndex,
.channel = channel,
.list = list});
response.list.write_back(list);
return response.result;
}
tresult PLUGIN_API Vst3PluginProxyImpl::getParameterIDFromFunctionName(
Steinberg::Vst::UnitID unitID,
Steinberg::FIDString functionName,
Steinberg::Vst::ParamID& paramID) {
if (functionName) {
const GetParameterIDFromFunctionNameResponse response =
bridge.send_message(
YaParameterFunctionName::GetParameterIDFromFunctionName{
.instance_id = instance_id(),
.unit_id = unitID,
.function_name = functionName});
paramID = response.param_id;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IParameterFunctionName::getParameterIDFromFunctionName()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::initialize(FUnknown* context) {
if (context) {
// We will create a proxy object that that supports all the same
// interfaces as `context`, and then we'll store `context` in this
// object. We can then use it to handle callbacks made by the Windows
// VST3 plugin to this context.
host_context = context;
// Automatically converted smart pointers for when the plugin performs a
// callback later
host_application = host_context;
plug_interface_support = host_context;
return bridge.send_message(YaPluginBase::Initialize{
.instance_id = instance_id(),
.host_context_args = Vst3HostContextProxy::ConstructArgs(
host_context, instance_id())});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to 'IPluginBase::initialize()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::terminate() {
return bridge.send_message(
YaPluginBase::Terminate{.instance_id = instance_id()});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getPrefetchableSupport(
Steinberg::Vst::PrefetchableSupport& prefetchable /*out*/) {
const GetPrefetchableSupportResponse response =
bridge.send_audio_processor_message(
YaPrefetchableSupport::GetPrefetchableSupport{.instance_id =
instance_id()});
prefetchable = response.prefetchable;
return response.result;
}
uint32 PLUGIN_API Vst3PluginProxyImpl::getProcessContextRequirements() {
return bridge.send_message(
YaProcessContextRequirements::GetProcessContextRequirements{
.instance_id = instance_id()});
}
tresult PLUGIN_API Vst3PluginProxyImpl::programDataSupported(
Steinberg::Vst::ProgramListID listId) {
return bridge.send_message(YaProgramListData::ProgramDataSupported{
.instance_id = instance_id(), .list_id = listId});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getProgramData(Steinberg::Vst::ProgramListID listId,
int32 programIndex,
Steinberg::IBStream* data) {
if (data) {
const GetProgramDataResponse response = bridge.send_message(
YaProgramListData::GetProgramData{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex,
.data = data});
assert(response.data.write_back(data) == Steinberg::kResultOk);
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IProgramListData::getProgramData()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setProgramData(Steinberg::Vst::ProgramListID listId,
int32 programIndex,
Steinberg::IBStream* data) {
if (data) {
return bridge.send_message(
YaProgramListData::SetProgramData{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex,
.data = data});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IProgramListData::setProgramData()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::unitDataSupported(Steinberg::Vst::UnitID unitId) {
return bridge.send_message(YaUnitData::UnitDataSupported{
.instance_id = instance_id(), .unit_id = unitId});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitData(Steinberg::Vst::UnitID unitId,
Steinberg::IBStream* data) {
if (data) {
const GetUnitDataResponse response =
bridge.send_message(YaUnitData::GetUnitData{
.instance_id = instance_id(), .unit_id = unitId, .data = data});
assert(response.data.write_back(data) == Steinberg::kResultOk);
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to 'IUnitData::getUnitData()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setUnitData(Steinberg::Vst::UnitID unitId,
Steinberg::IBStream* data) {
if (data) {
return bridge.send_message(YaUnitData::SetUnitData{
.instance_id = instance_id(), .unit_id = unitId, .data = data});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to 'IUnitData::setUnitData()'");
return Steinberg::kInvalidArgument;
}
}
int32 PLUGIN_API Vst3PluginProxyImpl::getUnitCount() {
return bridge.send_message(
YaUnitInfo::GetUnitCount{.instance_id = instance_id()});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitInfo(int32 unitIndex,
Steinberg::Vst::UnitInfo& info /*out*/) {
const GetUnitInfoResponse response =
bridge.send_message(YaUnitInfo::GetUnitInfo{
.instance_id = instance_id(), .unit_index = unitIndex});
info = response.info;
return response.result;
}
int32 PLUGIN_API Vst3PluginProxyImpl::getProgramListCount() {
return bridge.send_message(
YaUnitInfo::GetProgramListCount{.instance_id = instance_id()});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getProgramListInfo(
int32 listIndex,
Steinberg::Vst::ProgramListInfo& info /*out*/) {
const GetProgramListInfoResponse response =
bridge.send_message(YaUnitInfo::GetProgramListInfo{
.instance_id = instance_id(), .list_index = listIndex});
info = response.info;
return response.result;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getProgramName(Steinberg::Vst::ProgramListID listId,
int32 programIndex,
Steinberg::Vst::String128 name /*out*/) {
if (name) {
const GetProgramNameResponse response = bridge.send_message(
YaUnitInfo::GetProgramName{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex});
std::copy(response.name.begin(), response.name.end(), name);
name[response.name.size()] = 0;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to 'IUnitInfo::getProgramName()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getProgramInfo(
Steinberg::Vst::ProgramListID listId,
int32 programIndex,
Steinberg::Vst::CString attributeId /*in*/,
Steinberg::Vst::String128 attributeValue /*out*/) {
if (attributeId && attributeValue) {
const GetProgramInfoResponse response = bridge.send_message(
YaUnitInfo::GetProgramInfo{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex,
.attribute_id = attributeId});
std::copy(response.attribute_value.begin(),
response.attribute_value.end(), attributeValue);
attributeValue[response.attribute_value.size()] = 0;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to 'IUnitInfo::getProgramInfo()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API
Vst3PluginProxyImpl::hasProgramPitchNames(Steinberg::Vst::ProgramListID listId,
int32 programIndex) {
return bridge.send_message(
YaUnitInfo::HasProgramPitchNames{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex});
}
tresult PLUGIN_API Vst3PluginProxyImpl::getProgramPitchName(
Steinberg::Vst::ProgramListID listId,
int32 programIndex,
int16 midiPitch,
Steinberg::Vst::String128 name /*out*/) {
if (name) {
const GetProgramPitchNameResponse response = bridge.send_message(
YaUnitInfo::GetProgramPitchName{.instance_id = instance_id(),
.list_id = listId,
.program_index = programIndex,
.midi_pitch = midiPitch});
std::copy(response.name.begin(), response.name.end(), name);
name[response.name.size()] = 0;
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IUnitInfo::getProgramPitchName()'");
return Steinberg::kInvalidArgument;
}
}
Steinberg::Vst::UnitID PLUGIN_API Vst3PluginProxyImpl::getSelectedUnit() {
return bridge.send_message(
YaUnitInfo::GetSelectedUnit{.instance_id = instance_id()});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::selectUnit(Steinberg::Vst::UnitID unitId) {
return bridge.send_message(YaUnitInfo::SelectUnit{
.instance_id = instance_id(), .unit_id = unitId});
}
tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitByBus(Steinberg::Vst::MediaType type,
Steinberg::Vst::BusDirection dir,
int32 busIndex,
int32 channel,
Steinberg::Vst::UnitID& unitId /*out*/) {
const GetUnitByBusResponse response = bridge.send_message(
YaUnitInfo::GetUnitByBus{.instance_id = instance_id(),
.type = type,
.dir = dir,
.bus_index = busIndex,
.channel = channel});
unitId = response.unit_id;
return response.result;
}
tresult PLUGIN_API
Vst3PluginProxyImpl::setUnitProgramData(int32 listOrUnitId,
int32 programIndex,
Steinberg::IBStream* data) {
if (data) {
return bridge.send_message(
YaUnitInfo::SetUnitProgramData{.instance_id = instance_id(),
.list_or_unit_id = listOrUnitId,
.program_index = programIndex,
.data = data});
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IUnitInfo::setUnitProgramData()'");
return Steinberg::kInvalidArgument;
}
}
tresult PLUGIN_API Vst3PluginProxyImpl::getXmlRepresentationStream(
Steinberg::Vst::RepresentationInfo& info /*in*/,
Steinberg::IBStream* stream /*out*/) {
if (stream) {
const GetXmlRepresentationStreamResponse response = bridge.send_message(
YaXmlRepresentationController::GetXmlRepresentationStream{
.instance_id = instance_id(), .info = info, .stream = stream});
response.stream.write_back(stream);
return response.result;
} else {
bridge.logger.log(
"WARNING: Null pointer passed to "
"'IXmlRepresentationController::getXmlRepresentationStream()'");
return Steinberg::kInvalidArgument;
}
}
void Vst3PluginProxyImpl::clear_bus_cache() noexcept {
std::lock_guard lock(processing_bus_cache_mutex);
if (processing_bus_cache) {
processing_bus_cache.emplace();
}
}