1806 lines
60 KiB
C++
1806 lines
60 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2017 - ROLI Ltd.
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
The code included in this file is provided under the terms of the ISC license
|
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
|
without fee is hereby granted provided that the above copyright notice and
|
|
this permission notice appear in all copies.
|
|
|
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
|
DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
struct MidiServiceType
|
|
{
|
|
struct InputWrapper
|
|
{
|
|
virtual ~InputWrapper() {}
|
|
|
|
virtual String getDeviceName() = 0;
|
|
virtual void start() = 0;
|
|
virtual void stop() = 0;
|
|
};
|
|
|
|
struct OutputWrapper
|
|
{
|
|
virtual ~OutputWrapper() {}
|
|
|
|
virtual String getDeviceName() = 0;
|
|
virtual void sendMessageNow (const MidiMessage&) = 0;
|
|
};
|
|
|
|
MidiServiceType() {}
|
|
virtual ~MidiServiceType() {}
|
|
|
|
virtual StringArray getDevices (bool) = 0;
|
|
virtual int getDefaultDeviceIndex (bool) = 0;
|
|
|
|
virtual InputWrapper* createInputWrapper (MidiInput&, int, MidiInputCallback&) = 0;
|
|
virtual OutputWrapper* createOutputWrapper (int) = 0;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiServiceType)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct Win32MidiService : public MidiServiceType,
|
|
private Timer
|
|
{
|
|
Win32MidiService() {}
|
|
|
|
StringArray getDevices (bool isInput) override
|
|
{
|
|
return isInput ? Win32InputWrapper::getDevices()
|
|
: Win32OutputWrapper::getDevices();
|
|
}
|
|
|
|
int getDefaultDeviceIndex (bool isInput) override
|
|
{
|
|
return isInput ? Win32InputWrapper::getDefaultDeviceIndex()
|
|
: Win32OutputWrapper::getDefaultDeviceIndex();
|
|
}
|
|
|
|
InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override
|
|
{
|
|
return new Win32InputWrapper (*this, input, index, callback);
|
|
}
|
|
|
|
OutputWrapper* createOutputWrapper (int index) override
|
|
{
|
|
return new Win32OutputWrapper (*this, index);
|
|
}
|
|
|
|
private:
|
|
struct Win32InputWrapper;
|
|
|
|
//==============================================================================
|
|
struct MidiInCollector : public ReferenceCountedObject
|
|
{
|
|
MidiInCollector (Win32MidiService& s, const String& name) : deviceName (name), midiService (s) {}
|
|
|
|
~MidiInCollector()
|
|
{
|
|
stop();
|
|
|
|
if (deviceHandle != 0)
|
|
{
|
|
for (int count = 5; --count >= 0;)
|
|
{
|
|
if (midiInClose (deviceHandle) == MMSYSERR_NOERROR)
|
|
break;
|
|
|
|
Sleep (20);
|
|
}
|
|
}
|
|
}
|
|
|
|
using Ptr = ReferenceCountedObjectPtr<MidiInCollector>;
|
|
|
|
void addClient (Win32InputWrapper* c)
|
|
{
|
|
const ScopedLock sl (clientLock);
|
|
jassert (! clients.contains (c));
|
|
clients.add (c);
|
|
}
|
|
|
|
void removeClient (Win32InputWrapper* c)
|
|
{
|
|
const ScopedLock sl (clientLock);
|
|
clients.removeFirstMatchingValue (c);
|
|
startOrStop();
|
|
midiService.asyncCheckForUnusedCollectors();
|
|
}
|
|
|
|
void handleMessage (const uint8* bytes, uint32 timeStamp)
|
|
{
|
|
if (bytes[0] >= 0x80 && isStarted.load())
|
|
{
|
|
{
|
|
auto len = MidiMessage::getMessageLengthFromFirstByte (bytes[0]);
|
|
auto time = convertTimeStamp (timeStamp);
|
|
const ScopedLock sl (clientLock);
|
|
|
|
for (auto* c : clients)
|
|
c->pushMidiData (bytes, len, time);
|
|
}
|
|
|
|
writeFinishedBlocks();
|
|
}
|
|
}
|
|
|
|
void handleSysEx (MIDIHDR* hdr, uint32 timeStamp)
|
|
{
|
|
if (isStarted.load() && hdr->dwBytesRecorded > 0)
|
|
{
|
|
{
|
|
auto time = convertTimeStamp (timeStamp);
|
|
const ScopedLock sl (clientLock);
|
|
|
|
for (auto* c : clients)
|
|
c->pushMidiData (hdr->lpData, (int) hdr->dwBytesRecorded, time);
|
|
}
|
|
|
|
writeFinishedBlocks();
|
|
}
|
|
}
|
|
|
|
void startOrStop()
|
|
{
|
|
const ScopedLock sl (clientLock);
|
|
|
|
if (countRunningClients() == 0)
|
|
stop();
|
|
else
|
|
start();
|
|
}
|
|
|
|
void start()
|
|
{
|
|
if (deviceHandle != 0 && ! isStarted.load())
|
|
{
|
|
activeMidiCollectors.addIfNotAlreadyThere (this);
|
|
|
|
for (int i = 0; i < (int) numHeaders; ++i)
|
|
{
|
|
headers[i].prepare (deviceHandle);
|
|
headers[i].write (deviceHandle);
|
|
}
|
|
|
|
startTime = Time::getMillisecondCounterHiRes();
|
|
auto res = midiInStart (deviceHandle);
|
|
|
|
if (res == MMSYSERR_NOERROR)
|
|
isStarted = true;
|
|
else
|
|
unprepareAllHeaders();
|
|
}
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (isStarted.load())
|
|
{
|
|
isStarted = false;
|
|
midiInReset (deviceHandle);
|
|
midiInStop (deviceHandle);
|
|
activeMidiCollectors.removeFirstMatchingValue (this);
|
|
unprepareAllHeaders();
|
|
}
|
|
}
|
|
|
|
static void CALLBACK midiInCallback (HMIDIIN, UINT uMsg, DWORD_PTR dwInstance,
|
|
DWORD_PTR midiMessage, DWORD_PTR timeStamp)
|
|
{
|
|
auto* collector = reinterpret_cast<MidiInCollector*> (dwInstance);
|
|
|
|
// This is primarily a check for the collector being a dangling
|
|
// pointer, as the callback can sometimes be delayed
|
|
if (activeMidiCollectors.contains (collector))
|
|
{
|
|
if (uMsg == MIM_DATA)
|
|
collector->handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp);
|
|
else if (uMsg == MIM_LONGDATA)
|
|
collector->handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp);
|
|
}
|
|
}
|
|
|
|
String deviceName;
|
|
HMIDIIN deviceHandle = 0;
|
|
|
|
private:
|
|
Win32MidiService& midiService;
|
|
CriticalSection clientLock;
|
|
Array<Win32InputWrapper*> clients;
|
|
std::atomic<bool> isStarted { false };
|
|
double startTime = 0;
|
|
|
|
// This static array is used to prevent occasional callbacks to objects that are
|
|
// in the process of being deleted
|
|
static Array<MidiInCollector*, CriticalSection> activeMidiCollectors;
|
|
|
|
int countRunningClients() const
|
|
{
|
|
int num = 0;
|
|
|
|
for (auto* c : clients)
|
|
if (c->started)
|
|
++num;
|
|
|
|
return num;
|
|
}
|
|
|
|
struct MidiHeader
|
|
{
|
|
MidiHeader() {}
|
|
|
|
void prepare (HMIDIIN device)
|
|
{
|
|
zerostruct (hdr);
|
|
hdr.lpData = data;
|
|
hdr.dwBufferLength = (DWORD) numElementsInArray (data);
|
|
|
|
midiInPrepareHeader (device, &hdr, sizeof (hdr));
|
|
}
|
|
|
|
void unprepare (HMIDIIN device)
|
|
{
|
|
if ((hdr.dwFlags & WHDR_DONE) != 0)
|
|
{
|
|
int c = 10;
|
|
while (--c >= 0 && midiInUnprepareHeader (device, &hdr, sizeof (hdr)) == MIDIERR_STILLPLAYING)
|
|
Thread::sleep (20);
|
|
|
|
jassert (c >= 0);
|
|
}
|
|
}
|
|
|
|
void write (HMIDIIN device)
|
|
{
|
|
hdr.dwBytesRecorded = 0;
|
|
midiInAddBuffer (device, &hdr, sizeof (hdr));
|
|
}
|
|
|
|
void writeIfFinished (HMIDIIN device)
|
|
{
|
|
if ((hdr.dwFlags & WHDR_DONE) != 0)
|
|
write (device);
|
|
}
|
|
|
|
MIDIHDR hdr;
|
|
char data[256];
|
|
|
|
JUCE_DECLARE_NON_COPYABLE (MidiHeader)
|
|
};
|
|
|
|
enum { numHeaders = 32 };
|
|
MidiHeader headers[numHeaders];
|
|
|
|
void writeFinishedBlocks()
|
|
{
|
|
for (int i = 0; i < (int) numHeaders; ++i)
|
|
headers[i].writeIfFinished (deviceHandle);
|
|
}
|
|
|
|
void unprepareAllHeaders()
|
|
{
|
|
for (int i = 0; i < (int) numHeaders; ++i)
|
|
headers[i].unprepare (deviceHandle);
|
|
}
|
|
|
|
double convertTimeStamp (uint32 timeStamp)
|
|
{
|
|
auto t = startTime + timeStamp;
|
|
auto now = Time::getMillisecondCounterHiRes();
|
|
|
|
if (t > now)
|
|
{
|
|
if (t > now + 2.0)
|
|
startTime -= 1.0;
|
|
|
|
t = now;
|
|
}
|
|
|
|
return t * 0.001;
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInCollector)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct Win32InputWrapper : public InputWrapper
|
|
{
|
|
Win32InputWrapper (Win32MidiService& parentService,
|
|
MidiInput& midiInput, int index, MidiInputCallback& c)
|
|
: input (midiInput), callback (c)
|
|
{
|
|
collector = getOrCreateCollector (parentService, index);
|
|
collector->addClient (this);
|
|
}
|
|
|
|
~Win32InputWrapper()
|
|
{
|
|
collector->removeClient (this);
|
|
}
|
|
|
|
static MidiInCollector::Ptr getOrCreateCollector (Win32MidiService& parentService, int index)
|
|
{
|
|
auto names = getDevices();
|
|
UINT deviceID = MIDI_MAPPER;
|
|
String deviceName;
|
|
|
|
if (isPositiveAndBelow (index, names.size()))
|
|
{
|
|
deviceName = names[index];
|
|
deviceID = index;
|
|
}
|
|
|
|
const ScopedLock sl (parentService.activeCollectorLock);
|
|
|
|
for (auto& c : parentService.activeCollectors)
|
|
if (c->deviceName == deviceName)
|
|
return c;
|
|
|
|
MidiInCollector::Ptr c (new MidiInCollector (parentService, deviceName));
|
|
|
|
HMIDIIN h;
|
|
auto err = midiInOpen (&h, deviceID,
|
|
(DWORD_PTR) &MidiInCollector::midiInCallback,
|
|
(DWORD_PTR) (MidiInCollector*) c.get(),
|
|
CALLBACK_FUNCTION);
|
|
|
|
if (err != MMSYSERR_NOERROR)
|
|
throw std::runtime_error ("Failed to create Windows input device wrapper");
|
|
|
|
c->deviceHandle = h;
|
|
parentService.activeCollectors.add (c);
|
|
return c;
|
|
}
|
|
|
|
static StringArray getDevices()
|
|
{
|
|
StringArray s;
|
|
auto num = midiInGetNumDevs();
|
|
|
|
for (UINT i = 0; i < num; ++i)
|
|
{
|
|
MIDIINCAPS mc = { 0 };
|
|
|
|
if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR)
|
|
s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname)));
|
|
}
|
|
|
|
s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
|
|
return s;
|
|
}
|
|
|
|
static int getDefaultDeviceIndex() { return 0; }
|
|
|
|
void start() override { started = true; concatenator.reset(); collector->startOrStop(); }
|
|
void stop() override { started = false; collector->startOrStop(); concatenator.reset(); }
|
|
|
|
String getDeviceName() override { return collector->deviceName; }
|
|
|
|
void pushMidiData (const void* inputData, int numBytes, double time)
|
|
{
|
|
concatenator.pushMidiData (inputData, numBytes, time, &input, callback);
|
|
}
|
|
|
|
MidiInput& input;
|
|
MidiInputCallback& callback;
|
|
MidiDataConcatenator concatenator { 4096 };
|
|
MidiInCollector::Ptr collector;
|
|
bool started = false;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32InputWrapper)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct MidiOutHandle : public ReferenceCountedObject
|
|
{
|
|
using Ptr = ReferenceCountedObjectPtr<MidiOutHandle>;
|
|
|
|
MidiOutHandle (Win32MidiService& parent, const String& name, HMIDIOUT h)
|
|
: owner (parent), deviceName (name), handle (h)
|
|
{
|
|
owner.activeOutputHandles.add (this);
|
|
}
|
|
|
|
~MidiOutHandle()
|
|
{
|
|
if (handle != nullptr)
|
|
midiOutClose (handle);
|
|
|
|
owner.activeOutputHandles.removeFirstMatchingValue (this);
|
|
}
|
|
|
|
Win32MidiService& owner;
|
|
String deviceName;
|
|
HMIDIOUT handle;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutHandle)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct Win32OutputWrapper : public OutputWrapper
|
|
{
|
|
Win32OutputWrapper (Win32MidiService& p, int index) : parent (p)
|
|
{
|
|
auto names = getDevices();
|
|
UINT deviceID = MIDI_MAPPER;
|
|
|
|
if (isPositiveAndBelow (index, names.size()))
|
|
{
|
|
deviceName = names[index];
|
|
deviceID = index;
|
|
}
|
|
|
|
if (deviceID == MIDI_MAPPER)
|
|
{
|
|
// use the microsoft sw synth as a default - best not to allow deviceID
|
|
// to be MIDI_MAPPER, or else device sharing breaks
|
|
for (int i = 0; i < names.size(); ++i)
|
|
if (names[i].containsIgnoreCase ("microsoft"))
|
|
deviceID = (UINT) i;
|
|
}
|
|
|
|
for (int i = parent.activeOutputHandles.size(); --i >= 0;)
|
|
{
|
|
auto* activeHandle = parent.activeOutputHandles.getUnchecked (i);
|
|
|
|
if (activeHandle->deviceName == deviceName)
|
|
{
|
|
han = activeHandle;
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (int i = 4; --i >= 0;)
|
|
{
|
|
HMIDIOUT h = 0;
|
|
auto res = midiOutOpen (&h, deviceID, 0, 0, CALLBACK_NULL);
|
|
|
|
if (res == MMSYSERR_NOERROR)
|
|
{
|
|
han = new MidiOutHandle (parent, deviceName, h);
|
|
return;
|
|
}
|
|
|
|
if (res == MMSYSERR_ALLOCATED)
|
|
Sleep (100);
|
|
else
|
|
break;
|
|
}
|
|
|
|
throw std::runtime_error ("Failed to create Windows output device wrapper");
|
|
}
|
|
|
|
void sendMessageNow (const MidiMessage& message) override
|
|
{
|
|
if (message.getRawDataSize() > 3 || message.isSysEx())
|
|
{
|
|
MIDIHDR h = { 0 };
|
|
|
|
h.lpData = (char*) message.getRawData();
|
|
h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.getRawDataSize();
|
|
|
|
if (midiOutPrepareHeader (han->handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR)
|
|
{
|
|
auto res = midiOutLongMsg (han->handle, &h, sizeof (MIDIHDR));
|
|
|
|
if (res == MMSYSERR_NOERROR)
|
|
{
|
|
while ((h.dwFlags & MHDR_DONE) == 0)
|
|
Sleep (1);
|
|
|
|
int count = 500; // 1 sec timeout
|
|
|
|
while (--count >= 0)
|
|
{
|
|
res = midiOutUnprepareHeader (han->handle, &h, sizeof (MIDIHDR));
|
|
|
|
if (res == MIDIERR_STILLPLAYING)
|
|
Sleep (2);
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < 50; ++i)
|
|
{
|
|
if (midiOutShortMsg (han->handle, *(unsigned int*) message.getRawData()) != MIDIERR_NOTREADY)
|
|
break;
|
|
|
|
Sleep (1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Array<MIDIOUTCAPS> getDeviceCaps()
|
|
{
|
|
Array<MIDIOUTCAPS> devices;
|
|
auto num = midiOutGetNumDevs();
|
|
|
|
for (UINT i = 0; i < num; ++i)
|
|
{
|
|
MIDIOUTCAPS mc = { 0 };
|
|
|
|
if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR)
|
|
devices.add (mc);
|
|
}
|
|
|
|
return devices;
|
|
}
|
|
|
|
static StringArray getDevices()
|
|
{
|
|
StringArray s;
|
|
|
|
for (auto& mc : getDeviceCaps())
|
|
s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname)));
|
|
|
|
s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
|
|
return s;
|
|
}
|
|
|
|
static int getDefaultDeviceIndex()
|
|
{
|
|
int n = 0;
|
|
|
|
for (auto& mc : getDeviceCaps())
|
|
{
|
|
if ((mc.wTechnology & MOD_MAPPER) != 0)
|
|
return n;
|
|
|
|
++n;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
String getDeviceName() override { return deviceName; }
|
|
|
|
Win32MidiService& parent;
|
|
String deviceName;
|
|
MidiOutHandle::Ptr han;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32OutputWrapper)
|
|
};
|
|
|
|
//==============================================================================
|
|
void asyncCheckForUnusedCollectors()
|
|
{
|
|
startTimer (10);
|
|
}
|
|
|
|
void timerCallback() override
|
|
{
|
|
stopTimer();
|
|
|
|
const ScopedLock sl (activeCollectorLock);
|
|
|
|
for (int i = activeCollectors.size(); --i >= 0;)
|
|
if (activeCollectors.getObjectPointer(i)->getReferenceCount() == 1)
|
|
activeCollectors.remove (i);
|
|
}
|
|
|
|
CriticalSection activeCollectorLock;
|
|
ReferenceCountedArray<MidiInCollector> activeCollectors;
|
|
Array<MidiOutHandle*> activeOutputHandles;
|
|
};
|
|
|
|
Array<Win32MidiService::MidiInCollector*, CriticalSection> Win32MidiService::MidiInCollector::activeMidiCollectors;
|
|
|
|
//==============================================================================
|
|
//==============================================================================
|
|
#if JUCE_USE_WINRT_MIDI
|
|
|
|
#ifndef JUCE_FORCE_WINRT_MIDI
|
|
#define JUCE_FORCE_WINRT_MIDI 0
|
|
#endif
|
|
|
|
#ifndef JUCE_WINRT_MIDI_LOGGING
|
|
#define JUCE_WINRT_MIDI_LOGGING 0
|
|
#endif
|
|
|
|
#if JUCE_WINRT_MIDI_LOGGING
|
|
#define JUCE_WINRT_MIDI_LOG(x) DBG(x)
|
|
#else
|
|
#define JUCE_WINRT_MIDI_LOG(x)
|
|
#endif
|
|
|
|
using namespace Microsoft::WRL;
|
|
|
|
using namespace ABI::Windows::Foundation;
|
|
using namespace ABI::Windows::Foundation::Collections;
|
|
using namespace ABI::Windows::Devices::Midi;
|
|
using namespace ABI::Windows::Devices::Enumeration;
|
|
using namespace ABI::Windows::Storage::Streams;
|
|
|
|
//==============================================================================
|
|
struct WinRTMidiService : public MidiServiceType
|
|
{
|
|
public:
|
|
//==============================================================================
|
|
WinRTMidiService()
|
|
{
|
|
if (! WinRTWrapper::getInstance()->isInitialised())
|
|
throw std::runtime_error ("Failed to initialise the WinRT wrapper");
|
|
|
|
midiInFactory = WinRTWrapper::getInstance()->getWRLFactory<IMidiInPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]);
|
|
|
|
if (midiInFactory == nullptr)
|
|
throw std::runtime_error ("Failed to create midi in factory");
|
|
|
|
midiOutFactory = WinRTWrapper::getInstance()->getWRLFactory<IMidiOutPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]);
|
|
|
|
if (midiOutFactory == nullptr)
|
|
throw std::runtime_error ("Failed to create midi out factory");
|
|
|
|
// The WinRT BLE MIDI API doesn't provide callbacks when devices become disconnected,
|
|
// but it does require a disconnection via the API before a device will reconnect again.
|
|
// We can monitor the BLE connection state of paired devices to get callbacks when
|
|
// connections are broken.
|
|
bleDeviceWatcher.reset (new BLEDeviceWatcher());
|
|
|
|
if (! bleDeviceWatcher->start())
|
|
throw std::runtime_error ("Failed to start the BLE device watcher");
|
|
|
|
inputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiInPortStatics> (midiInFactory));
|
|
|
|
if (! inputDeviceWatcher->start())
|
|
throw std::runtime_error ("Failed to start the midi input device watcher");
|
|
|
|
outputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiOutPortStatics> (midiOutFactory));
|
|
|
|
if (! outputDeviceWatcher->start())
|
|
throw std::runtime_error ("Failed to start the midi output device watcher");
|
|
}
|
|
|
|
StringArray getDevices (bool isInput) override
|
|
{
|
|
return isInput ? inputDeviceWatcher ->getDevices()
|
|
: outputDeviceWatcher->getDevices();
|
|
}
|
|
|
|
int getDefaultDeviceIndex (bool isInput) override
|
|
{
|
|
return isInput ? inputDeviceWatcher ->getDefaultDeviceIndex()
|
|
: outputDeviceWatcher->getDefaultDeviceIndex();
|
|
}
|
|
|
|
InputWrapper* createInputWrapper (MidiInput& input, int index, MidiInputCallback& callback) override
|
|
{
|
|
return new WinRTInputWrapper (*this, input, index, callback);
|
|
}
|
|
|
|
OutputWrapper* createOutputWrapper (int index) override
|
|
{
|
|
return new WinRTOutputWrapper (*this, index);
|
|
}
|
|
|
|
private:
|
|
//==============================================================================
|
|
class DeviceCallbackHandler
|
|
{
|
|
public:
|
|
virtual ~DeviceCallbackHandler() {};
|
|
|
|
virtual HRESULT addDevice (IDeviceInformation*) = 0;
|
|
virtual HRESULT removeDevice (IDeviceInformationUpdate*) = 0;
|
|
virtual HRESULT updateDevice (IDeviceInformationUpdate*) = 0;
|
|
|
|
bool attach (HSTRING deviceSelector, DeviceInformationKind infoKind)
|
|
{
|
|
auto deviceInfoFactory = WinRTWrapper::getInstance()->getWRLFactory<IDeviceInformationStatics2> (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]);
|
|
|
|
if (deviceInfoFactory == nullptr)
|
|
return false;
|
|
|
|
// A quick way of getting an IVector<HSTRING>...
|
|
auto requestedProperties = []
|
|
{
|
|
auto devicePicker = WinRTWrapper::getInstance()->activateInstance<IDevicePicker> (&RuntimeClass_Windows_Devices_Enumeration_DevicePicker[0],
|
|
__uuidof (IDevicePicker));
|
|
jassert (devicePicker != nullptr);
|
|
|
|
IVector<HSTRING>* result;
|
|
auto hr = devicePicker->get_RequestedProperties (&result);
|
|
jassert (SUCCEEDED (hr));
|
|
|
|
hr = result->Clear();
|
|
jassert (SUCCEEDED (hr));
|
|
|
|
return result;
|
|
}();
|
|
|
|
StringArray propertyKeys = { "System.Devices.ContainerId",
|
|
"System.Devices.Aep.ContainerId",
|
|
"System.Devices.Aep.IsConnected" };
|
|
|
|
for (auto& key : propertyKeys)
|
|
{
|
|
WinRTWrapper::ScopedHString hstr (key);
|
|
auto hr = requestedProperties->Append (hstr.get());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
WinRTWrapper::ComPtr<IIterable<HSTRING>> iter;
|
|
auto hr = requestedProperties->QueryInterface (__uuidof (IIterable<HSTRING>), (void**) iter.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
hr = deviceInfoFactory->CreateWatcherWithKindAqsFilterAndAdditionalProperties (deviceSelector, iter, infoKind,
|
|
watcher.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
enumerationThread.startThread();
|
|
|
|
return true;
|
|
};
|
|
|
|
void detach()
|
|
{
|
|
enumerationThread.stopThread (2000);
|
|
|
|
if (watcher == nullptr)
|
|
return;
|
|
|
|
auto hr = watcher->Stop();
|
|
jassert (SUCCEEDED (hr));
|
|
|
|
if (deviceAddedToken.value != 0)
|
|
{
|
|
hr = watcher->remove_Added (deviceAddedToken);
|
|
jassert (SUCCEEDED (hr));
|
|
deviceAddedToken.value = 0;
|
|
}
|
|
|
|
if (deviceUpdatedToken.value != 0)
|
|
{
|
|
hr = watcher->remove_Updated (deviceUpdatedToken);
|
|
jassert (SUCCEEDED (hr));
|
|
deviceUpdatedToken.value = 0;
|
|
}
|
|
|
|
if (deviceRemovedToken.value != 0)
|
|
{
|
|
hr = watcher->remove_Removed (deviceRemovedToken);
|
|
jassert (SUCCEEDED (hr));
|
|
deviceRemovedToken.value = 0;
|
|
}
|
|
|
|
watcher = nullptr;
|
|
}
|
|
|
|
template<typename InfoType>
|
|
IInspectable* getValueFromDeviceInfo (String key, InfoType* info)
|
|
{
|
|
__FIMapView_2_HSTRING_IInspectable* properties;
|
|
info->get_Properties (&properties);
|
|
|
|
boolean found = false;
|
|
WinRTWrapper::ScopedHString keyHstr (key);
|
|
auto hr = properties->HasKey (keyHstr.get(), &found);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return nullptr;
|
|
}
|
|
|
|
if (! found)
|
|
return nullptr;
|
|
|
|
IInspectable* inspectable;
|
|
hr = properties->Lookup (keyHstr.get(), &inspectable);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return nullptr;
|
|
}
|
|
|
|
return inspectable;
|
|
}
|
|
|
|
String getGUIDFromInspectable (IInspectable& inspectable)
|
|
{
|
|
WinRTWrapper::ComPtr<IReference<GUID>> guidRef;
|
|
auto hr = inspectable.QueryInterface (__uuidof (IReference<GUID>),
|
|
(void**) guidRef.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
GUID result;
|
|
hr = guidRef->get_Value (&result);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
OLECHAR* resultString;
|
|
StringFromCLSID (result, &resultString);
|
|
|
|
return resultString;
|
|
}
|
|
|
|
bool getBoolFromInspectable (IInspectable& inspectable)
|
|
{
|
|
WinRTWrapper::ComPtr<IReference<bool>> boolRef;
|
|
auto hr = inspectable.QueryInterface (__uuidof (IReference<bool>),
|
|
(void**) boolRef.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
boolean result;
|
|
hr = boolRef->get_Value (&result);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
//==============================================================================
|
|
struct DeviceEnumerationThread : public Thread
|
|
{
|
|
DeviceEnumerationThread (DeviceCallbackHandler& h,
|
|
WinRTWrapper::ComPtr<IDeviceWatcher>& w,
|
|
EventRegistrationToken& added,
|
|
EventRegistrationToken& removed,
|
|
EventRegistrationToken& updated)
|
|
: Thread ("WinRT Device Enumeration Thread"), handler (h), watcher (w),
|
|
deviceAddedToken (added), deviceRemovedToken (removed), deviceUpdatedToken (updated)
|
|
{}
|
|
|
|
void run() override
|
|
{
|
|
auto handlerPtr = std::addressof (handler);
|
|
|
|
watcher->add_Added (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>> (
|
|
[handlerPtr](IDeviceWatcher*, IDeviceInformation* info) { return handlerPtr->addDevice (info); }
|
|
).Get(),
|
|
&deviceAddedToken);
|
|
|
|
watcher->add_Removed (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
|
|
[handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->removeDevice (infoUpdate); }
|
|
).Get(),
|
|
&deviceRemovedToken);
|
|
|
|
watcher->add_Updated (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
|
|
[handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->updateDevice (infoUpdate); }
|
|
).Get(),
|
|
&deviceRemovedToken);
|
|
|
|
watcher->Start();
|
|
}
|
|
|
|
DeviceCallbackHandler& handler;
|
|
WinRTWrapper::ComPtr<IDeviceWatcher>& watcher;
|
|
EventRegistrationToken& deviceAddedToken, deviceRemovedToken, deviceUpdatedToken;
|
|
};
|
|
|
|
//==============================================================================
|
|
WinRTWrapper::ComPtr<IDeviceWatcher> watcher;
|
|
|
|
EventRegistrationToken deviceAddedToken { 0 },
|
|
deviceRemovedToken { 0 },
|
|
deviceUpdatedToken { 0 };
|
|
|
|
DeviceEnumerationThread enumerationThread { *this, watcher,
|
|
deviceAddedToken,
|
|
deviceRemovedToken,
|
|
deviceUpdatedToken };
|
|
};
|
|
|
|
//==============================================================================
|
|
struct BLEDeviceWatcher final : private DeviceCallbackHandler
|
|
{
|
|
struct DeviceInfo
|
|
{
|
|
String containerID;
|
|
bool isConnected = false;
|
|
};
|
|
|
|
BLEDeviceWatcher() = default;
|
|
|
|
~BLEDeviceWatcher()
|
|
{
|
|
detach();
|
|
}
|
|
|
|
//==============================================================================
|
|
HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override
|
|
{
|
|
HSTRING deviceIDHst;
|
|
auto hr = addedDeviceInfo->get_Id (&deviceIDHst);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query added BLE device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto deviceID = WinRTWrapper::getInstance()->hStringToString (deviceIDHst);
|
|
JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID);
|
|
|
|
if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", addedDeviceInfo))
|
|
{
|
|
auto containerID = getGUIDFromInspectable (*containerIDValue);
|
|
|
|
if (containerID.isNotEmpty())
|
|
{
|
|
DeviceInfo info = { containerID };
|
|
|
|
if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", addedDeviceInfo))
|
|
info.isConnected = getBoolFromInspectable (*connectedValue);
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Adding BLE device: " << deviceID << " " << info.containerID
|
|
<< " " << (info.isConnected ? "connected" : "disconnected"));
|
|
devices.set (deviceID, info);
|
|
|
|
return S_OK;
|
|
}
|
|
}
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get a container ID for BLE device: " << deviceID);
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override
|
|
{
|
|
HSTRING removedDeviceIdHstr;
|
|
auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query removed BLE device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr);
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Removing BLE device: " << removedDeviceId);
|
|
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
if (devices.contains (removedDeviceId))
|
|
{
|
|
auto& info = devices.getReference (removedDeviceId);
|
|
listeners.call ([&info](Listener& l) { l.bleDeviceDisconnected (info.containerID); });
|
|
devices.remove (removedDeviceId);
|
|
JUCE_WINRT_MIDI_LOG ("Removed BLE device: " << removedDeviceId);
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT updateDevice (IDeviceInformationUpdate* updatedDeviceInfo) override
|
|
{
|
|
HSTRING updatedDeviceIdHstr;
|
|
auto hr = updatedDeviceInfo->get_Id (&updatedDeviceIdHstr);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query updated BLE device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto updatedDeviceId = WinRTWrapper::getInstance()->hStringToString (updatedDeviceIdHstr);
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Updating BLE device: " << updatedDeviceId);
|
|
|
|
if (auto* connectedValue = getValueFromDeviceInfo ("System.Devices.Aep.IsConnected", updatedDeviceInfo))
|
|
{
|
|
auto isConnected = getBoolFromInspectable (*connectedValue);
|
|
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
if (! devices.contains (updatedDeviceId))
|
|
return S_OK;
|
|
|
|
auto& info = devices.getReference (updatedDeviceId);
|
|
|
|
if (info.isConnected && ! isConnected)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("BLE device connection status change: " << updatedDeviceId << " " << info.containerID << " " << (isConnected ? "connected" : "disconnected"));
|
|
listeners.call ([&info](Listener& l) { l.bleDeviceDisconnected (info.containerID); });
|
|
}
|
|
|
|
info.isConnected = isConnected;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
//==============================================================================
|
|
bool start()
|
|
{
|
|
WinRTWrapper::ScopedHString deviceSelector ("System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\""
|
|
" AND System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True");
|
|
return attach (deviceSelector.get(), DeviceInformationKind::DeviceInformationKind_AssociationEndpoint);
|
|
}
|
|
|
|
//==============================================================================
|
|
struct Listener
|
|
{
|
|
virtual ~Listener() {};
|
|
virtual void bleDeviceAdded (const String& containerID) = 0;
|
|
virtual void bleDeviceDisconnected (const String& containerID) = 0;
|
|
};
|
|
|
|
void addListener (Listener* l)
|
|
{
|
|
listeners.add (l);
|
|
}
|
|
|
|
void removeListener (Listener* l)
|
|
{
|
|
listeners.remove (l);
|
|
}
|
|
|
|
//==============================================================================
|
|
ListenerList<Listener> listeners;
|
|
HashMap<String, DeviceInfo> devices;
|
|
CriticalSection deviceChanges;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher);
|
|
};
|
|
|
|
//==============================================================================
|
|
struct MIDIDeviceInfo
|
|
{
|
|
String deviceID, containerID, name;
|
|
bool isDefault = false;
|
|
};
|
|
|
|
//==============================================================================
|
|
template <typename COMFactoryType>
|
|
struct MidiIODeviceWatcher final : private DeviceCallbackHandler
|
|
{
|
|
MidiIODeviceWatcher (WinRTWrapper::ComPtr<COMFactoryType>& comFactory)
|
|
: factory (comFactory)
|
|
{
|
|
}
|
|
|
|
~MidiIODeviceWatcher()
|
|
{
|
|
detach();
|
|
}
|
|
|
|
HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override
|
|
{
|
|
MIDIDeviceInfo info;
|
|
|
|
HSTRING deviceID;
|
|
auto hr = addedDeviceInfo->get_Id (&deviceID);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query added MIDI device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
info.deviceID = WinRTWrapper::getInstance()->hStringToString (deviceID);
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Detected MIDI device: " << info.deviceID);
|
|
|
|
boolean isEnabled = false;
|
|
hr = addedDeviceInfo->get_IsEnabled (&isEnabled);
|
|
|
|
if (FAILED (hr) || ! isEnabled)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("MIDI device not enabled: " << info.deviceID);
|
|
return S_OK;
|
|
}
|
|
|
|
// We use the container ID to match a MIDI device with a generic BLE device, if possible
|
|
if (auto* containerIDValue = getValueFromDeviceInfo ("System.Devices.ContainerId", addedDeviceInfo))
|
|
info.containerID = getGUIDFromInspectable (*containerIDValue);
|
|
|
|
HSTRING name;
|
|
hr = addedDeviceInfo->get_Name (&name);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device name for " << info.deviceID);
|
|
return S_OK;
|
|
}
|
|
|
|
info.name = WinRTWrapper::getInstance()->hStringToString (name);
|
|
|
|
boolean isDefault = false;
|
|
hr = addedDeviceInfo->get_IsDefault (&isDefault);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query detected MIDI device defaultness for " << info.deviceID << " " << info.name);
|
|
return S_OK;
|
|
}
|
|
|
|
info.isDefault = isDefault;
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Adding MIDI device: " << info.deviceID << " " << info.containerID << " " << info.name);
|
|
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
connectedDevices.add (info);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override
|
|
{
|
|
HSTRING removedDeviceIdHstr;
|
|
auto hr = removedDeviceInfo->get_Id (&removedDeviceIdHstr);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr);
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Removing MIDI device: " << removedDeviceId);
|
|
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
for (int i = 0; i < connectedDevices.size(); ++i)
|
|
{
|
|
if (connectedDevices[i].deviceID == removedDeviceId)
|
|
{
|
|
connectedDevices.remove (i);
|
|
JUCE_WINRT_MIDI_LOG ("Removed MIDI device: " << removedDeviceId);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// This is never called
|
|
HRESULT updateDevice (IDeviceInformationUpdate*) override { return S_OK; }
|
|
|
|
bool start()
|
|
{
|
|
HSTRING deviceSelector;
|
|
auto hr = factory->GetDeviceSelector (&deviceSelector);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get MIDI device selector!");
|
|
return false;
|
|
}
|
|
|
|
return attach (deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface);
|
|
}
|
|
|
|
StringArray getDevices()
|
|
{
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
lastQueriedConnectedDevices = connectedDevices;
|
|
}
|
|
|
|
StringArray result;
|
|
|
|
for (auto info : lastQueriedConnectedDevices.get())
|
|
result.add (info.name);
|
|
|
|
return result;
|
|
}
|
|
|
|
int getDefaultDeviceIndex()
|
|
{
|
|
auto& lastDevices = lastQueriedConnectedDevices.get();
|
|
|
|
for (int i = 0; i < lastDevices.size(); ++i)
|
|
if (lastDevices[i].isDefault)
|
|
return i;
|
|
|
|
return 0;
|
|
}
|
|
|
|
MIDIDeviceInfo getDeviceInfoFromIndex (int index)
|
|
{
|
|
if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size()))
|
|
return lastQueriedConnectedDevices.get()[index];
|
|
|
|
return {};
|
|
}
|
|
|
|
String getDeviceID (const String& name)
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
for (auto info : connectedDevices)
|
|
if (info.name == name)
|
|
return info.deviceID;
|
|
|
|
return {};
|
|
}
|
|
|
|
WinRTWrapper::ComPtr<COMFactoryType>& factory;
|
|
|
|
Array<MIDIDeviceInfo> connectedDevices;
|
|
CriticalSection deviceChanges;
|
|
ThreadLocalValue<Array<MIDIDeviceInfo>> lastQueriedConnectedDevices;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher);
|
|
};
|
|
|
|
//==============================================================================
|
|
template <typename COMFactoryType, typename COMInterfaceType, typename COMType>
|
|
struct OpenMidiPortThread : public Thread
|
|
{
|
|
OpenMidiPortThread (String threadName, String midiDeviceID,
|
|
WinRTWrapper::ComPtr<COMFactoryType>& comFactory,
|
|
WinRTWrapper::ComPtr<COMInterfaceType>& comPort)
|
|
: Thread (threadName),
|
|
deviceID (midiDeviceID),
|
|
factory (comFactory),
|
|
port (comPort)
|
|
{
|
|
}
|
|
|
|
~OpenMidiPortThread()
|
|
{
|
|
stopThread (2000);
|
|
}
|
|
|
|
void run() override
|
|
{
|
|
WinRTWrapper::ScopedHString hDeviceId (deviceID);
|
|
WinRTWrapper::ComPtr<IAsyncOperation<COMType*>> asyncOp;
|
|
auto hr = factory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return;
|
|
|
|
hr = asyncOp->put_Completed (Callback<IAsyncOperationCompletedHandler<COMType*>> (
|
|
[this] (IAsyncOperation<COMType*>* asyncOpPtr, AsyncStatus)
|
|
{
|
|
if (asyncOpPtr == nullptr)
|
|
return E_ABORT;
|
|
|
|
auto hr = asyncOpPtr->GetResults (port.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
portOpened.signal();
|
|
return S_OK;
|
|
}
|
|
).Get());
|
|
|
|
// We need to use a timout here, rather than waiting indefinitely, as the
|
|
// WinRT API can occaisonally hang!
|
|
portOpened.wait (2000);
|
|
}
|
|
|
|
const String deviceID;
|
|
WinRTWrapper::ComPtr<COMFactoryType>& factory;
|
|
WinRTWrapper::ComPtr<COMInterfaceType>& port;
|
|
WaitableEvent portOpened { true };
|
|
};
|
|
|
|
//==============================================================================
|
|
template <typename MIDIIOStaticsType, typename MIDIPort>
|
|
class WinRTIOWrapper : private BLEDeviceWatcher::Listener
|
|
{
|
|
public:
|
|
WinRTIOWrapper (BLEDeviceWatcher& bleWatcher,
|
|
MidiIODeviceWatcher<MIDIIOStaticsType>& midiDeviceWatcher,
|
|
int index)
|
|
: bleDeviceWatcher (bleWatcher)
|
|
{
|
|
{
|
|
const ScopedLock lock (midiDeviceWatcher.deviceChanges);
|
|
deviceInfo = midiDeviceWatcher.getDeviceInfoFromIndex (index);
|
|
}
|
|
|
|
if (deviceInfo.deviceID.isEmpty())
|
|
throw std::runtime_error ("Invalid device index");
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID);
|
|
|
|
if (deviceInfo.containerID.isNotEmpty())
|
|
{
|
|
bleDeviceWatcher.addListener (this);
|
|
|
|
const ScopedLock lock (bleDeviceWatcher.deviceChanges);
|
|
|
|
HashMap<String, BLEDeviceWatcher::DeviceInfo>::Iterator iter (bleDeviceWatcher.devices);
|
|
|
|
while (iter.next())
|
|
{
|
|
if (iter.getValue().containerID == deviceInfo.containerID)
|
|
{
|
|
isBLEDevice = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual ~WinRTIOWrapper()
|
|
{
|
|
bleDeviceWatcher.removeListener (this);
|
|
|
|
disconnect();
|
|
}
|
|
|
|
//==============================================================================
|
|
virtual void disconnect()
|
|
{
|
|
if (midiPort != nullptr)
|
|
{
|
|
if (isBLEDevice)
|
|
midiPort->Release();
|
|
}
|
|
|
|
midiPort = nullptr;
|
|
}
|
|
|
|
private:
|
|
//==============================================================================
|
|
void bleDeviceAdded (const String& containerID) override
|
|
{
|
|
if (containerID == deviceInfo.containerID)
|
|
isBLEDevice = true;
|
|
}
|
|
|
|
void bleDeviceDisconnected (const String& containerID) override
|
|
{
|
|
if (containerID == deviceInfo.containerID)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Disconnecting MIDI port from BLE disconnection: " << deviceInfo.deviceID
|
|
<< " " << deviceInfo.containerID << " " << deviceInfo.name);
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
protected:
|
|
//==============================================================================
|
|
BLEDeviceWatcher& bleDeviceWatcher;
|
|
MIDIDeviceInfo deviceInfo;
|
|
bool isBLEDevice = false;
|
|
WinRTWrapper::ComPtr<MIDIPort> midiPort;
|
|
};
|
|
|
|
//==============================================================================
|
|
struct WinRTInputWrapper final : public InputWrapper,
|
|
private WinRTIOWrapper<IMidiInPortStatics, IMidiInPort>
|
|
|
|
{
|
|
WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb)
|
|
: WinRTIOWrapper <IMidiInPortStatics, IMidiInPort> (*service.bleDeviceWatcher, *service.inputDeviceWatcher, index),
|
|
inputDevice (input),
|
|
callback (cb)
|
|
{
|
|
OpenMidiPortThread<IMidiInPortStatics, IMidiInPort, MidiInPort> portThread ("Open WinRT MIDI input port",
|
|
deviceInfo.deviceID,
|
|
service.midiInFactory,
|
|
midiPort);
|
|
portThread.startThread();
|
|
portThread.waitForThreadToExit (-1);
|
|
|
|
if (midiPort == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation");
|
|
return;
|
|
}
|
|
|
|
startTime = Time::getMillisecondCounterHiRes();
|
|
|
|
auto hr = midiPort->add_MessageReceived (
|
|
Callback<ITypedEventHandler<MidiInPort*, MidiMessageReceivedEventArgs*>> (
|
|
[this](IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); }
|
|
).Get(),
|
|
&midiInMessageToken);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to set MIDI input callback");
|
|
jassertfalse;
|
|
}
|
|
}
|
|
|
|
~WinRTInputWrapper()
|
|
{
|
|
disconnect();
|
|
}
|
|
|
|
//==============================================================================
|
|
void start() override
|
|
{
|
|
if (! isStarted)
|
|
{
|
|
concatenator.reset();
|
|
isStarted = true;
|
|
}
|
|
}
|
|
|
|
void stop() override
|
|
{
|
|
if (isStarted)
|
|
{
|
|
isStarted = false;
|
|
concatenator.reset();
|
|
}
|
|
}
|
|
|
|
String getDeviceName() override { return deviceInfo.name; }
|
|
|
|
//==============================================================================
|
|
void disconnect() override
|
|
{
|
|
stop();
|
|
|
|
if (midiPort != nullptr && midiInMessageToken.value != 0)
|
|
midiPort->remove_MessageReceived (midiInMessageToken);
|
|
|
|
WinRTIOWrapper<IMidiInPortStatics, IMidiInPort>::disconnect();
|
|
}
|
|
|
|
//==============================================================================
|
|
HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args)
|
|
{
|
|
if (! isStarted)
|
|
return S_OK;
|
|
|
|
WinRTWrapper::ComPtr<IMidiMessage> message;
|
|
auto hr = args->get_Message (message.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
WinRTWrapper::ComPtr<IBuffer> buffer;
|
|
hr = message->get_RawData (buffer.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
WinRTWrapper::ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
|
|
hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
uint8_t* bufferData = nullptr;
|
|
hr = bufferByteAccess->Buffer (&bufferData);
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
uint32_t numBytes = 0;
|
|
hr = buffer->get_Length (&numBytes);
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
ABI::Windows::Foundation::TimeSpan timespan;
|
|
hr = message->get_Timestamp (×pan);
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
concatenator.pushMidiData (bufferData, numBytes,
|
|
convertTimeStamp (timespan.Duration),
|
|
&inputDevice, callback);
|
|
return S_OK;
|
|
}
|
|
|
|
double convertTimeStamp (int64 timestamp)
|
|
{
|
|
auto millisecondsSinceStart = static_cast<double> (timestamp) / 10000.0;
|
|
auto t = startTime + millisecondsSinceStart;
|
|
auto now = Time::getMillisecondCounterHiRes();
|
|
|
|
if (t > now)
|
|
{
|
|
if (t > now + 2.0)
|
|
startTime -= 1.0;
|
|
|
|
t = now;
|
|
}
|
|
|
|
return t * 0.001;
|
|
}
|
|
|
|
//==============================================================================
|
|
MidiInput& inputDevice;
|
|
MidiInputCallback& callback;
|
|
|
|
MidiDataConcatenator concatenator { 4096 };
|
|
EventRegistrationToken midiInMessageToken { 0 };
|
|
|
|
double startTime = 0;
|
|
bool isStarted = false;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTInputWrapper);
|
|
};
|
|
|
|
//==============================================================================
|
|
struct WinRTOutputWrapper final : public OutputWrapper,
|
|
private WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort>
|
|
{
|
|
WinRTOutputWrapper (WinRTMidiService& service, int index)
|
|
: WinRTIOWrapper <IMidiOutPortStatics, IMidiOutPort> (*service.bleDeviceWatcher, *service.outputDeviceWatcher, index)
|
|
{
|
|
OpenMidiPortThread<IMidiOutPortStatics, IMidiOutPort, IMidiOutPort> portThread ("Open WinRT MIDI output port",
|
|
deviceInfo.deviceID,
|
|
service.midiOutFactory,
|
|
midiPort);
|
|
portThread.startThread();
|
|
portThread.waitForThreadToExit (-1);
|
|
|
|
if (midiPort == nullptr)
|
|
throw std::runtime_error ("Timed out waiting for midi output port creation");
|
|
|
|
auto bufferFactory = WinRTWrapper::getInstance()->getWRLFactory<IBufferFactory> (&RuntimeClass_Windows_Storage_Streams_Buffer[0]);
|
|
|
|
if (bufferFactory == nullptr)
|
|
throw std::runtime_error ("Failed to create output buffer factory");
|
|
|
|
auto hr = bufferFactory->Create (static_cast<UINT32> (65536), buffer.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
throw std::runtime_error ("Failed to create output buffer");
|
|
|
|
hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
throw std::runtime_error ("Failed to get buffer byte access");
|
|
|
|
hr = bufferByteAccess->Buffer (&bufferData);
|
|
|
|
if (FAILED (hr))
|
|
throw std::runtime_error ("Failed to get buffer data pointer");
|
|
}
|
|
|
|
//==============================================================================
|
|
void sendMessageNow (const MidiMessage& message) override
|
|
{
|
|
if (midiPort == nullptr)
|
|
return;
|
|
|
|
auto numBytes = message.getRawDataSize();
|
|
auto hr = buffer->put_Length (numBytes);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
memcpy_s (bufferData, numBytes, message.getRawData(), numBytes);
|
|
midiPort->SendBuffer (buffer);
|
|
}
|
|
|
|
String getDeviceName() override { return deviceInfo.name; }
|
|
|
|
//==============================================================================
|
|
WinRTWrapper::ComPtr<IBuffer> buffer;
|
|
WinRTWrapper::ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
|
|
uint8_t* bufferData = nullptr;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTOutputWrapper);
|
|
};
|
|
|
|
WinRTWrapper::ComPtr<IMidiInPortStatics> midiInFactory;
|
|
WinRTWrapper::ComPtr<IMidiOutPortStatics> midiOutFactory;
|
|
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiInPortStatics>> inputDeviceWatcher;
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiOutPortStatics>> outputDeviceWatcher;
|
|
std::unique_ptr<BLEDeviceWatcher> bleDeviceWatcher;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService)
|
|
};
|
|
|
|
#endif // JUCE_USE_WINRT_MIDI
|
|
|
|
//==============================================================================
|
|
//==============================================================================
|
|
struct MidiService : public DeletedAtShutdown
|
|
{
|
|
MidiService()
|
|
{
|
|
#if JUCE_USE_WINRT_MIDI
|
|
#if ! JUCE_FORCE_WINRT_MIDI
|
|
auto windowsVersionInfo = []
|
|
{
|
|
RTL_OSVERSIONINFOW versionInfo = { 0 };
|
|
|
|
if (auto* mod = ::GetModuleHandleW (L"ntdll.dll"))
|
|
{
|
|
using RtlGetVersion = LONG (WINAPI*)(PRTL_OSVERSIONINFOW);
|
|
|
|
if (auto* rtlGetVersion = (RtlGetVersion) ::GetProcAddress (mod, "RtlGetVersion"))
|
|
{
|
|
versionInfo.dwOSVersionInfoSize = sizeof (versionInfo);
|
|
LONG STATUS_SUCCESS = 0;
|
|
|
|
if (rtlGetVersion (&versionInfo) != STATUS_SUCCESS)
|
|
versionInfo = { 0 };
|
|
}
|
|
}
|
|
|
|
return versionInfo;
|
|
}();
|
|
|
|
if (windowsVersionInfo.dwMajorVersion >= 10 && windowsVersionInfo.dwBuildNumber >= 17763)
|
|
#endif
|
|
{
|
|
try
|
|
{
|
|
internal.reset (new WinRTMidiService());
|
|
return;
|
|
}
|
|
catch (std::runtime_error&) {}
|
|
}
|
|
#endif
|
|
|
|
internal.reset (new Win32MidiService());
|
|
}
|
|
|
|
~MidiService()
|
|
{
|
|
clearSingletonInstance();
|
|
}
|
|
|
|
static MidiServiceType& getService()
|
|
{
|
|
jassert (getInstance()->internal != nullptr);
|
|
return *getInstance()->internal.get();
|
|
}
|
|
|
|
JUCE_DECLARE_SINGLETON (MidiService, false)
|
|
|
|
private:
|
|
std::unique_ptr<MidiServiceType> internal;
|
|
};
|
|
|
|
JUCE_IMPLEMENT_SINGLETON (MidiService)
|
|
|
|
//==============================================================================
|
|
StringArray MidiInput::getDevices()
|
|
{
|
|
return MidiService::getService().getDevices (true);
|
|
}
|
|
|
|
int MidiInput::getDefaultDeviceIndex()
|
|
{
|
|
return MidiService::getService().getDefaultDeviceIndex (true);
|
|
}
|
|
|
|
MidiInput::MidiInput (const String& deviceName) : name (deviceName)
|
|
{
|
|
}
|
|
|
|
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback)
|
|
{
|
|
if (callback == nullptr)
|
|
return nullptr;
|
|
|
|
std::unique_ptr<MidiInput> in (new MidiInput (String()));
|
|
std::unique_ptr<MidiServiceType::InputWrapper> wrapper;
|
|
|
|
try
|
|
{
|
|
wrapper.reset (MidiService::getService().createInputWrapper (*in, index, *callback));
|
|
}
|
|
catch (std::runtime_error&)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
in->setName (wrapper->getDeviceName());
|
|
in->internal = wrapper.release();
|
|
return in.release();
|
|
}
|
|
|
|
MidiInput::~MidiInput()
|
|
{
|
|
delete static_cast<MidiServiceType::InputWrapper*> (internal);
|
|
}
|
|
|
|
void MidiInput::start() { static_cast<MidiServiceType::InputWrapper*> (internal)->start(); }
|
|
void MidiInput::stop() { static_cast<MidiServiceType::InputWrapper*> (internal)->stop(); }
|
|
|
|
//==============================================================================
|
|
StringArray MidiOutput::getDevices()
|
|
{
|
|
return MidiService::getService().getDevices (false);
|
|
}
|
|
|
|
int MidiOutput::getDefaultDeviceIndex()
|
|
{
|
|
return MidiService::getService().getDefaultDeviceIndex (false);
|
|
}
|
|
|
|
MidiOutput* MidiOutput::openDevice (int index)
|
|
{
|
|
std::unique_ptr<MidiServiceType::OutputWrapper> wrapper;
|
|
|
|
try
|
|
{
|
|
wrapper.reset (MidiService::getService().createOutputWrapper (index));
|
|
}
|
|
catch (std::runtime_error&)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr<MidiOutput> out (new MidiOutput (wrapper->getDeviceName()));
|
|
out->internal = wrapper.release();
|
|
return out.release();
|
|
}
|
|
|
|
MidiOutput::~MidiOutput()
|
|
{
|
|
stopBackgroundThread();
|
|
delete static_cast<MidiServiceType::OutputWrapper*> (internal);
|
|
}
|
|
|
|
void MidiOutput::sendMessageNow (const MidiMessage& message)
|
|
{
|
|
static_cast<MidiServiceType::OutputWrapper*> (internal)->sendMessageNow (message);
|
|
}
|
|
|
|
} // namespace juce
|