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
|
||||
{
|
||||
MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {}
|
||||
explicit MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {}
|
||||
|
||||
MidiDeviceInfo deviceInfo;
|
||||
std::unique_ptr<MidiInput> inDevice;
|
||||
std::unique_ptr<MidiOutput> outDevice;
|
||||
|
||||
using Ptr = ReferenceCountedObjectPtr<MidiDeviceListEntry>;
|
||||
|
||||
void stopAndReset()
|
||||
{
|
||||
if (inDevice != nullptr)
|
||||
inDevice->stop();
|
||||
|
||||
inDevice .reset();
|
||||
outDevice.reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class MidiDemo : public Component,
|
||||
private Timer,
|
||||
private MidiKeyboardState::Listener,
|
||||
private MidiInputCallback,
|
||||
private AsyncUpdater
|
||||
|
|
@ -113,12 +121,11 @@ public:
|
|||
|
||||
setSize (732, 520);
|
||||
|
||||
startTimer (500);
|
||||
updateDeviceLists();
|
||||
}
|
||||
|
||||
~MidiDemo() override
|
||||
{
|
||||
stopTimer();
|
||||
midiInputs .clear();
|
||||
midiOutputs.clear();
|
||||
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
|
||||
{
|
||||
MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity));
|
||||
|
|
@ -211,17 +212,8 @@ public:
|
|||
|
||||
void closeDevice (bool isInput, int index)
|
||||
{
|
||||
if (isInput)
|
||||
{
|
||||
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();
|
||||
}
|
||||
auto& list = isInput ? midiInputs : midiOutputs;
|
||||
list[index]->stopAndReset();
|
||||
}
|
||||
|
||||
int getNumMidiInputs() const noexcept
|
||||
|
|
@ -423,7 +415,6 @@ private:
|
|||
|
||||
if (hasDeviceListChanged (availableDevices, isInputDeviceList))
|
||||
{
|
||||
|
||||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
|
||||
= isInputDeviceList ? midiInputs : midiOutputs;
|
||||
|
||||
|
|
@ -463,6 +454,12 @@ private:
|
|||
addAndMakeVisible (label);
|
||||
}
|
||||
|
||||
void updateDeviceLists()
|
||||
{
|
||||
for (const auto isInput : { true, false })
|
||||
updateDeviceList (isInput);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
|
||||
Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
|
||||
|
|
@ -473,12 +470,17 @@ private:
|
|||
TextEditor midiMonitor { "MIDI Monitor" };
|
||||
TextButton pairButton { "MIDI Bluetooth devices..." };
|
||||
|
||||
std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
|
||||
ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
|
||||
std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
|
||||
|
||||
CriticalSection midiMonitorLock;
|
||||
Array<MidiMessage> incomingMessages;
|
||||
|
||||
MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
|
||||
{
|
||||
updateDeviceLists();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -220,6 +220,12 @@ void AudioDeviceManager::audioDeviceListChanged()
|
|||
sendChangeMessage();
|
||||
}
|
||||
|
||||
void AudioDeviceManager::midiDeviceListChanged()
|
||||
{
|
||||
openLastRequestedMidiDevices (midiDeviceInfosFromXml, defaultMidiOutputDeviceInfo);
|
||||
sendChangeMessage();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static void addIfNotNull (OwnedArray<AudioIODeviceType>& list, AudioIODeviceType* const device)
|
||||
{
|
||||
|
|
@ -430,67 +436,64 @@ String AudioDeviceManager::initialiseFromXML (const XmlElement& xml,
|
|||
if (error.isNotEmpty() && selectDefaultDeviceOnFailure)
|
||||
error = initialise (numInputChansNeeded, numOutputChansNeeded, nullptr, false, preferredDefaultDeviceName);
|
||||
|
||||
midiDeviceInfosFromXml.clear();
|
||||
enabledMidiInputs.clear();
|
||||
|
||||
const auto midiInputs = [&]
|
||||
{
|
||||
Array<MidiDeviceInfo> result;
|
||||
|
||||
for (auto* c : xml.getChildWithTagNameIterator ("MIDIINPUT"))
|
||||
midiDeviceInfosFromXml.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") });
|
||||
result.add ({ c->getStringAttribute ("name"), c->getStringAttribute ("identifier") });
|
||||
|
||||
auto isIdentifierAvailable = [] (const Array<MidiDeviceInfo>& available, const String& identifier)
|
||||
{
|
||||
for (auto& device : available)
|
||||
if (device.identifier == identifier)
|
||||
return true;
|
||||
return result;
|
||||
}();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
auto getUpdatedIdentifierForName = [&] (const Array<MidiDeviceInfo>& available, const String& name) -> String
|
||||
{
|
||||
for (auto& device : available)
|
||||
if (device.name == name)
|
||||
return device.identifier;
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
auto inputs = MidiInput::getAvailableDevices();
|
||||
|
||||
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"),
|
||||
const 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);
|
||||
}
|
||||
openLastRequestedMidiDevices (midiInputs, defaultOutputDeviceInfo);
|
||||
|
||||
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,
|
||||
int numOutputChannelsNeeded)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -499,6 +499,10 @@ private:
|
|||
std::unique_ptr<XmlElement> lastExplicitSettings;
|
||||
mutable bool listNeedsScanning = true;
|
||||
AudioBuffer<float> tempBuffer;
|
||||
MidiDeviceListConnection midiDeviceListConnection = MidiDeviceListConnection::make ([this]
|
||||
{
|
||||
midiDeviceListChanged();
|
||||
});
|
||||
|
||||
struct MidiCallbackInfo
|
||||
{
|
||||
|
|
@ -537,6 +541,7 @@ private:
|
|||
void audioDeviceErrorInt (const String&);
|
||||
void handleIncomingMidiMessageInt (MidiInput*, const MidiMessage&);
|
||||
void audioDeviceListChanged();
|
||||
void midiDeviceListChanged();
|
||||
|
||||
String restartDevice (int blockSizeToUse, double sampleRateToUse,
|
||||
const BigInteger& ins, const BigInteger& outs);
|
||||
|
|
@ -554,6 +559,7 @@ private:
|
|||
String initialiseDefault (const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
||||
String initialiseFromXML (const XmlElement&, bool selectDefaultDeviceOnFailure,
|
||||
const String& preferredDefaultDeviceName, const AudioDeviceSetup*);
|
||||
void openLastRequestedMidiDevices (const Array<MidiDeviceInfo>&, const MidiDeviceInfo&);
|
||||
|
||||
AudioIODeviceType* findType (const String& inputName, const String& outputName);
|
||||
AudioIODeviceType* findType (const String& typeName);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
#include "juce_audio_devices.h"
|
||||
|
||||
#include "audio_io/juce_SampleRateHelpers.cpp"
|
||||
#include "midi_io/juce_MidiDevices.cpp"
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MAC || JUCE_IOS
|
||||
|
|
@ -249,6 +250,5 @@ namespace juce
|
|||
#include "audio_io/juce_AudioIODevice.cpp"
|
||||
#include "audio_io/juce_AudioIODeviceType.cpp"
|
||||
#include "midi_io/juce_MidiMessageCollector.cpp"
|
||||
#include "midi_io/juce_MidiDevices.cpp"
|
||||
#include "sources/juce_AudioSourcePlayer.cpp"
|
||||
#include "sources/juce_AudioTransportSource.cpp"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,81 @@
|
|||
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,
|
||||
[[maybe_unused]] const uint8* messageData,
|
||||
[[maybe_unused]] int numBytesSoFar,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,85 @@
|
|||
|
||||
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.
|
||||
|
|
@ -61,8 +140,9 @@ struct MidiDeviceInfo
|
|||
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); }
|
||||
auto tie() const { return std::tie (name, identifier); }
|
||||
bool operator== (const MidiDeviceInfo& other) const noexcept { return tie() == other.tie(); }
|
||||
bool operator!= (const MidiDeviceInfo& other) const noexcept { return tie() != other.tie(); }
|
||||
};
|
||||
|
||||
class MidiInputCallback;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ package com.rmsl.juce;
|
|||
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
|
@ -43,6 +44,7 @@ import android.bluetooth.le.ScanCallback;
|
|||
import android.bluetooth.BluetoothDevice;
|
||||
import android.media.midi.MidiOutputPort;
|
||||
import android.media.midi.MidiReceiver;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
|
@ -56,6 +58,7 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
|
||||
import static android.content.Context.MIDI_SERVICE;
|
||||
import static android.content.Context.BLUETOOTH_SERVICE;
|
||||
|
||||
public class JuceMidiSupport
|
||||
{
|
||||
|
|
@ -77,10 +80,18 @@ public class JuceMidiSupport
|
|||
String getName ();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
public static class BluetoothManager extends ScanCallback
|
||||
static BluetoothAdapter getDefaultBluetoothAdapter (Context ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
@ -92,7 +103,7 @@ public class JuceMidiSupport
|
|||
|
||||
public String getHumanReadableStringForBluetoothAddress (String address)
|
||||
{
|
||||
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter ().getRemoteDevice (address);
|
||||
BluetoothDevice btDevice = getDefaultBluetoothAdapter (appContext).getRemoteDevice (address);
|
||||
return btDevice.getName ();
|
||||
}
|
||||
|
||||
|
|
@ -103,11 +114,11 @@ public class JuceMidiSupport
|
|||
|
||||
public void startStopScan (boolean shouldStart)
|
||||
{
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter ();
|
||||
BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter (appContext);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +126,7 @@ public class JuceMidiSupport
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +151,7 @@ public class JuceMidiSupport
|
|||
|
||||
public boolean pairBluetoothMidiDevice (String address)
|
||||
{
|
||||
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter ().getRemoteDevice (address);
|
||||
BluetoothDevice btDevice = getDefaultBluetoothAdapter (appContext).getRemoteDevice (address);
|
||||
|
||||
if (btDevice == null)
|
||||
{
|
||||
|
|
@ -543,12 +554,8 @@ public class JuceMidiSupport
|
|||
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 ();
|
||||
|
||||
for (MidiDeviceInfo info : foundDevices)
|
||||
onDeviceAdded (info);
|
||||
|
||||
|
|
@ -810,6 +817,7 @@ public class JuceMidiSupport
|
|||
openPorts.remove (path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceAdded (MidiDeviceInfo info)
|
||||
{
|
||||
// only add standard midi devices
|
||||
|
|
@ -819,6 +827,7 @@ public class JuceMidiSupport
|
|||
manager.openDevice (info, this, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceRemoved (MidiDeviceInfo info)
|
||||
{
|
||||
synchronized (MidiDeviceManager.class)
|
||||
|
|
@ -856,8 +865,11 @@ public class JuceMidiSupport
|
|||
midiDevices.remove (devicePair);
|
||||
}
|
||||
}
|
||||
|
||||
handleDevicesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceStatusChanged (MidiDeviceStatus status)
|
||||
{
|
||||
}
|
||||
|
|
@ -933,6 +945,7 @@ public class JuceMidiSupport
|
|||
BluetoothGatt gatt = openTasks.get (deviceID).getGatt ();
|
||||
openTasks.remove (deviceID);
|
||||
midiDevices.add (new Pair<MidiDevice, BluetoothGatt> (theDevice, gatt));
|
||||
handleDevicesChanged();
|
||||
}
|
||||
} else
|
||||
{
|
||||
|
|
@ -973,7 +986,6 @@ public class JuceMidiSupport
|
|||
{
|
||||
for (MidiDeviceInfo info : deviceInfos)
|
||||
{
|
||||
int localIndex = 0;
|
||||
if (info.getId () == path.deviceId)
|
||||
{
|
||||
for (MidiDeviceInfo.PortInfo portInfo : info.getPorts ())
|
||||
|
|
@ -1048,11 +1060,11 @@ public class JuceMidiSupport
|
|||
}
|
||||
|
||||
private MidiManager manager;
|
||||
private HashMap<String, BluetoothGatt> btDevicesPairing;
|
||||
private HashMap<Integer, MidiDeviceOpenTask> openTasks;
|
||||
private ArrayList<Pair<MidiDevice, BluetoothGatt>> midiDevices;
|
||||
private HashMap<String, BluetoothGatt> btDevicesPairing = new HashMap<String, BluetoothGatt>();
|
||||
private HashMap<Integer, MidiDeviceOpenTask> openTasks = new HashMap<Integer, MidiDeviceOpenTask>();
|
||||
private ArrayList<Pair<MidiDevice, BluetoothGatt>> midiDevices = new ArrayList<Pair<MidiDevice, BluetoothGatt>>();
|
||||
private MidiDeviceInfo[] deviceInfos;
|
||||
private HashMap<MidiPortPath, WeakReference<JuceMidiPort>> openPorts;
|
||||
private HashMap<MidiPortPath, WeakReference<JuceMidiPort>> openPorts = new HashMap<MidiPortPath, WeakReference<JuceMidiPort>>();
|
||||
private Context appContext = null;
|
||||
}
|
||||
|
||||
|
|
@ -1070,9 +1082,9 @@ public class JuceMidiSupport
|
|||
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)
|
||||
return null;
|
||||
|
|
@ -1083,12 +1095,15 @@ public class JuceMidiSupport
|
|||
synchronized (JuceMidiSupport.class)
|
||||
{
|
||||
if (bluetoothManager == null)
|
||||
bluetoothManager = new BluetoothManager (context);
|
||||
bluetoothManager = new BluetoothMidiManager (context);
|
||||
}
|
||||
|
||||
return bluetoothManager;
|
||||
}
|
||||
|
||||
// To be called when devices become (un)available
|
||||
private native static void handleDevicesChanged();
|
||||
|
||||
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,36 +29,21 @@ namespace juce
|
|||
class AlsaClient : public ReferenceCountedObject
|
||||
{
|
||||
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()
|
||||
{
|
||||
inputThread.reset();
|
||||
|
||||
jassert (instance != nullptr);
|
||||
instance = nullptr;
|
||||
|
||||
jassert (activeCallbacks.get() == 0);
|
||||
|
||||
if (inputThread)
|
||||
inputThread->stopThread (3000);
|
||||
|
||||
if (handle != nullptr)
|
||||
{
|
||||
snd_seq_delete_simple_port (handle, announcementsIn);
|
||||
snd_seq_close (handle);
|
||||
}
|
||||
}
|
||||
|
||||
static String getAlsaMidiName()
|
||||
{
|
||||
|
|
@ -123,15 +108,7 @@ public:
|
|||
|
||||
void enableCallback (bool enable)
|
||||
{
|
||||
const auto oldValue = callbackEnabled.exchange (enable);
|
||||
|
||||
if (oldValue != enable)
|
||||
{
|
||||
if (enable)
|
||||
client.registerCallback();
|
||||
else
|
||||
client.unregisterCallback();
|
||||
}
|
||||
callbackEnabled = enable;
|
||||
}
|
||||
|
||||
bool sendMessageNow (const MidiMessage& message)
|
||||
|
|
@ -238,23 +215,6 @@ public:
|
|||
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)
|
||||
{
|
||||
const ScopedLock sl (callbackLock);
|
||||
|
|
@ -294,8 +254,34 @@ public:
|
|||
}
|
||||
|
||||
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;
|
||||
int clientId = 0;
|
||||
int announcementsIn = 0;
|
||||
OwnedArray<Port> ports;
|
||||
Atomic<int> activeCallbacks;
|
||||
CriticalSection callbackLock;
|
||||
|
|
@ -303,17 +289,52 @@ private:
|
|||
static AlsaClient* instance;
|
||||
|
||||
//==============================================================================
|
||||
class MidiInputThread : public Thread
|
||||
class SequencerThread
|
||||
{
|
||||
public:
|
||||
MidiInputThread (AlsaClient& c)
|
||||
: Thread ("JUCE MIDI Input"), client (c)
|
||||
explicit SequencerThread (AlsaClient& 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();
|
||||
|
||||
const int maxEventSize = 16 * 1024;
|
||||
|
|
@ -321,17 +342,20 @@ private:
|
|||
|
||||
if (snd_midi_event_new (maxEventSize, &midiParser) >= 0)
|
||||
{
|
||||
auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN);
|
||||
HeapBlock<pollfd> pfd (numPfds);
|
||||
snd_seq_poll_descriptors (seqHandle, pfd, (unsigned int) numPfds, POLLIN);
|
||||
const ScopeGuard freeMidiEvent { [&] { snd_midi_event_free (midiParser); } };
|
||||
|
||||
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;
|
||||
|
||||
do
|
||||
|
|
@ -340,33 +364,51 @@ private:
|
|||
|
||||
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?
|
||||
auto numBytes = snd_midi_event_decode (midiParser, buffer,
|
||||
maxEventSize, inputEvent);
|
||||
const auto numBytes = snd_midi_event_decode (midiParser,
|
||||
buffer.data(),
|
||||
maxEventSize,
|
||||
inputEvent);
|
||||
|
||||
snd_midi_event_reset_decode (midiParser);
|
||||
|
||||
concatenator.pushMidiData (buffer, (int) numBytes,
|
||||
concatenator.pushMidiData (buffer.data(), (int) numBytes,
|
||||
Time::getMillisecondCounter() * 0.001,
|
||||
inputEvent, client);
|
||||
|
||||
snd_seq_free_event (inputEvent);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
|
@ -659,6 +701,18 @@ void MidiOutput::sendMessageNow (const MidiMessage& 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
|
||||
|
||||
|
|
@ -693,6 +747,12 @@ StringArray MidiOutput::getDevices()
|
|||
int MidiOutput::getDefaultDeviceIndex() { return 0;}
|
||||
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
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -579,9 +579,10 @@ namespace CoreMidiHelpers
|
|||
#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()
|
||||
|
|
@ -594,9 +595,7 @@ namespace CoreMidiHelpers
|
|||
|
||||
static MIDIClientRef getGlobalMidiClient()
|
||||
{
|
||||
static MIDIClientRef globalMidiClient = 0;
|
||||
|
||||
if (globalMidiClient == 0)
|
||||
static const auto globalMidiClient = [&]
|
||||
{
|
||||
// Since OSX 10.6, the MIDIClientCreate function will only work
|
||||
// correctly when called from the message thread!
|
||||
|
|
@ -605,8 +604,10 @@ namespace CoreMidiHelpers
|
|||
enableSimulatorMidiSession();
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -1300,6 +1301,12 @@ void MidiOutput::sendMessageNow (const MidiMessage& 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
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ struct MidiServiceType
|
|||
struct Win32MidiService : public MidiServiceType,
|
||||
private Timer
|
||||
{
|
||||
Win32MidiService() {}
|
||||
Win32MidiService() = default;
|
||||
|
||||
Array<MidiDeviceInfo> getAvailableDevices (bool isInput) override
|
||||
{
|
||||
|
|
@ -1871,6 +1871,10 @@ struct MidiService : public DeletedAtShutdown
|
|||
|
||||
private:
|
||||
std::unique_ptr<MidiServiceType> internal;
|
||||
DeviceChangeDetector detector { L"JuceMidiDeviceDetector_", []
|
||||
{
|
||||
MidiDeviceListConnectionBroadcaster::get().notify();
|
||||
} };
|
||||
};
|
||||
|
||||
JUCE_IMPLEMENT_SINGLETON (MidiService)
|
||||
|
|
@ -2013,4 +2017,10 @@ void MidiOutput::sendMessageNow (const MidiMessage& message)
|
|||
internal->sendMessageNow (message);
|
||||
}
|
||||
|
||||
MidiDeviceListConnection MidiDeviceListConnection::make (std::function<void()> cb)
|
||||
{
|
||||
auto& broadcaster = MidiDeviceListConnectionBroadcaster::get();
|
||||
return { &broadcaster, broadcaster.add (std::move (cb)) };
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ namespace juce
|
|||
{
|
||||
|
||||
#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)
|
||||
#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 (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
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue