From d83360a7714700ddc7aa145eb55746d205ca88e2 Mon Sep 17 00:00:00 2001 From: Tom Poole Date: Mon, 22 Oct 2018 15:32:21 +0100 Subject: [PATCH] Windows: Updated the WinRT MIDI interface --- BREAKING-CHANGES.txt | 23 + .../juce_audio_devices/juce_audio_devices.h | 35 +- .../native/juce_win32_Midi.cpp | 821 ++++++++++++++---- 3 files changed, 693 insertions(+), 186 deletions(-) diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 9a7c2bc4e4..5b8d46e84a 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,29 @@ JUCE breaking changes Develop ======= +Change +------ +The use of WinRT MIDI functions has been disabled by default for any version +of Windows 10 before 1809 (October 2018 Update). + +Possible Issues +--------------- +If you were previously using WinRT MIDI functions on older versions of Windows +then the new behaviour is to revert to the old Win32 MIDI API. + +Workaround +---------- +Set the preprocessor macro JUCE_FORCE_WINRT_MIDI=1 (in addition to the +previously selected JUCE_USE_WINRT_MIDI=1) to allow the use of the WinRT API on +older versions of Windows. + +Rationale +--------- +Until now JUCE's support for the Windows 10 WinRT MIDI API was experimental, +due to longstanding issues within the API itself. These issues have been +addressed in the Windows 10 1809 (October 2018 Update) release. + + Change ------ The VST2 SDK embedded within JUCE has been removed. diff --git a/modules/juce_audio_devices/juce_audio_devices.h b/modules/juce_audio_devices/juce_audio_devices.h index 1ab48aa5a8..05ebb13128 100644 --- a/modules/juce_audio_devices/juce_audio_devices.h +++ b/modules/juce_audio_devices/juce_audio_devices.h @@ -59,6 +59,22 @@ #endif //============================================================================== +/** Config: JUCE_USE_WINRT_MIDI + Enables the use of the Windows Runtime API for MIDI, allowing connections + to Bluetooth Low Energy devices on Windows 10 version 1809 (October 2018 + Update) and later. If you enable this flag then older, unsupported, + versions of Windows will automatically fall back to using the regualar + Win32 MIDI API. + + You will need version 10.0.14393.0 of the Windows Standalone SDK to compile + and you may need to add the path to the WinRT headers. The path to the + headers will be something similar to + "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". +*/ +#ifndef JUCE_USE_WINRT_MIDI + #define JUCE_USE_WINRT_MIDI 0 +#endif + /** Config: JUCE_ASIO Enables ASIO audio devices (MS Windows only). Turning this on means that you'll need to have the Steinberg ASIO SDK installed @@ -146,25 +162,6 @@ #endif #endif -/** Config: JUCE_USE_WINRT_MIDI - *** - EXPERIMENTAL - Microsoft's Bluetooth MIDI stack has multiple issues, - use at your own risk! - *** - - Enables the use of the Windows Runtime API for MIDI, which supports - Bluetooth Low Energy connections on computers with the Anniversary Update - of Windows 10. - - To compile with this flag requires version 10.0.14393.0 of the Windows - Standalone SDK and you must add the path to the WinRT headers. This path - should be something similar to - "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". -*/ -#ifndef JUCE_USE_WINRT_MIDI - #define JUCE_USE_WINRT_MIDI 0 -#endif - /** Config: JUCE_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS Turning this on gives your app exclusive access to the system's audio on platforms which support it (currently iOS only). diff --git a/modules/juce_audio_devices/native/juce_win32_Midi.cpp b/modules/juce_audio_devices/native/juce_win32_Midi.cpp index 519caf15ec..0864a053ce 100644 --- a/modules/juce_audio_devices/native/juce_win32_Midi.cpp +++ b/modules/juce_audio_devices/native/juce_win32_Midi.cpp @@ -608,15 +608,31 @@ Array Win32MidiService::Mid //============================================================================== //============================================================================== #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; //============================================================================== -class WinRTMidiService : public MidiServiceType +struct WinRTMidiService : public MidiServiceType { public: //============================================================================== @@ -635,19 +651,26 @@ public: 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 (midiInFactory)); if (! inputDeviceWatcher->start()) - throw std::runtime_error ("Failed to start midi input device watcher"); + throw std::runtime_error ("Failed to start the midi input device watcher"); outputDeviceWatcher.reset (new MidiIODeviceWatcher (midiOutFactory)); if (! outputDeviceWatcher->start()) - throw std::runtime_error ("Failed to start midi output device watcher"); + throw std::runtime_error ("Failed to start the midi output device watcher"); } - ~WinRTMidiService() {} - StringArray getDevices (bool isInput) override { return isInput ? inputDeviceWatcher ->getDevices() @@ -670,185 +693,542 @@ public: return new WinRTOutputWrapper (*this, index); } - template - struct MidiIODeviceWatcher +private: + //============================================================================== + class DeviceCallbackHandler { - struct DeviceInfo + public: + virtual ~DeviceCallbackHandler() {}; + + virtual HRESULT addDevice (IDeviceInformation*) = 0; + virtual HRESULT removeDevice (IDeviceInformationUpdate*) = 0; + virtual HRESULT updateDevice (IDeviceInformationUpdate*) = 0; + + bool attach (HSTRING deviceSelector, DeviceInformationKind infoKind) { - String name, id; - bool isDefault = false; + auto deviceInfoFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]); + + if (deviceInfoFactory == nullptr) + return false; + + // A quick way of getting an IVector... + auto requestedProperties = [] + { + auto devicePicker = WinRTWrapper::getInstance()->activateInstance (&RuntimeClass_Windows_Devices_Enumeration_DevicePicker[0], + __uuidof (IDevicePicker)); + jassert (devicePicker != nullptr); + + IVector* 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> iter; + auto hr = requestedProperties->QueryInterface (__uuidof (IIterable), (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; }; - MidiIODeviceWatcher (WinRTWrapper::ComPtr& comFactory) : factory (comFactory) + 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; } - ~MidiIODeviceWatcher() + template + IInspectable* getValueFromDeviceInfo (String key, InfoType* info) { - stop(); + __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; } - bool start() + String getGUIDFromInspectable (IInspectable& inspectable) { - HSTRING deviceSelector; - auto hr = factory->GetDeviceSelector (&deviceSelector); + WinRTWrapper::ComPtr> guidRef; + auto hr = inspectable.QueryInterface (__uuidof (IReference), + (void**) guidRef.resetAndGetPointerAddress()); if (FAILED (hr)) - return false; + { + jassertfalse; + return {}; + } + + GUID result; + hr = guidRef->get_Value (&result); - auto deviceInformationFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]); + if (FAILED (hr)) + { + jassertfalse; + return {}; + } + + OLECHAR* resultString; + StringFromCLSID (result, &resultString); + + return resultString; + } + + bool getBoolFromInspectable (IInspectable& inspectable) + { + WinRTWrapper::ComPtr> boolRef; + auto hr = inspectable.QueryInterface (__uuidof (IReference), + (void**) boolRef.resetAndGetPointerAddress()); - if (deviceInformationFactory == nullptr) + if (FAILED (hr)) + { + jassertfalse; return false; + } - hr = deviceInformationFactory->CreateWatcherAqsFilter (deviceSelector, watcher.resetAndGetPointerAddress()); + boolean result; + hr = boolRef->get_Value (&result); if (FAILED (hr)) + { + jassertfalse; return false; + } - struct DeviceEnumerationThread : public Thread + return result; + } + + private: + //============================================================================== + struct DeviceEnumerationThread : public Thread + { + DeviceEnumerationThread (DeviceCallbackHandler& h, + WinRTWrapper::ComPtr& 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 { - DeviceEnumerationThread (String threadName, MidiIODeviceWatcher& p) - : Thread (threadName), parent (p) - {} + auto handlerPtr = std::addressof (handler); + + watcher->add_Added ( + Callback> ( + [handlerPtr](IDeviceWatcher*, IDeviceInformation* info) { return handlerPtr->addDevice (info); } + ).Get(), + &deviceAddedToken); + + watcher->add_Removed ( + Callback> ( + [handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->removeDevice (infoUpdate); } + ).Get(), + &deviceRemovedToken); + + watcher->add_Updated ( + Callback> ( + [handlerPtr](IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return handlerPtr->updateDevice (infoUpdate); } + ).Get(), + &deviceRemovedToken); + + watcher->Start(); + } - void run() override - { - auto parentPtr = &parent; - - parent.watcher->add_Added ( - Callback> ( - [parentPtr](IDeviceWatcher*, IDeviceInformation* info) { return parentPtr->addDevice (info); } - ).Get(), - &parent.deviceAddedToken); - - parent.watcher->add_Removed ( - Callback> ( - [parentPtr](IDeviceWatcher*, IDeviceInformationUpdate* info) { return parentPtr->removeDevice (info); } - ).Get(), - &parent.deviceRemovedToken); - - EventRegistrationToken deviceEnumerationCompletedToken { 0 }; - parent.watcher->add_EnumerationCompleted ( - Callback> ( - [this](IDeviceWatcher*, IInspectable*) { enumerationCompleted.signal(); return S_OK; } - ).Get(), - &deviceEnumerationCompletedToken); - - parent.watcher->Start(); - enumerationCompleted.wait(); - - if (deviceEnumerationCompletedToken.value != 0) - parent.watcher->remove_EnumerationCompleted (deviceEnumerationCompletedToken); - } + DeviceCallbackHandler& handler; + WinRTWrapper::ComPtr& watcher; + EventRegistrationToken& deviceAddedToken, deviceRemovedToken, deviceUpdatedToken; + }; - MidiIODeviceWatcher& parent; - WaitableEvent enumerationCompleted; - }; + //============================================================================== + WinRTWrapper::ComPtr watcher; - DeviceEnumerationThread enumerationThread ("WinRT Device Enumeration Thread", *this); - enumerationThread.startThread(); - enumerationThread.waitForThreadToExit (4000); + EventRegistrationToken deviceAddedToken { 0 }, + deviceRemovedToken { 0 }, + deviceUpdatedToken { 0 }; - return true; + DeviceEnumerationThread enumerationThread { *this, watcher, + deviceAddedToken, + deviceRemovedToken, + deviceUpdatedToken }; + }; + + //============================================================================== + struct BLEDeviceWatcher final : private DeviceCallbackHandler + { + struct DeviceInfo + { + String containerID; + bool isConnected = false; + }; + + BLEDeviceWatcher() = default; + + ~BLEDeviceWatcher() + { + detach(); } - bool stop() + //============================================================================== + HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override { - if (watcher == nullptr) - return true; + HSTRING deviceIDHst; + auto hr = addedDeviceInfo->get_Id (&deviceIDHst); - if (deviceAddedToken.value != 0) + if (FAILED (hr)) { - auto hr = watcher->remove_Added (deviceAddedToken); + JUCE_WINRT_MIDI_LOG ("Failed to query added BLE device ID!"); + return S_OK; + } - if (FAILED (hr)) - return false; + auto deviceID = WinRTWrapper::getInstance()->hStringToString (deviceIDHst); + JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID); - deviceAddedToken.value = 0; + 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; + } } - if (deviceRemovedToken.value != 0) + 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)) { - auto hr = watcher->remove_Removed (deviceRemovedToken); + JUCE_WINRT_MIDI_LOG ("Failed to query removed BLE device ID!"); + return S_OK; + } - if (FAILED (hr)) - return false; + auto removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr); - deviceRemovedToken.value = 0; + 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); + } } - auto hr = watcher->Stop(); + return S_OK; + } + + HRESULT updateDevice (IDeviceInformationUpdate* updatedDeviceInfo) override + { + HSTRING updatedDeviceIdHstr; + auto hr = updatedDeviceInfo->get_Id (&updatedDeviceIdHstr); if (FAILED (hr)) - return false; + { + JUCE_WINRT_MIDI_LOG ("Failed to query updated BLE device ID!"); + return S_OK; + } - watcher = nullptr; - return true; + 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); } - HRESULT addDevice (IDeviceInformation* addedDeviceInfo) + //============================================================================== + ListenerList listeners; + HashMap devices; + CriticalSection deviceChanges; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher); + }; + + //============================================================================== + struct MIDIDeviceInfo + { + String deviceID, containerID, name; + bool isDefault = false; + }; + + //============================================================================== + template + struct MidiIODeviceWatcher final : private DeviceCallbackHandler + { + MidiIODeviceWatcher (WinRTWrapper::ComPtr& comFactory) + : factory (comFactory) { - boolean isEnabled; - auto hr = addedDeviceInfo->get_IsEnabled (&isEnabled); + } + + ~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; + } - if (! isEnabled) - return S_OK; + info.deviceID = WinRTWrapper::getInstance()->hStringToString (deviceID); - const ScopedLock lock (deviceChanges); + JUCE_WINRT_MIDI_LOG ("Detected MIDI device: " << info.deviceID); - DeviceInfo info; + 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); - HSTRING id; - hr = addedDeviceInfo->get_Id (&id); + 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.id = WinRTWrapper::getInstance()->hStringToString (id); + info.isDefault = isDefault; - boolean isDefault; - hr = addedDeviceInfo->get_IsDefault (&isDefault); + JUCE_WINRT_MIDI_LOG ("Adding MIDI device: " << info.deviceID << " " << info.containerID << " " << info.name); - if (FAILED (hr)) - return S_OK; + { + const ScopedLock lock (deviceChanges); + connectedDevices.add (info); + } - info.isDefault = isDefault != 0; - connectedDevices.add (info); return S_OK; } - HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) + HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) override { - const ScopedLock lock (deviceChanges); - HSTRING removedDeviceIdHstr; - removedDeviceInfo->get_Id (&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); - for (int i = 0; i < connectedDevices.size(); ++i) + JUCE_WINRT_MIDI_LOG ("Removing MIDI device: " << removedDeviceId); + { - if (connectedDevices[i].id == removedDeviceId) + const ScopedLock lock (deviceChanges); + + for (int i = 0; i < connectedDevices.size(); ++i) { - connectedDevices.remove (i); - break; + 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() { { @@ -875,10 +1255,10 @@ public: return 0; } - String getDeviceNameFromIndex (int index) + MIDIDeviceInfo getDeviceInfoFromIndex (int index) { if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size())) - return lastQueriedConnectedDevices.get()[index].name; + return lastQueriedConnectedDevices.get()[index]; return {}; } @@ -889,21 +1269,16 @@ public: for (auto info : connectedDevices) if (info.name == name) - return info.id; + return info.deviceID; return {}; } WinRTWrapper::ComPtr& factory; - EventRegistrationToken deviceAddedToken { 0 }, - deviceRemovedToken { 0 }; - - WinRTWrapper::ComPtr watcher; - - Array connectedDevices; + Array connectedDevices; CriticalSection deviceChanges; - ThreadLocalValue> lastQueriedConnectedDevices; + ThreadLocalValue> lastQueriedConnectedDevices; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher); }; @@ -952,9 +1327,8 @@ public: } ).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. + // We need to use a timout here, rather than waiting indefinitely, as the + // WinRT API can occaisonally hang! portOpened.wait (2000); } @@ -965,54 +1339,133 @@ public: }; //============================================================================== - struct WinRTInputWrapper : public InputWrapper + template + class WinRTIOWrapper : private BLEDeviceWatcher::Listener { - WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb) - : inputDevice (input), - callback (cb) + public: + WinRTIOWrapper (BLEDeviceWatcher& bleWatcher, + MidiIODeviceWatcher& midiDeviceWatcher, + int index) + : bleDeviceWatcher (bleWatcher) { - const ScopedLock lock (service.inputDeviceWatcher->deviceChanges); - - deviceName = service.inputDeviceWatcher->getDeviceNameFromIndex (index); + { + const ScopedLock lock (midiDeviceWatcher.deviceChanges); + deviceInfo = midiDeviceWatcher.getDeviceInfoFromIndex (index); + } - if (deviceName.isEmpty()) + if (deviceInfo.deviceID.isEmpty()) throw std::runtime_error ("Invalid device index"); - auto deviceID = service.inputDeviceWatcher->getDeviceID (deviceName); + JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID); + + if (deviceInfo.containerID.isNotEmpty()) + { + bleDeviceWatcher.addListener (this); + + const ScopedLock lock (bleDeviceWatcher.deviceChanges); + + HashMap::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; + } - if (deviceID.isEmpty()) - throw std::runtime_error ("Device unavailable"); + 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; + }; + + //============================================================================== + struct WinRTInputWrapper final : public InputWrapper, + private WinRTIOWrapper + + { + WinRTInputWrapper (WinRTMidiService& service, MidiInput& input, int index, MidiInputCallback& cb) + : WinRTIOWrapper (*service.bleDeviceWatcher, *service.inputDeviceWatcher, index), + inputDevice (input), + callback (cb) + { OpenMidiPortThread portThread ("Open WinRT MIDI input port", - deviceID, + deviceInfo.deviceID, service.midiInFactory, - midiInPort); + midiPort); portThread.startThread(); portThread.waitForThreadToExit (-1); - if (midiInPort == nullptr) - throw std::runtime_error ("Timed out waiting for midi input port creation"); + if (midiPort == nullptr) + { + JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation"); + return; + } startTime = Time::getMillisecondCounterHiRes(); - auto hr = midiInPort->add_MessageReceived ( + auto hr = midiPort->add_MessageReceived ( Callback> ( - [this] (IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); } + [this](IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); } ).Get(), &midiInMessageToken); if (FAILED (hr)) - throw std::runtime_error ("Failed to set midi input callback"); + { + JUCE_WINRT_MIDI_LOG ("Failed to set MIDI input callback"); + jassertfalse; + } } ~WinRTInputWrapper() { - if (midiInMessageToken.value != 0) - midiInPort->remove_MessageReceived (midiInMessageToken); - - midiInPort = nullptr; + disconnect(); } + //============================================================================== void start() override { if (! isStarted) @@ -1031,8 +1484,20 @@ public: } } - String getDeviceName() override { return deviceName; } + String getDeviceName() override { return deviceInfo.name; } + + //============================================================================== + void disconnect() override + { + stop(); + + if (midiPort != nullptr && midiInMessageToken.value != 0) + midiPort->remove_MessageReceived (midiInMessageToken); + WinRTIOWrapper::disconnect(); + } + + //============================================================================== HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args) { if (! isStarted) @@ -1097,11 +1562,11 @@ public: return t * 0.001; } + //============================================================================== MidiInput& inputDevice; MidiInputCallback& callback; - String deviceName; + MidiDataConcatenator concatenator { 4096 }; - WinRTWrapper::ComPtr midiInPort; EventRegistrationToken midiInMessageToken { 0 }; double startTime = 0; @@ -1111,30 +1576,20 @@ public: }; //============================================================================== - struct WinRTOutputWrapper : public OutputWrapper + struct WinRTOutputWrapper final : public OutputWrapper, + private WinRTIOWrapper { WinRTOutputWrapper (WinRTMidiService& service, int index) + : WinRTIOWrapper (*service.bleDeviceWatcher, *service.outputDeviceWatcher, 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 portThread ("Open WinRT MIDI output port", - deviceID, + deviceInfo.deviceID, service.midiOutFactory, - midiOutPort); + midiPort); portThread.startThread(); portThread.waitForThreadToExit (-1); - if (midiOutPort == nullptr) + if (midiPort == nullptr) throw std::runtime_error ("Timed out waiting for midi output port creation"); auto bufferFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Storage_Streams_Buffer[0]); @@ -1158,24 +1613,28 @@ public: throw std::runtime_error ("Failed to get buffer data pointer"); } - ~WinRTOutputWrapper() {} - + //============================================================================== 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); - midiOutPort->SendBuffer (buffer); + midiPort->SendBuffer (buffer); } - String getDeviceName() override { return deviceName; } + String getDeviceName() override { return deviceInfo.name; } - String deviceName; - WinRTWrapper::ComPtr midiOutPort; + //============================================================================== WinRTWrapper::ComPtr buffer; WinRTWrapper::ComPtr bufferByteAccess; uint8_t* bufferData = nullptr; @@ -1188,25 +1647,53 @@ public: std::unique_ptr> inputDeviceWatcher; std::unique_ptr> outputDeviceWatcher; + std::unique_ptr bleDeviceWatcher; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService) }; #endif // JUCE_USE_WINRT_MIDI +//============================================================================== //============================================================================== struct MidiService : public DeletedAtShutdown { MidiService() { - #if JUCE_USE_WINRT_MIDI - try + #if JUCE_USE_WINRT_MIDI + #if ! JUCE_FORCE_WINRT_MIDI + auto windowsVersionInfo = [] { - internal.reset (new WinRTMidiService()); - return; - } - catch (std::runtime_error&) {} + 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()); }