@@ -52,9 +52,9 @@ | |||
//============================================================================== | |||
struct MidiDeviceListEntry : ReferenceCountedObject | |||
{ | |||
MidiDeviceListEntry (const String& deviceName) : name (deviceName) {} | |||
MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {} | |||
String name; | |||
MidiDeviceInfo deviceInfo; | |||
std::unique_ptr<MidiInput> inDevice; | |||
std::unique_ptr<MidiOutput> outDevice; | |||
@@ -187,7 +187,7 @@ public: | |||
if (isInput) | |||
{ | |||
jassert (midiInputs[index]->inDevice.get() == nullptr); | |||
midiInputs[index]->inDevice.reset (MidiInput::openDevice (index, this)); | |||
midiInputs[index]->inDevice.reset (MidiInput::openDevice (midiInputs[index]->deviceInfo.identifier, this)); | |||
if (midiInputs[index]->inDevice.get() == nullptr) | |||
{ | |||
@@ -200,7 +200,7 @@ public: | |||
else | |||
{ | |||
jassert (midiOutputs[index]->outDevice.get() == nullptr); | |||
midiOutputs[index]->outDevice.reset (MidiOutput::openDevice (index)); | |||
midiOutputs[index]->outDevice.reset (MidiOutput::openDevice (midiOutputs[index]->deviceInfo.identifier)); | |||
if (midiOutputs[index]->outDevice.get() == nullptr) | |||
{ | |||
@@ -278,14 +278,14 @@ private: | |||
if (isInput) | |||
{ | |||
if (rowNumber < parent.getNumMidiInputs()) | |||
g.drawText (parent.getMidiDevice (rowNumber, true)->name, | |||
g.drawText (parent.getMidiDevice (rowNumber, true)->deviceInfo.name, | |||
5, 0, width, height, | |||
Justification::centredLeft, true); | |||
} | |||
else | |||
{ | |||
if (rowNumber < parent.getNumMidiOutputs()) | |||
g.drawText (parent.getMidiDevice (rowNumber, false)->name, | |||
g.drawText (parent.getMidiDevice (rowNumber, false)->deviceInfo.name, | |||
5, 0, width, height, | |||
Justification::centredLeft, true); | |||
} | |||
@@ -368,34 +368,34 @@ private: | |||
} | |||
//============================================================================== | |||
bool hasDeviceListChanged (const StringArray& deviceNames, bool isInputDevice) | |||
bool hasDeviceListChanged (const Array<MidiDeviceInfo>& availableDevices, bool isInputDevice) | |||
{ | |||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs | |||
: midiOutputs; | |||
if (deviceNames.size() != midiDevices.size()) | |||
if (availableDevices.size() != midiDevices.size()) | |||
return true; | |||
for (auto i = 0; i < deviceNames.size(); ++i) | |||
if (deviceNames[i] != midiDevices[i]->name) | |||
for (auto i = 0; i < availableDevices.size(); ++i) | |||
if (availableDevices[i] != midiDevices[i]->deviceInfo) | |||
return true; | |||
return false; | |||
} | |||
ReferenceCountedObjectPtr<MidiDeviceListEntry> findDeviceWithName (const String& name, bool isInputDevice) const | |||
ReferenceCountedObjectPtr<MidiDeviceListEntry> findDevice (MidiDeviceInfo device, bool isInputDevice) const | |||
{ | |||
const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs | |||
: midiOutputs; | |||
for (auto midiDevice : midiDevices) | |||
if (midiDevice->name == name) | |||
return midiDevice; | |||
for (auto& d : midiDevices) | |||
if (d->deviceInfo == device) | |||
return d; | |||
return nullptr; | |||
} | |||
void closeUnpluggedDevices (StringArray& currentlyPluggedInDevices, bool isInputDevice) | |||
void closeUnpluggedDevices (const Array<MidiDeviceInfo>& currentlyPluggedInDevices, bool isInputDevice) | |||
{ | |||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs | |||
: midiOutputs; | |||
@@ -404,7 +404,7 @@ private: | |||
{ | |||
auto& d = *midiDevices[i]; | |||
if (! currentlyPluggedInDevices.contains (d.name)) | |||
if (! currentlyPluggedInDevices.contains (d.deviceInfo)) | |||
{ | |||
if (isInputDevice ? d.inDevice .get() != nullptr | |||
: d.outDevice.get() != nullptr) | |||
@@ -417,26 +417,26 @@ private: | |||
void updateDeviceList (bool isInputDeviceList) | |||
{ | |||
auto newDeviceNames = isInputDeviceList ? MidiInput::getDevices() | |||
: MidiOutput::getDevices(); | |||
auto availableDevices = isInputDeviceList ? MidiInput::getAvailableDevices() | |||
: MidiOutput::getAvailableDevices(); | |||
if (hasDeviceListChanged (newDeviceNames, isInputDeviceList)) | |||
if (hasDeviceListChanged (availableDevices, isInputDeviceList)) | |||
{ | |||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices | |||
= isInputDeviceList ? midiInputs : midiOutputs; | |||
closeUnpluggedDevices (newDeviceNames, isInputDeviceList); | |||
closeUnpluggedDevices (availableDevices, isInputDeviceList); | |||
ReferenceCountedArray<MidiDeviceListEntry> newDeviceList; | |||
// add all currently plugged-in devices to the device list | |||
for (auto newDeviceName : newDeviceNames) | |||
for (auto& newDevice : availableDevices) | |||
{ | |||
MidiDeviceListEntry::Ptr entry = findDeviceWithName (newDeviceName, isInputDeviceList); | |||
MidiDeviceListEntry::Ptr entry = findDevice (newDevice, isInputDeviceList); | |||
if (entry == nullptr) | |||
entry = new MidiDeviceListEntry (newDeviceName); | |||
entry = new MidiDeviceListEntry (newDevice); | |||
newDeviceList.add (entry); | |||
} | |||
@@ -52,6 +52,9 @@ class ConsoleUnitTestRunner : public UnitTestRunner | |||
//============================================================================== | |||
int main (int argc, char **argv) | |||
{ | |||
// Needed for tests that require a message thread | |||
ScopedJuceInitialiser_GUI guiInitialiser; | |||
ConsoleLogger logger; | |||
Logger::setCurrentLogger (&logger); | |||
@@ -179,7 +179,7 @@ | |||
#include "audio_io/juce_AudioIODevice.cpp" | |||
#include "audio_io/juce_AudioIODeviceType.cpp" | |||
#include "midi_io/juce_MidiMessageCollector.cpp" | |||
#include "midi_io/juce_MidiOutput.cpp" | |||
#include "midi_io/juce_MidiDevices.cpp" | |||
#include "sources/juce_AudioSourcePlayer.cpp" | |||
#include "sources/juce_AudioTransportSource.cpp" | |||
#include "native/juce_MidiDataConcatenator.h" | |||
@@ -171,9 +171,8 @@ | |||
#endif | |||
//============================================================================== | |||
#include "midi_io/juce_MidiInput.h" | |||
#include "midi_io/juce_MidiDevices.h" | |||
#include "midi_io/juce_MidiMessageCollector.h" | |||
#include "midi_io/juce_MidiOutput.h" | |||
#include "audio_io/juce_AudioIODevice.h" | |||
#include "audio_io/juce_AudioIODeviceType.h" | |||
#include "audio_io/juce_SystemAudioVolume.h" | |||
@@ -23,18 +23,8 @@ | |||
namespace juce | |||
{ | |||
struct MidiOutput::PendingMessage | |||
{ | |||
PendingMessage (const void* data, int len, double timeStamp) | |||
: message (data, len, timeStamp) | |||
{} | |||
MidiMessage message; | |||
PendingMessage* next; | |||
}; | |||
MidiOutput::MidiOutput (const String& deviceName) | |||
: Thread ("midi out"), name (deviceName) | |||
MidiOutput::MidiOutput (const String& deviceName, const String& deviceIdentifier) | |||
: Thread ("midi out"), deviceInfo (deviceName, deviceIdentifier) | |||
{ | |||
} | |||
@@ -116,7 +106,7 @@ void MidiOutput::run() | |||
{ | |||
while (! threadShouldExit()) | |||
{ | |||
uint32 now = Time::getMillisecondCounter(); | |||
auto now = Time::getMillisecondCounter(); | |||
uint32 eventTime = 0; | |||
uint32 timeToWait = 500; | |||
@@ -167,4 +157,106 @@ void MidiOutput::run() | |||
clearAllPendingMessages(); | |||
} | |||
#if JUCE_UNIT_TESTS | |||
class MidiDevicesUnitTests : public UnitTest | |||
{ | |||
public: | |||
MidiDevicesUnitTests() : UnitTest ("MidiInput/MidiOutput", "MIDI/MPE") {} | |||
void runTest() override | |||
{ | |||
beginTest ("default device (input)"); | |||
{ | |||
auto devices = MidiInput::getAvailableDevices(); | |||
auto defaultDevice = MidiInput::getDefaultDevice(); | |||
if (devices.size() == 0) | |||
expect (defaultDevice == MidiDeviceInfo()); | |||
else | |||
expect (devices.contains (defaultDevice)); | |||
} | |||
beginTest ("default device (output)"); | |||
{ | |||
auto devices = MidiOutput::getAvailableDevices(); | |||
auto defaultDevice = MidiOutput::getDefaultDevice(); | |||
if (devices.size() == 0) | |||
expect (defaultDevice == MidiDeviceInfo()); | |||
else | |||
expect (devices.contains (defaultDevice)); | |||
} | |||
#if JUCE_MAC || JUCE_LINUX || JUCE_IOS | |||
String testDeviceName ("TestDevice"); | |||
String testDeviceName2 ("TestDevice2"); | |||
struct MessageCallbackHandler : public MidiInputCallback | |||
{ | |||
void handleIncomingMidiMessage (MidiInput* source, const MidiMessage& message) override | |||
{ | |||
messageSource = source; | |||
messageReceived = message; | |||
} | |||
MidiInput* messageSource = nullptr; | |||
MidiMessage messageReceived; | |||
}; | |||
MessageCallbackHandler handler; | |||
beginTest ("create device (input)"); | |||
{ | |||
std::unique_ptr<MidiInput> device (MidiInput::createNewDevice (testDeviceName, &handler)); | |||
expect (device.get() != nullptr); | |||
expect (device->getName() == testDeviceName); | |||
device->setName (testDeviceName2); | |||
expect (device->getName() == testDeviceName2); | |||
} | |||
beginTest ("create device (output)"); | |||
{ | |||
std::unique_ptr<MidiOutput> device (MidiOutput::createNewDevice (testDeviceName)); | |||
expect (device.get() != nullptr); | |||
expect (device->getName() == testDeviceName); | |||
} | |||
auto testMessage = MidiMessage::noteOn (5, 12, (uint8) 51); | |||
beginTest ("send messages"); | |||
{ | |||
std::unique_ptr<MidiInput> midiInput (MidiInput::createNewDevice (testDeviceName, &handler)); | |||
expect (midiInput.get() != nullptr); | |||
midiInput->start(); | |||
auto inputInfo = midiInput->getDeviceInfo(); | |||
expect (MidiOutput::getAvailableDevices().contains (inputInfo)); | |||
std::unique_ptr<MidiOutput> midiOutput (MidiOutput::openDevice (midiInput->getIdentifier())); | |||
expect (midiOutput.get() != nullptr); | |||
midiOutput->sendMessageNow (testMessage); | |||
// Pump the message thread for a bit to allow the message to be delivered | |||
MessageManager::getInstance()->runDispatchLoopUntil (100); | |||
expect (handler.messageSource == midiInput.get()); | |||
expect (handler.messageReceived.getChannel() == testMessage.getChannel()); | |||
expect (handler.messageReceived.getNoteNumber() == testMessage.getNoteNumber()); | |||
expect (handler.messageReceived.getVelocity() == testMessage.getVelocity()); | |||
midiInput->stop(); | |||
} | |||
#endif | |||
} | |||
}; | |||
static MidiDevicesUnitTests MidiDevicesUnitTests; | |||
#endif | |||
} // namespace juce |
@@ -0,0 +1,365 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
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 | |||
{ | |||
//============================================================================== | |||
/** | |||
This struct contains information about a MIDI input or output device. | |||
You can get one of these structs by calling the static getAvailableDevices() or | |||
getDefaultDevice() methods of MidiInput and MidiOutput or by calling getDeviceInfo() | |||
on an instance of these classes. Devices can be opened by passing the identifier to | |||
the openDevice() method. | |||
*/ | |||
struct MidiDeviceInfo | |||
{ | |||
MidiDeviceInfo() = default; | |||
MidiDeviceInfo (const String& deviceName, const String& deviceIdentifier) | |||
: name (deviceName), identifier (deviceIdentifier) | |||
{ | |||
} | |||
/** The name of this device. | |||
This will be provided by the OS unless the device has been created with the | |||
createNewDevice() method. | |||
Note that the name is not guaranteed to be unique and two devices with the | |||
same name will be indistinguishable. If you want to address a specific device | |||
it is better to use the identifier. | |||
*/ | |||
String name; | |||
/** The identifier for this device. | |||
This will be provided by the OS and it's format will differ on different systems | |||
e.g. on macOS it will be a number whereas on Windows it will be a long alphanumeric string. | |||
*/ | |||
String identifier; | |||
//============================================================================== | |||
bool operator== (const MidiDeviceInfo& other) const noexcept { return name == other.name && identifier == other.identifier; } | |||
bool operator!= (const MidiDeviceInfo& other) const noexcept { return ! operator== (other); } | |||
}; | |||
class MidiInputCallback; | |||
//============================================================================== | |||
/** | |||
Represents a midi input device. | |||
To create one of these, use the static getAvailableDevices() method to find out what | |||
inputs are available, and then use the openDevice() method to try to open one. | |||
@see MidiOutput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiInput final | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Returns a list of the available midi input devices. | |||
You can open one of the devices by passing its identifier into the openDevice() method. | |||
@see MidiDeviceInfo, getDevices, getDefaultDeviceIndex, openDevice | |||
*/ | |||
static Array<MidiDeviceInfo> getAvailableDevices(); | |||
/** Returns the MidiDeviceInfo of the default midi input device to use. */ | |||
static MidiDeviceInfo getDefaultDevice(); | |||
/** Tries to open one of the midi input devices. | |||
This will return a MidiInput object if it manages to open it. You can then | |||
call start() and stop() on this device, and delete it when no longer needed. | |||
If the device can't be opened, this will return nullptr. | |||
@param deviceIdentifier the ID of the device to open - use the getAvailableDevices() method to | |||
find the available devices that can be opened | |||
@param callback the object that will receive the midi messages from this device | |||
@see MidiInputCallback, getDevices | |||
*/ | |||
static MidiInput* openDevice (const String& deviceIdentifier, MidiInputCallback* callback); | |||
#if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN | |||
/** This will try to create a new midi input device (only available on Linux, macOS and iOS). | |||
This will attempt to create a new midi input device with the specified name for other | |||
apps to connect to. | |||
Returns nullptr if a device can't be created. | |||
@param deviceName the name of the device to create | |||
@param callback the object that will receive the midi messages from this device | |||
*/ | |||
static MidiInput* createNewDevice (const String& deviceName, MidiInputCallback* callback); | |||
#endif | |||
//============================================================================== | |||
/** Destructor. */ | |||
~MidiInput(); | |||
/** Starts the device running. | |||
After calling this, the device will start sending midi messages to the MidiInputCallback | |||
object that was specified when the openDevice() method was called. | |||
@see stop | |||
*/ | |||
void start(); | |||
/** Stops the device running. | |||
@see start | |||
*/ | |||
void stop(); | |||
/** Returns the MidiDeviceInfo struct containing some information about this device. */ | |||
MidiDeviceInfo getDeviceInfo() const noexcept { return deviceInfo; } | |||
/** Returns the identifier of this device. */ | |||
String getIdentifier() const noexcept { return deviceInfo.identifier; } | |||
/** Returns the name of this device. */ | |||
String getName() const noexcept { return deviceInfo.name; } | |||
/** Sets a custom name for the device. */ | |||
void setName (const String& newName) noexcept { deviceInfo.name = newName; } | |||
//============================================================================== | |||
/** Deprecated. */ | |||
static StringArray getDevices(); | |||
/** Deprecated. */ | |||
static int getDefaultDeviceIndex(); | |||
/** Deprecated. */ | |||
static MidiInput* openDevice (int, MidiInputCallback*); | |||
private: | |||
//============================================================================== | |||
explicit MidiInput (const String&, const String&); | |||
MidiDeviceInfo deviceInfo; | |||
void* internal = nullptr; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInput) | |||
}; | |||
//============================================================================== | |||
/** | |||
Receives incoming messages from a physical MIDI input device. | |||
This class is overridden to handle incoming midi messages. See the MidiInput | |||
class for more details. | |||
@see MidiInput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiInputCallback | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~MidiInputCallback() = default; | |||
/** Receives an incoming message. | |||
A MidiInput object will call this method when a midi event arrives. It'll be | |||
called on a high-priority system thread, so avoid doing anything time-consuming | |||
in here, and avoid making any UI calls. You might find the MidiBuffer class helpful | |||
for queueing incoming messages for use later. | |||
@param source the MidiInput object that generated the message | |||
@param message the incoming message. The message's timestamp is set to a value | |||
equivalent to (Time::getMillisecondCounter() / 1000.0) to specify the | |||
time when the message arrived | |||
*/ | |||
virtual void handleIncomingMidiMessage (MidiInput* source, | |||
const MidiMessage& message) = 0; | |||
/** Notification sent each time a packet of a multi-packet sysex message arrives. | |||
If a long sysex message is broken up into multiple packets, this callback is made | |||
for each packet that arrives until the message is finished, at which point | |||
the normal handleIncomingMidiMessage() callback will be made with the entire | |||
message. | |||
The message passed in will contain the start of a sysex, but won't be finished | |||
with the terminating 0xf7 byte. | |||
*/ | |||
virtual void handlePartialSysexMessage (MidiInput* source, | |||
const uint8* messageData, | |||
int numBytesSoFar, | |||
double timestamp) | |||
{ | |||
ignoreUnused (source, messageData, numBytesSoFar, timestamp); | |||
} | |||
}; | |||
//============================================================================== | |||
/** | |||
Represents a midi output device. | |||
To create one of these, use the static getAvailableDevices() method to find out what | |||
outputs are available, and then use the openDevice() method to try to open one. | |||
@see MidiInput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiOutput final : private Thread | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Returns a list of the available midi output devices. | |||
You can open one of the devices by passing its identifier into the openDevice() method. | |||
@see MidiDeviceInfo, getDevices, getDefaultDeviceIndex, openDevice | |||
*/ | |||
static Array<MidiDeviceInfo> getAvailableDevices(); | |||
/** Returns the MidiDeviceInfo of the default midi output device to use. */ | |||
static MidiDeviceInfo getDefaultDevice(); | |||
/** Tries to open one of the midi output devices. | |||
This will return a MidiOutput object if it manages to open it. You can then | |||
send messages to this device, and delete it when no longer needed. | |||
If the device can't be opened, this will return nullptr. | |||
@param deviceIdentifier the ID of the device to open - use the getAvailableDevices() method to | |||
find the available devices that can be opened | |||
@see getDevices | |||
*/ | |||
static MidiOutput* openDevice (const String& deviceIdentifier); | |||
#if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN | |||
/** This will try to create a new midi output device (only available on Linux, macOS and iOS). | |||
This will attempt to create a new midi output device with the specified name that other | |||
apps can connect to and use as their midi input. | |||
Returns nullptr if a device can't be created. | |||
@param deviceName the name of the device to create | |||
*/ | |||
static MidiOutput* createNewDevice (const String& deviceName); | |||
#endif | |||
//============================================================================== | |||
/** Destructor. */ | |||
~MidiOutput() override; | |||
/** Returns the MidiDeviceInfo struct containing some information about this device. */ | |||
MidiDeviceInfo getDeviceInfo() const noexcept { return deviceInfo; } | |||
/** Returns the identifier of this device. */ | |||
String getIdentifier() const noexcept { return deviceInfo.identifier; } | |||
/** Returns the name of this device. */ | |||
String getName() const noexcept { return deviceInfo.name; } | |||
/** Sets a custom name for the device. */ | |||
void setName (const String& newName) noexcept { deviceInfo.name = newName; } | |||
//============================================================================== | |||
/** Sends out a MIDI message immediately. */ | |||
void sendMessageNow (const MidiMessage& message); | |||
/** Sends out a sequence of MIDI messages immediately. */ | |||
void sendBlockOfMessagesNow (const MidiBuffer& buffer); | |||
/** This lets you supply a block of messages that will be sent out at some point | |||
in the future. | |||
The MidiOutput class has an internal thread that can send out timestamped | |||
messages - this appends a set of messages to its internal buffer, ready for | |||
sending. | |||
This will only work if you've already started the thread with startBackgroundThread(). | |||
A time is specified, at which the block of messages should be sent. This time uses | |||
the same time base as Time::getMillisecondCounter(), and must be in the future. | |||
The samplesPerSecondForBuffer parameter indicates the number of samples per second | |||
used by the MidiBuffer. Each event in a MidiBuffer has a sample position, and the | |||
samplesPerSecondForBuffer value is needed to convert this sample position to a | |||
real time. | |||
*/ | |||
void sendBlockOfMessages (const MidiBuffer& buffer, | |||
double millisecondCounterToStartAt, | |||
double samplesPerSecondForBuffer); | |||
/** Gets rid of any midi messages that had been added by sendBlockOfMessages(). */ | |||
void clearAllPendingMessages(); | |||
/** Starts up a background thread so that the device can send blocks of data. | |||
Call this to get the device ready, before using sendBlockOfMessages(). | |||
*/ | |||
void startBackgroundThread(); | |||
/** Stops the background thread, and clears any pending midi events. | |||
@see startBackgroundThread | |||
*/ | |||
void stopBackgroundThread(); | |||
//============================================================================== | |||
/** Deprecated. */ | |||
static StringArray getDevices(); | |||
/** Deprecated. */ | |||
static int getDefaultDeviceIndex(); | |||
/** Deprecated. */ | |||
static MidiOutput* openDevice (int); | |||
private: | |||
//============================================================================== | |||
struct PendingMessage | |||
{ | |||
PendingMessage (const void* data, int len, double timeStamp) | |||
: message (data, len, timeStamp) | |||
{ | |||
} | |||
MidiMessage message; | |||
PendingMessage* next; | |||
}; | |||
//============================================================================== | |||
explicit MidiOutput (const String&, const String&); | |||
void run() override; | |||
MidiDeviceInfo deviceInfo; | |||
void* internal = nullptr; | |||
CriticalSection lock; | |||
PendingMessage* firstMessage = nullptr; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutput) | |||
}; | |||
} // namespace juce |
@@ -1,180 +0,0 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
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 | |||
{ | |||
class MidiInput; | |||
//============================================================================== | |||
/** | |||
Receives incoming messages from a physical MIDI input device. | |||
This class is overridden to handle incoming midi messages. See the MidiInput | |||
class for more details. | |||
@see MidiInput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiInputCallback | |||
{ | |||
public: | |||
/** Destructor. */ | |||
virtual ~MidiInputCallback() = default; | |||
/** Receives an incoming message. | |||
A MidiInput object will call this method when a midi event arrives. It'll be | |||
called on a high-priority system thread, so avoid doing anything time-consuming | |||
in here, and avoid making any UI calls. You might find the MidiBuffer class helpful | |||
for queueing incoming messages for use later. | |||
@param source the MidiInput object that generated the message | |||
@param message the incoming message. The message's timestamp is set to a value | |||
equivalent to (Time::getMillisecondCounter() / 1000.0) to specify the | |||
time when the message arrived. | |||
*/ | |||
virtual void handleIncomingMidiMessage (MidiInput* source, | |||
const MidiMessage& message) = 0; | |||
/** Notification sent each time a packet of a multi-packet sysex message arrives. | |||
If a long sysex message is broken up into multiple packets, this callback is made | |||
for each packet that arrives until the message is finished, at which point | |||
the normal handleIncomingMidiMessage() callback will be made with the entire | |||
message. | |||
The message passed in will contain the start of a sysex, but won't be finished | |||
with the terminating 0xf7 byte. | |||
*/ | |||
virtual void handlePartialSysexMessage (MidiInput* source, | |||
const uint8* messageData, | |||
int numBytesSoFar, | |||
double timestamp) | |||
{ | |||
ignoreUnused (source, messageData, numBytesSoFar, timestamp); | |||
} | |||
}; | |||
//============================================================================== | |||
/** | |||
Represents a midi input device. | |||
To create one of these, use the static getDevices() method to find out what inputs are | |||
available, and then use the openDevice() method to try to open one. | |||
@see MidiOutput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiInput final | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Returns a list of the available midi input devices. | |||
You can open one of the devices by passing its index into the | |||
openDevice() method. | |||
@see getDefaultDeviceIndex, openDevice | |||
*/ | |||
static StringArray getDevices(); | |||
/** Returns the index of the default midi input device to use. | |||
This refers to the index in the list returned by getDevices(). | |||
*/ | |||
static int getDefaultDeviceIndex(); | |||
/** Tries to open one of the midi input devices. | |||
This will return a MidiInput object if it manages to open it. You can then | |||
call start() and stop() on this device, and delete it when no longer needed. | |||
If the device can't be opened, this will return a null pointer. | |||
@param deviceIndex the index of a device from the list returned by getDevices() | |||
@param callback the object that will receive the midi messages from this device. | |||
@see MidiInputCallback, getDevices | |||
*/ | |||
static MidiInput* openDevice (int deviceIndex, | |||
MidiInputCallback* callback); | |||
#if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN | |||
/** This will try to create a new midi input device (Not available on Windows). | |||
This will attempt to create a new midi input device with the specified name, | |||
for other apps to connect to. | |||
Returns nullptr if a device can't be created. | |||
@param deviceName the name to use for the new device | |||
@param callback the object that will receive the midi messages from this device. | |||
*/ | |||
static MidiInput* createNewDevice (const String& deviceName, | |||
MidiInputCallback* callback); | |||
#endif | |||
//============================================================================== | |||
/** Destructor. */ | |||
~MidiInput(); | |||
/** Returns the name of this device. */ | |||
const String& getName() const noexcept { return name; } | |||
/** Allows you to set a custom name for the device, in case you don't like the name | |||
it was given when created. | |||
*/ | |||
void setName (const String& newName) noexcept { name = newName; } | |||
//============================================================================== | |||
/** Starts the device running. | |||
After calling this, the device will start sending midi messages to the | |||
MidiInputCallback object that was specified when the openDevice() method | |||
was called. | |||
@see stop | |||
*/ | |||
void start(); | |||
/** Stops the device running. | |||
@see start | |||
*/ | |||
void stop(); | |||
private: | |||
//============================================================================== | |||
String name; | |||
void* internal = nullptr; | |||
// The input objects are created with the openDevice() method. | |||
explicit MidiInput (const String&); | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInput) | |||
}; | |||
} // namespace juce |
@@ -1,145 +0,0 @@ | |||
/* | |||
============================================================================== | |||
This file is part of the JUCE library. | |||
Copyright (c) 2017 - ROLI Ltd. | |||
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 | |||
{ | |||
//============================================================================== | |||
/** | |||
Controls a physical MIDI output device. | |||
To create one of these, use the static getDevices() method to get a list of the | |||
available output devices, then use the openDevice() method to try to open one. | |||
@see MidiInput | |||
@tags{Audio} | |||
*/ | |||
class JUCE_API MidiOutput final : private Thread | |||
{ | |||
public: | |||
//============================================================================== | |||
/** Returns a list of the available midi output devices. | |||
You can open one of the devices by passing its index into the | |||
openDevice() method. | |||
@see getDefaultDeviceIndex, openDevice | |||
*/ | |||
static StringArray getDevices(); | |||
/** Returns the index of the default midi output device to use. | |||
This refers to the index in the list returned by getDevices(). | |||
*/ | |||
static int getDefaultDeviceIndex(); | |||
/** Tries to open one of the midi output devices. | |||
This will return a MidiOutput object if it manages to open it. You can then | |||
send messages to this device, and delete it when no longer needed. | |||
If the device can't be opened, this will return a null pointer. | |||
@param deviceIndex the index of a device from the list returned by getDevices() | |||
@see getDevices | |||
*/ | |||
static MidiOutput* openDevice (int deviceIndex); | |||
#if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN | |||
/** This will try to create a new midi output device (Not available on Windows). | |||
This will attempt to create a new midi output device that other apps can connect | |||
to and use as their midi input. | |||
Returns nullptr if a device can't be created. | |||
@param deviceName the name to use for the new device | |||
*/ | |||
static MidiOutput* createNewDevice (const String& deviceName); | |||
#endif | |||
//============================================================================== | |||
/** Destructor. */ | |||
~MidiOutput() override; | |||
/** Returns the name of this device. */ | |||
const String& getName() const noexcept { return name; } | |||
/** Sends out a MIDI message immediately. */ | |||
void sendMessageNow (const MidiMessage& message); | |||
/** Sends out a sequence of MIDI messages immediately. */ | |||
void sendBlockOfMessagesNow (const MidiBuffer& buffer); | |||
//============================================================================== | |||
/** This lets you supply a block of messages that will be sent out at some point | |||
in the future. | |||
The MidiOutput class has an internal thread that can send out timestamped | |||
messages - this appends a set of messages to its internal buffer, ready for | |||
sending. | |||
This will only work if you've already started the thread with startBackgroundThread(). | |||
A time is specified, at which the block of messages should be sent. This time uses | |||
the same time base as Time::getMillisecondCounter(), and must be in the future. | |||
The samplesPerSecondForBuffer parameter indicates the number of samples per second | |||
used by the MidiBuffer. Each event in a MidiBuffer has a sample position, and the | |||
samplesPerSecondForBuffer value is needed to convert this sample position to a | |||
real time. | |||
*/ | |||
void sendBlockOfMessages (const MidiBuffer& buffer, | |||
double millisecondCounterToStartAt, | |||
double samplesPerSecondForBuffer); | |||
/** Gets rid of any midi messages that had been added by sendBlockOfMessages(). */ | |||
void clearAllPendingMessages(); | |||
/** Starts up a background thread so that the device can send blocks of data. | |||
Call this to get the device ready, before using sendBlockOfMessages(). | |||
*/ | |||
void startBackgroundThread(); | |||
/** Stops the background thread, and clears any pending midi events. | |||
@see startBackgroundThread | |||
*/ | |||
void stopBackgroundThread(); | |||
private: | |||
//============================================================================== | |||
void* internal = nullptr; | |||
CriticalSection lock; | |||
struct PendingMessage; | |||
PendingMessage* firstMessage = nullptr; | |||
String name; | |||
MidiOutput (const String& midiName); // These objects are created with the openDevice() method. | |||
void run() override; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutput) | |||
}; | |||
} // namespace juce |
@@ -51,6 +51,8 @@ public class JuceMidiSupport | |||
// send will do nothing on an input port | |||
void sendMidi (byte[] msg, int offset, int count); | |||
String getName (); | |||
} | |||
//============================================================================== | |||
@@ -256,6 +258,12 @@ public class JuceMidiSupport | |||
{ | |||
} | |||
@Override | |||
public String getName () | |||
{ | |||
return owner.getPortName (portPath); | |||
} | |||
MidiDeviceManager owner; | |||
MidiOutputPort androidPort; | |||
MidiPortPath portPath; | |||
@@ -331,6 +339,12 @@ public class JuceMidiSupport | |||
androidPort = null; | |||
} | |||
@Override | |||
public String getName () | |||
{ | |||
return owner.getPortName (portPath); | |||
} | |||
MidiDeviceManager owner; | |||
MidiInputPort androidPort; | |||
MidiPortPath portPath; | |||
@@ -343,7 +357,6 @@ public class JuceMidiSupport | |||
deviceId = deviceIdToUse; | |||
isInput = direction; | |||
portIndex = androidIndex; | |||
} | |||
public int deviceId; | |||
@@ -555,17 +568,17 @@ public class JuceMidiSupport | |||
super.finalize (); | |||
} | |||
public String[] getJuceAndroidMidiInputDevices () | |||
public String[] getJuceAndroidMidiOutputDeviceNameAndIDs () | |||
{ | |||
return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); | |||
return getJuceAndroidMidiDeviceNameAndIDs (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); | |||
} | |||
public String[] getJuceAndroidMidiOutputDevices () | |||
public String[] getJuceAndroidMidiInputDeviceNameAndIDs () | |||
{ | |||
return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT); | |||
return getJuceAndroidMidiDeviceNameAndIDs (MidiDeviceInfo.PortInfo.TYPE_INPUT); | |||
} | |||
private String[] getJuceAndroidMidiDevices (int portType) | |||
private String[] getJuceAndroidMidiDeviceNameAndIDs (int portType) | |||
{ | |||
// only update the list when JUCE asks for a new list | |||
synchronized (MidiDeviceManager.class) | |||
@@ -573,22 +586,24 @@ public class JuceMidiSupport | |||
deviceInfos = getDeviceInfos (); | |||
} | |||
ArrayList<String> portNames = new ArrayList<String> (); | |||
ArrayList<String> portNameAndIDs = new ArrayList<String> (); | |||
int index = 0; | |||
for (MidiPortPath portInfo = getPortPathForJuceIndex (portType, index); portInfo != null; portInfo = getPortPathForJuceIndex (portType, ++index)) | |||
portNames.add (getPortName (portInfo)); | |||
for (MidiPortPath portInfo : getAllPorts (portType)) | |||
{ | |||
portNameAndIDs.add (getPortName (portInfo)); | |||
portNameAndIDs.add (Integer.toString (portInfo.hashCode ())); | |||
} | |||
String[] names = new String[portNames.size ()]; | |||
return portNames.toArray (names); | |||
String[] names = new String[portNameAndIDs.size ()]; | |||
return portNameAndIDs.toArray (names); | |||
} | |||
private JuceMidiPort openMidiPortWithJuceIndex (int index, long host, boolean isInput) | |||
private JuceMidiPort openMidiPortWithID (int deviceID, long host, boolean isInput) | |||
{ | |||
synchronized (MidiDeviceManager.class) | |||
{ | |||
int portTypeToFind = (isInput ? MidiDeviceInfo.PortInfo.TYPE_OUTPUT : MidiDeviceInfo.PortInfo.TYPE_INPUT); | |||
MidiPortPath portInfo = getPortPathForJuceIndex (portTypeToFind, index); | |||
int portTypeToFind = (isInput ? MidiDeviceInfo.PortInfo.TYPE_INPUT : MidiDeviceInfo.PortInfo.TYPE_OUTPUT); | |||
MidiPortPath portInfo = getPortPathForID (portTypeToFind, deviceID); | |||
if (portInfo != null) | |||
{ | |||
@@ -633,14 +648,14 @@ public class JuceMidiSupport | |||
return null; | |||
} | |||
public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host) | |||
public JuceMidiPort openMidiInputPortWithID (int deviceID, long host) | |||
{ | |||
return openMidiPortWithJuceIndex (index, host, true); | |||
return openMidiPortWithID (deviceID, host, true); | |||
} | |||
public JuceMidiPort openMidiOutputPortWithJuceIndex (int index) | |||
public JuceMidiPort openMidiOutputPortWithID (int deviceID) | |||
{ | |||
return openMidiPortWithJuceIndex (index, 0, false); | |||
return openMidiPortWithID (deviceID, 0, false); | |||
} | |||
/* 0: unpaired, 1: paired, 2: pairing */ | |||
@@ -773,24 +788,6 @@ public class JuceMidiSupport | |||
openPorts.remove (path); | |||
} | |||
public String getInputPortNameForJuceIndex (int index) | |||
{ | |||
MidiPortPath portInfo = getPortPathForJuceIndex (MidiDeviceInfo.PortInfo.TYPE_OUTPUT, index); | |||
if (portInfo != null) | |||
return getPortName (portInfo); | |||
return ""; | |||
} | |||
public String getOutputPortNameForJuceIndex (int index) | |||
{ | |||
MidiPortPath portInfo = getPortPathForJuceIndex (MidiDeviceInfo.PortInfo.TYPE_INPUT, index); | |||
if (portInfo != null) | |||
return getPortName (portInfo); | |||
return ""; | |||
} | |||
public void onDeviceAdded (MidiDeviceInfo info) | |||
{ | |||
// only add standard midi devices | |||
@@ -980,24 +977,24 @@ public class JuceMidiSupport | |||
return ""; | |||
} | |||
public MidiPortPath getPortPathForJuceIndex (int portType, int juceIndex) | |||
public ArrayList<MidiPortPath> getAllPorts (int portType) | |||
{ | |||
int portIdx = 0; | |||
ArrayList<MidiPortPath> ports = new ArrayList<MidiPortPath> (); | |||
for (MidiDeviceInfo info : deviceInfos) | |||
{ | |||
for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ()) | |||
{ | |||
if (portInfo.getType () == portType) | |||
{ | |||
if (portIdx == juceIndex) | |||
return new MidiPortPath (info.getId (), | |||
(portType == MidiDeviceInfo.PortInfo.TYPE_INPUT), | |||
portInfo.getPortNumber ()); | |||
ports.add (new MidiPortPath (info.getId (), (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT), | |||
portInfo.getPortNumber ())); | |||
portIdx++; | |||
} | |||
} | |||
} | |||
return ports; | |||
} | |||
public MidiPortPath getPortPathForID (int portType, int deviceID) | |||
{ | |||
for (MidiPortPath port : getAllPorts (portType)) | |||
if (port.hashCode () == deviceID) | |||
return port; | |||
return null; | |||
} | |||
@@ -66,9 +66,9 @@ public: | |||
} | |||
} | |||
static StringArray getDevices (bool input) | |||
static Array<MidiDeviceInfo> getDevices (bool input) | |||
{ | |||
StringArray devices; | |||
Array<MidiDeviceInfo> devices; | |||
for (auto& card : findAllALSACardIDs()) | |||
findMidiDevices (devices, input, card); | |||
@@ -96,7 +96,7 @@ private: | |||
} | |||
// Adds all midi devices to the devices array of the given input/output type on the given card | |||
static void findMidiDevices (StringArray& devices, bool input, int cardNum) | |||
static void findMidiDevices (Array<MidiDeviceInfo>& devices, bool input, int cardNum) | |||
{ | |||
snd_ctl_t* ctl = nullptr; | |||
auto status = snd_ctl_open (&ctl, ("hw:" + String (cardNum)).toRawUTF8(), 0); | |||
@@ -131,9 +131,10 @@ private: | |||
status = snd_ctl_rawmidi_info (ctl, info); | |||
if (status == 0) | |||
devices.add ("hw:" + String (cardNum) + "," | |||
+ String (device) + "," | |||
+ String (sub)); | |||
{ | |||
String deviceName ("hw:" + String (cardNum) + "," + String (device) + "," + String (sub)); | |||
devices.add (MidiDeviceInfo (deviceName, deviceName)); | |||
} | |||
} | |||
} | |||
@@ -507,58 +508,74 @@ AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Bela() | |||
return new BelaAudioIODeviceType(); | |||
} | |||
//============================================================================== | |||
// TODO: Add Bela MidiOutput support | |||
StringArray MidiOutput::getDevices() { return {}; } | |||
int MidiOutput::getDefaultDeviceIndex() { return 0; } | |||
MidiOutput* MidiOutput::openDevice (int) { return {}; } | |||
MidiOutput* MidiOutput::createNewDevice (const String&) { return {}; } | |||
MidiOutput::~MidiOutput() {} | |||
void MidiOutput::sendMessageNow (const MidiMessage&) {} | |||
MidiInput::MidiInput (const String& deviceName, const String& deviceID) | |||
: deviceInfo (deviceName, deviceID) | |||
{ | |||
} | |||
//============================================================================== | |||
MidiInput::MidiInput (const String& nm) : name (nm) {} | |||
MidiInput::~MidiInput() { delete static_cast<BelaMidiInput*> (internal); } | |||
void MidiInput::start() { static_cast<BelaMidiInput*> (internal)->start(); } | |||
void MidiInput::stop() { static_cast<BelaMidiInput*> (internal)->stop(); } | |||
MidiInput::~MidiInput() | |||
void Array<MidiDeviceInfo> MidiInput::getAvailableDevices() | |||
{ | |||
delete static_cast<BelaMidiInput*> (internal); | |||
return BelaMidiInput::getDevices (true); | |||
} | |||
void MidiInput::start() { static_cast<BelaMidiInput*> (internal)->start(); } | |||
void MidiInput::stop() { static_cast<BelaMidiInput*> (internal)->stop(); } | |||
MidiDeviceInfo MidiInput::getDefaultDevice() | |||
{ | |||
return getAvailableDevices().getFirst(); | |||
} | |||
int MidiInput::getDefaultDeviceIndex() | |||
MidiInput* MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) | |||
{ | |||
return 0; | |||
if (deviceIdentifier.isEmpty()) | |||
return nullptr; | |||
std::unique_ptr<MidiInput> midiInput (new MidiInput (deviceIdentifier, deviceIdentifier)); | |||
midiInput->internal = new BelaMidiInput (deviceIdentifier, result, callback); | |||
return midiInput.release(); | |||
} | |||
StringArray MidiInput::getDevices() | |||
MidiInput* MidiInput::createNewDevice (const String&, MidiInputCallback*) | |||
{ | |||
return BelaMidiInput::getDevices (true); | |||
// N/A on Bela | |||
jassertfalse; | |||
return nullptr; | |||
} | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
StringArray MidiInput::getDevices() | |||
{ | |||
auto devices = getDevices(); | |||
StringArray deviceNames; | |||
if (index >= 0 && index < devices.size()) | |||
{ | |||
auto deviceName = devices[index]; | |||
auto result = new MidiInput (deviceName); | |||
result->internal = new BelaMidiInput (deviceName, result, callback); | |||
return result; | |||
} | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
return deviceNames; | |||
} | |||
return {}; | |||
int MidiInput::getDefaultDeviceIndex() | |||
{ | |||
return 0; | |||
} | |||
MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
{ | |||
jassertfalse; // N/A on Bela | |||
return {}; | |||
return openDevice (getAvailableDevices()[index].identifier, callback); | |||
} | |||
//============================================================================== | |||
// TODO: Add Bela MidiOutput support | |||
MidiOutput::~MidiOutput() {} | |||
void MidiOutput::sendMessageNow (const MidiMessage&) {} | |||
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices() { return {}; } | |||
MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } | |||
MidiOutput* MidiOutput::openDevice (const String&) { return nullptr; } | |||
MidiOutput* MidiOutput::createNewDevice (const String&) { return nullptr; } | |||
StringArray MidiOutput::getDevices() { return {}; } | |||
int MidiOutput::getDefaultDeviceIndex() { return 0;} | |||
MidiOutput* MidiOutput::openDevice (int) { return nullptr; } | |||
} // namespace juce |
@@ -67,12 +67,12 @@ public: | |||
static String getAlsaMidiName() | |||
{ | |||
#ifdef JUCE_ALSA_MIDI_NAME | |||
return JUCE_ALSA_MIDI_NAME; | |||
return JUCE_ALSA_MIDI_NAME; | |||
#else | |||
if (auto* app = JUCEApplicationBase::getInstance()) | |||
return app->getApplicationName(); | |||
if (auto* app = JUCEApplicationBase::getInstance()) | |||
return app->getApplicationName(); | |||
return "JUCE"; | |||
return "JUCE"; | |||
#endif | |||
} | |||
@@ -198,7 +198,8 @@ public: | |||
isInput ? (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_WRITE : 0)) | |||
: (SND_SEQ_PORT_CAP_READ | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); | |||
portId = snd_seq_create_simple_port (seqHandle, name.toUTF8(), caps, | |||
portName = name; | |||
portId = snd_seq_create_simple_port (seqHandle, portName.toUTF8(), caps, | |||
SND_SEQ_PORT_TYPE_MIDI_GENERIC | | |||
SND_SEQ_PORT_TYPE_APPLICATION); | |||
} | |||
@@ -215,13 +216,15 @@ public: | |||
} | |||
AlsaClient& client; | |||
MidiInputCallback* callback = nullptr; | |||
snd_midi_event_t* midiParser = nullptr; | |||
MidiInput* midiInput = nullptr; | |||
int maxEventSize = 4096; | |||
int portId = -1; | |||
bool callbackEnabled = false; | |||
bool isInput = false; | |||
String portName; | |||
int maxEventSize = 4096, portId = -1; | |||
bool callbackEnabled = false, isInput = false; | |||
}; | |||
static Ptr getInstance() | |||
@@ -359,11 +362,16 @@ private: | |||
AlsaClient* AlsaClient::instance = nullptr; | |||
//============================================================================== | |||
static String getFormattedPortIdentifier (int clientId, int portId) | |||
{ | |||
return String (clientId) + "-" + String (portId); | |||
} | |||
static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, | |||
snd_seq_client_info_t* clientInfo, | |||
bool forInput, | |||
StringArray& deviceNamesFound, | |||
int deviceIndexToOpen) | |||
Array<MidiDeviceInfo>& devices, | |||
const String& deviceIdentifierToOpen) | |||
{ | |||
AlsaClient::Port* port = nullptr; | |||
@@ -371,7 +379,7 @@ static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, | |||
snd_seq_port_info_t* portInfo = nullptr; | |||
snd_seq_port_info_alloca (&portInfo); | |||
jassert (portInfo); | |||
jassert (portInfo != nullptr); | |||
auto numPorts = snd_seq_client_info_get_num_ports (clientInfo); | |||
auto sourceClient = snd_seq_client_info_get_client (clientInfo); | |||
@@ -384,19 +392,19 @@ static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, | |||
&& (snd_seq_port_info_get_capability (portInfo) | |||
& (forInput ? SND_SEQ_PORT_CAP_SUBS_READ : SND_SEQ_PORT_CAP_SUBS_WRITE)) != 0) | |||
{ | |||
String portName = snd_seq_port_info_get_name(portInfo); | |||
String portName (snd_seq_port_info_get_name (portInfo)); | |||
auto portID = snd_seq_port_info_get_port (portInfo); | |||
deviceNamesFound.add (portName); | |||
MidiDeviceInfo device (portName, getFormattedPortIdentifier (sourceClient, portID)); | |||
devices.add (device); | |||
if (deviceNamesFound.size() == deviceIndexToOpen + 1) | |||
if (deviceIdentifierToOpen.isNotEmpty() && deviceIdentifierToOpen == device.identifier) | |||
{ | |||
auto sourcePort = snd_seq_port_info_get_port (portInfo); | |||
if (sourcePort != -1) | |||
if (portID != -1) | |||
{ | |||
port = client->createPort (portName, forInput, false); | |||
jassert (port->isValid()); | |||
port->connectWith (sourceClient, sourcePort); | |||
port->connectWith (sourceClient, portID); | |||
break; | |||
} | |||
} | |||
@@ -407,8 +415,8 @@ static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, | |||
} | |||
static AlsaClient::Port* iterateMidiDevices (bool forInput, | |||
StringArray& deviceNamesFound, | |||
int deviceIndexToOpen) | |||
Array<MidiDeviceInfo>& devices, | |||
const String& deviceIdentifierToOpen) | |||
{ | |||
AlsaClient::Port* port = nullptr; | |||
auto client = AlsaClient::getInstance(); | |||
@@ -432,85 +440,95 @@ static AlsaClient::Port* iterateMidiDevices (bool forInput, | |||
{ | |||
if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) | |||
{ | |||
auto sourceClient = snd_seq_client_info_get_client (clientInfo); | |||
port = iterateMidiClient (client, clientInfo, forInput, | |||
devices, deviceIdentifierToOpen); | |||
if (sourceClient != client->getId() && sourceClient != SND_SEQ_CLIENT_SYSTEM) | |||
{ | |||
port = iterateMidiClient (client, clientInfo, forInput, | |||
deviceNamesFound, deviceIndexToOpen); | |||
if (port != nullptr) | |||
break; | |||
} | |||
if (port != nullptr) | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
deviceNamesFound.appendNumbersToDuplicates (true, true); | |||
return port; | |||
} | |||
} // namespace | |||
StringArray MidiOutput::getDevices() | |||
//============================================================================== | |||
Array<MidiDeviceInfo> MidiInput::getAvailableDevices() | |||
{ | |||
StringArray devices; | |||
iterateMidiDevices (false, devices, -1); | |||
Array<MidiDeviceInfo> devices; | |||
iterateMidiDevices (true, devices, {}); | |||
return devices; | |||
} | |||
int MidiOutput::getDefaultDeviceIndex() | |||
MidiDeviceInfo MidiInput::getDefaultDevice() | |||
{ | |||
return 0; | |||
return getAvailableDevices().getFirst(); | |||
} | |||
MidiOutput* MidiOutput::openDevice (int deviceIndex) | |||
MidiInput* MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) | |||
{ | |||
MidiOutput* newDevice = nullptr; | |||
if (deviceIdentifier.isEmpty()) | |||
return nullptr; | |||
StringArray devices; | |||
auto* port = iterateMidiDevices (false, devices, deviceIndex); | |||
Array<MidiDeviceInfo> devices; | |||
auto* port = iterateMidiDevices (true, devices, deviceIdentifier); | |||
if (port == nullptr) | |||
return nullptr; | |||
jassert (port->isValid()); | |||
newDevice = new MidiOutput (devices [deviceIndex]); | |||
port->setupOutput(); | |||
newDevice->internal = port; | |||
std::unique_ptr<MidiInput> midiInput (new MidiInput (port->portName, deviceIdentifier)); | |||
return newDevice; | |||
port->setupInput (midiInput.get(), callback); | |||
midiInput->internal = port; | |||
return midiInput.release(); | |||
} | |||
MidiOutput* MidiOutput::createNewDevice (const String& deviceName) | |||
MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) | |||
{ | |||
MidiOutput* newDevice = nullptr; | |||
auto client = AlsaClient::getInstance(); | |||
auto* port = client->createPort (deviceName, false, true); | |||
jassert (port != nullptr && port->isValid()); | |||
auto* port = client->createPort (deviceName, true, true); | |||
newDevice = new MidiOutput (deviceName); | |||
port->setupOutput(); | |||
newDevice->internal = port; | |||
jassert (port->isValid()); | |||
std::unique_ptr<MidiInput> midiInput (new MidiInput (deviceName, getFormattedPortIdentifier (client->getId(), port->portId))); | |||
return newDevice; | |||
port->setupInput (midiInput.get(), callback); | |||
midiInput->internal = port; | |||
return midiInput.release(); | |||
} | |||
MidiOutput::~MidiOutput() | |||
StringArray MidiInput::getDevices() | |||
{ | |||
stopBackgroundThread(); | |||
AlsaClient::getInstance()->deletePort (static_cast<AlsaClient::Port*> (internal)); | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
deviceNames.appendNumbersToDuplicates (true, true); | |||
return deviceNames; | |||
} | |||
void MidiOutput::sendMessageNow (const MidiMessage& message) | |||
int MidiInput::getDefaultDeviceIndex() | |||
{ | |||
static_cast<AlsaClient::Port*> (internal)->sendMessageNow (message); | |||
return 0; | |||
} | |||
//============================================================================== | |||
MidiInput::MidiInput (const String& nm) : name (nm) | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
{ | |||
return openDevice (getAvailableDevices()[index].identifier, callback); | |||
} | |||
MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) | |||
: deviceInfo (deviceName, deviceIdentifier) | |||
{ | |||
} | |||
@@ -530,68 +548,118 @@ void MidiInput::stop() | |||
static_cast<AlsaClient::Port*> (internal)->enableCallback (false); | |||
} | |||
int MidiInput::getDefaultDeviceIndex() | |||
//============================================================================== | |||
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices() | |||
{ | |||
return 0; | |||
Array<MidiDeviceInfo> devices; | |||
iterateMidiDevices (false, devices, {}); | |||
return devices; | |||
} | |||
StringArray MidiInput::getDevices() | |||
MidiDeviceInfo MidiOutput::getDefaultDevice() | |||
{ | |||
StringArray devices; | |||
iterateMidiDevices (true, devices, -1); | |||
return devices; | |||
return getAvailableDevices().getFirst(); | |||
} | |||
MidiInput* MidiInput::openDevice (int deviceIndex, MidiInputCallback* callback) | |||
MidiOutput* MidiOutput::openDevice (const String& deviceIdentifier) | |||
{ | |||
StringArray devices; | |||
auto* port = iterateMidiDevices (true, devices, deviceIndex); | |||
if (deviceIdentifier.isEmpty()) | |||
return nullptr; | |||
Array<MidiDeviceInfo> devices; | |||
auto* port = iterateMidiDevices (false, devices, deviceIdentifier); | |||
if (port == nullptr) | |||
return nullptr; | |||
jassert (port->isValid()); | |||
auto newDevice = new MidiInput (devices [deviceIndex]); | |||
port->setupInput (newDevice, callback); | |||
newDevice->internal = port; | |||
return newDevice; | |||
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (port->portName, deviceIdentifier)); | |||
port->setupOutput(); | |||
midiOutput->internal = port; | |||
return midiOutput.release(); | |||
} | |||
MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) | |||
MidiOutput* MidiOutput::createNewDevice (const String& deviceName) | |||
{ | |||
auto client = AlsaClient::getInstance(); | |||
auto* port = client->createPort (deviceName, true, true); | |||
auto* port = client->createPort (deviceName, false, true); | |||
jassert (port->isValid()); | |||
jassert (port != nullptr && port->isValid()); | |||
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (deviceName, getFormattedPortIdentifier (client->getId(), port->portId))); | |||
auto newDevice = new MidiInput (deviceName); | |||
port->setupInput (newDevice, callback); | |||
newDevice->internal = port; | |||
return newDevice; | |||
port->setupOutput(); | |||
midiOutput->internal = port; | |||
return midiOutput.release(); | |||
} | |||
StringArray MidiOutput::getDevices() | |||
{ | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
deviceNames.appendNumbersToDuplicates (true, true); | |||
return deviceNames; | |||
} | |||
int MidiOutput::getDefaultDeviceIndex() | |||
{ | |||
return 0; | |||
} | |||
MidiOutput* MidiOutput::openDevice (int index) | |||
{ | |||
return openDevice (getAvailableDevices()[index].identifier); | |||
} | |||
MidiOutput::~MidiOutput() | |||
{ | |||
stopBackgroundThread(); | |||
AlsaClient::getInstance()->deletePort (static_cast<AlsaClient::Port*> (internal)); | |||
} | |||
void MidiOutput::sendMessageNow (const MidiMessage& message) | |||
{ | |||
static_cast<AlsaClient::Port*> (internal)->sendMessageNow (message); | |||
} | |||
//============================================================================== | |||
#else | |||
// (These are just stub functions if ALSA is unavailable...) | |||
MidiInput::MidiInput (const String& deviceName, const String& deviceID) | |||
: deviceInfo (deviceName, deviceID) | |||
{ | |||
} | |||
StringArray MidiOutput::getDevices() { return {}; } | |||
int MidiOutput::getDefaultDeviceIndex() { return 0; } | |||
MidiOutput* MidiOutput::openDevice (int) { return nullptr; } | |||
MidiOutput* MidiOutput::createNewDevice (const String&) { return nullptr; } | |||
MidiOutput::~MidiOutput() {} | |||
void MidiOutput::sendMessageNow (const MidiMessage&) {} | |||
MidiInput::MidiInput (const String& nm) : name (nm) {} | |||
MidiInput::~MidiInput() {} | |||
void MidiInput::start() {} | |||
void MidiInput::stop() {} | |||
int MidiInput::getDefaultDeviceIndex() { return 0; } | |||
StringArray MidiInput::getDevices() { return {}; } | |||
MidiInput* MidiInput::openDevice (int, MidiInputCallback*) { return nullptr; } | |||
MidiInput::~MidiInput() {} | |||
void MidiInput::start() {} | |||
void MidiInput::stop() {} | |||
Array<MidiDeviceInfo> MidiInput::getAvailableDevices() { return {}; } | |||
MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } | |||
MidiInput* MidiInput::openDevice (const String&, MidiInputCallback*) { return nullptr; } | |||
MidiInput* MidiInput::createNewDevice (const String&, MidiInputCallback*) { return nullptr; } | |||
StringArray MidiInput::getDevices() { return {}; } | |||
int MidiInput::getDefaultDeviceIndex() { return 0;} | |||
MidiInput* MidiInput::openDevice (int, MidiInputCallback*) { return nullptr; } | |||
MidiOutput::~MidiOutput() {} | |||
void MidiOutput::sendMessageNow (const MidiMessage&) {} | |||
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices() { return {}; } | |||
MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } | |||
MidiOutput* MidiOutput::openDevice (const String&) { return nullptr; } | |||
MidiOutput* MidiOutput::createNewDevice (const String&) { return nullptr; } | |||
StringArray MidiOutput::getDevices() { return {}; } | |||
int MidiOutput::getDefaultDeviceIndex() { return 0;} | |||
MidiOutput* MidiOutput::openDevice (int) { return nullptr; } | |||
#endif | |||
@@ -29,6 +29,22 @@ namespace juce | |||
namespace CoreMidiHelpers | |||
{ | |||
//============================================================================== | |||
struct ScopedCFString | |||
{ | |||
ScopedCFString() = default; | |||
ScopedCFString (String s) : cfString (s.toCFString()) {} | |||
~ScopedCFString() noexcept | |||
{ | |||
if (cfString != nullptr) | |||
CFRelease (cfString); | |||
} | |||
CFStringRef cfString = {}; | |||
}; | |||
//============================================================================== | |||
static bool checkError (OSStatus err, int lineNum) | |||
{ | |||
if (err == noErr) | |||
@@ -45,79 +61,62 @@ namespace CoreMidiHelpers | |||
#undef CHECK_ERROR | |||
#define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__) | |||
//============================================================================== | |||
struct ScopedCFString | |||
static MidiDeviceInfo getMidiObjectInfo (MIDIObjectRef entity) | |||
{ | |||
ScopedCFString() noexcept {} | |||
~ScopedCFString() noexcept { if (cfString != nullptr) CFRelease (cfString); } | |||
MidiDeviceInfo info; | |||
CFStringRef cfString = {}; | |||
}; | |||
static String getMidiObjectName (MIDIObjectRef entity) | |||
{ | |||
String result; | |||
CFStringRef str = nullptr; | |||
MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str); | |||
if (str != nullptr) | |||
{ | |||
result = String::fromCFString (str); | |||
CFRelease (str); | |||
} | |||
ScopedCFString str; | |||
return result; | |||
} | |||
if (CHECK_ERROR (MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str.cfString))) | |||
info.name = String::fromCFString (str.cfString); | |||
} | |||
static void enableSimulatorMidiSession() | |||
{ | |||
#if TARGET_OS_SIMULATOR | |||
static bool hasEnabledNetworkSession = false; | |||
SInt32 objectID = 0; | |||
if (! hasEnabledNetworkSession) | |||
{ | |||
MIDINetworkSession* session = [MIDINetworkSession defaultSession]; | |||
session.enabled = YES; | |||
session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; | |||
if (CHECK_ERROR (MIDIObjectGetIntegerProperty (entity, kMIDIPropertyUniqueID, &objectID))) | |||
info.identifier = String (objectID); | |||
hasEnabledNetworkSession = true; | |||
} | |||
#endif | |||
return info; | |||
} | |||
static String getEndpointName (MIDIEndpointRef endpoint, bool isExternal) | |||
static MidiDeviceInfo getEndpointInfo (MIDIEndpointRef endpoint, bool isExternal) | |||
{ | |||
auto result = getMidiObjectName (endpoint); | |||
MIDIEntityRef entity = 0; // NB: don't attempt to use nullptr for refs - it fails in some types of build. | |||
// 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 result; // probably virtual | |||
return getMidiObjectInfo (endpoint); | |||
auto result = getMidiObjectInfo (endpoint); | |||
if (result.isEmpty()) | |||
result = getMidiObjectName (entity); // endpoint name is empty - try the entity | |||
// endpoint is empty - try the entity | |||
if (result == MidiDeviceInfo()) | |||
result = getMidiObjectInfo (entity); | |||
// now consider the device's name | |||
// now consider the device | |||
MIDIDeviceRef device = 0; | |||
MIDIEntityGetDevice (entity, &device); | |||
if (device != 0) | |||
{ | |||
auto deviceName = getMidiObjectName (device); | |||
auto info = getMidiObjectInfo (device); | |||
if (deviceName.isNotEmpty()) | |||
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 = deviceName; | |||
result = info; | |||
} | |||
else if (! result.startsWithIgnoreCase (deviceName)) | |||
else if (! result.name.startsWithIgnoreCase (info.name)) | |||
{ | |||
// prepend the device name to the entity name | |||
result = (deviceName + " " + result).trimEnd(); | |||
// prepend the device name and identifier to the entity's | |||
result.name = (info.name + " " + result.name).trimEnd(); | |||
result.identifier = info.identifier + " " + result.identifier; | |||
} | |||
} | |||
} | |||
@@ -125,9 +124,9 @@ namespace CoreMidiHelpers | |||
return result; | |||
} | |||
static String getConnectedEndpointName (MIDIEndpointRef endpoint) | |||
static MidiDeviceInfo getConnectedEndpointInfo (MIDIEndpointRef endpoint) | |||
{ | |||
String result; | |||
MidiDeviceInfo result; | |||
// Does the endpoint have connections? | |||
CFDataRef connections = nullptr; | |||
@@ -141,37 +140,38 @@ namespace CoreMidiHelpers | |||
if (numConnections > 0) | |||
{ | |||
auto pid = reinterpret_cast<const SInt32*> (CFDataGetBytePtr (connections)); | |||
auto* pid = reinterpret_cast<const SInt32*> (CFDataGetBytePtr (connections)); | |||
for (int i = 0; i < numConnections; ++i, ++pid) | |||
{ | |||
auto uid = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); | |||
auto id = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); | |||
MIDIObjectRef connObject; | |||
MIDIObjectType connObjectType; | |||
auto err = MIDIObjectFindByUniqueID (uid, &connObject, &connObjectType); | |||
auto err = MIDIObjectFindByUniqueID (id, &connObject, &connObjectType); | |||
if (err == noErr) | |||
{ | |||
String s; | |||
MidiDeviceInfo deviceInfo; | |||
if (connObjectType == kMIDIObjectType_ExternalSource | |||
|| connObjectType == kMIDIObjectType_ExternalDestination) | |||
{ | |||
// Connected to an external device's endpoint (10.3 and later). | |||
s = getEndpointName (static_cast<MIDIEndpointRef> (connObject), true); | |||
deviceInfo = getEndpointInfo (static_cast<MIDIEndpointRef> (connObject), true); | |||
} | |||
else | |||
{ | |||
// Connected to an external device (10.2) (or something else, catch-all) | |||
s = getMidiObjectName (connObject); | |||
deviceInfo = getMidiObjectInfo (connObject); | |||
} | |||
if (s.isNotEmpty()) | |||
if (deviceInfo != MidiDeviceInfo()) | |||
{ | |||
if (result.isNotEmpty()) | |||
result += ", "; | |||
if (result.name.isNotEmpty()) result.name += ", "; | |||
if (result.identifier.isNotEmpty()) result.identifier += ", "; | |||
result += s; | |||
result.name += deviceInfo.name; | |||
result.identifier += deviceInfo.identifier; | |||
} | |||
} | |||
} | |||
@@ -180,17 +180,19 @@ namespace CoreMidiHelpers | |||
CFRelease (connections); | |||
} | |||
if (result.isEmpty()) // Here, either the endpoint had no connections, or we failed to obtain names for them. | |||
result = getEndpointName (endpoint, false); | |||
// 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 void setUniqueIdForMidiPort (MIDIObjectRef device, const String& portName, bool isInput) | |||
static int createUniqueIDForMidiPort (String deviceName, bool isInput) | |||
{ | |||
String portUniqueId; | |||
#if defined (JucePlugin_CFBundleIdentifier) | |||
portUniqueId = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); | |||
String uniqueID; | |||
#ifdef JucePlugin_CFBundleIdentifier | |||
uniqueID = JUCE_STRINGIFY (JucePlugin_CFBundleIdentifier); | |||
#else | |||
auto appBundle = File::getSpecialLocation (File::currentApplicationFile); | |||
@@ -203,19 +205,33 @@ namespace CoreMidiHelpers | |||
if (bundleRef != nullptr) | |||
{ | |||
if (auto bundleId = CFBundleGetIdentifier (bundleRef)) | |||
portUniqueId = String::fromCFString (bundleId); | |||
uniqueID = String::fromCFString (bundleId); | |||
CFRelease (bundleRef); | |||
} | |||
} | |||
#endif | |||
if (portUniqueId.isNotEmpty()) | |||
if (uniqueID.isNotEmpty()) | |||
uniqueID += "." + deviceName + (isInput ? ".input" : ".output"); | |||
return uniqueID.hashCode(); | |||
} | |||
static void enableSimulatorMidiSession() | |||
{ | |||
#if TARGET_OS_SIMULATOR | |||
static bool hasEnabledNetworkSession = false; | |||
if (! hasEnabledNetworkSession) | |||
{ | |||
portUniqueId += "." + portName + (isInput ? ".input" : ".output"); | |||
MIDINetworkSession* session = [MIDINetworkSession defaultSession]; | |||
session.enabled = YES; | |||
session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; | |||
CHECK_ERROR (MIDIObjectSetStringProperty (device, kMIDIPropertyUniqueID, portUniqueId.toCFString())); | |||
hasEnabledNetworkSession = true; | |||
} | |||
#endif | |||
} | |||
static void globalSystemChangeCallback (const MIDINotification*, void*) | |||
@@ -243,15 +259,14 @@ namespace CoreMidiHelpers | |||
enableSimulatorMidiSession(); | |||
CoreMidiHelpers::ScopedCFString name; | |||
name.cfString = getGlobalMidiClientName().toCFString(); | |||
CoreMidiHelpers::ScopedCFString name (getGlobalMidiClientName()); | |||
CHECK_ERROR (MIDIClientCreate (name.cfString, &globalSystemChangeCallback, nullptr, &globalMidiClient)); | |||
} | |||
return globalMidiClient; | |||
} | |||
static StringArray findDevices (bool forInput) | |||
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. | |||
@@ -263,26 +278,25 @@ namespace CoreMidiHelpers | |||
return {}; | |||
} | |||
StringArray s; | |||
enableSimulatorMidiSession(); | |||
auto num = forInput ? MIDIGetNumberOfSources() | |||
: MIDIGetNumberOfDestinations(); | |||
Array<MidiDeviceInfo> devices; | |||
auto numDevices = (forInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDestinations()); | |||
for (ItemCount i = 0; i < num; ++i) | |||
for (ItemCount i = 0; i < numDevices; ++i) | |||
{ | |||
String name; | |||
MidiDeviceInfo deviceInfo; | |||
if (auto dest = forInput ? MIDIGetSource (i) : MIDIGetDestination (i)) | |||
name = getConnectedEndpointName (dest); | |||
deviceInfo = getConnectedEndpointInfo (dest); | |||
if (name.isEmpty()) | |||
name = "<error>"; | |||
if (deviceInfo == MidiDeviceInfo()) | |||
deviceInfo.name = deviceInfo.identifier = "<error>"; | |||
s.add (name); | |||
devices.add (deviceInfo); | |||
} | |||
return s; | |||
return devices; | |||
} | |||
//============================================================================== | |||
@@ -290,7 +304,7 @@ namespace CoreMidiHelpers | |||
{ | |||
public: | |||
MidiPortAndEndpoint (MIDIPortRef p, MIDIEndpointRef ep) noexcept | |||
: port (p), endPoint (ep) | |||
: port (p), endpoint (ep) | |||
{ | |||
} | |||
@@ -299,20 +313,21 @@ namespace CoreMidiHelpers | |||
if (port != 0) | |||
MIDIPortDispose (port); | |||
if (port == 0 && endPoint != 0) // if port == nullptr, it means we created the endpoint, so it's safe to delete it | |||
MIDIEndpointDispose (endPoint); | |||
// 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); | |||
MIDISend (port, endpoint, packets); | |||
else | |||
MIDIReceived (endPoint, packets); | |||
MIDIReceived (endpoint, packets); | |||
} | |||
MIDIPortRef port; | |||
MIDIEndpointRef endPoint; | |||
MIDIEndpointRef endpoint; | |||
}; | |||
//============================================================================== | |||
@@ -334,7 +349,7 @@ namespace CoreMidiHelpers | |||
} | |||
if (portAndEndpoint != nullptr && portAndEndpoint->port != 0) | |||
CHECK_ERROR (MIDIPortDisconnectSource (portAndEndpoint->port, portAndEndpoint->endPoint)); | |||
CHECK_ERROR (MIDIPortDisconnectSource (portAndEndpoint->port, portAndEndpoint->endpoint)); | |||
} | |||
void handlePackets (const MIDIPacketList* pktlist) | |||
@@ -370,63 +385,250 @@ namespace CoreMidiHelpers | |||
{ | |||
static_cast<MidiPortAndCallback*> (readProcRefCon)->handlePackets (pktlist); | |||
} | |||
static Array<MIDIEndpointRef> getEndpoints (bool isInput) | |||
{ | |||
Array<MIDIEndpointRef> endpoints; | |||
auto numDevices = (isInput ? MIDIGetNumberOfSources() : MIDIGetNumberOfDevices()); | |||
for (ItemCount i = 0; i < numDevices; ++i) | |||
endpoints.add (isInput ? MIDIGetSource (i) : MIDIGetDestination (i)); | |||
return endpoints; | |||
} | |||
} | |||
//============================================================================== | |||
StringArray MidiOutput::getDevices() { return CoreMidiHelpers::findDevices (false); } | |||
int MidiOutput::getDefaultDeviceIndex() { return 0; } | |||
Array<MidiDeviceInfo> MidiInput::getAvailableDevices() | |||
{ | |||
return CoreMidiHelpers::findDevices (true); | |||
} | |||
MidiOutput* MidiOutput::openDevice (int index) | |||
MidiDeviceInfo MidiInput::getDefaultDevice() | |||
{ | |||
return getAvailableDevices().getFirst(); | |||
} | |||
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)) | |||
{ | |||
if (deviceIdentifier == getConnectedEndpointInfo (endpoint).identifier) | |||
{ | |||
ScopedCFString cfName; | |||
if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) | |||
{ | |||
MIDIPortRef port; | |||
std::unique_ptr<MidiPortAndCallback> mpc (new MidiPortAndCallback (*callback)); | |||
if (CHECK_ERROR (MIDIInputPortCreate (client, cfName.cfString, midiInputProc, mpc.get(), &port))) | |||
{ | |||
if (CHECK_ERROR (MIDIPortConnectSource (port, endpoint, nullptr))) | |||
{ | |||
mpc->portAndEndpoint.reset (new MidiPortAndEndpoint (port, endpoint)); | |||
std::unique_ptr<MidiInput> midiInput (new MidiInput (String::fromCFString (cfName.cfString), deviceIdentifier)); | |||
mpc->input = midiInput.get(); | |||
midiInput->internal = mpc.get(); | |||
const ScopedLock sl (callbackLock); | |||
activeCallbacks.add (mpc.release()); | |||
return midiInput.release(); | |||
} | |||
else | |||
{ | |||
CHECK_ERROR (MIDIPortDispose (port)); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return nullptr; | |||
} | |||
MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) | |||
{ | |||
MidiOutput* mo = nullptr; | |||
using namespace CoreMidiHelpers; | |||
jassert (callback != nullptr); | |||
if (auto client = CoreMidiHelpers::getGlobalMidiClient()) | |||
if (auto client = getGlobalMidiClient()) | |||
{ | |||
if (isPositiveAndBelow (index, MIDIGetNumberOfDestinations())) | |||
auto mpc = std::make_unique<MidiPortAndCallback> (*callback); | |||
mpc->active = false; | |||
MIDIEndpointRef endpoint; | |||
ScopedCFString name (deviceName); | |||
if (CHECK_ERROR (MIDIDestinationCreate (client, name.cfString, midiInputProc, mpc.get(), &endpoint))) | |||
{ | |||
auto endPoint = MIDIGetDestination ((ItemCount) index); | |||
auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true); | |||
if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) | |||
{ | |||
mpc->portAndEndpoint.reset (new MidiPortAndEndpoint (0, endpoint)); | |||
std::unique_ptr<MidiInput> midiInput (new MidiInput (deviceName, String (deviceIdentifier))); | |||
mpc->input = midiInput.get(); | |||
midiInput->internal = mpc.get(); | |||
const ScopedLock sl (callbackLock); | |||
activeCallbacks.add (mpc.release()); | |||
return midiInput.release(); | |||
} | |||
} | |||
} | |||
return nullptr; | |||
} | |||
StringArray MidiInput::getDevices() | |||
{ | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
return deviceNames; | |||
} | |||
int MidiInput::getDefaultDeviceIndex() | |||
{ | |||
return 0; | |||
} | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
{ | |||
return openDevice (getAvailableDevices()[index].identifier, callback); | |||
} | |||
MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) | |||
: deviceInfo (deviceName, deviceIdentifier) | |||
{ | |||
} | |||
CoreMidiHelpers::ScopedCFString pname; | |||
MidiInput::~MidiInput() | |||
{ | |||
delete static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal); | |||
} | |||
if (CHECK_ERROR (MIDIObjectGetStringProperty (endPoint, kMIDIPropertyName, &pname.cfString))) | |||
void MidiInput::start() | |||
{ | |||
const ScopedLock sl (CoreMidiHelpers::callbackLock); | |||
static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal)->active = true; | |||
} | |||
void MidiInput::stop() | |||
{ | |||
const ScopedLock sl (CoreMidiHelpers::callbackLock); | |||
static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal)->active = false; | |||
} | |||
//============================================================================== | |||
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices() | |||
{ | |||
return CoreMidiHelpers::findDevices (false); | |||
} | |||
MidiDeviceInfo MidiOutput::getDefaultDevice() | |||
{ | |||
return getAvailableDevices().getFirst(); | |||
} | |||
MidiOutput* MidiOutput::openDevice (const String& deviceIdentifier) | |||
{ | |||
if (deviceIdentifier.isEmpty()) | |||
return nullptr; | |||
using namespace CoreMidiHelpers; | |||
if (auto client = getGlobalMidiClient()) | |||
{ | |||
for (auto& endpoint : getEndpoints (false)) | |||
{ | |||
if (deviceIdentifier == getConnectedEndpointInfo (endpoint).identifier) | |||
{ | |||
MIDIPortRef port; | |||
auto deviceName = CoreMidiHelpers::getConnectedEndpointName (endPoint); | |||
ScopedCFString cfName; | |||
if (CHECK_ERROR (MIDIOutputPortCreate (client, pname.cfString, &port))) | |||
if (CHECK_ERROR (MIDIObjectGetStringProperty (endpoint, kMIDIPropertyName, &cfName.cfString))) | |||
{ | |||
mo = new MidiOutput (deviceName); | |||
mo->internal = new CoreMidiHelpers::MidiPortAndEndpoint (port, endPoint); | |||
MIDIPortRef port; | |||
if (CHECK_ERROR (MIDIOutputPortCreate (client, cfName.cfString, &port))) | |||
{ | |||
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (String::fromCFString (cfName.cfString), deviceIdentifier)); | |||
midiOutput->internal = new MidiPortAndEndpoint (port, endpoint); | |||
return midiOutput.release(); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return mo; | |||
return nullptr; | |||
} | |||
MidiOutput* MidiOutput::createNewDevice (const String& deviceName) | |||
{ | |||
if (auto client = CoreMidiHelpers::getGlobalMidiClient()) | |||
using namespace CoreMidiHelpers; | |||
if (auto client = getGlobalMidiClient()) | |||
{ | |||
MIDIEndpointRef endPoint; | |||
MIDIEndpointRef endpoint; | |||
CoreMidiHelpers::ScopedCFString name; | |||
name.cfString = deviceName.toCFString(); | |||
ScopedCFString name (deviceName); | |||
if (CHECK_ERROR (MIDISourceCreate (client, name.cfString, &endPoint))) | |||
if (CHECK_ERROR (MIDISourceCreate (client, name.cfString, &endpoint))) | |||
{ | |||
CoreMidiHelpers::setUniqueIdForMidiPort (endPoint, deviceName, false); | |||
auto deviceIdentifier = createUniqueIDForMidiPort (deviceName, true); | |||
if (CHECK_ERROR (MIDIObjectSetIntegerProperty (endpoint, kMIDIPropertyUniqueID, (SInt32) deviceIdentifier))) | |||
{ | |||
std::unique_ptr<MidiOutput> midiOutput (new MidiOutput (deviceName, String (deviceIdentifier))); | |||
midiOutput->internal = new MidiPortAndEndpoint (0, endpoint); | |||
auto mo = new MidiOutput (deviceName); | |||
mo->internal = new CoreMidiHelpers::MidiPortAndEndpoint (0, endPoint); | |||
return mo; | |||
return midiOutput.release(); | |||
} | |||
} | |||
} | |||
return nullptr; | |||
} | |||
StringArray MidiOutput::getDevices() | |||
{ | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
return deviceNames; | |||
} | |||
int MidiOutput::getDefaultDeviceIndex() | |||
{ | |||
return 0; | |||
} | |||
MidiOutput* MidiOutput::openDevice (int index) | |||
{ | |||
return openDevice (getAvailableDevices()[index].identifier); | |||
} | |||
MidiOutput::~MidiOutput() | |||
{ | |||
stopBackgroundThread(); | |||
@@ -493,111 +695,6 @@ void MidiOutput::sendMessageNow (const MidiMessage& message) | |||
static_cast<CoreMidiHelpers::MidiPortAndEndpoint*> (internal)->send (packetToSend); | |||
} | |||
//============================================================================== | |||
StringArray MidiInput::getDevices() { return CoreMidiHelpers::findDevices (true); } | |||
int MidiInput::getDefaultDeviceIndex() { return 0; } | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
{ | |||
jassert (callback != nullptr); | |||
using namespace CoreMidiHelpers; | |||
MidiInput* newInput = nullptr; | |||
if (auto client = getGlobalMidiClient()) | |||
{ | |||
if (isPositiveAndBelow (index, MIDIGetNumberOfSources())) | |||
{ | |||
if (auto endPoint = MIDIGetSource ((ItemCount) index)) | |||
{ | |||
ScopedCFString name; | |||
if (CHECK_ERROR (MIDIObjectGetStringProperty (endPoint, kMIDIPropertyName, &name.cfString))) | |||
{ | |||
MIDIPortRef port; | |||
std::unique_ptr<MidiPortAndCallback> mpc (new MidiPortAndCallback (*callback)); | |||
if (CHECK_ERROR (MIDIInputPortCreate (client, name.cfString, midiInputProc, mpc.get(), &port))) | |||
{ | |||
if (CHECK_ERROR (MIDIPortConnectSource (port, endPoint, nullptr))) | |||
{ | |||
mpc->portAndEndpoint.reset (new MidiPortAndEndpoint (port, endPoint)); | |||
newInput = new MidiInput (getDevices() [index]); | |||
mpc->input = newInput; | |||
newInput->internal = mpc.get(); | |||
const ScopedLock sl (callbackLock); | |||
activeCallbacks.add (mpc.release()); | |||
} | |||
else | |||
{ | |||
CHECK_ERROR (MIDIPortDispose (port)); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return newInput; | |||
} | |||
MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) | |||
{ | |||
jassert (callback != nullptr); | |||
using namespace CoreMidiHelpers; | |||
if (auto client = getGlobalMidiClient()) | |||
{ | |||
std::unique_ptr<MidiPortAndCallback> mpc (new MidiPortAndCallback (*callback)); | |||
mpc->active = false; | |||
MIDIEndpointRef endPoint; | |||
ScopedCFString name; | |||
name.cfString = deviceName.toCFString(); | |||
if (CHECK_ERROR (MIDIDestinationCreate (client, name.cfString, midiInputProc, mpc.get(), &endPoint))) | |||
{ | |||
setUniqueIdForMidiPort (endPoint, deviceName, true); | |||
mpc->portAndEndpoint.reset (new MidiPortAndEndpoint (0, endPoint)); | |||
auto mi = new MidiInput (deviceName); | |||
mpc->input = mi; | |||
mi->internal = mpc.get(); | |||
const ScopedLock sl (callbackLock); | |||
activeCallbacks.add (mpc.release()); | |||
return mi; | |||
} | |||
} | |||
return nullptr; | |||
} | |||
MidiInput::MidiInput (const String& nm) : name (nm) | |||
{ | |||
} | |||
MidiInput::~MidiInput() | |||
{ | |||
delete static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal); | |||
} | |||
void MidiInput::start() | |||
{ | |||
const ScopedLock sl (CoreMidiHelpers::callbackLock); | |||
static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal)->active = true; | |||
} | |||
void MidiInput::stop() | |||
{ | |||
const ScopedLock sl (CoreMidiHelpers::callbackLock); | |||
static_cast<CoreMidiHelpers::MidiPortAndCallback*> (internal)->active = false; | |||
} | |||
#undef CHECK_ERROR | |||
} // namespace juce |
@@ -20,6 +20,12 @@ | |||
============================================================================== | |||
*/ | |||
#ifndef DRV_QUERYDEVICEINTERFACE | |||
#define DRV_RESERVED 0x0800 | |||
#define DRV_QUERYDEVICEINTERFACE (DRV_RESERVED + 12) | |||
#define DRV_QUERYDEVICEINTERFACESIZE (DRV_RESERVED + 13) | |||
#endif | |||
namespace juce | |||
{ | |||
@@ -29,7 +35,9 @@ struct MidiServiceType | |||
{ | |||
virtual ~InputWrapper() {} | |||
virtual String getDeviceIdentifier() = 0; | |||
virtual String getDeviceName() = 0; | |||
virtual void start() = 0; | |||
virtual void stop() = 0; | |||
}; | |||
@@ -38,18 +46,20 @@ struct MidiServiceType | |||
{ | |||
virtual ~OutputWrapper() {} | |||
virtual String getDeviceIdentifier() = 0; | |||
virtual String getDeviceName() = 0; | |||
virtual void sendMessageNow (const MidiMessage&) = 0; | |||
}; | |||
MidiServiceType() {} | |||
virtual ~MidiServiceType() {} | |||
virtual StringArray getDevices (bool) = 0; | |||
virtual int getDefaultDeviceIndex (bool) = 0; | |||
virtual Array<MidiDeviceInfo> getAvailableDevices (bool) = 0; | |||
virtual MidiDeviceInfo getDefaultDevice (bool) = 0; | |||
virtual InputWrapper* createInputWrapper (MidiInput&, int, MidiInputCallback&) = 0; | |||
virtual OutputWrapper* createOutputWrapper (int) = 0; | |||
virtual InputWrapper* createInputWrapper (MidiInput&, const String&, MidiInputCallback&) = 0; | |||
virtual OutputWrapper* createOutputWrapper (const String&) = 0; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiServiceType) | |||
}; | |||
@@ -60,26 +70,26 @@ struct Win32MidiService : public MidiServiceType, | |||
{ | |||
Win32MidiService() {} | |||
StringArray getDevices (bool isInput) override | |||
Array<MidiDeviceInfo> getAvailableDevices (bool isInput) override | |||
{ | |||
return isInput ? Win32InputWrapper::getDevices() | |||
: Win32OutputWrapper::getDevices(); | |||
return isInput ? Win32InputWrapper::getAvailableDevices() | |||
: Win32OutputWrapper::getAvailableDevices(); | |||
} | |||
int getDefaultDeviceIndex (bool isInput) override | |||
MidiDeviceInfo getDefaultDevice (bool isInput) override | |||
{ | |||
return isInput ? Win32InputWrapper::getDefaultDeviceIndex() | |||
: Win32OutputWrapper::getDefaultDeviceIndex(); | |||
return isInput ? Win32InputWrapper::getDefaultDevice() | |||
: Win32OutputWrapper::getDefaultDevice(); | |||
} | |||
InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override | |||
InputWrapper* createInputWrapper (MidiInput& input, const String& deviceIdentifier, MidiInputCallback& callback) override | |||
{ | |||
return new Win32InputWrapper (*this, input, index, callback); | |||
return new Win32InputWrapper (*this, input, deviceIdentifier, callback); | |||
} | |||
OutputWrapper* createOutputWrapper (int index) override | |||
OutputWrapper* createOutputWrapper (const String& deviceIdentifier) override | |||
{ | |||
return new Win32OutputWrapper (*this, index); | |||
return new Win32OutputWrapper (*this, deviceIdentifier); | |||
} | |||
private: | |||
@@ -88,7 +98,10 @@ private: | |||
//============================================================================== | |||
struct MidiInCollector : public ReferenceCountedObject | |||
{ | |||
MidiInCollector (Win32MidiService& s, const String& name) : deviceName (name), midiService (s) {} | |||
MidiInCollector (Win32MidiService& s, MidiDeviceInfo d) | |||
: deviceInfo (d), midiService (s) | |||
{ | |||
} | |||
~MidiInCollector() | |||
{ | |||
@@ -216,7 +229,7 @@ private: | |||
} | |||
} | |||
String deviceName; | |||
MidiDeviceInfo deviceInfo; | |||
HMIDIIN deviceHandle = 0; | |||
private: | |||
@@ -319,13 +332,59 @@ private: | |||
}; | |||
//============================================================================== | |||
struct Win32InputWrapper : public InputWrapper | |||
template<class WrapperType> | |||
struct Win32MidiDeviceQuery | |||
{ | |||
Win32InputWrapper (Win32MidiService& parentService, | |||
MidiInput& midiInput, int index, MidiInputCallback& c) | |||
static Array<MidiDeviceInfo> getAvailableDevices() | |||
{ | |||
StringArray deviceNames, deviceIDs; | |||
auto deviceCaps = WrapperType::getDeviceCaps(); | |||
for (int i = 0; i < deviceCaps.size(); ++i) | |||
{ | |||
deviceNames.add ({ deviceCaps[i].szPname, (size_t) numElementsInArray (deviceCaps[i].szPname) }); | |||
deviceIDs.add (getInterfaceIDForDevice ((UINT) i)); | |||
} | |||
deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
Array<MidiDeviceInfo> devices; | |||
for (int i = 0; i < deviceNames.size(); ++i) | |||
devices.add ({ deviceNames[i], deviceIDs[i] }); | |||
return devices; | |||
} | |||
private: | |||
static String getInterfaceIDForDevice (UINT id) | |||
{ | |||
ULONG size = 0; | |||
if (WrapperType::sendMidiMessage ((UINT_PTR) id, DRV_QUERYDEVICEINTERFACESIZE, (DWORD_PTR) &size, 0) == MMSYSERR_NOERROR) | |||
{ | |||
WCHAR interfaceName[512] = {}; | |||
if (isPositiveAndBelow (size, sizeof (interfaceName)) | |||
&& WrapperType::sendMidiMessage ((UINT_PTR) id, DRV_QUERYDEVICEINTERFACE, | |||
(DWORD_PTR) interfaceName, sizeof (interfaceName)) == MMSYSERR_NOERROR) | |||
{ | |||
return interfaceName; | |||
} | |||
} | |||
return {}; | |||
} | |||
}; | |||
struct Win32InputWrapper : public InputWrapper, | |||
public Win32MidiDeviceQuery<Win32InputWrapper> | |||
{ | |||
Win32InputWrapper (Win32MidiService& parentService, MidiInput& midiInput, const String& deviceIdentifier, MidiInputCallback& c) | |||
: input (midiInput), callback (c) | |||
{ | |||
collector = getOrCreateCollector (parentService, index); | |||
collector = getOrCreateCollector (parentService, deviceIdentifier); | |||
collector->addClient (this); | |||
} | |||
@@ -334,25 +393,31 @@ private: | |||
collector->removeClient (this); | |||
} | |||
static MidiInCollector::Ptr getOrCreateCollector (Win32MidiService& parentService, int index) | |||
static MidiInCollector::Ptr getOrCreateCollector (Win32MidiService& parentService, const String& deviceIdentifier) | |||
{ | |||
auto names = getDevices(); | |||
UINT deviceID = MIDI_MAPPER; | |||
String deviceName; | |||
auto devices = getAvailableDevices(); | |||
if (isPositiveAndBelow (index, names.size())) | |||
for (int i = 0; i < devices.size(); ++i) | |||
{ | |||
deviceName = names[index]; | |||
deviceID = index; | |||
auto d = devices.getUnchecked (i); | |||
if (d.identifier == deviceIdentifier) | |||
{ | |||
deviceID = i; | |||
deviceName = d.name; | |||
break; | |||
} | |||
} | |||
const ScopedLock sl (parentService.activeCollectorLock); | |||
for (auto& c : parentService.activeCollectors) | |||
if (c->deviceName == deviceName) | |||
if (c->deviceInfo.identifier == deviceIdentifier) | |||
return c; | |||
MidiInCollector::Ptr c (new MidiInCollector (parentService, deviceName)); | |||
MidiInCollector::Ptr c (new MidiInCollector (parentService, { deviceName, deviceIdentifier })); | |||
HMIDIIN h; | |||
auto err = midiInOpen (&h, deviceID, | |||
@@ -368,29 +433,33 @@ private: | |||
return c; | |||
} | |||
static StringArray getDevices() | |||
static DWORD sendMidiMessage (UINT_PTR deviceID, UINT msg, DWORD_PTR arg1, DWORD_PTR arg2) | |||
{ | |||
StringArray s; | |||
auto num = midiInGetNumDevs(); | |||
return midiInMessage ((HMIDIIN) deviceID, msg, arg1, arg2); | |||
} | |||
static Array<MIDIINCAPS> getDeviceCaps() | |||
{ | |||
Array<MIDIINCAPS> devices; | |||
for (UINT i = 0; i < num; ++i) | |||
for (UINT i = 0; i < midiInGetNumDevs(); ++i) | |||
{ | |||
MIDIINCAPS mc = { 0 }; | |||
if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) | |||
s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname))); | |||
devices.add (mc); | |||
} | |||
s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
return s; | |||
return devices; | |||
} | |||
static int getDefaultDeviceIndex() { return 0; } | |||
static MidiDeviceInfo getDefaultDevice() { return getAvailableDevices().getFirst(); } | |||
void start() override { started = true; concatenator.reset(); collector->startOrStop(); } | |||
void stop() override { started = false; collector->startOrStop(); concatenator.reset(); } | |||
String getDeviceName() override { return collector->deviceName; } | |||
String getDeviceIdentifier() override { return collector->deviceInfo.identifier; } | |||
String getDeviceName() override { return collector->deviceInfo.name; } | |||
void pushMidiData (const void* inputData, int numBytes, double time) | |||
{ | |||
@@ -411,8 +480,8 @@ private: | |||
{ | |||
using Ptr = ReferenceCountedObjectPtr<MidiOutHandle>; | |||
MidiOutHandle (Win32MidiService& parent, const String& name, HMIDIOUT h) | |||
: owner (parent), deviceName (name), handle (h) | |||
MidiOutHandle (Win32MidiService& parent, MidiDeviceInfo d, HMIDIOUT h) | |||
: owner (parent), deviceInfo (d), handle (h) | |||
{ | |||
owner.activeOutputHandles.add (this); | |||
} | |||
@@ -426,32 +495,41 @@ private: | |||
} | |||
Win32MidiService& owner; | |||
String deviceName; | |||
MidiDeviceInfo deviceInfo; | |||
HMIDIOUT handle; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutHandle) | |||
}; | |||
//============================================================================== | |||
struct Win32OutputWrapper : public OutputWrapper | |||
struct Win32OutputWrapper : public OutputWrapper, | |||
public Win32MidiDeviceQuery<Win32OutputWrapper> | |||
{ | |||
Win32OutputWrapper (Win32MidiService& p, int index) : parent (p) | |||
Win32OutputWrapper (Win32MidiService& p, const String& deviceIdentifier) | |||
: parent (p) | |||
{ | |||
auto names = getDevices(); | |||
auto devices = getAvailableDevices(); | |||
UINT deviceID = MIDI_MAPPER; | |||
String deviceName; | |||
if (isPositiveAndBelow (index, names.size())) | |||
for (int i = 0; i < devices.size(); ++i) | |||
{ | |||
deviceName = names[index]; | |||
deviceID = index; | |||
auto d = devices.getUnchecked (i); | |||
if (d.identifier == deviceIdentifier) | |||
{ | |||
deviceID = i; | |||
deviceName = d.name; | |||
break; | |||
} | |||
} | |||
if (deviceID == MIDI_MAPPER) | |||
{ | |||
// use the microsoft sw synth as a default - best not to allow deviceID | |||
// to be MIDI_MAPPER, or else device sharing breaks | |||
for (int i = 0; i < names.size(); ++i) | |||
if (names[i].containsIgnoreCase ("microsoft")) | |||
for (int i = 0; i < devices.size(); ++i) | |||
if (devices[i].name.containsIgnoreCase ("microsoft")) | |||
deviceID = (UINT) i; | |||
} | |||
@@ -459,7 +537,7 @@ private: | |||
{ | |||
auto* activeHandle = parent.activeOutputHandles.getUnchecked (i); | |||
if (activeHandle->deviceName == deviceName) | |||
if (activeHandle->deviceInfo.identifier == deviceIdentifier) | |||
{ | |||
han = activeHandle; | |||
return; | |||
@@ -473,7 +551,7 @@ private: | |||
if (res == MMSYSERR_NOERROR) | |||
{ | |||
han = new MidiOutHandle (parent, deviceName, h); | |||
han = new MidiOutHandle (parent, { deviceName, deviceIdentifier }, h); | |||
return; | |||
} | |||
@@ -530,12 +608,16 @@ private: | |||
} | |||
} | |||
static DWORD sendMidiMessage (UINT_PTR deviceID, UINT msg, DWORD_PTR arg1, DWORD_PTR arg2) | |||
{ | |||
return midiOutMessage ((HMIDIOUT) deviceID, msg, arg1, arg2); | |||
} | |||
static Array<MIDIOUTCAPS> getDeviceCaps() | |||
{ | |||
Array<MIDIOUTCAPS> devices; | |||
auto num = midiOutGetNumDevs(); | |||
for (UINT i = 0; i < num; ++i) | |||
for (UINT i = 0; i < midiOutGetNumDevs(); ++i) | |||
{ | |||
MIDIOUTCAPS mc = { 0 }; | |||
@@ -546,36 +628,26 @@ private: | |||
return devices; | |||
} | |||
static StringArray getDevices() | |||
static MidiDeviceInfo getDefaultDevice() | |||
{ | |||
StringArray s; | |||
for (auto& mc : getDeviceCaps()) | |||
s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname))); | |||
s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
return s; | |||
} | |||
static int getDefaultDeviceIndex() | |||
{ | |||
int n = 0; | |||
for (auto& mc : getDeviceCaps()) | |||
auto defaultIndex = []() | |||
{ | |||
if ((mc.wTechnology & MOD_MAPPER) != 0) | |||
return n; | |||
auto deviceCaps = getDeviceCaps(); | |||
++n; | |||
} | |||
for (int i = 0; i < deviceCaps.size(); ++i) | |||
if ((deviceCaps[i].wTechnology & MOD_MAPPER) != 0) | |||
return i; | |||
return 0; | |||
return 0; | |||
}(); | |||
return getAvailableDevices()[defaultIndex]; | |||
} | |||
String getDeviceName() override { return deviceName; } | |||
String getDeviceIdentifier() override { return han->deviceInfo.identifier; } | |||
String getDeviceName() override { return han->deviceInfo.name; } | |||
Win32MidiService& parent; | |||
String deviceName; | |||
MidiOutHandle::Ptr han; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32OutputWrapper) | |||
@@ -671,26 +743,26 @@ public: | |||
throw std::runtime_error ("Failed to start the midi output device watcher"); | |||
} | |||
StringArray getDevices (bool isInput) override | |||
Array<MidiDeviceInfo> getAvailableDevices (bool isInput) override | |||
{ | |||
return isInput ? inputDeviceWatcher ->getDevices() | |||
: outputDeviceWatcher->getDevices(); | |||
return isInput ? inputDeviceWatcher ->getAvailableDevices() | |||
: outputDeviceWatcher->getAvailableDevices(); | |||
} | |||
int getDefaultDeviceIndex (bool isInput) override | |||
MidiDeviceInfo getDefaultDevice (bool isInput) override | |||
{ | |||
return isInput ? inputDeviceWatcher ->getDefaultDeviceIndex() | |||
: outputDeviceWatcher->getDefaultDeviceIndex(); | |||
return isInput ? inputDeviceWatcher ->getDefaultDevice() | |||
: outputDeviceWatcher->getDefaultDevice(); | |||
} | |||
InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override | |||
InputWrapper* createInputWrapper (MidiInput& input, const String& deviceIdentifier, MidiInputCallback& callback) override | |||
{ | |||
return new WinRTInputWrapper (*this, input, index, callback); | |||
return new WinRTInputWrapper (*this, input, deviceIdentifier, callback); | |||
} | |||
OutputWrapper* createOutputWrapper (int index) override | |||
OutputWrapper* createOutputWrapper (const String& deviceIdentifier) override | |||
{ | |||
return new WinRTOutputWrapper (*this, index); | |||
return new WinRTOutputWrapper (*this, deviceIdentifier); | |||
} | |||
private: | |||
@@ -1098,7 +1170,7 @@ private: | |||
}; | |||
//============================================================================== | |||
struct MIDIDeviceInfo | |||
struct WinRTMIDIDeviceInfo | |||
{ | |||
String deviceID, containerID, name; | |||
bool isDefault = false; | |||
@@ -1120,7 +1192,7 @@ private: | |||
HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override | |||
{ | |||
MIDIDeviceInfo info; | |||
WinRTMIDIDeviceInfo info; | |||
HSTRING deviceID; | |||
auto hr = addedDeviceInfo->get_Id (&deviceID); | |||
@@ -1229,56 +1301,59 @@ private: | |||
return attach (deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface); | |||
} | |||
StringArray getDevices() | |||
Array<MidiDeviceInfo> getAvailableDevices() | |||
{ | |||
{ | |||
const ScopedLock lock (deviceChanges); | |||
lastQueriedConnectedDevices = connectedDevices; | |||
} | |||
StringArray result; | |||
StringArray deviceNames, deviceIDs; | |||
for (auto info : lastQueriedConnectedDevices.get()) | |||
result.add (info.name); | |||
{ | |||
deviceNames.add (info.name); | |||
deviceIDs .add (info.containerID); | |||
} | |||
return result; | |||
} | |||
deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); | |||
int getDefaultDeviceIndex() | |||
{ | |||
auto& lastDevices = lastQueriedConnectedDevices.get(); | |||
Array<MidiDeviceInfo> devices; | |||
for (int i = 0; i < lastDevices.size(); ++i) | |||
if (lastDevices[i].isDefault) | |||
return i; | |||
for (int i = 0; i < deviceNames.size(); ++i) | |||
devices.add ({ deviceNames[i], deviceIDs[i] }); | |||
return 0; | |||
return devices; | |||
} | |||
MIDIDeviceInfo getDeviceInfoFromIndex (int index) | |||
MidiDeviceInfo getDefaultDevice() | |||
{ | |||
if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size())) | |||
return lastQueriedConnectedDevices.get()[index]; | |||
auto& lastDevices = lastQueriedConnectedDevices.get(); | |||
for (auto& d : lastDevices) | |||
if (d.isDefault) | |||
return { d.name, d.containerID }; | |||
return {}; | |||
} | |||
String getDeviceID (const String& name) | |||
WinRTMIDIDeviceInfo getWinRTDeviceInfoForDevice (const String& deviceIdentifier) | |||
{ | |||
const ScopedLock lock (deviceChanges); | |||
auto devices = getAvailableDevices(); | |||
for (auto info : connectedDevices) | |||
if (info.name == name) | |||
return info.deviceID; | |||
for (int i = 0; i < devices.size(); ++i) | |||
if (devices.getUnchecked (i).identifier == deviceIdentifier) | |||
return lastQueriedConnectedDevices.get()[i]; | |||
return {}; | |||
} | |||
WinRTWrapper::ComPtr<COMFactoryType>& factory; | |||
Array<MIDIDeviceInfo> connectedDevices; | |||
Array<WinRTMIDIDeviceInfo> connectedDevices; | |||
CriticalSection deviceChanges; | |||
ThreadLocalValue<Array<MIDIDeviceInfo>> lastQueriedConnectedDevices; | |||
ThreadLocalValue<Array<WinRTMIDIDeviceInfo>> lastQueriedConnectedDevices; | |||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher); | |||
}; | |||
@@ -1345,12 +1420,12 @@ private: | |||
public: | |||
WinRTIOWrapper (BLEDeviceWatcher& bleWatcher, | |||
MidiIODeviceWatcher<MIDIIOStaticsType>& midiDeviceWatcher, | |||
int index) | |||
const String& deviceIdentifier) | |||
: bleDeviceWatcher (bleWatcher) | |||
{ | |||
{ | |||
const ScopedLock lock (midiDeviceWatcher.deviceChanges); | |||
deviceInfo = midiDeviceWatcher.getDeviceInfoFromIndex (index); | |||
deviceInfo = midiDeviceWatcher.getWinRTDeviceInfoForDevice (deviceIdentifier); | |||
} | |||
if (deviceInfo.deviceID.isEmpty()) | |||
@@ -1417,7 +1492,7 @@ private: | |||
protected: | |||
//============================================================================== | |||
BLEDeviceWatcher& bleDeviceWatcher; | |||
MIDIDeviceInfo deviceInfo; | |||
WinRTMIDIDeviceInfo deviceInfo; | |||
bool isBLEDevice = false; | |||
WinRTWrapper::ComPtr<MIDIPort> midiPort; | |||
}; | |||
@@ -1427,8 +1502,8 @@ private: | |||
private WinRTIOWrapper<IMidiInPortStatics, IMidiInPort> | |||
{ | |||
WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb) | |||
: WinRTIOWrapper <IMidiInPortStatics, IMidiInPort> (*service.bleDeviceWatcher, *service.inputDeviceWatcher, index), | |||
WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, const String& deviceIdentifier, MidiInputCallback& cb) | |||
: WinRTIOWrapper <IMidiInPortStatics, IMidiInPort> (*service.bleDeviceWatcher, *service.inputDeviceWatcher, deviceIdentifier), | |||
inputDevice (input), | |||
callback (cb) | |||
{ | |||
@@ -1484,7 +1559,8 @@ private: | |||
} | |||
} | |||
String getDeviceName() override { return deviceInfo.name; } | |||
String getDeviceIdentifier() override { return deviceInfo.containerID; } | |||
String getDeviceName() override { return deviceInfo.name; } | |||
//============================================================================== | |||
void disconnect() override | |||
@@ -1579,8 +1655,8 @@ private: | |||
struct WinRTOutputWrapper final : public OutputWrapper, | |||
private WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort> | |||
{ | |||
WinRTOutputWrapper (WinRTMidiService& service, int index) | |||
: WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort> (*service.bleDeviceWatcher, *service.outputDeviceWatcher, index) | |||
WinRTOutputWrapper (WinRTMidiService& service, const String& deviceIdentifier) | |||
: WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort> (*service.bleDeviceWatcher, *service.outputDeviceWatcher, deviceIdentifier) | |||
{ | |||
OpenMidiPortThread<IMidiOutPortStatics, IMidiOutPort, IMidiOutPort> portThread ("Open WinRT MIDI output port", | |||
deviceInfo.deviceID, | |||
@@ -1632,7 +1708,8 @@ private: | |||
midiPort->SendBuffer (buffer); | |||
} | |||
String getDeviceName() override { return deviceInfo.name; } | |||
String getDeviceIdentifier() override { return deviceInfo.containerID; } | |||
String getDeviceName() override { return deviceInfo.name; } | |||
//============================================================================== | |||
WinRTWrapper::ComPtr<IBuffer> buffer; | |||
@@ -1718,42 +1795,74 @@ private: | |||
JUCE_IMPLEMENT_SINGLETON (MidiService) | |||
//============================================================================== | |||
StringArray MidiInput::getDevices() | |||
static int findDefaultDeviceIndex (const Array<MidiDeviceInfo>& available, const MidiDeviceInfo& defaultDevice) | |||
{ | |||
return MidiService::getService().getDevices (true); | |||
for (int i = 0; i < available.size(); ++i) | |||
if (available.getUnchecked (i) == defaultDevice) | |||
return i; | |||
return 0; | |||
} | |||
int MidiInput::getDefaultDeviceIndex() | |||
Array<MidiDeviceInfo> MidiInput::getAvailableDevices() | |||
{ | |||
return MidiService::getService().getDefaultDeviceIndex (true); | |||
return MidiService::getService().getAvailableDevices (true); | |||
} | |||
MidiInput::MidiInput (const String& deviceName) : name (deviceName) | |||
MidiDeviceInfo MidiInput::getDefaultDevice() | |||
{ | |||
return MidiService::getService().getDefaultDevice (true); | |||
} | |||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) | |||
MidiInput* MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) | |||
{ | |||
if (callback == nullptr) | |||
if (deviceIdentifier.isEmpty() || callback == nullptr) | |||
return nullptr; | |||
std::unique_ptr<MidiInput> in (new MidiInput (String())); | |||
MidiInput input ({}, {}); | |||
std::unique_ptr<MidiServiceType::InputWrapper> wrapper; | |||
try | |||
{ | |||
wrapper.reset (MidiService::getService().createInputWrapper (*in, index, *callback)); | |||
wrapper.reset (MidiService::getService().createInputWrapper (input, deviceIdentifier, *callback)); | |||
} | |||
catch (std::runtime_error&) | |||
{ | |||
return nullptr; | |||
} | |||
in->setName (wrapper->getDeviceName()); | |||
std::unique_ptr<MidiInput> in; | |||
in.reset (new MidiInput (wrapper->getDeviceName(), deviceIdentifier)); | |||
in->internal = wrapper.release(); | |||
return in.release(); | |||
} | |||
StringArray MidiInput::getDevices() | |||
{ | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
return deviceNames; | |||
} | |||
int MidiInput::getDefaultDeviceIndex() | |||
{ | |||
return findDefaultDeviceIndex (getAvailableDevices(), getDefaultDevice()); | |||
} | |||
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() | |||
{ | |||
delete static_cast<MidiServiceType::InputWrapper*> (internal); | |||
@@ -1763,34 +1872,60 @@ void MidiInput::start() { static_cast<MidiServiceType::InputWrapper*> (interna | |||
void MidiInput::stop() { static_cast<MidiServiceType::InputWrapper*> (internal)->stop(); } | |||
//============================================================================== | |||
StringArray MidiOutput::getDevices() | |||
Array<MidiDeviceInfo> MidiOutput::getAvailableDevices() | |||
{ | |||
return MidiService::getService().getDevices (false); | |||
return MidiService::getService().getAvailableDevices (false); | |||
} | |||
int MidiOutput::getDefaultDeviceIndex() | |||
MidiDeviceInfo MidiOutput::getDefaultDevice() | |||
{ | |||
return MidiService::getService().getDefaultDeviceIndex (false); | |||
return MidiService::getService().getDefaultDevice (false); | |||
} | |||
MidiOutput* MidiOutput::openDevice (int index) | |||
MidiOutput* MidiOutput::openDevice (const String& deviceIdentifier) | |||
{ | |||
if (deviceIdentifier.isEmpty()) | |||
return nullptr; | |||
std::unique_ptr<MidiServiceType::OutputWrapper> wrapper; | |||
try | |||
{ | |||
wrapper.reset (MidiService::getService().createOutputWrapper (index)); | |||
wrapper.reset (MidiService::getService().createOutputWrapper (deviceIdentifier)); | |||
} | |||
catch (std::runtime_error&) | |||
{ | |||
return nullptr; | |||
} | |||
std::unique_ptr<MidiOutput> out (new MidiOutput (wrapper->getDeviceName())); | |||
std::unique_ptr<MidiOutput> out; | |||
out.reset (new MidiOutput (wrapper->getDeviceName(), deviceIdentifier)); | |||
out->internal = wrapper.release(); | |||
return out.release(); | |||
} | |||
StringArray MidiOutput::getDevices() | |||
{ | |||
StringArray deviceNames; | |||
for (auto& d : getAvailableDevices()) | |||
deviceNames.add (d.name); | |||
return deviceNames; | |||
} | |||
int MidiOutput::getDefaultDeviceIndex() | |||
{ | |||
return findDefaultDeviceIndex (getAvailableDevices(), getDefaultDevice()); | |||
} | |||
MidiOutput* MidiOutput::openDevice (int index) | |||
{ | |||
return openDevice (getAvailableDevices()[index].identifier); | |||
} | |||
MidiOutput::~MidiOutput() | |||
{ | |||
stopBackgroundThread(); | |||