1319 lines
42 KiB
C++
1319 lines
42 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
|
|
using namespace Microsoft::WRL;
|
|
|
|
using namespace ABI::Windows::Foundation;
|
|
using namespace ABI::Windows::Devices::Midi;
|
|
using namespace ABI::Windows::Devices::Enumeration;
|
|
using namespace ABI::Windows::Storage::Streams;
|
|
|
|
//==============================================================================
|
|
class 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");
|
|
|
|
inputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiInPortStatics> (midiInFactory));
|
|
|
|
if (! inputDeviceWatcher->start())
|
|
throw std::runtime_error ("Failed to start midi input device watcher");
|
|
|
|
outputDeviceWatcher.reset (new MidiIODeviceWatcher<IMidiOutPortStatics> (midiOutFactory));
|
|
|
|
if (! outputDeviceWatcher->start())
|
|
throw std::runtime_error ("Failed to start midi output device watcher");
|
|
}
|
|
|
|
~WinRTMidiService() {}
|
|
|
|
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);
|
|
}
|
|
|
|
template <typename COMFactoryType>
|
|
struct MidiIODeviceWatcher
|
|
{
|
|
struct DeviceInfo
|
|
{
|
|
String name, id;
|
|
bool isDefault = false;
|
|
};
|
|
|
|
MidiIODeviceWatcher (ComSmartPtr<COMFactoryType>& comFactory) : factory (comFactory)
|
|
{
|
|
}
|
|
|
|
~MidiIODeviceWatcher()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
bool start()
|
|
{
|
|
HSTRING deviceSelector;
|
|
auto hr = factory->GetDeviceSelector (&deviceSelector);
|
|
|
|
if (FAILED (hr))
|
|
return false;
|
|
|
|
auto deviceInformationFactory = WinRTWrapper::getInstance()->getWRLFactory<IDeviceInformationStatics> (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]);
|
|
|
|
if (deviceInformationFactory == nullptr)
|
|
return false;
|
|
|
|
hr = deviceInformationFactory->CreateWatcherAqsFilter (deviceSelector, watcher.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return false;
|
|
|
|
struct DeviceEnumerationThread : public Thread
|
|
{
|
|
DeviceEnumerationThread (String threadName, MidiIODeviceWatcher<COMFactoryType>& p)
|
|
: Thread (threadName), parent (p)
|
|
{}
|
|
|
|
void run() override
|
|
{
|
|
auto parentPtr = &parent;
|
|
|
|
parent.watcher->add_Added (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>> (
|
|
[parentPtr](IDeviceWatcher*, IDeviceInformation* info) { return parentPtr->addDevice (info); }
|
|
).Get(),
|
|
&parent.deviceAddedToken);
|
|
|
|
parent.watcher->add_Removed (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
|
|
[parentPtr](IDeviceWatcher*, IDeviceInformationUpdate* info) { return parentPtr->removeDevice (info); }
|
|
).Get(),
|
|
&parent.deviceRemovedToken);
|
|
|
|
EventRegistrationToken deviceEnumerationCompletedToken { 0 };
|
|
parent.watcher->add_EnumerationCompleted (
|
|
Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>> (
|
|
[this](IDeviceWatcher*, IInspectable*) { enumerationCompleted.signal(); return S_OK; }
|
|
).Get(),
|
|
&deviceEnumerationCompletedToken);
|
|
|
|
parent.watcher->Start();
|
|
enumerationCompleted.wait();
|
|
|
|
if (deviceEnumerationCompletedToken.value != 0)
|
|
parent.watcher->remove_EnumerationCompleted (deviceEnumerationCompletedToken);
|
|
}
|
|
|
|
MidiIODeviceWatcher<COMFactoryType>& parent;
|
|
WaitableEvent enumerationCompleted;
|
|
};
|
|
|
|
DeviceEnumerationThread enumerationThread ("WinRT Device Enumeration Thread", *this);
|
|
enumerationThread.startThread();
|
|
enumerationThread.waitForThreadToExit (4000);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool stop()
|
|
{
|
|
if (watcher == nullptr)
|
|
return true;
|
|
|
|
if (deviceAddedToken.value != 0)
|
|
{
|
|
auto hr = watcher->remove_Added (deviceAddedToken);
|
|
|
|
if (FAILED (hr))
|
|
return false;
|
|
|
|
deviceAddedToken.value = 0;
|
|
}
|
|
|
|
if (deviceRemovedToken.value != 0)
|
|
{
|
|
auto hr = watcher->remove_Removed (deviceRemovedToken);
|
|
|
|
if (FAILED (hr))
|
|
return false;
|
|
|
|
deviceRemovedToken.value = 0;
|
|
}
|
|
|
|
auto hr = watcher->Stop();
|
|
|
|
if (FAILED (hr))
|
|
return false;
|
|
|
|
watcher = nullptr;
|
|
return true;
|
|
}
|
|
|
|
HRESULT addDevice (IDeviceInformation* addedDeviceInfo)
|
|
{
|
|
boolean isEnabled;
|
|
auto hr = addedDeviceInfo->get_IsEnabled (&isEnabled);
|
|
|
|
if (FAILED (hr))
|
|
return S_OK;
|
|
|
|
if (! isEnabled)
|
|
return S_OK;
|
|
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
DeviceInfo info;
|
|
|
|
HSTRING name;
|
|
hr = addedDeviceInfo->get_Name (&name);
|
|
|
|
if (FAILED (hr))
|
|
return S_OK;
|
|
|
|
info.name = WinRTWrapper::getInstance()->hStringToString (name);
|
|
|
|
HSTRING id;
|
|
hr = addedDeviceInfo->get_Id (&id);
|
|
|
|
if (FAILED (hr))
|
|
return S_OK;
|
|
|
|
info.id = WinRTWrapper::getInstance()->hStringToString (id);
|
|
|
|
boolean isDefault;
|
|
hr = addedDeviceInfo->get_IsDefault (&isDefault);
|
|
|
|
if (FAILED (hr))
|
|
return S_OK;
|
|
|
|
info.isDefault = isDefault != 0;
|
|
connectedDevices.add (info);
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo)
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
HSTRING removedDeviceIdHstr;
|
|
removedDeviceInfo->get_Id (&removedDeviceIdHstr);
|
|
auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr);
|
|
|
|
for (int i = 0; i < connectedDevices.size(); ++i)
|
|
{
|
|
if (connectedDevices[i].id == removedDeviceId)
|
|
{
|
|
connectedDevices.remove (i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
String getDeviceNameFromIndex (int index)
|
|
{
|
|
if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size()))
|
|
return lastQueriedConnectedDevices.get()[index].name;
|
|
|
|
return {};
|
|
}
|
|
|
|
String getDeviceID (const String& name)
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
|
|
for (auto info : connectedDevices)
|
|
if (info.name == name)
|
|
return info.id;
|
|
|
|
return {};
|
|
}
|
|
|
|
ComSmartPtr<COMFactoryType>& factory;
|
|
|
|
EventRegistrationToken deviceAddedToken { 0 },
|
|
deviceRemovedToken { 0 };
|
|
|
|
ComSmartPtr<IDeviceWatcher> watcher;
|
|
|
|
Array<DeviceInfo> connectedDevices;
|
|
CriticalSection deviceChanges;
|
|
ThreadLocalValue<Array<DeviceInfo>> 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,
|
|
ComSmartPtr<COMFactoryType>& comFactory,
|
|
ComSmartPtr<COMInterfaceType>& comPort)
|
|
: Thread (threadName),
|
|
deviceID (midiDeviceID),
|
|
factory (comFactory),
|
|
port (comPort)
|
|
{
|
|
}
|
|
|
|
~OpenMidiPortThread()
|
|
{
|
|
stopThread (2000);
|
|
}
|
|
|
|
void run() override
|
|
{
|
|
WinRTWrapper::ScopedHString hDeviceId (deviceID);
|
|
ComSmartPtr<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());
|
|
|
|
// When using Bluetooth the asynchronous port opening operation will occasionally
|
|
// hang, so we use a timeout. We will be able to remove this when Microsoft
|
|
// improves the Bluetooth MIDI stack.
|
|
portOpened.wait (2000);
|
|
}
|
|
|
|
const String deviceID;
|
|
ComSmartPtr<COMFactoryType>& factory;
|
|
ComSmartPtr<COMInterfaceType>& port;
|
|
WaitableEvent portOpened { true };
|
|
};
|
|
|
|
//==============================================================================
|
|
struct WinRTInputWrapper : public InputWrapper
|
|
{
|
|
WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb)
|
|
: inputDevice (input),
|
|
callback (cb)
|
|
{
|
|
const ScopedLock lock (service.inputDeviceWatcher->deviceChanges);
|
|
|
|
deviceName = service.inputDeviceWatcher->getDeviceNameFromIndex (index);
|
|
|
|
if (deviceName.isEmpty())
|
|
throw std::runtime_error ("Invalid device index");
|
|
|
|
auto deviceID = service.inputDeviceWatcher->getDeviceID (deviceName);
|
|
|
|
if (deviceID.isEmpty())
|
|
throw std::runtime_error ("Device unavailable");
|
|
|
|
OpenMidiPortThread<IMidiInPortStatics, IMidiInPort, MidiInPort> portThread ("Open WinRT MIDI input port",
|
|
deviceID,
|
|
service.midiInFactory,
|
|
midiInPort);
|
|
portThread.startThread();
|
|
portThread.waitForThreadToExit (-1);
|
|
|
|
if (midiInPort == nullptr)
|
|
throw std::runtime_error ("Timed out waiting for midi input port creation");
|
|
|
|
startTime = Time::getMillisecondCounterHiRes();
|
|
|
|
auto hr = midiInPort->add_MessageReceived (
|
|
Callback<ITypedEventHandler<MidiInPort*, MidiMessageReceivedEventArgs*>> (
|
|
[this] (IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); }
|
|
).Get(),
|
|
&midiInMessageToken);
|
|
|
|
if (FAILED (hr))
|
|
throw std::runtime_error ("Failed to set midi input callback");
|
|
}
|
|
|
|
~WinRTInputWrapper()
|
|
{
|
|
if (midiInMessageToken.value != 0)
|
|
midiInPort->remove_MessageReceived (midiInMessageToken);
|
|
|
|
midiInPort = nullptr;
|
|
}
|
|
|
|
void start() override
|
|
{
|
|
if (! isStarted)
|
|
{
|
|
concatenator.reset();
|
|
isStarted = true;
|
|
}
|
|
}
|
|
|
|
void stop() override
|
|
{
|
|
if (isStarted)
|
|
{
|
|
isStarted = false;
|
|
concatenator.reset();
|
|
}
|
|
}
|
|
|
|
String getDeviceName() override { return deviceName; }
|
|
|
|
HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args)
|
|
{
|
|
if (! isStarted)
|
|
return S_OK;
|
|
|
|
ComSmartPtr<IMidiMessage> message;
|
|
auto hr = args->get_Message (message.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
ComSmartPtr<IBuffer> buffer;
|
|
hr = message->get_RawData (buffer.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return hr;
|
|
|
|
ComSmartPtr<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;
|
|
String deviceName;
|
|
MidiDataConcatenator concatenator { 4096 };
|
|
ComSmartPtr<IMidiInPort> midiInPort;
|
|
EventRegistrationToken midiInMessageToken { 0 };
|
|
|
|
double startTime = 0;
|
|
bool isStarted = false;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTInputWrapper);
|
|
};
|
|
|
|
//==============================================================================
|
|
struct WinRTOutputWrapper : public OutputWrapper
|
|
{
|
|
WinRTOutputWrapper (WinRTMidiService& service, int index)
|
|
{
|
|
const ScopedLock lock (service.outputDeviceWatcher->deviceChanges);
|
|
|
|
deviceName = service.outputDeviceWatcher->getDeviceNameFromIndex (index);
|
|
|
|
if (deviceName.isEmpty())
|
|
throw std::runtime_error ("Invalid device index");
|
|
|
|
auto deviceID = service.outputDeviceWatcher->getDeviceID (deviceName);
|
|
|
|
if (deviceID.isEmpty())
|
|
throw std::runtime_error ("Device unavailable");
|
|
|
|
OpenMidiPortThread<IMidiOutPortStatics, IMidiOutPort, IMidiOutPort> portThread ("Open WinRT MIDI output port",
|
|
deviceID,
|
|
service.midiOutFactory,
|
|
midiOutPort);
|
|
portThread.startThread();
|
|
portThread.waitForThreadToExit (-1);
|
|
|
|
if (midiOutPort == 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");
|
|
}
|
|
|
|
~WinRTOutputWrapper() {}
|
|
|
|
void sendMessageNow (const MidiMessage& message) override
|
|
{
|
|
auto numBytes = message.getRawDataSize();
|
|
auto hr = buffer->put_Length (numBytes);
|
|
|
|
if (FAILED (hr))
|
|
jassertfalse;
|
|
|
|
memcpy_s (bufferData, numBytes, message.getRawData(), numBytes);
|
|
midiOutPort->SendBuffer (buffer);
|
|
}
|
|
|
|
String getDeviceName() override { return deviceName; }
|
|
|
|
String deviceName;
|
|
ComSmartPtr<IMidiOutPort> midiOutPort;
|
|
ComSmartPtr<IBuffer> buffer;
|
|
ComSmartPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
|
|
uint8_t* bufferData = nullptr;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTOutputWrapper);
|
|
};
|
|
|
|
ComSmartPtr<IMidiInPortStatics> midiInFactory;
|
|
ComSmartPtr<IMidiOutPortStatics> midiOutFactory;
|
|
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiInPortStatics>> inputDeviceWatcher;
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiOutPortStatics>> outputDeviceWatcher;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService)
|
|
};
|
|
|
|
#endif // JUCE_USE_WINRT_MIDI
|
|
|
|
//==============================================================================
|
|
struct MidiService : public DeletedAtShutdown
|
|
{
|
|
MidiService()
|
|
{
|
|
#if JUCE_USE_WINRT_MIDI
|
|
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
|