1
0
Fork 0
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:
reuk 2023-01-16 15:27:38 +00:00
parent 49a954d473
commit 26a872ba9f
No known key found for this signature in database
GPG key ID: 9ADCD339CFC98A11
12 changed files with 1237 additions and 509 deletions

View file

@ -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)
}; };

View file

@ -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)
{ {

View file

@ -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);

View file

@ -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"

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
//============================================================================== //==============================================================================