mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
AudioDeviceManager: Send changeNotification when MIDI devices change
This patch also updates the MidiDemo to automatically refresh the device lists when the set of available devices changes.
This commit is contained in:
parent
49a954d473
commit
26a872ba9f
12 changed files with 1237 additions and 509 deletions
|
|
@ -52,19 +52,27 @@
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
struct MidiDeviceListEntry : ReferenceCountedObject
|
struct MidiDeviceListEntry : ReferenceCountedObject
|
||||||
{
|
{
|
||||||
MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {}
|
explicit MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {}
|
||||||
|
|
||||||
MidiDeviceInfo deviceInfo;
|
MidiDeviceInfo deviceInfo;
|
||||||
std::unique_ptr<MidiInput> inDevice;
|
std::unique_ptr<MidiInput> inDevice;
|
||||||
std::unique_ptr<MidiOutput> outDevice;
|
std::unique_ptr<MidiOutput> outDevice;
|
||||||
|
|
||||||
using Ptr = ReferenceCountedObjectPtr<MidiDeviceListEntry>;
|
using Ptr = ReferenceCountedObjectPtr<MidiDeviceListEntry>;
|
||||||
|
|
||||||
|
void stopAndReset()
|
||||||
|
{
|
||||||
|
if (inDevice != nullptr)
|
||||||
|
inDevice->stop();
|
||||||
|
|
||||||
|
inDevice .reset();
|
||||||
|
outDevice.reset();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
class MidiDemo : public Component,
|
class MidiDemo : public Component,
|
||||||
private Timer,
|
|
||||||
private MidiKeyboardState::Listener,
|
private MidiKeyboardState::Listener,
|
||||||
private MidiInputCallback,
|
private MidiInputCallback,
|
||||||
private AsyncUpdater
|
private AsyncUpdater
|
||||||
|
|
@ -113,12 +121,11 @@ public:
|
||||||
|
|
||||||
setSize (732, 520);
|
setSize (732, 520);
|
||||||
|
|
||||||
startTimer (500);
|
updateDeviceLists();
|
||||||
}
|
}
|
||||||
|
|
||||||
~MidiDemo() override
|
~MidiDemo() override
|
||||||
{
|
{
|
||||||
stopTimer();
|
|
||||||
midiInputs .clear();
|
midiInputs .clear();
|
||||||
midiOutputs.clear();
|
midiOutputs.clear();
|
||||||
keyboardState.removeListener (this);
|
keyboardState.removeListener (this);
|
||||||
|
|
@ -128,12 +135,6 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
void timerCallback() override
|
|
||||||
{
|
|
||||||
updateDeviceList (true);
|
|
||||||
updateDeviceList (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
|
void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
|
||||||
{
|
{
|
||||||
MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity));
|
MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity));
|
||||||
|
|
@ -211,17 +212,8 @@ public:
|
||||||
|
|
||||||
void closeDevice (bool isInput, int index)
|
void closeDevice (bool isInput, int index)
|
||||||
{
|
{
|
||||||
if (isInput)
|
auto& list = isInput ? midiInputs : midiOutputs;
|
||||||
{
|
list[index]->stopAndReset();
|
||||||
jassert (midiInputs[index]->inDevice.get() != nullptr);
|
|
||||||
midiInputs[index]->inDevice->stop();
|
|
||||||
midiInputs[index]->inDevice.reset();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
jassert (midiOutputs[index]->outDevice.get() != nullptr);
|
|
||||||
midiOutputs[index]->outDevice.reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int getNumMidiInputs() const noexcept
|
int getNumMidiInputs() const noexcept
|
||||||
|
|
@ -423,7 +415,6 @@ private:
|
||||||
|
|
||||||
if (hasDeviceListChanged (availableDevices, isInputDeviceList))
|
if (hasDeviceListChanged (availableDevices, isInputDeviceList))
|
||||||
{
|
{
|
||||||
|
|
||||||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
|
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
|
||||||
= isInputDeviceList ? midiInputs : midiOutputs;
|
= isInputDeviceList ? midiInputs : midiOutputs;
|
||||||
|
|
||||||
|
|
@ -463,6 +454,12 @@ private:
|
||||||
addAndMakeVisible (label);
|
addAndMakeVisible (label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateDeviceLists()
|
||||||
|
{
|
||||||
|
for (const auto isInput : { true, false })
|
||||||
|
updateDeviceList (isInput);
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
|
Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
|
||||||
Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
|
Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
|
||||||
|
|
@ -473,12 +470,17 @@ private:
|
||||||
TextEditor midiMonitor { "MIDI Monitor" };
|
TextEditor midiMonitor { "MIDI Monitor" };
|
||||||
TextButton pairButton { "MIDI Bluetooth devices..." };
|
TextButton pairButton { "MIDI Bluetooth devices..." };
|
||||||
|
|
||||||
std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
|
|
||||||
ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
|
ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
|
||||||
|
std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
|
||||||
|
|
||||||
CriticalSection midiMonitorLock;
|
CriticalSection midiMonitorLock;
|
||||||
Array<MidiMessage> incomingMessages;
|
Array<MidiMessage> incomingMessages;
|
||||||
|
|
||||||
|
MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
|
||||||
|
{
|
||||||
|
updateDeviceLists();
|
||||||
|
});
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,12 @@ void AudioDeviceManager::audioDeviceListChanged()
|
||||||
sendChangeMessage();
|
sendChangeMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioDeviceManager::midiDeviceListChanged()
|
||||||
|
{
|
||||||
|
openLastRequestedMidiDevices (midiDeviceInfosFromXml, defaultMidiOutputDeviceInfo);
|
||||||
|
sendChangeMessage();
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
static void addIfNotNull (OwnedArray<AudioIODeviceType>& list, AudioIODeviceType* const device)
|
static void addIfNotNull (OwnedArray<AudioIODeviceType>& list, AudioIODeviceType* const device)
|
||||||
{
|
{
|
||||||
|
|
@ -430,67 +436,64 @@ String AudioDeviceManager::initialiseFromXML (const XmlElement& xml,
|
||||||
if (error.isNotEmpty() && selectDefaultDeviceOnFailure)
|
if (error.isNotEmpty() && selectDefaultDeviceOnFailure)
|
||||||
error = initialise (numInputChansNeeded, numOutputChansNeeded, nullptr, false, preferredDefaultDeviceName);
|
error = initialise (numInputChansNeeded, numOutputChansNeeded, nullptr, false, preferredDefaultDeviceName);
|
||||||
|
|
||||||
midiDeviceInfosFromXml.clear();
|
|
||||||
enabledMidiInputs.clear();
|
enabledMidiInputs.clear();
|
||||||
|
|
||||||
for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT"))
|
const auto midiInputs = [&]
|
||||||
midiDeviceInfosFromXml.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") });
|
|
||||||
|
|
||||||
auto isIdentifierAvailable = [] (const Array<MidiDeviceInfo>& available, const String& identifier)
|
|
||||||
{
|
{
|
||||||
for (auto& device : available)
|
Array<MidiDeviceInfo> result;
|
||||||
if (device.identifier == identifier)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT"))
|
||||||
};
|
result.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") });
|
||||||
|
|
||||||
auto getUpdatedIdentifierForName = [&] (const Array<MidiDeviceInfo>& available, const String& name) -> String
|
return result;
|
||||||
{
|
}();
|
||||||
for (auto& device : available)
|
|
||||||
if (device.name == name)
|
|
||||||
return device.identifier;
|
|
||||||
|
|
||||||
return {};
|
const MidiDeviceInfo defaultOutputDeviceInfo (xml.getStringAttribute ("defaultMidiOutput"),
|
||||||
};
|
xml.getStringAttribute ("defaultMidiOutputDevice"));
|
||||||
|
|
||||||
auto inputs = MidiInput::getAvailableDevices();
|
openLastRequestedMidiDevices (midiInputs, defaultOutputDeviceInfo);
|
||||||
|
|
||||||
for (auto& info : midiDeviceInfosFromXml)
|
|
||||||
{
|
|
||||||
if (isIdentifierAvailable (inputs, info.identifier))
|
|
||||||
{
|
|
||||||
setMidiInputDeviceEnabled (info.identifier, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto identifier = getUpdatedIdentifierForName (inputs, info.name);
|
|
||||||
|
|
||||||
if (identifier.isNotEmpty())
|
|
||||||
setMidiInputDeviceEnabled (identifier, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MidiDeviceInfo defaultOutputDeviceInfo (xml.getStringAttribute ("defaultMidiOutput"),
|
|
||||||
xml.getStringAttribute ("defaultMidiOutputDevice"));
|
|
||||||
|
|
||||||
auto outputs = MidiOutput::getAvailableDevices();
|
|
||||||
|
|
||||||
if (isIdentifierAvailable (outputs, defaultOutputDeviceInfo.identifier))
|
|
||||||
{
|
|
||||||
setDefaultMidiOutputDevice (defaultOutputDeviceInfo.identifier);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto identifier = getUpdatedIdentifierForName (outputs, defaultOutputDeviceInfo.name);
|
|
||||||
|
|
||||||
if (identifier.isNotEmpty())
|
|
||||||
setDefaultMidiOutputDevice (identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AudioDeviceManager::openLastRequestedMidiDevices (const Array<MidiDeviceInfo>& desiredInputs, const MidiDeviceInfo& defaultOutput)
|
||||||
|
{
|
||||||
|
const auto openDeviceIfAvailable = [&] (const Array<MidiDeviceInfo>& devices,
|
||||||
|
const MidiDeviceInfo& deviceToOpen,
|
||||||
|
auto&& doOpen)
|
||||||
|
{
|
||||||
|
const auto iterWithMatchingIdentifier = std::find_if (devices.begin(), devices.end(), [&] (const auto& x)
|
||||||
|
{
|
||||||
|
return x.identifier == deviceToOpen.identifier;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (iterWithMatchingIdentifier != devices.end())
|
||||||
|
{
|
||||||
|
doOpen (deviceToOpen.identifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto iterWithMatchingName = std::find_if (devices.begin(), devices.end(), [&] (const auto& x)
|
||||||
|
{
|
||||||
|
return x.name == deviceToOpen.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (iterWithMatchingName != devices.end())
|
||||||
|
doOpen (iterWithMatchingName->identifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
midiDeviceInfosFromXml = desiredInputs;
|
||||||
|
|
||||||
|
const auto inputs = MidiInput::getAvailableDevices();
|
||||||
|
|
||||||
|
for (const auto& info : midiDeviceInfosFromXml)
|
||||||
|
openDeviceIfAvailable (inputs, info, [&] (const auto identifier) { setMidiInputDeviceEnabled (identifier, true); });
|
||||||
|
|
||||||
|
const auto outputs = MidiOutput::getAvailableDevices();
|
||||||
|
|
||||||
|
openDeviceIfAvailable (outputs, defaultOutput, [&] (const auto identifier) { setDefaultMidiOutputDevice (identifier); });
|
||||||
|
}
|
||||||
|
|
||||||
String AudioDeviceManager::initialiseWithDefaultDevices (int numInputChannelsNeeded,
|
String AudioDeviceManager::initialiseWithDefaultDevices (int numInputChannelsNeeded,
|
||||||
int numOutputChannelsNeeded)
|
int numOutputChannelsNeeded)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,10 @@ private:
|
||||||
std::unique_ptr<XmlElement> lastExplicitSettings;
|
std::unique_ptr<XmlElement> lastExplicitSettings;
|
||||||
mutable bool listNeedsScanning = true;
|
mutable bool listNeedsScanning = true;
|
||||||
AudioBuffer<float> tempBuffer;
|
AudioBuffer<float> tempBuffer;
|
||||||
|
MidiDeviceListConnection midiDeviceListConnection = MidiDeviceListConnection::make ([this]
|
||||||
|
{
|
||||||
|
midiDeviceListChanged();
|
||||||
|
});
|
||||||
|
|
||||||
struct MidiCallbackInfo
|
struct MidiCallbackInfo
|
||||||
{
|
{
|
||||||
|
|
@ -537,6 +541,7 @@ private:
|
||||||
void audioDeviceErrorInt (const String&);
|
void audioDeviceErrorInt (const String&);
|
||||||
void handleIncomingMidiMessageInt (MidiInput*, const MidiMessage&);
|
void handleIncomingMidiMessageInt (MidiInput*, const MidiMessage&);
|
||||||
void audioDeviceListChanged();
|
void audioDeviceListChanged();
|
||||||
|
void midiDeviceListChanged();
|
||||||
|
|
||||||
String restartDevice (int blockSizeToUse, double sampleRateToUse,
|
String restartDevice (int blockSizeToUse, double sampleRateToUse,
|
||||||
const BigInteger& ins, const BigInteger& outs);
|
const BigInteger& ins, const BigInteger& outs);
|
||||||
|
|
@ -554,6 +559,7 @@ private:
|
||||||
String initialiseDefault (const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
String initialiseDefault (const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
||||||
String initialiseFromXML (const XmlElement&, bool selectDefaultDeviceOnFailure,
|
String initialiseFromXML (const XmlElement&, bool selectDefaultDeviceOnFailure,
|
||||||
const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
||||||
|
void openLastRequestedMidiDevices (const Array<MidiDeviceInfo>&, const MidiDeviceInfo&);
|
||||||
|
|
||||||
AudioIODeviceType* findType (const String& inputName, const String& outputName);
|
AudioIODeviceType* findType (const String& inputName, const String& outputName);
|
||||||
AudioIODeviceType* findType (const String& typeName);
|
AudioIODeviceType* findType (const String& typeName);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
#include "juce_audio_devices.h"
|
#include "juce_audio_devices.h"
|
||||||
|
|
||||||
#include "audio_io/juce_SampleRateHelpers.cpp"
|
#include "audio_io/juce_SampleRateHelpers.cpp"
|
||||||
|
#include "midi_io/juce_MidiDevices.cpp"
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
#if JUCE_MAC || JUCE_IOS
|
#if JUCE_MAC || JUCE_IOS
|
||||||
|
|
@ -249,6 +250,5 @@ namespace juce
|
||||||
#include "audio_io/juce_AudioIODevice.cpp"
|
#include "audio_io/juce_AudioIODevice.cpp"
|
||||||
#include "audio_io/juce_AudioIODeviceType.cpp"
|
#include "audio_io/juce_AudioIODeviceType.cpp"
|
||||||
#include "midi_io/juce_MidiMessageCollector.cpp"
|
#include "midi_io/juce_MidiMessageCollector.cpp"
|
||||||
#include "midi_io/juce_MidiDevices.cpp"
|
|
||||||
#include "sources/juce_AudioSourcePlayer.cpp"
|
#include "sources/juce_AudioSourcePlayer.cpp"
|
||||||
#include "sources/juce_AudioTransportSource.cpp"
|
#include "sources/juce_AudioTransportSource.cpp"
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,81 @@
|
||||||
namespace juce
|
namespace juce
|
||||||
{
|
{
|
||||||
|
|
||||||
|
class MidiDeviceListConnectionBroadcaster : private AsyncUpdater
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~MidiDeviceListConnectionBroadcaster() override
|
||||||
|
{
|
||||||
|
cancelPendingUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnection::Key add (std::function<void()> callback)
|
||||||
|
{
|
||||||
|
JUCE_ASSERT_MESSAGE_THREAD
|
||||||
|
return callbacks.emplace (key++, std::move (callback)).first->first;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove (const MidiDeviceListConnection::Key k)
|
||||||
|
{
|
||||||
|
JUCE_ASSERT_MESSAGE_THREAD
|
||||||
|
callbacks.erase (k);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify()
|
||||||
|
{
|
||||||
|
if (MessageManager::getInstance()->isThisTheMessageThread())
|
||||||
|
{
|
||||||
|
cancelPendingUpdate();
|
||||||
|
|
||||||
|
const State newState;
|
||||||
|
|
||||||
|
if (std::exchange (lastNotifiedState, newState) != newState)
|
||||||
|
for (auto it = callbacks.begin(); it != callbacks.end();)
|
||||||
|
NullCheckedInvocation::invoke ((it++)->second);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
triggerAsyncUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto& get()
|
||||||
|
{
|
||||||
|
static MidiDeviceListConnectionBroadcaster result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
MidiDeviceListConnectionBroadcaster() = default;
|
||||||
|
|
||||||
|
class State
|
||||||
|
{
|
||||||
|
Array<MidiDeviceInfo> ins = MidiInput::getAvailableDevices(), outs = MidiOutput::getAvailableDevices();
|
||||||
|
auto tie() const { return std::tie (ins, outs); }
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool operator== (const State& other) const { return tie() == other.tie(); }
|
||||||
|
bool operator!= (const State& other) const { return tie() != other.tie(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
void handleAsyncUpdate() override
|
||||||
|
{
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<MidiDeviceListConnection::Key, std::function<void()>> callbacks;
|
||||||
|
State lastNotifiedState;
|
||||||
|
MidiDeviceListConnection::Key key = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
MidiDeviceListConnection::~MidiDeviceListConnection() noexcept
|
||||||
|
{
|
||||||
|
if (broadcaster != nullptr)
|
||||||
|
broadcaster->remove (key);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
void MidiInputCallback::handlePartialSysexMessage ([[maybe_unused]] MidiInput* source,
|
void MidiInputCallback::handlePartialSysexMessage ([[maybe_unused]] MidiInput* source,
|
||||||
[[maybe_unused]] const uint8* messageData,
|
[[maybe_unused]] const uint8* messageData,
|
||||||
[[maybe_unused]] int numBytesSoFar,
|
[[maybe_unused]] int numBytesSoFar,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,85 @@
|
||||||
|
|
||||||
namespace juce
|
namespace juce
|
||||||
{
|
{
|
||||||
|
|
||||||
|
class MidiDeviceListConnectionBroadcaster;
|
||||||
|
|
||||||
|
/**
|
||||||
|
To find out when the available MIDI devices change, call MidiDeviceListConnection::make(),
|
||||||
|
passing a lambda that will be called on each configuration change.
|
||||||
|
|
||||||
|
To stop the lambda receiving callbacks, destroy the MidiDeviceListConnection instance returned
|
||||||
|
from make(), or call reset() on it.
|
||||||
|
|
||||||
|
@code
|
||||||
|
// Start listening for configuration changes
|
||||||
|
auto connection = MidiDeviceListConnection::make ([]
|
||||||
|
{
|
||||||
|
// This will print a message when devices are connected/disconnected
|
||||||
|
DBG ("MIDI devices changed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop listening
|
||||||
|
connection.reset();
|
||||||
|
@endcode
|
||||||
|
*/
|
||||||
|
class MidiDeviceListConnection
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Key = uint64_t;
|
||||||
|
|
||||||
|
/** Constructs an inactive connection.
|
||||||
|
*/
|
||||||
|
MidiDeviceListConnection() = default;
|
||||||
|
|
||||||
|
MidiDeviceListConnection (const MidiDeviceListConnection&) = delete;
|
||||||
|
MidiDeviceListConnection (MidiDeviceListConnection&& other) noexcept
|
||||||
|
: broadcaster (std::exchange (other.broadcaster, nullptr)),
|
||||||
|
key (std::exchange (other.key, Key{}))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnection& operator= (const MidiDeviceListConnection&) = delete;
|
||||||
|
MidiDeviceListConnection& operator= (MidiDeviceListConnection&& other) noexcept
|
||||||
|
{
|
||||||
|
MidiDeviceListConnection (std::move (other)).swap (*this);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
~MidiDeviceListConnection() noexcept;
|
||||||
|
|
||||||
|
/** Clears this connection.
|
||||||
|
|
||||||
|
If this object had an active connection, that connection will be deactivated, and the
|
||||||
|
corresponding callback will be removed from the MidiDeviceListConnectionBroadcaster.
|
||||||
|
*/
|
||||||
|
void reset() noexcept
|
||||||
|
{
|
||||||
|
MidiDeviceListConnection().swap (*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers a function to be called whenever the midi device list changes.
|
||||||
|
|
||||||
|
The callback will only be active for as long as the return MidiDeviceListConnection remains
|
||||||
|
alive. To stop receiving device change notifications, destroy the Connection object, e.g.
|
||||||
|
by allowing it to fall out of scope.
|
||||||
|
*/
|
||||||
|
static MidiDeviceListConnection make (std::function<void()>);
|
||||||
|
|
||||||
|
private:
|
||||||
|
MidiDeviceListConnection (MidiDeviceListConnectionBroadcaster* b, const Key k)
|
||||||
|
: broadcaster (b), key (k) {}
|
||||||
|
|
||||||
|
void swap (MidiDeviceListConnection& other) noexcept
|
||||||
|
{
|
||||||
|
std::swap (other.broadcaster, broadcaster);
|
||||||
|
std::swap (other.key, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnectionBroadcaster* broadcaster = nullptr;
|
||||||
|
Key key = {};
|
||||||
|
};
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
/**
|
/**
|
||||||
This struct contains information about a MIDI input or output device.
|
This struct contains information about a MIDI input or output device.
|
||||||
|
|
@ -61,8 +140,9 @@ struct MidiDeviceInfo
|
||||||
String identifier;
|
String identifier;
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
bool operator== (const MidiDeviceInfo& other) const noexcept { return name == other.name && identifier == other.identifier; }
|
auto tie() const { return std::tie (name, identifier); }
|
||||||
bool operator!= (const MidiDeviceInfo& other) const noexcept { return ! operator== (other); }
|
bool operator== (const MidiDeviceInfo& other) const noexcept { return tie() == other.tie(); }
|
||||||
|
bool operator!= (const MidiDeviceInfo& other) const noexcept { return tie() != other.tie(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
class MidiInputCallback;
|
class MidiInputCallback;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ package com.rmsl.juce;
|
||||||
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter;
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothManager;
|
||||||
import android.bluetooth.BluetoothGatt;
|
import android.bluetooth.BluetoothGatt;
|
||||||
import android.bluetooth.BluetoothGattCallback;
|
import android.bluetooth.BluetoothGattCallback;
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
|
@ -43,6 +44,7 @@ import android.bluetooth.le.ScanCallback;
|
||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.media.midi.MidiOutputPort;
|
import android.media.midi.MidiOutputPort;
|
||||||
import android.media.midi.MidiReceiver;
|
import android.media.midi.MidiReceiver;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.ParcelUuid;
|
import android.os.ParcelUuid;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
@ -56,6 +58,7 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static android.content.Context.MIDI_SERVICE;
|
import static android.content.Context.MIDI_SERVICE;
|
||||||
|
import static android.content.Context.BLUETOOTH_SERVICE;
|
||||||
|
|
||||||
public class JuceMidiSupport
|
public class JuceMidiSupport
|
||||||
{
|
{
|
||||||
|
|
@ -77,10 +80,18 @@ public class JuceMidiSupport
|
||||||
String getName ();
|
String getName ();
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
static BluetoothAdapter getDefaultBluetoothAdapter (Context ctx)
|
||||||
public static class BluetoothManager extends ScanCallback
|
|
||||||
{
|
{
|
||||||
BluetoothManager (Context contextToUse)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S_V2)
|
||||||
|
return BluetoothAdapter.getDefaultAdapter();
|
||||||
|
|
||||||
|
return ((BluetoothManager) ctx.getSystemService (BLUETOOTH_SERVICE)).getAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
public static class BluetoothMidiManager extends ScanCallback
|
||||||
|
{
|
||||||
|
BluetoothMidiManager (Context contextToUse)
|
||||||
{
|
{
|
||||||
appContext = contextToUse;
|
appContext = contextToUse;
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +103,7 @@ public class JuceMidiSupport
|
||||||
|
|
||||||
public String getHumanReadableStringForBluetoothAddress (String address)
|
public String getHumanReadableStringForBluetoothAddress (String address)
|
||||||
{
|
{
|
||||||
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter ().getRemoteDevice (address);
|
BluetoothDevice btDevice = getDefaultBluetoothAdapter (appContext).getRemoteDevice (address);
|
||||||
return btDevice.getName ();
|
return btDevice.getName ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,11 +114,11 @@ public class JuceMidiSupport
|
||||||
|
|
||||||
public void startStopScan (boolean shouldStart)
|
public void startStopScan (boolean shouldStart)
|
||||||
{
|
{
|
||||||
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter ();
|
BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter (appContext);
|
||||||
|
|
||||||
if (bluetoothAdapter == null)
|
if (bluetoothAdapter == null)
|
||||||
{
|
{
|
||||||
Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter");
|
Log.d ("JUCE", "BluetoothMidiManager error: could not get default Bluetooth adapter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +126,7 @@ public class JuceMidiSupport
|
||||||
|
|
||||||
if (bluetoothLeScanner == null)
|
if (bluetoothLeScanner == null)
|
||||||
{
|
{
|
||||||
Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner");
|
Log.d ("JUCE", "BluetoothMidiManager error: could not get Bluetooth LE scanner");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +151,7 @@ public class JuceMidiSupport
|
||||||
|
|
||||||
public boolean pairBluetoothMidiDevice (String address)
|
public boolean pairBluetoothMidiDevice (String address)
|
||||||
{
|
{
|
||||||
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter ().getRemoteDevice (address);
|
BluetoothDevice btDevice = getDefaultBluetoothAdapter (appContext).getRemoteDevice (address);
|
||||||
|
|
||||||
if (btDevice == null)
|
if (btDevice == null)
|
||||||
{
|
{
|
||||||
|
|
@ -543,12 +554,8 @@ public class JuceMidiSupport
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openPorts = new HashMap<MidiPortPath, WeakReference<JuceMidiPort>> ();
|
|
||||||
midiDevices = new ArrayList<Pair<MidiDevice, BluetoothGatt>> ();
|
|
||||||
openTasks = new HashMap<Integer, MidiDeviceOpenTask> ();
|
|
||||||
btDevicesPairing = new HashMap<String, BluetoothGatt> ();
|
|
||||||
|
|
||||||
MidiDeviceInfo[] foundDevices = manager.getDevices ();
|
MidiDeviceInfo[] foundDevices = manager.getDevices ();
|
||||||
|
|
||||||
for (MidiDeviceInfo info : foundDevices)
|
for (MidiDeviceInfo info : foundDevices)
|
||||||
onDeviceAdded (info);
|
onDeviceAdded (info);
|
||||||
|
|
||||||
|
|
@ -810,6 +817,7 @@ public class JuceMidiSupport
|
||||||
openPorts.remove (path);
|
openPorts.remove (path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onDeviceAdded (MidiDeviceInfo info)
|
public void onDeviceAdded (MidiDeviceInfo info)
|
||||||
{
|
{
|
||||||
// only add standard midi devices
|
// only add standard midi devices
|
||||||
|
|
@ -819,6 +827,7 @@ public class JuceMidiSupport
|
||||||
manager.openDevice (info, this, null);
|
manager.openDevice (info, this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onDeviceRemoved (MidiDeviceInfo info)
|
public void onDeviceRemoved (MidiDeviceInfo info)
|
||||||
{
|
{
|
||||||
synchronized (MidiDeviceManager.class)
|
synchronized (MidiDeviceManager.class)
|
||||||
|
|
@ -856,8 +865,11 @@ public class JuceMidiSupport
|
||||||
midiDevices.remove (devicePair);
|
midiDevices.remove (devicePair);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDevicesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onDeviceStatusChanged (MidiDeviceStatus status)
|
public void onDeviceStatusChanged (MidiDeviceStatus status)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -933,6 +945,7 @@ public class JuceMidiSupport
|
||||||
BluetoothGatt gatt = openTasks.get (deviceID).getGatt ();
|
BluetoothGatt gatt = openTasks.get (deviceID).getGatt ();
|
||||||
openTasks.remove (deviceID);
|
openTasks.remove (deviceID);
|
||||||
midiDevices.add (new Pair<MidiDevice, BluetoothGatt> (theDevice, gatt));
|
midiDevices.add (new Pair<MidiDevice, BluetoothGatt> (theDevice, gatt));
|
||||||
|
handleDevicesChanged();
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
|
|
@ -973,7 +986,6 @@ public class JuceMidiSupport
|
||||||
{
|
{
|
||||||
for (MidiDeviceInfo info : deviceInfos)
|
for (MidiDeviceInfo info : deviceInfos)
|
||||||
{
|
{
|
||||||
int localIndex = 0;
|
|
||||||
if (info.getId () == path.deviceId)
|
if (info.getId () == path.deviceId)
|
||||||
{
|
{
|
||||||
for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ())
|
for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ())
|
||||||
|
|
@ -1048,11 +1060,11 @@ public class JuceMidiSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
private MidiManager manager;
|
private MidiManager manager;
|
||||||
private HashMap<String, BluetoothGatt> btDevicesPairing;
|
private HashMap<String, BluetoothGatt> btDevicesPairing = new HashMap<String, BluetoothGatt>();
|
||||||
private HashMap<Integer, MidiDeviceOpenTask> openTasks;
|
private HashMap<Integer, MidiDeviceOpenTask> openTasks = new HashMap<Integer, MidiDeviceOpenTask>();
|
||||||
private ArrayList<Pair<MidiDevice, BluetoothGatt>> midiDevices;
|
private ArrayList<Pair<MidiDevice, BluetoothGatt>> midiDevices = new ArrayList<Pair<MidiDevice, BluetoothGatt>>();
|
||||||
private MidiDeviceInfo[] deviceInfos;
|
private MidiDeviceInfo[] deviceInfos;
|
||||||
private HashMap<MidiPortPath, WeakReference<JuceMidiPort>> openPorts;
|
private HashMap<MidiPortPath, WeakReference<JuceMidiPort>> openPorts = new HashMap<MidiPortPath, WeakReference<JuceMidiPort>>();
|
||||||
private Context appContext = null;
|
private Context appContext = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1070,9 +1082,9 @@ public class JuceMidiSupport
|
||||||
return midiDeviceManager;
|
return midiDeviceManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BluetoothManager getAndroidBluetoothManager (Context context)
|
public static BluetoothMidiManager getAndroidBluetoothManager (Context context)
|
||||||
{
|
{
|
||||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter ();
|
BluetoothAdapter adapter = getDefaultBluetoothAdapter (context);
|
||||||
|
|
||||||
if (adapter == null)
|
if (adapter == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1083,12 +1095,15 @@ public class JuceMidiSupport
|
||||||
synchronized (JuceMidiSupport.class)
|
synchronized (JuceMidiSupport.class)
|
||||||
{
|
{
|
||||||
if (bluetoothManager == null)
|
if (bluetoothManager == null)
|
||||||
bluetoothManager = new BluetoothManager (context);
|
bluetoothManager = new BluetoothMidiManager (context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bluetoothManager;
|
return bluetoothManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To be called when devices become (un)available
|
||||||
|
private native static void handleDevicesChanged();
|
||||||
|
|
||||||
private static MidiDeviceManager midiDeviceManager = null;
|
private static MidiDeviceManager midiDeviceManager = null;
|
||||||
private static BluetoothManager bluetoothManager = null;
|
private static BluetoothMidiManager bluetoothManager = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -29,35 +29,20 @@ namespace juce
|
||||||
class AlsaClient : public ReferenceCountedObject
|
class AlsaClient : public ReferenceCountedObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
AlsaClient()
|
|
||||||
{
|
|
||||||
jassert (instance == nullptr);
|
|
||||||
|
|
||||||
snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0);
|
|
||||||
|
|
||||||
if (handle != nullptr)
|
|
||||||
{
|
|
||||||
snd_seq_nonblock (handle, SND_SEQ_NONBLOCK);
|
|
||||||
snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8());
|
|
||||||
clientId = snd_seq_client_id (handle);
|
|
||||||
|
|
||||||
// It's good idea to pre-allocate a good number of elements
|
|
||||||
ports.ensureStorageAllocated (32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~AlsaClient()
|
~AlsaClient()
|
||||||
{
|
{
|
||||||
|
inputThread.reset();
|
||||||
|
|
||||||
jassert (instance != nullptr);
|
jassert (instance != nullptr);
|
||||||
instance = nullptr;
|
instance = nullptr;
|
||||||
|
|
||||||
jassert (activeCallbacks.get() == 0);
|
jassert (activeCallbacks.get() == 0);
|
||||||
|
|
||||||
if (inputThread)
|
|
||||||
inputThread->stopThread (3000);
|
|
||||||
|
|
||||||
if (handle != nullptr)
|
if (handle != nullptr)
|
||||||
|
{
|
||||||
|
snd_seq_delete_simple_port (handle, announcementsIn);
|
||||||
snd_seq_close (handle);
|
snd_seq_close (handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getAlsaMidiName()
|
static String getAlsaMidiName()
|
||||||
|
|
@ -123,15 +108,7 @@ public:
|
||||||
|
|
||||||
void enableCallback (bool enable)
|
void enableCallback (bool enable)
|
||||||
{
|
{
|
||||||
const auto oldValue = callbackEnabled.exchange (enable);
|
callbackEnabled = enable;
|
||||||
|
|
||||||
if (oldValue != enable)
|
|
||||||
{
|
|
||||||
if (enable)
|
|
||||||
client.registerCallback();
|
|
||||||
else
|
|
||||||
client.unregisterCallback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sendMessageNow (const MidiMessage& message)
|
bool sendMessageNow (const MidiMessage& message)
|
||||||
|
|
@ -238,23 +215,6 @@ public:
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerCallback()
|
|
||||||
{
|
|
||||||
if (inputThread == nullptr)
|
|
||||||
inputThread.reset (new MidiInputThread (*this));
|
|
||||||
|
|
||||||
if (++activeCallbacks == 1)
|
|
||||||
inputThread->startThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregisterCallback()
|
|
||||||
{
|
|
||||||
jassert (activeCallbacks.get() > 0);
|
|
||||||
|
|
||||||
if (--activeCallbacks == 0 && inputThread->isThreadRunning())
|
|
||||||
inputThread->signalThreadShouldExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message)
|
void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message)
|
||||||
{
|
{
|
||||||
const ScopedLock sl (callbackLock);
|
const ScopedLock sl (callbackLock);
|
||||||
|
|
@ -294,8 +254,34 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AlsaClient()
|
||||||
|
{
|
||||||
|
jassert (instance == nullptr);
|
||||||
|
|
||||||
|
snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0);
|
||||||
|
|
||||||
|
if (handle != nullptr)
|
||||||
|
{
|
||||||
|
snd_seq_nonblock (handle, SND_SEQ_NONBLOCK);
|
||||||
|
snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8());
|
||||||
|
clientId = snd_seq_client_id (handle);
|
||||||
|
|
||||||
|
// It's good idea to pre-allocate a good number of elements
|
||||||
|
ports.ensureStorageAllocated (32);
|
||||||
|
|
||||||
|
announcementsIn = snd_seq_create_simple_port (handle,
|
||||||
|
TRANS ("announcements").toRawUTF8(),
|
||||||
|
SND_SEQ_PORT_CAP_WRITE,
|
||||||
|
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION);
|
||||||
|
snd_seq_connect_from (handle, announcementsIn, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE);
|
||||||
|
|
||||||
|
inputThread.emplace (*this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
snd_seq_t* handle = nullptr;
|
snd_seq_t* handle = nullptr;
|
||||||
int clientId = 0;
|
int clientId = 0;
|
||||||
|
int announcementsIn = 0;
|
||||||
OwnedArray<Port> ports;
|
OwnedArray<Port> ports;
|
||||||
Atomic<int> activeCallbacks;
|
Atomic<int> activeCallbacks;
|
||||||
CriticalSection callbackLock;
|
CriticalSection callbackLock;
|
||||||
|
|
@ -303,17 +289,52 @@ private:
|
||||||
static AlsaClient* instance;
|
static AlsaClient* instance;
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
class MidiInputThread : public Thread
|
class SequencerThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MidiInputThread (AlsaClient& c)
|
explicit SequencerThread (AlsaClient& c)
|
||||||
: Thread ("JUCE MIDI Input"), client (c)
|
: client (c)
|
||||||
{
|
{
|
||||||
jassert (client.get() != nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void run() override
|
~SequencerThread() noexcept
|
||||||
{
|
{
|
||||||
|
shouldStop = true;
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// If we directly call MidiDeviceListConnectionBroadcaster::get() from the background thread,
|
||||||
|
// there's a possibility that we'll deadlock in the following scenario:
|
||||||
|
// - The main thread calls MidiDeviceListConnectionBroadcaster::get() for the first time
|
||||||
|
// (e.g. to register a listener). The static MidiDeviceListConnectionBroadcaster singleton
|
||||||
|
// begins construction. During the constructor, an AlsaClient is created to iterate midi
|
||||||
|
// ins/outs.
|
||||||
|
// - The AlsaClient starts a new SequencerThread. If connections are updated, the
|
||||||
|
// SequencerThread may call MidiDeviceListConnectionBroadcaster::get().notify()
|
||||||
|
// while the MidiDeviceListConnectionBroadcaster singleton is still being created.
|
||||||
|
// - The SequencerThread blocks until the MidiDeviceListConnectionBroadcaster has been
|
||||||
|
// created on the main thread, but the MidiDeviceListConnectionBroadcaster's constructor
|
||||||
|
// can't complete until the AlsaClient's destructor has run, which in turn requires the
|
||||||
|
// SequencerThread to join.
|
||||||
|
class UpdateNotifier : private AsyncUpdater
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~UpdateNotifier() override { cancelPendingUpdate(); }
|
||||||
|
using AsyncUpdater::triggerAsyncUpdate;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleAsyncUpdate() override { MidiDeviceListConnectionBroadcaster::get().notify(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
AlsaClient& client;
|
||||||
|
MidiDataConcatenator concatenator { 2048 };
|
||||||
|
std::atomic<bool> shouldStop { false };
|
||||||
|
UpdateNotifier notifier;
|
||||||
|
std::thread thread { [this]
|
||||||
|
{
|
||||||
|
Thread::setCurrentThreadName ("JUCE MIDI Input");
|
||||||
|
|
||||||
auto seqHandle = client.get();
|
auto seqHandle = client.get();
|
||||||
|
|
||||||
const int maxEventSize = 16 * 1024;
|
const int maxEventSize = 16 * 1024;
|
||||||
|
|
@ -321,17 +342,20 @@ private:
|
||||||
|
|
||||||
if (snd_midi_event_new (maxEventSize, &midiParser) >= 0)
|
if (snd_midi_event_new (maxEventSize, &midiParser) >= 0)
|
||||||
{
|
{
|
||||||
auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN);
|
const ScopeGuard freeMidiEvent { [&] { snd_midi_event_free (midiParser); } };
|
||||||
HeapBlock<pollfd> pfd (numPfds);
|
|
||||||
snd_seq_poll_descriptors (seqHandle, pfd, (unsigned int) numPfds, POLLIN);
|
|
||||||
|
|
||||||
HeapBlock<uint8> buffer (maxEventSize);
|
const auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN);
|
||||||
|
std::vector<pollfd> pfd (static_cast<size_t> (numPfds));
|
||||||
|
snd_seq_poll_descriptors (seqHandle, pfd.data(), (unsigned int) numPfds, POLLIN);
|
||||||
|
|
||||||
while (! threadShouldExit())
|
std::vector<uint8> buffer (maxEventSize);
|
||||||
|
|
||||||
|
while (! shouldStop)
|
||||||
{
|
{
|
||||||
if (poll (pfd, (nfds_t) numPfds, 100) > 0) // there was a "500" here which is a bit long when we exit the program and have to wait for a timeout on this poll call
|
// This timeout shouldn't be too long, so that the program can exit in a timely manner
|
||||||
|
if (poll (pfd.data(), (nfds_t) numPfds, 100) > 0)
|
||||||
{
|
{
|
||||||
if (threadShouldExit())
|
if (shouldStop)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|
@ -340,33 +364,51 @@ private:
|
||||||
|
|
||||||
if (snd_seq_event_input (seqHandle, &inputEvent) >= 0)
|
if (snd_seq_event_input (seqHandle, &inputEvent) >= 0)
|
||||||
{
|
{
|
||||||
|
const ScopeGuard freeInputEvent { [&] { snd_seq_free_event (inputEvent); } };
|
||||||
|
|
||||||
|
constexpr int systemEvents[]
|
||||||
|
{
|
||||||
|
SND_SEQ_EVENT_CLIENT_CHANGE,
|
||||||
|
SND_SEQ_EVENT_CLIENT_START,
|
||||||
|
SND_SEQ_EVENT_CLIENT_EXIT,
|
||||||
|
SND_SEQ_EVENT_PORT_CHANGE,
|
||||||
|
SND_SEQ_EVENT_PORT_START,
|
||||||
|
SND_SEQ_EVENT_PORT_EXIT,
|
||||||
|
SND_SEQ_EVENT_PORT_SUBSCRIBED,
|
||||||
|
SND_SEQ_EVENT_PORT_UNSUBSCRIBED,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto foundEvent = std::find (std::begin (systemEvents),
|
||||||
|
std::end (systemEvents),
|
||||||
|
inputEvent->type);
|
||||||
|
|
||||||
|
if (foundEvent != std::end (systemEvents))
|
||||||
|
{
|
||||||
|
notifier.triggerAsyncUpdate();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// xxx what about SYSEXes that are too big for the buffer?
|
// xxx what about SYSEXes that are too big for the buffer?
|
||||||
auto numBytes = snd_midi_event_decode (midiParser, buffer,
|
const auto numBytes = snd_midi_event_decode (midiParser,
|
||||||
maxEventSize, inputEvent);
|
buffer.data(),
|
||||||
|
maxEventSize,
|
||||||
|
inputEvent);
|
||||||
|
|
||||||
snd_midi_event_reset_decode (midiParser);
|
snd_midi_event_reset_decode (midiParser);
|
||||||
|
|
||||||
concatenator.pushMidiData (buffer, (int) numBytes,
|
concatenator.pushMidiData (buffer.data(), (int) numBytes,
|
||||||
Time::getMillisecondCounter() * 0.001,
|
Time::getMillisecondCounter() * 0.001,
|
||||||
inputEvent, client);
|
inputEvent, client);
|
||||||
|
|
||||||
snd_seq_free_event (inputEvent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (snd_seq_event_input_pending (seqHandle, 0) > 0);
|
while (snd_seq_event_input_pending (seqHandle, 0) > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snd_midi_event_free (midiParser);
|
|
||||||
}
|
}
|
||||||
}
|
} };
|
||||||
|
|
||||||
private:
|
|
||||||
AlsaClient& client;
|
|
||||||
MidiDataConcatenator concatenator { 2048 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<MidiInputThread> inputThread;
|
std::optional<SequencerThread> inputThread;
|
||||||
};
|
};
|
||||||
|
|
||||||
AlsaClient* AlsaClient::instance = nullptr;
|
AlsaClient* AlsaClient::instance = nullptr;
|
||||||
|
|
@ -659,6 +701,18 @@ void MidiOutput::sendMessageNow (const MidiMessage& message)
|
||||||
internal->ptr->sendMessageNow (message);
|
internal->ptr->sendMessageNow (message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnection MidiDeviceListConnection::make (std::function<void()> cb)
|
||||||
|
{
|
||||||
|
auto& broadcaster = MidiDeviceListConnectionBroadcaster::get();
|
||||||
|
// We capture the AlsaClient instance here to ensure that it remains alive for at least as long
|
||||||
|
// as the MidiDeviceListConnection. This is necessary because system change messages will only
|
||||||
|
// be processed when the AlsaClient's SequencerThread is running.
|
||||||
|
return { &broadcaster, broadcaster.add ([fn = std::move (cb), client = AlsaClient::getInstance()]
|
||||||
|
{
|
||||||
|
NullCheckedInvocation::invoke (fn);
|
||||||
|
}) };
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
|
@ -693,6 +747,12 @@ StringArray MidiOutput::getDevices()
|
||||||
int MidiOutput::getDefaultDeviceIndex() { return 0;}
|
int MidiOutput::getDefaultDeviceIndex() { return 0;}
|
||||||
std::unique_ptr<MidiOutput> MidiOutput::openDevice (int) { return {}; }
|
std::unique_ptr<MidiOutput> MidiOutput::openDevice (int) { return {}; }
|
||||||
|
|
||||||
|
MidiDeviceListConnection MidiDeviceListConnection::make (std::function<void()> cb)
|
||||||
|
{
|
||||||
|
auto& broadcaster = MidiDeviceListConnectionBroadcaster::get();
|
||||||
|
return { &broadcaster, broadcaster.add (std::move (cb)) };
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace juce
|
} // namespace juce
|
||||||
|
|
|
||||||
|
|
@ -579,9 +579,10 @@ namespace CoreMidiHelpers
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static void globalSystemChangeCallback (const MIDINotification*, void*)
|
static void globalSystemChangeCallback (const MIDINotification* notification, void*)
|
||||||
{
|
{
|
||||||
// TODO.. Should pass-on this notification..
|
if (notification != nullptr && notification->messageID == kMIDIMsgSetupChanged)
|
||||||
|
MidiDeviceListConnectionBroadcaster::get().notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGlobalMidiClientName()
|
static String getGlobalMidiClientName()
|
||||||
|
|
@ -594,9 +595,7 @@ namespace CoreMidiHelpers
|
||||||
|
|
||||||
static MIDIClientRef getGlobalMidiClient()
|
static MIDIClientRef getGlobalMidiClient()
|
||||||
{
|
{
|
||||||
static MIDIClientRef globalMidiClient = 0;
|
static const auto globalMidiClient = [&]
|
||||||
|
|
||||||
if (globalMidiClient == 0)
|
|
||||||
{
|
{
|
||||||
// Since OSX 10.6, the MIDIClientCreate function will only work
|
// Since OSX 10.6, the MIDIClientCreate function will only work
|
||||||
// correctly when called from the message thread!
|
// correctly when called from the message thread!
|
||||||
|
|
@ -605,8 +604,10 @@ namespace CoreMidiHelpers
|
||||||
enableSimulatorMidiSession();
|
enableSimulatorMidiSession();
|
||||||
|
|
||||||
CFUniquePtr<CFStringRef> name (getGlobalMidiClientName().toCFString());
|
CFUniquePtr<CFStringRef> name (getGlobalMidiClientName().toCFString());
|
||||||
CHECK_ERROR (MIDIClientCreate (name.get(), &globalSystemChangeCallback, nullptr, &globalMidiClient));
|
MIDIClientRef result{};
|
||||||
}
|
CHECK_ERROR (MIDIClientCreate (name.get(), globalSystemChangeCallback, nullptr, &result));
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
|
||||||
return globalMidiClient;
|
return globalMidiClient;
|
||||||
}
|
}
|
||||||
|
|
@ -1300,6 +1301,12 @@ void MidiOutput::sendMessageNow (const MidiMessage& message)
|
||||||
internal->send (message);
|
internal->send (message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnection MidiDeviceListConnection::make (std::function<void()> cb)
|
||||||
|
{
|
||||||
|
auto& broadcaster = MidiDeviceListConnectionBroadcaster::get();
|
||||||
|
return { &broadcaster, broadcaster.add (std::move (cb)) };
|
||||||
|
}
|
||||||
|
|
||||||
#undef CHECK_ERROR
|
#undef CHECK_ERROR
|
||||||
|
|
||||||
} // namespace juce
|
} // namespace juce
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ struct MidiServiceType
|
||||||
struct Win32MidiService : public MidiServiceType,
|
struct Win32MidiService : public MidiServiceType,
|
||||||
private Timer
|
private Timer
|
||||||
{
|
{
|
||||||
Win32MidiService() {}
|
Win32MidiService() = default;
|
||||||
|
|
||||||
Array<MidiDeviceInfo> getAvailableDevices (bool isInput) override
|
Array<MidiDeviceInfo> getAvailableDevices (bool isInput) override
|
||||||
{
|
{
|
||||||
|
|
@ -1871,6 +1871,10 @@ struct MidiService : public DeletedAtShutdown
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<MidiServiceType> internal;
|
std::unique_ptr<MidiServiceType> internal;
|
||||||
|
DeviceChangeDetector detector { L"JuceMidiDeviceDetector_", []
|
||||||
|
{
|
||||||
|
MidiDeviceListConnectionBroadcaster::get().notify();
|
||||||
|
} };
|
||||||
};
|
};
|
||||||
|
|
||||||
JUCE_IMPLEMENT_SINGLETON (MidiService)
|
JUCE_IMPLEMENT_SINGLETON (MidiService)
|
||||||
|
|
@ -2013,4 +2017,10 @@ void MidiOutput::sendMessageNow (const MidiMessage& message)
|
||||||
internal->sendMessageNow (message);
|
internal->sendMessageNow (message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MidiDeviceListConnection MidiDeviceListConnection::make (std::function<void()> cb)
|
||||||
|
{
|
||||||
|
auto& broadcaster = MidiDeviceListConnectionBroadcaster::get();
|
||||||
|
return { &broadcaster, broadcaster.add (std::move (cb)) };
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace juce
|
} // namespace juce
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ namespace juce
|
||||||
{
|
{
|
||||||
|
|
||||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||||
STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothManager;")
|
STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothMidiManager;")
|
||||||
|
|
||||||
DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 23)
|
DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 23)
|
||||||
#undef JNI_CLASS_MEMBERS
|
#undef JNI_CLASS_MEMBERS
|
||||||
|
|
@ -40,7 +40,7 @@ DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiS
|
||||||
METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
|
METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
|
||||||
METHOD (startStopScan, "startStopScan", "(Z)V")
|
METHOD (startStopScan, "startStopScan", "(Z)V")
|
||||||
|
|
||||||
DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBluetoothManager, "com/rmsl/juce/JuceMidiSupport$BluetoothManager", 23)
|
DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBluetoothManager, "com/rmsl/juce/JuceMidiSupport$BluetoothMidiManager", 23)
|
||||||
#undef JNI_CLASS_MEMBERS
|
#undef JNI_CLASS_MEMBERS
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue