1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

CoreMIDI: Enable support for new API

This commit is contained in:
reuk 2020-11-11 19:11:23 +00:00
parent 051f49c52e
commit 9032f589eb
No known key found for this signature in database
GPG key ID: 9ADCD339CFC98A11
25 changed files with 5260 additions and 734 deletions

View file

@ -1,732 +0,0 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
#ifndef JUCE_LOG_COREMIDI_ERRORS
#define JUCE_LOG_COREMIDI_ERRORS 1
#endif
namespace CoreMidiHelpers
{
//==============================================================================
static bool checkError (OSStatus err, int lineNum)
{
if (err == noErr)
return true;
#if JUCE_LOG_COREMIDI_ERRORS
Logger::writeToLog ("CoreMIDI error: " + String (lineNum) + " - " + String::toHexString ((int) err));
#endif
ignoreUnused (lineNum);
return false;
}
#undef CHECK_ERROR
#define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__)
static MidiDeviceInfo getMidiObjectInfo (MIDIObjectRef entity)
{
MidiDeviceInfo info;
{
ScopedCFString str;
if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.cfString)))
info.name = String::fromCFString (str.cfString);
}
SInt32 objectID = 0;
if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID)))
{
info.identifier = String (objectID);
}
else
{
ScopedCFString str;
if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyUniqueID, &str.cfString)))
info.identifier = String::fromCFString (str.cfString);
}
return info;
}
static MidiDeviceInfo getEndpointInfo (MIDIEndpointRef endpoint, bool isExternal)
{
// NB: don't attempt to use nullptr for refs - it fails in some types of build.
MIDIEntityRef entity = 0;
MIDIEndpointGetEntity (endpoint, &entity);
// probably virtual
if (entity == 0)
return getMidiObjectInfo (endpoint);
auto result = getMidiObjectInfo (endpoint);
// endpoint is empty - try the entity
if (result == MidiDeviceInfo())
result = getMidiObjectInfo (entity);
// now consider the device
MIDIDeviceRef device = 0;
MIDIEntityGetDevice (entity, &device);
if (device != 0)
{
auto info = getMidiObjectInfo (device);
if (info != MidiDeviceInfo())
{
// if an external device has only one entity, throw away
// the endpoint name and just use the device name
if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2)
{
result = info;
}
else if (! result.name.startsWithIgnoreCase (info.name))
{
// prepend the device name and identifier to the entity's
result.name = (info.name + " " + result.name).trimEnd();
result.identifier = info.identifier + " " + result.identifier;
}
}
}
return result;
}
static MidiDeviceInfo getConnectedEndpointInfo (MIDIEndpointRef endpoint)
{
MidiDeviceInfo result;
// Does the endpoint have connections?
CFDataRef connections = nullptr;
int numConnections = 0;
MIDIObjectGetDataProperty (endpoint, kMIDIPropertyConnectionUniqueID, &connections);
if (connections != nullptr)
{
numConnections = ((int) CFDataGetLength (connections)) / (int) sizeof (MIDIUniqueID);
if (numConnections > 0)
{
auto* pid = reinterpret_cast<const SInt32*> (CFDataGetBytePtr (connections));
for (int i = 0; i < numConnections; ++i, ++pid)
{
auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid);
MIDIObjectRef connObject;
MIDIObjectType connObjectType;
auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType);
if (err == noErr)
{
MidiDeviceInfo deviceInfo;
if (connObjectType == kMIDIObjectType_ExternalSource
|| connObjectType == kMIDIObjectType_ExternalDestination)
{
// Connected to an external device's endpoint (10.3 and later).
deviceInfo = getEndpointInfo (static_cast<MIDIEndpointRef> (connObject), true);
}
else
{
// Connected to an external device (10.2) (or something else, catch-all)
deviceInfo = getMidiObjectInfo (connObject);
}
if (deviceInfo != MidiDeviceInfo())
{
if (result.name.isNotEmpty()) result.name += ", ";
if (result.identifier.isNotEmpty()) result.identifier += ", ";
result.name += deviceInfo.name;
result.identifier += deviceInfo.identifier;
}
}
}
}
CFRelease (connections);
}
// Here, either the endpoint had no connections, or we failed to obtain names for them.
if (result == MidiDeviceInfo())
return getEndpointInfo (endpoint, false);
return result;
}
static int createUniqueIDForMidiPort (String deviceName, bool isInput)
{
String uniqueID;
#ifdef JucePlugin_CFBundleIdentifier
uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier);
#else
auto appBundle = File::getSpecialLocation (File::currentApplicationFile);
ScopedCFString appBundlePath (appBundle.getFullPathName());
if (auto bundleURL = CFURLCreateWithFileSystemPath (kCFAllocatorDefault, appBundlePath.cfString, kCFURLPOSIXPathStyle, true))
{
auto bundleRef = CFBundleCreate (kCFAllocatorDefault, bundleURL);
CFRelease (bundleURL);
if (bundleRef != nullptr)
{
if (auto bundleId = CFBundleGetIdentifier (bundleRef))
uniqueID = String::fromCFString (bundleId);
CFRelease (bundleRef);
}
}
#endif
if (uniqueID.isEmpty())
uniqueID = String (Random::getSystemRandom().nextInt (1024));
uniqueID += "." + deviceName + (isInput ? ".input" : ".output");
return uniqueID.hashCode();
}
static void enableSimulatorMidiSession()
{
#if TARGET_OS_SIMULATOR
static bool hasEnabledNetworkSession = false;
if (! hasEnabledNetworkSession)
{
MIDINetworkSession* session = [MIDINetworkSession defaultSession];
session.enabled = YES;
session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone;
hasEnabledNetworkSession = true;
}
#endif
}
static void globalSystemChangeCallback (const MIDINotification*, void*)
{
// TODO.. Should pass-on this notification..
}
static String getGlobalMidiClientName()
{
if (auto* app = JUCEApplicationBase::getInstance())
return app->getApplicationName();
return "JUCE";
}
static MIDIClientRef getGlobalMidiClient()
{
static MIDIClientRef globalMidiClient = 0;
if (globalMidiClient == 0)
{
// Since OSX 10.6, the MIDIClientCreate function will only work
// correctly when called from the message thread!
JUCE_ASSERT_MESSAGE_THREAD
enableSimulatorMidiSession();
ScopedCFString name (getGlobalMidiClientName());
CHECK_ERROR (MIDIClientCreate (name.cfString, &globalSystemChangeCallback, nullptr, &globalMidiClient));
}
return globalMidiClient;
}
static Array<MidiDeviceInfo> findDevices (bool forInput)
{
// It seems that OSX can be a bit picky about the thread that's first used to
// search for devices. It's safest to use the message thread for calling this.
JUCE_ASSERT_MESSAGE_THREAD
if (getGlobalMidiClient() == 0)
{
jassertfalse;
return {};
}
enableSimulatorMidiSession();
Array<MidiDeviceInfo> devices;
auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations());
for (ItemCount i = 0; i < numDevices; ++i)
{
MidiDeviceInfo deviceInfo;
if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i))
deviceInfo = getConnectedEndpointInfo (dest);
if (deviceInfo == MidiDeviceInfo())
deviceInfo.name = deviceInfo.identifier = "<error>";
devices.add (deviceInfo);
}
return devices;
}
//==============================================================================
class MidiPortAndEndpoint
{
public:
MidiPortAndEndpoint (MIDIPortRef p, MIDIEndpointRef ep) noexcept
: port (p), endpoint (ep)
{
}
~MidiPortAndEndpoint() noexcept
{
if (port != 0)
MIDIPortDispose (port);
// if port == nullptr, it means we created the endpoint, so it's safe to delete it
if (port == 0 && endpoint != 0)
MIDIEndpointDispose (endpoint);
}
void send (const MIDIPacketList* packets) noexcept
{
if (port != 0)
MIDISend (port, endpoint, packets);
else
MIDIReceived (endpoint, packets);
}
MIDIPortRef port;
MIDIEndpointRef endpoint;
};
//==============================================================================
struct MidiPortAndCallback;
CriticalSection callbackLock;
Array<MidiPortAndCallback*> activeCallbacks;
struct MidiPortAndCallback
{
MidiPortAndCallback (MidiInputCallback& cb) : callback (cb) {}
~MidiPortAndCallback()
{
active = false;
{
const ScopedLock sl (callbackLock);
activeCallbacks.removeFirstMatchingValue (this);
}
if (portAndEndpoint != nullptr && portAndEndpoint->port != 0)
CHECK_ERROR (MIDIPortDisconnectSource (portAndEndpoint->port, portAndEndpoint->endpoint));
}
void handlePackets (const MIDIPacketList* pktlist)
{
auto time = Time::getMillisecondCounterHiRes() * 0.001;
const ScopedLock sl (callbackLock);
if (activeCallbacks.contains (this) && active)
{
auto* packet = &pktlist->packet[0];
for (unsigned int i = 0; i < pktlist->numPackets; ++i)
{
auto len = readUnaligned<decltype (packet->length)> (&(packet->length));
concatenator.pushMidiData (packet->data, (int) len, time, input, callback);
packet = MIDIPacketNext (packet);
}
}
}
MidiInput* input = nullptr;
std::unique_ptr<MidiPortAndEndpoint> portAndEndpoint;
std::atomic<bool> active { false };
private:
MidiInputCallback& callback;
MidiDataConcatenator concatenator { 2048 };
};
static void midiInputProc (const MIDIPacketList* pktlist, void* readProcRefCon, void* /*srcConnRefCon*/)
{
static_cast<MidiPortAndCallback*> (readProcRefCon)->handlePackets (pktlist);
}
static Array<MIDIEndpointRef> getEndpoints (bool isInput)
{
Array<MIDIEndpointRef> endpoints;
auto numDevices = (isInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations());
for (ItemCount i = 0; i < numDevices; ++i)
endpoints.add (isInput ? MIDIGetSource (i) : MIDIGetDestination (i));
return endpoints;
}
}
class MidiInput::Pimpl : public CoreMidiHelpers::MidiPortAndCallback
{
public:
using MidiPortAndCallback::MidiPortAndCallback;
};
//==============================================================================
Array<MidiDeviceInfo> MidiInput::getAvailableDevices()
{
return CoreMidiHelpers::findDevices (true);
}
MidiDeviceInfo MidiInput::getDefaultDevice()
{
return getAvailableDevices().getFirst();
}
std::unique_ptr<MidiInput> MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback)
{
if (deviceIdentifier.isEmpty())
return nullptr;
using namespace CoreMidiHelpers;
if (auto client = getGlobalMidiClient())
{
for (auto& endpoint : getEndpoints (true))
{
auto endpointInfo = getConnectedEndpointInfo (endpoint);
if (deviceIdentifier == endpointInfo.identifier)
{
ScopedCFString cfName;
if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString)))
{
MIDIPortRef port;
auto mpc = std::make_unique<Pimpl> (*callback);
if (CHECK_ERROR (MIDIInputPortCreate (client, cfName.cfString, midiInputProc, mpc.get(), &port)))
{
if (CHECK_ERROR (MIDIPortConnectSource (port, endpoint, nullptr)))
{
mpc->portAndEndpoint = std::make_unique<MidiPortAndEndpoint> (port, endpoint);
std::unique_ptr<MidiInput> midiInput (new MidiInput (endpointInfo.name, endpointInfo.identifier));
mpc->input = midiInput.get();
auto* ptr = mpc.get();
midiInput->internal = std::move (mpc);
const ScopedLock sl (callbackLock);
activeCallbacks.add (ptr);
return midiInput;
}
else
{
CHECK_ERROR (MIDIPortDispose (port));
}
}
}
}
}
}
return {};
}
std::unique_ptr<MidiInput> MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback)
{
using namespace CoreMidiHelpers;
jassert (callback != nullptr);
if (auto client = getGlobalMidiClient())
{
auto mpc = std::make_unique<Pimpl> (*callback);
mpc->active = false;
MIDIEndpointRef endpoint;
ScopedCFString name (deviceName);
auto err = MIDIDestinationCreate (client, name.cfString, midiInputProc, mpc.get(), &endpoint);
#if JUCE_IOS
if (err == kMIDINotPermitted)
{
// If you've hit this assertion then you probably haven't enabled the "Audio Background Capability"
// setting in the iOS exporter for your app - this is required if you want to create a MIDI device!
jassertfalse;
return nullptr;
}
#endif
if (CHECK_ERROR (err))
{
auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true);
if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier)))
{
mpc->portAndEndpoint = std::make_unique<MidiPortAndEndpoint> ((UInt32) 0, endpoint);
std::unique_ptr<MidiInput> midiInput (new MidiInput (deviceName, String (deviceIdentifier)));
mpc->input = midiInput.get();
auto* ptr = mpc.get();
midiInput->internal = std::move (mpc);
const ScopedLock sl (callbackLock);
activeCallbacks.add (ptr);
return midiInput;
}
}
}
return {};
}
StringArray MidiInput::getDevices()
{
StringArray deviceNames;
for (auto& d : getAvailableDevices())
deviceNames.add (d.name);
return deviceNames;
}
int MidiInput::getDefaultDeviceIndex()
{
return 0;
}
std::unique_ptr<MidiInput> MidiInput::openDevice (int index, MidiInputCallback* callback)
{
return openDevice (getAvailableDevices()[index].identifier, callback);
}
MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier)
: deviceInfo (deviceName, deviceIdentifier)
{
}
MidiInput::~MidiInput() = default;
void MidiInput::start()
{
const ScopedLock sl (CoreMidiHelpers::callbackLock);
internal->active = true;
}
void MidiInput::stop()
{
const ScopedLock sl (CoreMidiHelpers::callbackLock);
internal->active = false;
}
//==============================================================================
class MidiOutput::Pimpl : public CoreMidiHelpers::MidiPortAndEndpoint
{
public:
using MidiPortAndEndpoint::MidiPortAndEndpoint;
};
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices()
{
return CoreMidiHelpers::findDevices (false);
}
MidiDeviceInfo MidiOutput::getDefaultDevice()
{
return getAvailableDevices().getFirst();
}
std::unique_ptr<MidiOutput> MidiOutput::openDevice (const String& deviceIdentifier)
{
if (deviceIdentifier.isEmpty())
return nullptr;
using namespace CoreMidiHelpers;
if (auto client = getGlobalMidiClient())
{
for (auto& endpoint : getEndpoints (false))
{
auto endpointInfo = getConnectedEndpointInfo (endpoint);
if (deviceIdentifier == endpointInfo.identifier)
{
ScopedCFString cfName;
if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString)))
{
MIDIPortRef port;
if (CHECK_ERROR (MIDIOutputPortCreate (client, cfName.cfString, &port)))
{
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (endpointInfo.name, endpointInfo.identifier));
midiOutput->internal = std::make_unique<Pimpl> (port, endpoint);
return midiOutput;
}
}
}
}
}
return {};
}
std::unique_ptr<MidiOutput> MidiOutput::createNewDevice (const String& deviceName)
{
using namespace CoreMidiHelpers;
if (auto client = getGlobalMidiClient())
{
MIDIEndpointRef endpoint;
ScopedCFString name (deviceName);
auto err = MIDISourceCreate (client, name.cfString, &endpoint);
#if JUCE_IOS
if (err == kMIDINotPermitted)
{
// If you've hit this assertion then you probably haven't enabled the "Audio Background Capability"
// setting in the iOS exporter for your app - this is required if you want to create a MIDI device!
jassertfalse;
return nullptr;
}
#endif
if (CHECK_ERROR (err))
{
auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, false);
if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier)))
{
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (deviceName, String (deviceIdentifier)));
midiOutput->internal = std::make_unique<Pimpl> ((UInt32) 0, endpoint);
return midiOutput;
}
}
}
return {};
}
StringArray MidiOutput::getDevices()
{
StringArray deviceNames;
for (auto& d : getAvailableDevices())
deviceNames.add (d.name);
return deviceNames;
}
int MidiOutput::getDefaultDeviceIndex()
{
return 0;
}
std::unique_ptr<MidiOutput> MidiOutput::openDevice (int index)
{
return openDevice (getAvailableDevices()[index].identifier);
}
MidiOutput::~MidiOutput()
{
stopBackgroundThread();
}
void MidiOutput::sendMessageNow (const MidiMessage& message)
{
#if JUCE_IOS
const MIDITimeStamp timeStamp = mach_absolute_time();
#else
const MIDITimeStamp timeStamp = AudioGetCurrentHostTime();
#endif
HeapBlock<MIDIPacketList> allocatedPackets;
MIDIPacketList stackPacket;
auto* packetToSend = &stackPacket;
auto dataSize = (size_t) message.getRawDataSize();
if (message.isSysEx())
{
const int maxPacketSize = 256;
int pos = 0, bytesLeft = (int) dataSize;
const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize;
allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1);
packetToSend = allocatedPackets;
packetToSend->numPackets = (UInt32) numPackets;
auto* p = packetToSend->packet;
for (int i = 0; i < numPackets; ++i)
{
p->timeStamp = timeStamp;
p->length = (UInt16) jmin (maxPacketSize, bytesLeft);
memcpy (p->data, message.getRawData() + pos, p->length);
pos += p->length;
bytesLeft -= p->length;
p = MIDIPacketNext (p);
}
}
else if (dataSize < 65536) // max packet size
{
auto stackCapacity = sizeof (stackPacket.packet->data);
if (dataSize > stackCapacity)
{
allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1);
packetToSend = allocatedPackets;
}
packetToSend->numPackets = 1;
auto& p = *(packetToSend->packet);
p.timeStamp = timeStamp;
p.length = (UInt16) dataSize;
memcpy (p.data, message.getRawData(), dataSize);
}
else
{
jassertfalse; // packet too large to send!
return;
}
internal->send (packetToSend);
}
#undef CHECK_ERROR
} // namespace juce

File diff suppressed because it is too large Load diff