Skip to content

Commit 32febf2

Browse files
fix: prevent MIDI out devices from dropping notes on macOS
1 parent 8308948 commit 32febf2

7 files changed

Lines changed: 317 additions & 20 deletions

File tree

data/developers.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ mmoole. (mmoole)
1111
schnitzeltony
1212
splisp
1313
Heath Dutton (heathdutton)
14+
Alexandr Zyurkalov (Alexander-Zyurkalov)

src/engine/midioutput.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
#include "midioutput.h"
5+
namespace element {
6+
#if JUCE_MAC
7+
8+
MIDIEndpointRef findDestination (const juce::MidiDeviceInfo& deviceInfo)
9+
{
10+
SInt32 targetUID = deviceInfo.identifier.containsChar (' ')
11+
? deviceInfo.identifier.fromLastOccurrenceOf (" ", false, false).getIntValue()
12+
: deviceInfo.identifier.getIntValue();
13+
ItemCount count = MIDIGetNumberOfDestinations();
14+
15+
for (ItemCount i = 0; i < count; ++i)
16+
{
17+
MIDIEndpointRef endpoint = MIDIGetDestination (i);
18+
SInt32 uid = 0;
19+
MIDIObjectGetIntegerProperty (endpoint, kMIDIPropertyUniqueID, &uid);
20+
21+
if (uid == targetUID)
22+
return endpoint;
23+
}
24+
25+
return 0;
26+
}
27+
28+
ElementMidiOutput::ElementMidiOutput (const juce::MidiDeviceInfo& deviceInfo)
29+
{
30+
destination = findDestination (deviceInfo);
31+
if (destination)
32+
{
33+
MIDIClientCreate (CFSTR ("ElementMIDIOutput"), nullptr, nullptr, &client);
34+
MIDIOutputPortCreate (client, deviceInfo.name.toCFString(), &port);
35+
mach_timebase_info (&timebase);
36+
}
37+
}
38+
39+
juce::Result ElementMidiOutput::openDevice (const juce::MidiDeviceInfo& deviceInfo, std::unique_ptr<ElementMidiOutput>& out)
40+
{
41+
out = std::make_unique<ElementMidiOutput> (deviceInfo);
42+
if (! out->destination)
43+
{
44+
out.reset();
45+
return juce::Result::fail ("MIDI destination not found: " + deviceInfo.name);
46+
}
47+
return juce::Result::ok();
48+
}
49+
50+
void ElementMidiOutput::closeDevice()
51+
{
52+
if (! destination)
53+
return;
54+
MIDIFlushOutput (destination);
55+
MIDIClientDispose (client);
56+
destination = 0;
57+
}
58+
59+
MIDITimeStamp ElementMidiOutput::futureTimestamp (uint64_t nanosFromNow) const
60+
{
61+
MIDITimeStamp ticks = nanosFromNow * timebase.denom / timebase.numer;
62+
return mach_absolute_time() + ticks;
63+
}
64+
65+
juce::Result ElementMidiOutput::sendBlockOfMessages (const juce::MidiBuffer& midi, double delayMs, double sampleRate) const
66+
{
67+
constexpr size_t BUFFER_SIZE_FOR_PACKET_LIST = 1024;
68+
uint8_t buffer[BUFFER_SIZE_FOR_PACKET_LIST];
69+
auto* packetList = reinterpret_cast<MIDIPacketList*> (buffer);
70+
MIDIPacket* packet = MIDIPacketListInit (packetList);
71+
72+
for (juce::MidiMessageMetadata message : midi)
73+
{
74+
const double nanoSeconds = message.samplePosition / sampleRate * 1'000'000'000.0 + delayMs * 1'000'000.0;
75+
const auto ticks = futureTimestamp (static_cast<uint64_t> (std::round (nanoSeconds)));
76+
packet = MIDIPacketListAdd (packetList, BUFFER_SIZE_FOR_PACKET_LIST, packet, ticks, message.numBytes, message.data);
77+
}
78+
79+
OSStatus status = MIDISend (port, destination, packetList);
80+
if (status != noErr)
81+
return juce::Result::fail ("MIDISend failed with OSStatus " + juce::String (status));
82+
return juce::Result::ok();
83+
}
84+
85+
#else
86+
87+
void ElementMidiOutput::closeDevice()
88+
{
89+
if (output)
90+
{
91+
output->stopBackgroundThread();
92+
output->clearAllPendingMessages();
93+
output.reset();
94+
}
95+
}
96+
97+
ElementMidiOutput::ElementMidiOutput (const juce::MidiDeviceInfo& deviceInfo)
98+
{
99+
output = juce::MidiOutput::openDevice (deviceInfo.identifier);
100+
101+
if (output)
102+
{
103+
output->clearAllPendingMessages();
104+
output->startBackgroundThread();
105+
}
106+
else
107+
{
108+
DBG ("[element] could not open MIDI output: " << deviceInfo.name);
109+
}
110+
}
111+
112+
juce::Result ElementMidiOutput::openDevice (const juce::MidiDeviceInfo& deviceInfo, std::unique_ptr<ElementMidiOutput>& out)
113+
{
114+
out = std::make_unique<ElementMidiOutput> (deviceInfo);
115+
if (! out->output)
116+
{
117+
out.reset();
118+
return juce::Result::fail ("Could not open MIDI output: " + deviceInfo.name);
119+
}
120+
return juce::Result::ok();
121+
}
122+
123+
juce::Result ElementMidiOutput::sendBlockOfMessages (const juce::MidiBuffer& midi, double delayMs, double sampleRate) const
124+
{
125+
#if JUCE_WINDOWS
126+
output->sendBlockOfMessagesNow (midi);
127+
#else
128+
output->sendBlockOfMessages (
129+
midi, delayMs + juce::Time::getMillisecondCounterHiRes(), sampleRate);
130+
#endif
131+
return juce::Result::ok();
132+
}
133+
#endif
134+
135+
} // namespace element

src/engine/midioutput.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
#pragma once
5+
6+
#include "element/juce.hpp"
7+
8+
#if JUCE_MAC
9+
#include <CoreMIDI/CoreMIDI.h>
10+
//#include <CoreFoundation/CoreFoundation.h>
11+
#include <mach/mach_time.h>
12+
#endif
13+
14+
#include "midioutput.h"
15+
#include <memory>
16+
17+
namespace element {
18+
19+
class ElementMidiOutput
20+
{
21+
public:
22+
ElementMidiOutput() = delete;
23+
ElementMidiOutput (const juce::MidiDeviceInfo& deviceInfo);
24+
25+
void closeDevice();
26+
static juce::Result openDevice (const juce::MidiDeviceInfo& deviceInfo, std::unique_ptr<ElementMidiOutput>& out);
27+
juce::Result sendBlockOfMessages (const juce::MidiBuffer& midi, double delayMs, double sampleRate) const;
28+
29+
private:
30+
#if JUCE_MAC
31+
MIDIClientRef client {};
32+
MIDIEndpointRef destination {};
33+
MIDIPortRef port {};
34+
mach_timebase_info_data_t timebase{};
35+
MIDITimeStamp futureTimestamp (uint64_t nanosFromNow) const;
36+
#else
37+
std::unique_ptr<juce::MidiOutput> output;
38+
#endif
39+
};
40+
41+
} // namespace element

src/nodes/mididevice.cpp

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -205,22 +205,19 @@ void MidiDeviceProcessor::setDevice (const MidiDeviceInfo& newDevice)
205205
{
206206
if (output)
207207
{
208-
output->stopBackgroundThread();
209-
output->clearAllPendingMessages();
208+
output->closeDevice();
210209
output.reset();
211210
}
212211

213-
output = MidiOutput::openDevice (deviceWanted.identifier);
212+
auto openResult = ElementMidiOutput::openDevice (deviceWanted, output);
214213

215-
if (output)
214+
if (openResult.wasOk())
216215
{
217-
output->clearAllPendingMessages();
218-
output->startBackgroundThread();
219216
device = deviceWanted;
220217
}
221218
else
222219
{
223-
DBG ("[element] could not open MIDI output: " << deviceWanted.name);
220+
DBG ("[element] " << openResult.getErrorMessage());
224221
}
225222
}
226223

@@ -252,10 +249,10 @@ Result MidiDeviceProcessor::closeDevice()
252249
{
253250
if (output)
254251
{
255-
output->clearAllPendingMessages();
256-
std::unique_ptr<MidiOutput> closer;
252+
std::unique_ptr<ElementMidiOutput> closer;
257253
{
258254
ScopedLock sl (getCallbackLock());
255+
output->closeDevice();
259256
std::swap (output, closer);
260257
}
261258
closer.reset();
@@ -319,12 +316,7 @@ void MidiDeviceProcessor::processBlock (AudioBuffer<float>& audio, MidiBuffer& m
319316
if (output && ! midi.isEmpty())
320317
{
321318
const auto delayMs = midiOutLatency.get();
322-
#if JUCE_WINDOWS
323-
output->sendBlockOfMessagesNow (midi);
324-
#else
325-
output->sendBlockOfMessages (
326-
midi, delayMs + Time::getMillisecondCounterHiRes(), getSampleRate());
327-
#endif
319+
output->sendBlockOfMessages (midi, delayMs, getSampleRate());
328320
}
329321

330322
midi.clear (0, nframes);
@@ -403,15 +395,15 @@ bool MidiDeviceProcessor::deviceIsAvailable (const String& name)
403395
if (info.name == name)
404396
return true;
405397
}
406-
return true;
398+
return false;
407399
}
408400

409401
bool MidiDeviceProcessor::deviceIsAvailable (const MidiDeviceInfo& dev)
410402
{
411403
for (const auto& info : getAvailableDevices())
412404
if (info.identifier == dev.identifier)
413405
return true;
414-
return true;
406+
return false;
415407
}
416408

417409
void MidiDeviceProcessor::timerCallback()

src/nodes/mididevice.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <element/signals.hpp>
77
#include "nodes/baseprocessor.hpp"
8+
#include <engine/midioutput.h>
89

910
namespace element {
1011

@@ -154,7 +155,7 @@ class MidiDeviceProcessor : public BaseProcessor,
154155
MidiDeviceInfo deviceWanted; // The device as saved in Stage and chosen by users.
155156
MidiMessageCollector inputMessages;
156157
std::unique_ptr<MidiInput> input;
157-
std::unique_ptr<MidiOutput> output;
158+
std::unique_ptr<ElementMidiOutput> output;
158159
Atomic<double> midiOutLatency { 0.0 };
159160

160161
void waitForDevice() {}

test/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ file(GLOB_RECURSE TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
66
juce_add_console_app(test_element)
77
target_sources(test_element PRIVATE ${TEST_SOURCES})
88
target_link_libraries(test_element PRIVATE kv::element)
9-
target_include_directories(test_element
10-
PRIVATE
9+
target_include_directories(test_element
10+
PRIVATE
1111
"${CMAKE_CURRENT_SOURCE_DIR}"
1212
${GENERATED_INCLUDE_DIR}
1313
${Boost_INCLUDE_DIRS})
@@ -32,6 +32,9 @@ add_test(NAME "MidiChannelMapTest" COMMAND test_element --run_test=MidiChannelMa
3232
add_test(NAME "MidiClockTest" COMMAND test_element --run_test=MidiClockTest)
3333
add_test(NAME "MidiProgramMapTests" COMMAND test_element --run_test=MidiProgramMapTests)
3434
add_test(NAME "MidiScriptTests" COMMAND test_element --run_test=MidiScriptTests)
35+
if (APPLE)
36+
add_test(NAME "MidiOutputTests" COMMAND test_element --run_test=MidiOutputTests)
37+
endif()
3538
add_test(NAME "NodeFactoryTests" COMMAND test_element --run_test=NodeFactoryTests)
3639
add_test(NAME "NodeObjectTests" COMMAND test_element --run_test=NodeObjectTests)
3740
add_test(NAME "NodeTests" COMMAND test_element --run_test=NodeTests)

0 commit comments

Comments
 (0)