diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bd76ef..b4f47434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Added support for plugins that send MIDI events back to the host. This plugins + such as Cthulhu and Scaler to output notes and CC for another plugin to work + with. + ### Changed - Changed the plugin detection mechanism to support yet another way of diff --git a/src/plugin/host-bridge.cpp b/src/plugin/host-bridge.cpp index beba5ae4..40dda0e5 100644 --- a/src/plugin/host-bridge.cpp +++ b/src/plugin/host-bridge.cpp @@ -253,9 +253,30 @@ HostBridge::HostBridge(audioMasterCallback host_callback) host_callback_handler = std::thread([&]() { try { while (true) { + // TODO: Think of a nicer way to structure this and the similar + // handler in `PluginBridge::handle_dispatch_midi_events` receive_event( - vst_host_callback, std::pair(logger, false), - passthrough_event(&plugin, host_callback_function)); + vst_host_callback, std::nullopt, [&](Event& event) { + // MIDI events sent from the plugin back to the host are + // a special case here. They have to sent during the + // `processReplacing()` function or else the host will + // ignore them. Because of this we'll temporarily save + // any MIDI events we receive here, and then we'll + // actually send them to the host at the end of the + // `process_replacing()` function. + if (event.opcode == audioMasterProcessEvents) { + std::lock_guard lock(incoming_midi_events_mutex); + + incoming_midi_events.push_back( + std::get(event.payload)); + EventResult response{1, nullptr}; + + return response; + } else { + return passthrough_event( + &plugin, host_callback_function)(event); + } + }); } } catch (const boost::system::system_error&) { // This happens when the sockets got closed because the plugin @@ -531,6 +552,20 @@ void HostBridge::process_replacing(AEffect* /*plugin*/, std::copy(response.buffers[channel].begin(), response.buffers[channel].end(), outputs[channel]); } + + // Plugins are allowed to send MIDI events during processing using a host + // callback. These have to be processed during the actual + // `processReplacing()` function or else the hsot will ignore them. To + // prevent these events from getting delayed by a sample we'll process them + // after the plugin is done processing audio rather than during the time + // we're still waiting on the plugin. + std::lock_guard lock(incoming_midi_events_mutex); + for (DynamicVstEvents& events : incoming_midi_events) { + host_callback_function(&plugin, audioMasterProcessEvents, 0, 0, + &events.as_c_events(), 0.0); + } + + incoming_midi_events.clear(); } float HostBridge::get_parameter(AEffect* /*plugin*/, int index) { diff --git a/src/plugin/host-bridge.h b/src/plugin/host-bridge.h index 4b4ed5e3..3b04ba35 100644 --- a/src/plugin/host-bridge.h +++ b/src/plugin/host-bridge.h @@ -226,4 +226,20 @@ class HostBridge { * `processReplacing` calls. */ std::vector process_buffer; + + /** + * Sending MIDI events sent to the host by the plugin using + * `audioMasterProcessEvents` function has to be done during the processing + * function. If they are sent during any other time or from another thread, + * then the host will just discard them. Because we're receiving our host + * callbacks on a separate thread, we have to temporarily store any events + * we receive so we can send them to the host at the end of + * `process_replacing()`. + */ + std::vector incoming_midi_events; + /** + * Mutex for locking the above event queue, since recieving and processing + * now happens in two different threads. + */ + std::mutex incoming_midi_events_mutex; }; diff --git a/src/wine-host/plugin-bridge.cpp b/src/wine-host/plugin-bridge.cpp index 052c0756..14262951 100644 --- a/src/wine-host/plugin-bridge.cpp +++ b/src/wine-host/plugin-bridge.cpp @@ -361,6 +361,9 @@ class HostCallbackDataConverter : DefaultDataConverter { // done inside of `passthrough_event`. return AEffect(*plugin); break; + case audioMasterProcessEvents: + return DynamicVstEvents(*static_cast(data)); + break; // We detect whether an opcode should return a string by checking // whether there's a zeroed out buffer behind the void pointer. This // works for any host, but not all plugins zero out their buffers.