diff --git a/src/rtmidi.cpp b/src/rtmidi.cpp index e456470d..f4c42a6b 100644 --- a/src/rtmidi.cpp +++ b/src/rtmidi.cpp @@ -1,4 +1,9 @@ +#include #include +#include +#include +#include +#include #pragma GCC diagnostic push #ifndef __clang__ @@ -9,6 +14,7 @@ #include #include +#include namespace rack { @@ -53,18 +59,34 @@ struct RtMidiInputDevice : midi::InputDevice { }; +/** Makes priority_queue prioritize by the earliest MIDI message. */ +static auto messageEarlier = [](const midi::Message& a, const midi::Message& b) { + return a.timestamp > b.timestamp; +}; + + struct RtMidiOutputDevice : midi::OutputDevice { RtMidiOut* rtMidiOut; std::string name; - RtMidiOutputDevice(int driverId, int deviceId) { + std::priority_queue, decltype(messageEarlier)> messageQueue; + + std::thread thread; + std::mutex mutex; + std::condition_variable cv; + bool stopped = false; + + RtMidiOutputDevice(int driverId, int deviceId) : messageQueue(messageEarlier) { rtMidiOut = new RtMidiOut((RtMidi::Api) driverId, "VCV Rack"); assert(rtMidiOut); name = rtMidiOut->getPortName(deviceId); rtMidiOut->openPort(deviceId, "VCV Rack output"); + + start(); } ~RtMidiOutputDevice() { + stop(); rtMidiOut->closePort(); delete rtMidiOut; } @@ -74,8 +96,58 @@ struct RtMidiOutputDevice : midi::OutputDevice { } void sendMessage(const midi::Message &message) override { - std::vector bytes(message.bytes.begin(), message.bytes.end()); - rtMidiOut->sendMessage(&bytes); + // sendMessageNow(message); + std::lock_guard lock(mutex); + messageQueue.push(message); + cv.notify_one(); + } + + // Consumer thread methods + + void start() { + thread = std::thread(&RtMidiOutputDevice::run, this); + } + + void run() { + std::unique_lock lock(mutex); + while (!stopped) { + if (messageQueue.empty()) { + // No messages. Wait on the CV to be notified. + cv.wait(lock); + } + else { + // Get earliest message + const midi::Message& message = messageQueue.top(); + int64_t duration = message.timestamp - system::getNanoseconds(); + + // If we need to wait, release the lock and wait for the timeout, or if the CV is notified. + // This correctly handles MIDI messages with no timestamp, because duration will be negative. + if ((duration > 0) && cv.wait_for(lock, std::chrono::nanoseconds(duration)) != std::cv_status::timeout) + continue; + + // Send and remove from queue + sendMessageNow(message); + messageQueue.pop(); + } + } + } + + void sendMessageNow(const midi::Message &message) { + try { + rtMidiOut->sendMessage(message.bytes.data(), message.bytes.size()); + } + catch (RtMidiError& e) { + // Ignore error + } + } + + void stop() { + { + std::lock_guard lock(mutex); + stopped = true; + cv.notify_one(); + } + thread.join(); } };