mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
Includes support for communication with USB and Bluetooth devices, as well as virtual devices.
3176 lines
120 KiB
C++
3176 lines
120 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#ifndef DRV_QUERYDEVICEINTERFACE
|
|
#define DRV_RESERVED 0x0800
|
|
#define DRV_QUERYDEVICEINTERFACE (DRV_RESERVED + 12)
|
|
#define DRV_QUERYDEVICEINTERFACESIZE (DRV_RESERVED + 13)
|
|
#endif
|
|
|
|
namespace juce
|
|
{
|
|
|
|
#if JUCE_USE_WINDOWS_MIDI_SERVICES
|
|
|
|
namespace wm2 = winrt::Microsoft::Windows::Devices::Midi2;
|
|
namespace wm2v = wm2::Endpoints::Virtual;
|
|
namespace mwdmi = Microsoft::Windows::Devices::Midi2::Initialization;
|
|
|
|
class MidiServices
|
|
{
|
|
public:
|
|
[[nodiscard]] static std::unique_ptr<ump::Endpoints::Impl::Native> makeEndpoints (ump::EndpointsListener& l)
|
|
{
|
|
return EndpointsImplNative::make (l);
|
|
}
|
|
|
|
private:
|
|
/* For both input and output.
|
|
It's most resource-efficient to have only one connection to each endpoint.
|
|
Therefore, we keep track of endpoints we've opened, and share the endpoints between
|
|
Input and Output instances when possible.
|
|
*/
|
|
class SharedConnection : private AsyncUpdater
|
|
{
|
|
public:
|
|
template <typename... Args>
|
|
static std::unique_ptr<SharedConnection> make (const wm2::MidiSession& session,
|
|
const winrt::param::hstring& id,
|
|
Args&&... args)
|
|
{
|
|
auto connection = session.CreateEndpointConnection (id);
|
|
|
|
if (connection == nullptr)
|
|
return {};
|
|
|
|
setUpConnection (connection, args...);
|
|
|
|
auto result = rawToUniquePtr (new SharedConnection (session, std::move (connection)));
|
|
result->inputToken = result->connection.MessageReceived ([self = result.get()] (const auto&, const wm2::MidiMessageReceivedEventArgs& args)
|
|
{
|
|
std::array<uint32_t, 4> words{};
|
|
args.FillWordArray (0, words);
|
|
|
|
const ump::Iterator begin { words.data(), words.size() };
|
|
const auto end = std::next (begin);
|
|
|
|
const auto elapsedTime = args.Timestamp() - self->startTimeNative;
|
|
const auto juceTimeMillis = self->startTimeMillis + wm2::MidiClock::ConvertTimestampTicksToMilliseconds (elapsedTime);
|
|
|
|
self->consumers.call ([&] (auto& c) { c.consume (begin, end, juceTimeMillis * 0.001); });
|
|
});
|
|
|
|
result->disconnectToken = result->connection.EndpointDeviceDisconnected ([self = result.get()] (auto&&...)
|
|
{
|
|
self->triggerAsyncUpdate();
|
|
});
|
|
|
|
if (! result->connection.Open())
|
|
return {};
|
|
|
|
return result;
|
|
}
|
|
|
|
~SharedConnection()
|
|
{
|
|
connection.MessageReceived (inputToken);
|
|
connection.EndpointDeviceDisconnected (disconnectToken);
|
|
|
|
cancelPendingUpdate();
|
|
|
|
if (session != nullptr && connection != nullptr)
|
|
session.DisconnectEndpointConnection (connection.ConnectionId());
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const
|
|
{
|
|
const auto id = toString (connection.ConnectedEndpointDeviceId());
|
|
return ump::EndpointId::makeSrcDst (id, id);
|
|
}
|
|
|
|
void addConsumer (ump::Consumer& c)
|
|
{
|
|
consumers.add (c);
|
|
}
|
|
|
|
void removeConsumer (ump::Consumer& c)
|
|
{
|
|
consumers.remove (c);
|
|
}
|
|
|
|
void addDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.add (&l);
|
|
}
|
|
|
|
void removeDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.remove (&l);
|
|
}
|
|
|
|
bool send (ump::Iterator b, ump::Iterator e)
|
|
{
|
|
const auto result = connection.SendMultipleMessagesWordArray (0,
|
|
0,
|
|
(uint32_t) std::distance (b->data(), e->data()),
|
|
{ b->data(), e->data() });
|
|
return wm2::MidiSendMessageResults::Succeeded == result;
|
|
}
|
|
|
|
private:
|
|
SharedConnection (wm2::MidiSession s, wm2::MidiEndpointConnection c)
|
|
: session (std::move (s)), connection (std::move (c)) {}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
disconnectListeners.call ([] (auto& c) { c.disconnected(); });
|
|
}
|
|
|
|
static void setUpConnection (const wm2::MidiEndpointConnection&)
|
|
{
|
|
// Do nothing
|
|
}
|
|
|
|
static void setUpConnection (const wm2::MidiEndpointConnection& connection,
|
|
const wm2::IMidiEndpointMessageProcessingPlugin& plugin)
|
|
{
|
|
connection.AddMessageProcessingPlugin (plugin);
|
|
}
|
|
|
|
const uint64_t startTimeNative = wm2::MidiClock::Now();
|
|
const uint32_t startTimeMillis = Time::getMillisecondCounter();
|
|
|
|
wm2::MidiSession session;
|
|
wm2::MidiEndpointConnection connection;
|
|
WaitFreeListeners<ump::Consumer> consumers;
|
|
ListenerList<ump::DisconnectionListener> disconnectListeners;
|
|
winrt::event_token inputToken, disconnectToken;
|
|
};
|
|
|
|
class InputImplNative : public ump::Input::Impl::Native,
|
|
private ump::Consumer
|
|
{
|
|
public:
|
|
~InputImplNative() override
|
|
{
|
|
|
|
shared->removeDisconnectListener (listener);
|
|
shared->removeConsumer (*this);
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override
|
|
{
|
|
return shared->getEndpointId();
|
|
}
|
|
|
|
ump::PacketProtocol getProtocol() const override
|
|
{
|
|
return converter.getProtocol();
|
|
}
|
|
|
|
static std::unique_ptr<InputImplNative> make (std::shared_ptr<SharedConnection> x,
|
|
ump::DisconnectionListener& l,
|
|
ump::PacketProtocol protocol,
|
|
Consumer& consumer)
|
|
{
|
|
if (x == nullptr)
|
|
return {};
|
|
|
|
return rawToUniquePtr (new InputImplNative { x, l, protocol, consumer });
|
|
}
|
|
|
|
private:
|
|
InputImplNative (std::shared_ptr<SharedConnection> s,
|
|
ump::DisconnectionListener& l,
|
|
ump::PacketProtocol p,
|
|
Consumer& c)
|
|
: listener (l),
|
|
consumer (c),
|
|
shared (s),
|
|
converter (p)
|
|
{
|
|
shared->addConsumer (*this);
|
|
shared->addDisconnectListener (listener);
|
|
}
|
|
|
|
void consume (ump::Iterator b, ump::Iterator e, double time) override
|
|
{
|
|
converter.convert (b, e, [&] (ump::View v)
|
|
{
|
|
const ump::Iterator b { v.data(), v.size() };
|
|
consumer.consume (b, std::next (b), time);
|
|
});
|
|
}
|
|
|
|
ump::DisconnectionListener& listener;
|
|
Consumer& consumer;
|
|
std::shared_ptr<SharedConnection> shared;
|
|
ump::GenericUMPConverter converter;
|
|
};
|
|
|
|
class OutputImplNative : public ump::Output::Impl::Native
|
|
{
|
|
public:
|
|
~OutputImplNative() override
|
|
{
|
|
shared->removeDisconnectListener (listener);
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override
|
|
{
|
|
return shared->getEndpointId();
|
|
}
|
|
|
|
bool send (ump::Iterator b, ump::Iterator e) override
|
|
{
|
|
return shared->send (b, e);
|
|
}
|
|
|
|
static std::unique_ptr<OutputImplNative> make (std::shared_ptr<SharedConnection> c,
|
|
ump::DisconnectionListener& l)
|
|
{
|
|
if (c == nullptr)
|
|
return {};
|
|
|
|
return rawToUniquePtr (new OutputImplNative { std::move (c), l });
|
|
}
|
|
|
|
private:
|
|
OutputImplNative (std::shared_ptr<SharedConnection> c, ump::DisconnectionListener& l)
|
|
: listener (l), shared (std::move (c))
|
|
{
|
|
shared->addDisconnectListener (listener);
|
|
}
|
|
|
|
ump::DisconnectionListener& listener;
|
|
std::shared_ptr<SharedConnection> shared;
|
|
};
|
|
|
|
class VirtualEndpoint
|
|
{
|
|
public:
|
|
ump::EndpointId getId() const
|
|
{
|
|
const auto id = toString (device.DeviceEndpointDeviceId());
|
|
return ump::EndpointId::makeSrcDst (id, id);
|
|
}
|
|
|
|
ump::Endpoint getEndpoint() const
|
|
{
|
|
return endpoint;
|
|
}
|
|
|
|
ump::StaticDeviceInfo getStaticDeviceInfo() const
|
|
{
|
|
return staticInfo;
|
|
}
|
|
|
|
bool setBlock (uint8_t i, const ump::Block& b)
|
|
{
|
|
if (! device.UpdateFunctionBlock (makeBlock (i, b)))
|
|
return false;
|
|
|
|
endpoint.getBlocks()[i] = b;
|
|
return true;
|
|
}
|
|
|
|
bool setName (const String& x)
|
|
{
|
|
if (! device.UpdateEndpointName (x.toWideCharPointer()))
|
|
return false;
|
|
|
|
endpoint = endpoint.withName (x);
|
|
return true;
|
|
}
|
|
|
|
static std::unique_ptr<VirtualEndpoint> make (std::shared_ptr<SharedConnection> c,
|
|
wm2v::MidiVirtualDevice d,
|
|
ump::Endpoint ep,
|
|
ump::StaticDeviceInfo si)
|
|
{
|
|
if (d == nullptr)
|
|
return {};
|
|
|
|
return rawToUniquePtr (new VirtualEndpoint { std::move (c),
|
|
std::move (d),
|
|
std::move (ep),
|
|
std::move (si) });
|
|
}
|
|
|
|
private:
|
|
VirtualEndpoint (std::shared_ptr<SharedConnection> s,
|
|
wm2v::MidiVirtualDevice d,
|
|
ump::Endpoint ep,
|
|
ump::StaticDeviceInfo si)
|
|
: shared (std::move (s)),
|
|
device (std::move (d)),
|
|
endpoint (ep),
|
|
staticInfo (std::move (si))
|
|
{
|
|
}
|
|
|
|
std::shared_ptr<SharedConnection> shared;
|
|
wm2v::MidiVirtualDevice device;
|
|
ump::Endpoint endpoint;
|
|
ump::StaticDeviceInfo staticInfo;
|
|
};
|
|
|
|
struct VirtualEndpointRegistry
|
|
{
|
|
virtual ~VirtualEndpointRegistry() = default;
|
|
virtual void virtualEndpointAdded (std::shared_ptr<VirtualEndpoint>) = 0;
|
|
};
|
|
|
|
class VirtualEndpointImplNative : public ump::VirtualEndpoint::Impl::Native,
|
|
public ump::LegacyVirtualInput::Impl::Native,
|
|
public ump::LegacyVirtualOutput::Impl::Native
|
|
{
|
|
public:
|
|
ump::EndpointId getId() const override
|
|
{
|
|
return endpoint->getId();
|
|
}
|
|
|
|
bool setBlock (uint8_t i, const ump::Block& b) override
|
|
{
|
|
return endpoint->setBlock (i, b);
|
|
}
|
|
|
|
bool setName (const String& x) override
|
|
{
|
|
return endpoint->setName (x);
|
|
}
|
|
|
|
static std::unique_ptr<VirtualEndpointImplNative> make (std::shared_ptr<VirtualEndpoint> ep)
|
|
{
|
|
if (ep == nullptr)
|
|
return {};
|
|
|
|
return rawToUniquePtr (new VirtualEndpointImplNative { std::move (ep) });
|
|
}
|
|
|
|
private:
|
|
explicit VirtualEndpointImplNative (std::shared_ptr<VirtualEndpoint> ep)
|
|
: endpoint (std::move (ep)) {}
|
|
|
|
std::shared_ptr<VirtualEndpoint> endpoint;
|
|
};
|
|
|
|
class SessionImplNative : public ump::Session::Impl::Native
|
|
{
|
|
public:
|
|
~SessionImplNative() override
|
|
{
|
|
session.Close();
|
|
}
|
|
|
|
String getName() const override
|
|
{
|
|
return toString (session.Name());
|
|
}
|
|
|
|
std::unique_ptr<ump::Input::Impl::Native> connectInput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& id,
|
|
ump::PacketProtocol p,
|
|
ump::Consumer& consumer) override
|
|
{
|
|
const auto strong = findOrOpenConnection (id.src.toWideCharPointer());
|
|
return InputImplNative::make (strong, listener, p, consumer);
|
|
}
|
|
|
|
std::unique_ptr<ump::Output::Impl::Native> connectOutput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& id) override
|
|
{
|
|
const auto strong = findOrOpenConnection (id.dst.toWideCharPointer());
|
|
return OutputImplNative::make (strong, listener);
|
|
}
|
|
|
|
std::unique_ptr<ump::VirtualEndpoint::Impl::Native> createNativeVirtualEndpoint (const String& name,
|
|
const ump::DeviceInfo& info,
|
|
const String& productInstance,
|
|
ump::PacketProtocol protocol,
|
|
Span<const ump::Block> blocks,
|
|
ump::BlocksAreStatic areStatic) override
|
|
{
|
|
return createNativeVirtualEndpointImpl (name, info, productInstance, protocol, blocks, areStatic);
|
|
}
|
|
|
|
std::unique_ptr<ump::LegacyVirtualInput::Impl::Native> createLegacyVirtualInput (const String& name) override
|
|
{
|
|
const ump::Block blocks[] { ump::IOHelpers::makeLegacyBlock (true) };
|
|
return createNativeVirtualEndpointImpl (name, {}, {}, ump::PacketProtocol::MIDI_1_0, blocks, ump::BlocksAreStatic::yes);
|
|
}
|
|
|
|
std::unique_ptr<ump::LegacyVirtualOutput::Impl::Native> createLegacyVirtualOutput (const String& name) override
|
|
{
|
|
const ump::Block blocks[] { ump::IOHelpers::makeLegacyBlock (false) };
|
|
return createNativeVirtualEndpointImpl (name, {}, {}, ump::PacketProtocol::MIDI_1_0, blocks, ump::BlocksAreStatic::yes);
|
|
}
|
|
|
|
static std::unique_ptr<SessionImplNative> make (VirtualEndpointRegistry& r, const String& name)
|
|
{
|
|
if (auto s = wm2::MidiSession::Create (name.toWideCharPointer()))
|
|
return rawToUniquePtr (new SessionImplNative (r, s));
|
|
|
|
return {};
|
|
}
|
|
|
|
private:
|
|
SessionImplNative (VirtualEndpointRegistry& r, const wm2::MidiSession& s)
|
|
: registry (r), session (s) {}
|
|
|
|
std::unique_ptr<VirtualEndpointImplNative> createNativeVirtualEndpointImpl (const String& name,
|
|
const ump::DeviceInfo& info,
|
|
const String& productInstance,
|
|
ump::PacketProtocol protocol,
|
|
Span<const ump::Block> blocks,
|
|
ump::BlocksAreStatic areStatic)
|
|
{
|
|
wm2::MidiDeclaredEndpointInfo e;
|
|
e.Name = name.toWideCharPointer();
|
|
e.HasStaticFunctionBlocks = areStatic == ump::BlocksAreStatic::yes;
|
|
e.DeclaredFunctionBlockCount = (uint8_t) blocks.size();
|
|
e.ProductInstanceId = productInstance.toWideCharPointer();
|
|
e.SupportsMidi10Protocol = protocol == ump::PacketProtocol::MIDI_1_0;
|
|
e.SupportsMidi20Protocol = protocol == ump::PacketProtocol::MIDI_2_0;
|
|
e.SpecificationVersionMajor = 1;
|
|
e.SpecificationVersionMinor = 1;
|
|
e.SupportsReceivingJitterReductionTimestamps = false;
|
|
e.SupportsSendingJitterReductionTimestamps = false;
|
|
|
|
wm2v::MidiVirtualDeviceCreationConfig config { e.Name,
|
|
ump::Endpoints::Impl::getGlobalMidiClientName().toWideCharPointer(),
|
|
L"",
|
|
e,
|
|
makeDeviceInfo (info) };
|
|
|
|
for (const auto [index, value] : enumerate (blocks, uint8_t{}))
|
|
config.FunctionBlocks().Append (makeBlock (index, value));
|
|
|
|
auto device = wm2v::MidiVirtualDeviceManager::CreateVirtualDevice (config);
|
|
|
|
if (device == nullptr)
|
|
return {};
|
|
|
|
// In order to function, the device needs a client plugin installed, which in turn
|
|
// requires opening a connection to the endpoint.
|
|
auto connection = findOrOpenConnection (device.DeviceEndpointDeviceId(), device);
|
|
|
|
if (connection == nullptr)
|
|
return {};
|
|
|
|
auto endpoint = ump::Endpoint{}.withName (name)
|
|
.withDeviceInfo (info)
|
|
.withProductInstanceId (productInstance)
|
|
.withProtocol (protocol)
|
|
.withMidi1Support (protocol == ump::PacketProtocol::MIDI_1_0)
|
|
.withMidi2Support (protocol == ump::PacketProtocol::MIDI_2_0)
|
|
.withStaticBlocks (areStatic == ump::BlocksAreStatic::yes)
|
|
.withBlocks (blocks);
|
|
|
|
auto staticInfo = ump::StaticDeviceInfo{}.withName (name)
|
|
.withManufacturer ("")
|
|
.withProduct ("")
|
|
.withTransport (ump::Transport::ump)
|
|
.withHasSource (true)
|
|
.withHasDestination (true);
|
|
|
|
std::shared_ptr virtualEndpoint = VirtualEndpoint::make (std::move (connection),
|
|
std::move (device),
|
|
endpoint,
|
|
staticInfo);
|
|
|
|
if (virtualEndpoint == nullptr)
|
|
return {};
|
|
|
|
registry.virtualEndpointAdded (virtualEndpoint);
|
|
|
|
return VirtualEndpointImplNative::make (std::move (virtualEndpoint));
|
|
}
|
|
|
|
template <typename... Args>
|
|
std::shared_ptr<SharedConnection> findOrOpenConnection (const winrt::hstring& id,
|
|
Args&&... args)
|
|
{
|
|
auto& weak = weakConnections[toString (id)];
|
|
|
|
if (const auto strong = weak.lock())
|
|
return strong;
|
|
|
|
const std::shared_ptr strong = SharedConnection::make (session, id, args...);
|
|
weak = strong;
|
|
return strong;
|
|
}
|
|
|
|
VirtualEndpointRegistry& registry;
|
|
std::map<String, std::weak_ptr<SharedConnection>> weakConnections;
|
|
wm2::MidiSession session;
|
|
};
|
|
|
|
static wm2::MidiFunctionBlock makeBlock (uint8_t index, const ump::Block& b)
|
|
{
|
|
const auto direction = std::invoke ([&]
|
|
{
|
|
switch (b.getDirection())
|
|
{
|
|
case ump::BlockDirection::bidirectional: return wm2::MidiFunctionBlockDirection::Bidirectional;
|
|
case ump::BlockDirection::sender: return wm2::MidiFunctionBlockDirection::BlockOutput;
|
|
case ump::BlockDirection::receiver: return wm2::MidiFunctionBlockDirection::BlockInput;
|
|
case ump::BlockDirection::unknown: return wm2::MidiFunctionBlockDirection::Undefined;
|
|
}
|
|
|
|
return wm2::MidiFunctionBlockDirection{};
|
|
});
|
|
|
|
const auto hint = std::invoke ([&]
|
|
{
|
|
switch (b.getUiHint())
|
|
{
|
|
case ump::BlockUiHint::bidirectional: return wm2::MidiFunctionBlockUIHint::Bidirectional;
|
|
case ump::BlockUiHint::sender: return wm2::MidiFunctionBlockUIHint::Sender;
|
|
case ump::BlockUiHint::receiver: return wm2::MidiFunctionBlockUIHint::Receiver;
|
|
case ump::BlockUiHint::unknown: return wm2::MidiFunctionBlockUIHint::Unknown;
|
|
}
|
|
|
|
return wm2::MidiFunctionBlockUIHint{};
|
|
});
|
|
|
|
const auto proxy = std::invoke ([&]
|
|
{
|
|
switch (b.getMIDI1ProxyKind())
|
|
{
|
|
case ump::BlockMIDI1ProxyKind::inapplicable: return wm2::MidiFunctionBlockRepresentsMidi10Connection::Not10;
|
|
case ump::BlockMIDI1ProxyKind::restrictedBandwidth: return wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthRestricted;
|
|
case ump::BlockMIDI1ProxyKind::unrestrictedBandwidth: return wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthUnrestricted;
|
|
}
|
|
|
|
return wm2::MidiFunctionBlockRepresentsMidi10Connection{};
|
|
});
|
|
|
|
wm2::MidiFunctionBlock result;
|
|
result.Name (b.getName().toWideCharPointer());
|
|
result.Number (index);
|
|
result.IsActive (b.isEnabled());
|
|
result.FirstGroup (wm2::MidiGroup { b.getFirstGroup() });
|
|
result.GroupCount (b.getNumGroups());
|
|
result.MaxSystemExclusive8Streams (b.getMaxSysex8Streams());
|
|
result.Direction (direction);
|
|
result.UIHint (hint);
|
|
result.RepresentsMidi10Connection (proxy);
|
|
|
|
return result;
|
|
}
|
|
|
|
struct IndexedBlock
|
|
{
|
|
uint8_t index;
|
|
ump::Block block;
|
|
};
|
|
|
|
static IndexedBlock makeBlock (const wm2::MidiFunctionBlock& b)
|
|
{
|
|
const auto index = b.Number();
|
|
|
|
const auto direction = std::invoke ([&]
|
|
{
|
|
switch (b.Direction())
|
|
{
|
|
case wm2::MidiFunctionBlockDirection::Bidirectional: return ump::BlockDirection::bidirectional;
|
|
case wm2::MidiFunctionBlockDirection::BlockOutput: return ump::BlockDirection::sender;
|
|
case wm2::MidiFunctionBlockDirection::BlockInput: return ump::BlockDirection::receiver;
|
|
case wm2::MidiFunctionBlockDirection::Undefined: return ump::BlockDirection::unknown;
|
|
}
|
|
|
|
return ump::BlockDirection{};
|
|
});
|
|
|
|
const auto hint = std::invoke ([&]
|
|
{
|
|
switch (b.UIHint())
|
|
{
|
|
case wm2::MidiFunctionBlockUIHint::Bidirectional: return ump::BlockUiHint::bidirectional;
|
|
case wm2::MidiFunctionBlockUIHint::Sender: return ump::BlockUiHint::sender;
|
|
case wm2::MidiFunctionBlockUIHint::Receiver: return ump::BlockUiHint::receiver;
|
|
case wm2::MidiFunctionBlockUIHint::Unknown: return ump::BlockUiHint::unknown;
|
|
}
|
|
|
|
return ump::BlockUiHint{};
|
|
});
|
|
|
|
const auto proxy = std::invoke ([&]
|
|
{
|
|
switch (b.RepresentsMidi10Connection())
|
|
{
|
|
case wm2::MidiFunctionBlockRepresentsMidi10Connection::Not10: return ump::BlockMIDI1ProxyKind::inapplicable;
|
|
case wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthRestricted: return ump::BlockMIDI1ProxyKind::restrictedBandwidth;
|
|
case wm2::MidiFunctionBlockRepresentsMidi10Connection::YesBandwidthUnrestricted: return ump::BlockMIDI1ProxyKind::unrestrictedBandwidth;
|
|
case wm2::MidiFunctionBlockRepresentsMidi10Connection::Reserved: break;
|
|
}
|
|
|
|
return ump::BlockMIDI1ProxyKind{};
|
|
});
|
|
|
|
const auto block = ump::Block{}.withDirection (direction)
|
|
.withUiHint (hint)
|
|
.withMIDI1ProxyKind (proxy)
|
|
.withFirstGroup (b.FirstGroup().Index())
|
|
.withNumGroups (b.GroupCount())
|
|
.withEnabled (b.IsActive())
|
|
.withName (toString (b.Name()))
|
|
.withMaxSysex8Streams (b.MaxSystemExclusive8Streams());
|
|
|
|
return { index, block };
|
|
}
|
|
|
|
static wm2::MidiDeclaredDeviceIdentity makeDeviceInfo (ump::DeviceInfo x)
|
|
{
|
|
wm2::MidiDeclaredDeviceIdentity result{};
|
|
|
|
result.SystemExclusiveIdByte1 = (uint8_t) x.manufacturer[0];
|
|
result.SystemExclusiveIdByte2 = (uint8_t) x.manufacturer[1];
|
|
result.SystemExclusiveIdByte3 = (uint8_t) x.manufacturer[2];
|
|
|
|
result.DeviceFamilyLsb = (uint8_t) x.family[0];
|
|
result.DeviceFamilyMsb = (uint8_t) x.family[1];
|
|
|
|
result.DeviceFamilyModelNumberLsb = (uint8_t) x.modelNumber[0];
|
|
result.DeviceFamilyModelNumberMsb = (uint8_t) x.modelNumber[1];
|
|
|
|
result.SoftwareRevisionLevelByte1 = (uint8_t) x.revision[0];
|
|
result.SoftwareRevisionLevelByte2 = (uint8_t) x.revision[1];
|
|
result.SoftwareRevisionLevelByte3 = (uint8_t) x.revision[2];
|
|
result.SoftwareRevisionLevelByte4 = (uint8_t) x.revision[3];
|
|
|
|
return result;
|
|
}
|
|
|
|
static ump::DeviceInfo makeDeviceInfo (const wm2::MidiDeclaredDeviceIdentity& x)
|
|
{
|
|
return ump::DeviceInfo
|
|
{
|
|
{ std::byte (x.SystemExclusiveIdByte1),
|
|
std::byte (x.SystemExclusiveIdByte2),
|
|
std::byte (x.SystemExclusiveIdByte3) },
|
|
|
|
{ std::byte (x.DeviceFamilyLsb),
|
|
std::byte (x.DeviceFamilyMsb) },
|
|
|
|
{ std::byte (x.DeviceFamilyModelNumberLsb),
|
|
std::byte (x.DeviceFamilyModelNumberMsb) },
|
|
|
|
{ std::byte (x.SoftwareRevisionLevelByte1),
|
|
std::byte (x.SoftwareRevisionLevelByte2),
|
|
std::byte (x.SoftwareRevisionLevelByte3),
|
|
std::byte (x.SoftwareRevisionLevelByte4) },
|
|
};
|
|
}
|
|
|
|
class EndpointsImplNative : public ump::Endpoints::Impl::Native,
|
|
private VirtualEndpointRegistry,
|
|
private AsyncUpdater
|
|
{
|
|
public:
|
|
~EndpointsImplNative() override
|
|
{
|
|
watcher.Stop();
|
|
cancelPendingUpdate();
|
|
}
|
|
|
|
ump::Backend getBackend() const
|
|
{
|
|
return ump::Backend::wms;
|
|
}
|
|
|
|
bool isVirtualMidiUmpServiceActive() const override
|
|
{
|
|
return wm2v::MidiVirtualDeviceManager::IsTransportAvailable();
|
|
}
|
|
|
|
bool isVirtualMidiBytestreamServiceActive() const override
|
|
{
|
|
return wm2v::MidiVirtualDeviceManager::IsTransportAvailable();
|
|
}
|
|
|
|
void getEndpoints (std::vector<ump::EndpointId>& buffer) const override
|
|
{
|
|
std::transform (cachedEndpoints.begin(),
|
|
cachedEndpoints.end(),
|
|
std::back_inserter (buffer),
|
|
[] (auto& pair) { return pair.first; });
|
|
}
|
|
|
|
std::optional<ump::Endpoint> getEndpoint (const ump::EndpointId& id) const override
|
|
{
|
|
if (const auto iter = virtualEndpoints.find (id); iter != virtualEndpoints.end())
|
|
if (auto strong = iter->second.lock())
|
|
return strong->getEndpoint();
|
|
|
|
if (const auto iter = cachedEndpoints.find (id); iter != cachedEndpoints.end())
|
|
return iter->second.endpoint;
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<ump::StaticDeviceInfo> getStaticDeviceInfo (const ump::EndpointId& id) const override
|
|
{
|
|
if (const auto iter = virtualEndpoints.find (id); iter != virtualEndpoints.end())
|
|
if (auto strong = iter->second.lock())
|
|
return strong->getStaticDeviceInfo();
|
|
|
|
if (const auto iter = cachedEndpoints.find (id); iter != cachedEndpoints.end())
|
|
return iter->second.info;
|
|
|
|
return {};
|
|
}
|
|
|
|
std::unique_ptr<ump::Session::Impl::Native> makeSession (const String& name) override
|
|
{
|
|
return SessionImplNative::make (*this, name);
|
|
}
|
|
|
|
static std::unique_ptr<EndpointsImplNative> make (ump::EndpointsListener& l)
|
|
{
|
|
SharedResourcePointer<SdkInitialiser> initialiser;
|
|
|
|
if (! initialiser->isValid())
|
|
{
|
|
// If you hit this, you've tried to initialise Windows MIDI Services but the
|
|
// initialisation failed. Did you forget to install the Windows MIDI Services SDK?
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
auto watcher = wm2::MidiEndpointDeviceWatcher::Create();
|
|
|
|
if (! watcher)
|
|
return {};
|
|
|
|
return rawToUniquePtr (new EndpointsImplNative { watcher, l });
|
|
}
|
|
|
|
private:
|
|
EndpointsImplNative (wm2::MidiEndpointDeviceWatcher w, ump::EndpointsListener& l)
|
|
: listener (l), watcher (w)
|
|
{
|
|
watcher.Added ([this] (auto&, const wm2::MidiEndpointDeviceInformationAddedEventArgs& args)
|
|
{
|
|
const auto device = args.AddedDevice();
|
|
const auto id = toString (device.EndpointDeviceId());
|
|
const auto endpoint = makeEndpoint (device);
|
|
|
|
const std::scoped_lock lock { mutex };
|
|
pendingWork.push_back ([this, id, endpoint]
|
|
{
|
|
cachedEndpoints.insert_or_assign (ump::EndpointId::makeSrcDst (id, id), endpoint);
|
|
});
|
|
|
|
triggerAsyncUpdate();
|
|
});
|
|
|
|
watcher.Updated ([this] (auto&, const wm2::MidiEndpointDeviceInformationUpdatedEventArgs& args)
|
|
{
|
|
const auto id = toString (args.EndpointDeviceId());
|
|
|
|
if (const auto info = wm2::MidiEndpointDeviceInformation::CreateFromEndpointDeviceId (args.EndpointDeviceId()))
|
|
{
|
|
const auto endpoint = makeEndpoint (info);
|
|
|
|
const std::scoped_lock lock { mutex };
|
|
pendingWork.push_back ([this, id, endpoint]
|
|
{
|
|
cachedEndpoints.insert_or_assign (ump::EndpointId::makeSrcDst (id, id), endpoint);
|
|
});
|
|
|
|
triggerAsyncUpdate();
|
|
}
|
|
});
|
|
|
|
watcher.Removed ([this] (auto&, const wm2::MidiEndpointDeviceInformationRemovedEventArgs& args)
|
|
{
|
|
const auto id = toString (args.EndpointDeviceId());
|
|
|
|
const std::scoped_lock lock { mutex };
|
|
pendingWork.push_back ([this, id]
|
|
{
|
|
cachedEndpoints.erase (ump::EndpointId::makeSrcDst (id, id));
|
|
});
|
|
|
|
triggerAsyncUpdate();
|
|
});
|
|
|
|
watcher.EnumerationCompleted ([this] (auto&, auto&)
|
|
{
|
|
const std::scoped_lock lock { mutex };
|
|
pendingWork.push_back ([this]
|
|
{
|
|
const auto devices = watcher.EnumeratedEndpointDevices();
|
|
|
|
for (const auto& [id, device] : devices)
|
|
{
|
|
auto endpoint = makeEndpoint (device);
|
|
cachedEndpoints.insert_or_assign (endpoint.id, endpoint);
|
|
}
|
|
});
|
|
|
|
triggerAsyncUpdate();
|
|
});
|
|
|
|
watcher.Start();
|
|
}
|
|
|
|
void virtualEndpointAdded (std::shared_ptr<VirtualEndpoint> ep) override
|
|
{
|
|
if (ep != nullptr)
|
|
virtualEndpoints[ep->getId()] = ep;
|
|
}
|
|
|
|
std::function<void()> popWork()
|
|
{
|
|
const std::scoped_lock lock { mutex };
|
|
|
|
if (pendingWork.empty())
|
|
return {};
|
|
|
|
auto result = pendingWork.front();
|
|
pendingWork.pop_front();
|
|
return result;
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
while (auto fn = popWork())
|
|
fn();
|
|
|
|
listener.endpointsChanged();
|
|
}
|
|
|
|
static ump::EndpointAndStaticInfo makeEndpoint (const wm2::MidiEndpointDeviceInformation& info)
|
|
{
|
|
const auto transport = std::invoke ([&]
|
|
{
|
|
const auto t = info.GetTransportSuppliedInfo().NativeDataFormat;
|
|
|
|
if (t == wm2::MidiEndpointNativeDataFormat::Midi1ByteFormat)
|
|
return ump::Transport::bytestream;
|
|
|
|
return ump::Transport::ump;
|
|
});
|
|
|
|
const auto itemProtocol = std::invoke ([&]
|
|
{
|
|
const auto p = info.GetDeclaredStreamConfiguration().Protocol;
|
|
|
|
if (p == wm2::MidiProtocol::Midi1 || transport == ump::Transport::bytestream)
|
|
return ump::PacketProtocol::MIDI_1_0;
|
|
|
|
return ump::PacketProtocol::MIDI_2_0;
|
|
});
|
|
|
|
const auto deviceInfo = makeDeviceInfo (info.GetDeclaredDeviceIdentity());
|
|
|
|
const auto id = toString (info.EndpointDeviceId());
|
|
|
|
std::array<String, 16> legacyIds;
|
|
|
|
for (auto [index, value] : enumerate (legacyIds))
|
|
{
|
|
auto obj = new DynamicObject;
|
|
obj->setProperty ("endpoint", id);
|
|
obj->setProperty ("group", index);
|
|
value = JSON::toString (obj, true);
|
|
}
|
|
|
|
std::vector<ump::Block> blocks;
|
|
|
|
for (const auto& fb : info.GetDeclaredFunctionBlocks())
|
|
blocks.push_back (makeBlock (fb).block);
|
|
|
|
if (blocks.empty())
|
|
{
|
|
for (const auto& gtb : info.GetGroupTerminalBlocks())
|
|
blocks.push_back (makeBlock (gtb.AsEquivalentFunctionBlock()).block);
|
|
}
|
|
|
|
const auto e = info.GetDeclaredEndpointInfo();
|
|
const auto manufacturer = info.GetContainerDeviceInformation()
|
|
.Properties()
|
|
.TryLookup (L"System.Devices.Manufacturer");
|
|
const auto product = info.GetContainerDeviceInformation()
|
|
.Properties()
|
|
.TryLookup (L"System.Devices.ModelName");
|
|
|
|
const auto endpoint = ump::Endpoint{}.withName (toString (info.Name()))
|
|
.withProtocol (itemProtocol)
|
|
.withBlocks (blocks)
|
|
.withDeviceInfo (deviceInfo)
|
|
.withProductInstanceId (toString (info.GetDeclaredEndpointInfo().ProductInstanceId))
|
|
.withUMPVersion (e.SpecificationVersionMajor, e.SpecificationVersionMinor)
|
|
.withMidi1Support (e.SupportsMidi10Protocol)
|
|
.withMidi2Support (e.SupportsMidi20Protocol)
|
|
.withStaticBlocks (e.HasStaticFunctionBlocks)
|
|
.withReceiveJRSupport (e.SupportsReceivingJitterReductionTimestamps)
|
|
.withTransmitJRSupport (e.SupportsSendingJitterReductionTimestamps);
|
|
|
|
const auto hasBlockDirection = [&] (auto direction)
|
|
{
|
|
const auto blockCanUseDirection = [&] (const wm2::MidiFunctionBlock& x)
|
|
{
|
|
const auto d = x.Direction();
|
|
return d == wm2::MidiFunctionBlockDirection::Bidirectional || d == direction;
|
|
};
|
|
|
|
const auto fb = info.GetDeclaredFunctionBlocks();
|
|
const auto gt = info.GetGroupTerminalBlocks();
|
|
|
|
return std::any_of (fb.begin(), fb.end(), blockCanUseDirection)
|
|
|| std::any_of (gt.begin(), gt.end(), [&] (const wm2::MidiGroupTerminalBlock& x)
|
|
{
|
|
return blockCanUseDirection (x.AsEquivalentFunctionBlock());
|
|
});
|
|
};
|
|
|
|
const auto staticInfo = ump::StaticDeviceInfo{}.withName (toString (info.Name()))
|
|
.withManufacturer (toString (winrt::unbox_value_or<winrt::hstring> (manufacturer, L"")))
|
|
.withProduct (toString (winrt::unbox_value_or<winrt::hstring> (product, L"")))
|
|
.withHasSource (hasBlockDirection (wm2::MidiFunctionBlockDirection::BlockOutput))
|
|
.withHasDestination (hasBlockDirection (wm2::MidiFunctionBlockDirection::BlockInput))
|
|
.withLegacyIdentifiersSrc (legacyIds)
|
|
.withLegacyIdentifiersDst (legacyIds)
|
|
.withTransport (transport);
|
|
|
|
return { endpoint, staticInfo, ump::EndpointId::makeSrcDst (id, id) };
|
|
}
|
|
|
|
class SdkInitialiser
|
|
{
|
|
public:
|
|
SdkInitialiser() = default;
|
|
|
|
bool isValid() const { return ptr != nullptr; }
|
|
|
|
private:
|
|
ComSmartPtr<mwdmi::IMidiClientInitializer> ptr = std::invoke ([]() -> ComSmartPtr<mwdmi::IMidiClientInitializer>
|
|
{
|
|
try
|
|
{
|
|
winrt::init_apartment (winrt::apartment_type::single_threaded);
|
|
}
|
|
catch (...)
|
|
{
|
|
// We tried...
|
|
}
|
|
|
|
ComSmartPtr<mwdmi::IMidiClientInitializer> result;
|
|
|
|
if (FAILED (CoCreateInstance (__uuidof (mwdmi::MidiClientInitializerUuid),
|
|
nullptr,
|
|
CLSCTX::CLSCTX_INPROC_SERVER | CLSCTX::CLSCTX_FROM_DEFAULT_CONTEXT,
|
|
__uuidof (mwdmi::IMidiClientInitializer),
|
|
(void**) result.resetAndGetPointerAddress())))
|
|
return {};
|
|
|
|
if (result == nullptr)
|
|
return {};
|
|
|
|
if (FAILED (result->EnsureServiceAvailable()))
|
|
return {};
|
|
|
|
return result;
|
|
});
|
|
};
|
|
|
|
SharedResourcePointer<SdkInitialiser> initialiser;
|
|
|
|
std::mutex mutex;
|
|
std::deque<std::function<void()>> pendingWork;
|
|
|
|
ump::EndpointsListener& listener;
|
|
std::map<ump::EndpointId, ump::EndpointAndStaticInfo> cachedEndpoints;
|
|
std::map<ump::EndpointId, std::weak_ptr<VirtualEndpoint>> virtualEndpoints;
|
|
wm2::MidiEndpointDeviceWatcher watcher;
|
|
};
|
|
|
|
static String toString (const winrt::hstring& str)
|
|
{
|
|
return CharPointer_UTF16 { str.data() };
|
|
}
|
|
|
|
MidiServices() = delete;
|
|
};
|
|
|
|
#endif
|
|
|
|
RTL_OSVERSIONINFOW getWindowsVersionInfo();
|
|
|
|
struct WindowsMidiHelpers
|
|
{
|
|
#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
|
|
|
|
struct Winrt
|
|
{
|
|
using DeviceInformation = ABI::Windows::Devices::Enumeration::DeviceInformation;
|
|
using DeviceInformationKind = ABI::Windows::Devices::Enumeration::DeviceInformationKind;
|
|
using DeviceInformationUpdate = ABI::Windows::Devices::Enumeration::DeviceInformationUpdate;
|
|
using DeviceWatcher = ABI::Windows::Devices::Enumeration::DeviceWatcher;
|
|
using IBuffer = ABI::Windows::Storage::Streams::IBuffer;
|
|
using IBufferFactory = ABI::Windows::Storage::Streams::IBufferFactory;
|
|
using IBufferByteAccess = Windows::Storage::Streams::IBufferByteAccess;
|
|
using IDeviceInformation = ABI::Windows::Devices::Enumeration::IDeviceInformation;
|
|
using IDeviceInformationStatics2 = ABI::Windows::Devices::Enumeration::IDeviceInformationStatics2;
|
|
using IDeviceInformationUpdate = ABI::Windows::Devices::Enumeration::IDeviceInformationUpdate;
|
|
using IDevicePicker = ABI::Windows::Devices::Enumeration::IDevicePicker;
|
|
using IDeviceWatcher = ABI::Windows::Devices::Enumeration::IDeviceWatcher;
|
|
using IMidiInPort = ABI::Windows::Devices::Midi::IMidiInPort;
|
|
using IMidiInPortStatics = ABI::Windows::Devices::Midi::IMidiInPortStatics;
|
|
using IMidiMessage = ABI::Windows::Devices::Midi::IMidiMessage;
|
|
using IMidiMessageReceivedEventArgs = ABI::Windows::Devices::Midi::IMidiMessageReceivedEventArgs;
|
|
using IMidiOutPort = ABI::Windows::Devices::Midi::IMidiOutPort;
|
|
using IMidiOutPortStatics = ABI::Windows::Devices::Midi::IMidiOutPortStatics;
|
|
using MidiInPort = ABI::Windows::Devices::Midi::MidiInPort;
|
|
using MidiMessageReceivedEventArgs = ABI::Windows::Devices::Midi::MidiMessageReceivedEventArgs;
|
|
|
|
template <typename T, typename U> using ITypedEventHandler = ABI::Windows::Foundation::ITypedEventHandler<T, U>;
|
|
template <typename T> using IAsyncOperation = ABI::Windows::Foundation::IAsyncOperation<T>;
|
|
template <typename T> using IAsyncOperationCompletedHandler = ABI::Windows::Foundation::IAsyncOperationCompletedHandler<T>;
|
|
template <typename T> using IIterable = ABI::Windows::Foundation::Collections::IIterable<T>;
|
|
template <typename T> using IReference = ABI::Windows::Foundation::IReference<T>;
|
|
template <typename T> using IVector = ABI::Windows::Foundation::Collections::IVector<T>;
|
|
|
|
class DeviceCallbackHandler
|
|
{
|
|
public:
|
|
struct Delegate
|
|
{
|
|
virtual ~Delegate() = default;
|
|
virtual HRESULT addDevice (IDeviceInformation*) = 0;
|
|
virtual HRESULT removeDevice (IDeviceInformationUpdate*) = 0;
|
|
virtual HRESULT updateDevice (IDeviceInformationUpdate*) = 0;
|
|
};
|
|
|
|
static std::unique_ptr<DeviceCallbackHandler> make (Delegate* delegate,
|
|
HSTRING deviceSelector,
|
|
DeviceInformationKind infoKind)
|
|
{
|
|
auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!");
|
|
return {};
|
|
}
|
|
|
|
auto deviceInfoFactory = wrtWrapper->getWRLFactory<IDeviceInformationStatics2> (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]);
|
|
|
|
if (deviceInfoFactory == nullptr)
|
|
return {};
|
|
|
|
// A quick way of getting an IVector<HSTRING>...
|
|
auto requestedProperties = [wrtWrapper]
|
|
{
|
|
auto devicePicker = wrtWrapper->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 {};
|
|
}
|
|
}
|
|
|
|
ComSmartPtr<IIterable<HSTRING>> iter;
|
|
auto hr = requestedProperties->QueryInterface (__uuidof (IIterable<HSTRING>), (void**) iter.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
auto result = rawToUniquePtr (new DeviceCallbackHandler);
|
|
result->delegate = delegate;
|
|
|
|
hr = deviceInfoFactory->CreateWatcherWithKindAqsFilterAndAdditionalProperties (deviceSelector, iter, infoKind,
|
|
result->watcher.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
result->thread = std::thread { [r = result.get()]
|
|
{
|
|
Thread::setCurrentThreadName (SystemStats::getJUCEVersion() + ": WinRT Device Enumeration Thread");
|
|
|
|
r->watcher->add_Added (
|
|
Microsoft::WRL::Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>> (
|
|
[r] (IDeviceWatcher*, IDeviceInformation* info) { return r->delegate->addDevice (info); }
|
|
).Get(),
|
|
&r->deviceAddedToken);
|
|
|
|
r->watcher->add_Removed (
|
|
Microsoft::WRL::Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
|
|
[r] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return r->delegate->removeDevice (infoUpdate); }
|
|
).Get(),
|
|
&r->deviceRemovedToken);
|
|
|
|
r->watcher->add_Updated (
|
|
Microsoft::WRL::Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>> (
|
|
[r] (IDeviceWatcher*, IDeviceInformationUpdate* infoUpdate) { return r->delegate->updateDevice (infoUpdate); }
|
|
).Get(),
|
|
&r->deviceUpdatedToken);
|
|
|
|
r->watcher->Start();
|
|
} };
|
|
|
|
return result;
|
|
}
|
|
|
|
~DeviceCallbackHandler()
|
|
{
|
|
if (thread.joinable())
|
|
thread.join();
|
|
|
|
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)
|
|
{
|
|
ComSmartPtr<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)
|
|
{
|
|
ComSmartPtr<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:
|
|
DeviceCallbackHandler() = default;
|
|
|
|
Delegate* delegate = nullptr;
|
|
ComSmartPtr<IDeviceWatcher> watcher;
|
|
EventRegistrationToken deviceAddedToken { 0 }, deviceRemovedToken { 0 }, deviceUpdatedToken { 0 };
|
|
std::thread thread;
|
|
};
|
|
|
|
struct BLEDeviceWatcherListener
|
|
{
|
|
virtual ~BLEDeviceWatcherListener() = default;
|
|
virtual void bleDeviceAdded (const String& containerID) = 0;
|
|
virtual void bleDeviceDisconnected (const String& containerID) = 0;
|
|
};
|
|
|
|
class BLEDeviceWatcher : private DeviceCallbackHandler::Delegate
|
|
{
|
|
public:
|
|
static std::unique_ptr<BLEDeviceWatcher> make()
|
|
{
|
|
WinRTWrapper::ScopedHString deviceSelector ("System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\""
|
|
" AND System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True");
|
|
|
|
auto result = rawToUniquePtr (new BLEDeviceWatcher);
|
|
|
|
if (auto watcher = DeviceCallbackHandler::make (result.get(), deviceSelector.get(), DeviceInformationKind::DeviceInformationKind_AssociationEndpoint))
|
|
{
|
|
result->watcher = std::move (watcher);
|
|
return result;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool isBleDevice (const String& containerId) const
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
return bleContainerIds.find (containerId) != bleContainerIds.end();
|
|
}
|
|
|
|
void addListener (BLEDeviceWatcherListener& l)
|
|
{
|
|
listeners.add (l);
|
|
}
|
|
|
|
void removeListener (BLEDeviceWatcherListener& l)
|
|
{
|
|
listeners.remove (l);
|
|
}
|
|
|
|
private:
|
|
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* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!");
|
|
return false;
|
|
}
|
|
|
|
auto deviceID = wrtWrapper->hStringToString (deviceIDHst);
|
|
JUCE_WINRT_MIDI_LOG ("Detected paired BLE device: " << deviceID);
|
|
|
|
if (auto* containerIDValue = watcher->getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", addedDeviceInfo))
|
|
{
|
|
auto containerID = watcher->getGUIDFromInspectable (*containerIDValue);
|
|
|
|
if (containerID.isNotEmpty())
|
|
{
|
|
listeners.call ([&containerID] (auto& l) { l.bleDeviceAdded (containerID); });
|
|
|
|
const ScopedLock lock (deviceChanges);
|
|
bleContainerIds.insert (containerID);
|
|
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
|
|
{
|
|
if (auto* containerIDValue = watcher->getValueFromDeviceInfo ("System.Devices.Aep.ContainerId", removedDeviceInfo))
|
|
{
|
|
auto containerID = watcher->getGUIDFromInspectable (*containerIDValue);
|
|
|
|
if (containerID.isNotEmpty())
|
|
{
|
|
listeners.call ([&containerID] (auto& l) { l.bleDeviceDisconnected (containerID); });
|
|
|
|
const ScopedLock lock (deviceChanges);
|
|
bleContainerIds.erase (containerID);
|
|
return S_OK;
|
|
}
|
|
}
|
|
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT updateDevice (IDeviceInformationUpdate*) override
|
|
{
|
|
// This shouldn't change the device container
|
|
return S_OK;
|
|
}
|
|
|
|
BLEDeviceWatcher() = default;
|
|
|
|
WaitFreeListeners<BLEDeviceWatcherListener> listeners;
|
|
std::set<String> bleContainerIds;
|
|
CriticalSection deviceChanges;
|
|
std::unique_ptr<DeviceCallbackHandler> watcher;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BLEDeviceWatcher)
|
|
};
|
|
|
|
struct WinRTMIDIDeviceInfo
|
|
{
|
|
String deviceID, containerID, name;
|
|
bool isDefault = false;
|
|
};
|
|
|
|
struct MidiIODeviceWatcherListener
|
|
{
|
|
virtual ~MidiIODeviceWatcherListener() = default;
|
|
virtual void deviceAdded (const String&) = 0;
|
|
virtual void deviceRemoved (const String&) = 0;
|
|
virtual void deviceUpdated (const String&) = 0;
|
|
};
|
|
|
|
template <typename COMFactoryType>
|
|
class MidiIODeviceWatcher : private DeviceCallbackHandler::Delegate
|
|
{
|
|
public:
|
|
void getAvailableDevices (std::vector<ump::EndpointAndStaticInfo>& storage)
|
|
{
|
|
{
|
|
const ScopedLock lock (deviceChanges);
|
|
lastQueriedConnectedDevices = connectedDevices;
|
|
}
|
|
|
|
StringArray deviceNames, deviceIDs;
|
|
|
|
for (auto info : lastQueriedConnectedDevices.get())
|
|
{
|
|
deviceNames.add (info.name);
|
|
deviceIDs .add (info.containerID);
|
|
}
|
|
|
|
deviceNames.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
|
|
deviceIDs .appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
|
|
|
|
constexpr auto direction = std::invoke ([]
|
|
{
|
|
if constexpr (std::is_same_v<COMFactoryType, IMidiInPortStatics>)
|
|
return ump::BlockDirection::sender;
|
|
else if constexpr (std::is_same_v<COMFactoryType, IMidiOutPortStatics>)
|
|
return ump::BlockDirection::receiver;
|
|
});
|
|
|
|
for (int i = 0; i < deviceNames.size(); ++i)
|
|
{
|
|
const auto fullInfo = ump::IOHelpers::makeProxyEndpoint (MidiDeviceInfo { deviceNames[i], deviceIDs[i] }, direction);
|
|
storage.push_back (fullInfo);
|
|
}
|
|
}
|
|
|
|
WinRTMIDIDeviceInfo getWinRTDeviceInfoForDevice (const ump::EndpointId& deviceIdentifier)
|
|
{
|
|
std::vector<ump::EndpointAndStaticInfo> endpoints;
|
|
getAvailableDevices (endpoints);
|
|
|
|
for (const auto [index, value] : enumerate (endpoints))
|
|
if (value.id == deviceIdentifier)
|
|
return lastQueriedConnectedDevices.get()[(int) index];
|
|
|
|
return {};
|
|
}
|
|
|
|
static std::unique_ptr<MidiIODeviceWatcher> make (ComSmartPtr<COMFactoryType> f)
|
|
{
|
|
HSTRING deviceSelector;
|
|
auto hr = f->GetDeviceSelector (&deviceSelector);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get MIDI device selector!");
|
|
return {};
|
|
}
|
|
|
|
auto result = rawToUniquePtr (new MidiIODeviceWatcher);
|
|
result->factory = f;
|
|
|
|
if (auto watcher = DeviceCallbackHandler::make (result.get(), deviceSelector, DeviceInformationKind::DeviceInformationKind_DeviceInterface))
|
|
{
|
|
result->watcher = std::move (watcher);
|
|
return result;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void addListener (MidiIODeviceWatcherListener& l)
|
|
{
|
|
listeners.add (l);
|
|
}
|
|
|
|
void removeListener (MidiIODeviceWatcherListener& l)
|
|
{
|
|
listeners.remove (l);
|
|
}
|
|
|
|
private:
|
|
MidiIODeviceWatcher() = default;
|
|
|
|
HRESULT addDevice (IDeviceInformation* addedDeviceInfo) override
|
|
{
|
|
WinRTMIDIDeviceInfo 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;
|
|
}
|
|
|
|
auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!");
|
|
return false;
|
|
}
|
|
|
|
info.deviceID = wrtWrapper->hStringToString (deviceID);
|
|
|
|
listeners.call ([&] (auto& l) { l.deviceAdded (info.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 = watcher->getValueFromDeviceInfo ("System.Devices.ContainerId", addedDeviceInfo))
|
|
info.containerID = watcher->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 = wrtWrapper->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* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto removedDeviceId = wrtWrapper->hStringToString (removedDeviceIdHstr);
|
|
|
|
listeners.call ([&] (auto& l) { l.deviceRemoved (removedDeviceId); });
|
|
|
|
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* c) override
|
|
{
|
|
HSTRING removedDeviceIdHstr;
|
|
auto hr = c->get_Id (&removedDeviceIdHstr);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to query removed MIDI device ID!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Failed to get the WinRTWrapper singleton!");
|
|
return S_OK;
|
|
}
|
|
|
|
auto id = wrtWrapper->hStringToString (removedDeviceIdHstr);
|
|
|
|
listeners.call ([&] (auto& l) { l.deviceUpdated (id); });
|
|
return S_OK;
|
|
}
|
|
|
|
ComSmartPtr<COMFactoryType> factory;
|
|
|
|
Array<WinRTMIDIDeviceInfo> connectedDevices;
|
|
CriticalSection deviceChanges;
|
|
ThreadLocalValue<Array<WinRTMIDIDeviceInfo>> lastQueriedConnectedDevices;
|
|
WaitFreeListeners<MidiIODeviceWatcherListener> listeners;
|
|
|
|
std::unique_ptr<DeviceCallbackHandler> watcher;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher)
|
|
};
|
|
|
|
struct DeviceRemovalListener
|
|
{
|
|
virtual ~DeviceRemovalListener() = default;
|
|
virtual void deviceRemoved (const String&) = 0;
|
|
};
|
|
|
|
class Client : private MidiIODeviceWatcherListener,
|
|
private BLEDeviceWatcherListener,
|
|
private AsyncUpdater
|
|
{
|
|
public:
|
|
~Client() override
|
|
{
|
|
inputDeviceWatcher->removeListener (*this);
|
|
outputDeviceWatcher->removeListener (*this);
|
|
bleDeviceWatcher->removeListener (*this);
|
|
}
|
|
|
|
WinRTMIDIDeviceInfo getWinRTDeviceInfoForInput (const ump::EndpointId& deviceIdentifier) const
|
|
{
|
|
return inputDeviceWatcher->getWinRTDeviceInfoForDevice (deviceIdentifier);
|
|
}
|
|
|
|
WinRTMIDIDeviceInfo getWinRTDeviceInfoForOutput (const ump::EndpointId& deviceIdentifier) const
|
|
{
|
|
return outputDeviceWatcher->getWinRTDeviceInfoForDevice (deviceIdentifier);
|
|
}
|
|
|
|
bool isBleDevice (const String& containerId) const
|
|
{
|
|
return bleDeviceWatcher->isBleDevice (containerId);
|
|
}
|
|
|
|
void addBleListener (BLEDeviceWatcherListener& l)
|
|
{
|
|
bleDeviceWatcher->addListener (l);
|
|
}
|
|
|
|
void removeBleListener (BLEDeviceWatcherListener& l)
|
|
{
|
|
bleDeviceWatcher->removeListener (l);
|
|
}
|
|
|
|
void getEndpoints (std::vector<ump::EndpointId>& x)
|
|
{
|
|
std::transform (cachedEndpoints.begin(),
|
|
cachedEndpoints.end(),
|
|
std::back_inserter (x),
|
|
[] (const auto& p) { return p.first; });
|
|
}
|
|
|
|
std::optional<ump::Endpoint> getEndpoint (const ump::EndpointId& x) const
|
|
{
|
|
const auto iter = cachedEndpoints.find (x);
|
|
|
|
if (iter == cachedEndpoints.end())
|
|
return {};
|
|
|
|
return iter->second.endpoint;
|
|
}
|
|
|
|
std::optional<ump::StaticDeviceInfo> getStaticDeviceInfo (const ump::EndpointId& x) const
|
|
{
|
|
const auto iter = cachedEndpoints.find (x);
|
|
|
|
if (iter == cachedEndpoints.end())
|
|
return {};
|
|
|
|
return iter->second.info;
|
|
}
|
|
|
|
static std::unique_ptr<Client> make (ump::EndpointsListener& l)
|
|
{
|
|
const auto windowsVersionInfo = getWindowsVersionInfo();
|
|
|
|
if (windowsVersionInfo.dwMajorVersion < 10 || windowsVersionInfo.dwBuildNumber < 17763)
|
|
return {};
|
|
|
|
auto* wrtWrapper = WinRTWrapper::getInstance();
|
|
|
|
if (! wrtWrapper->isInitialised())
|
|
return {};
|
|
|
|
auto midiInFactory = wrtWrapper->getWRLFactory<IMidiInPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]);
|
|
|
|
if (midiInFactory == nullptr)
|
|
return {};
|
|
|
|
auto midiOutFactory = wrtWrapper->getWRLFactory<IMidiOutPortStatics> (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]);
|
|
|
|
if (midiOutFactory == nullptr)
|
|
return {};
|
|
|
|
// 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.
|
|
auto bleDeviceWatcher = BLEDeviceWatcher::make();
|
|
|
|
if (bleDeviceWatcher == nullptr)
|
|
return {};
|
|
|
|
auto inputDeviceWatcher = MidiIODeviceWatcher<IMidiInPortStatics>::make (midiInFactory);
|
|
|
|
if (inputDeviceWatcher == nullptr)
|
|
return {};
|
|
|
|
auto outputDeviceWatcher = MidiIODeviceWatcher<IMidiOutPortStatics>::make (midiOutFactory);
|
|
|
|
if (outputDeviceWatcher == nullptr)
|
|
return {};
|
|
|
|
auto result = rawToUniquePtr (new Client { l });
|
|
result->midiInFactory = std::move (midiInFactory);
|
|
result->midiOutFactory = std::move (midiOutFactory);
|
|
|
|
result->inputDeviceWatcher = std::move (inputDeviceWatcher);
|
|
result->outputDeviceWatcher = std::move (outputDeviceWatcher);
|
|
result->bleDeviceWatcher = std::move (bleDeviceWatcher);
|
|
|
|
result->inputDeviceWatcher->addListener (*result);
|
|
result->outputDeviceWatcher->addListener (*result);
|
|
result->bleDeviceWatcher->addListener (*result);
|
|
|
|
result->updateCachedEndpoints();
|
|
|
|
return result;
|
|
}
|
|
|
|
ComSmartPtr<IMidiInPortStatics> getMidiInFactory() const { return midiInFactory; }
|
|
ComSmartPtr<IMidiOutPortStatics> getMidiOutFactory() const { return midiOutFactory; }
|
|
|
|
void addDeviceRemovalListener (DeviceRemovalListener& l)
|
|
{
|
|
listeners.add (l);
|
|
}
|
|
|
|
void removeDeviceRemovalListener (DeviceRemovalListener& l)
|
|
{
|
|
listeners.remove (l);
|
|
}
|
|
|
|
private:
|
|
explicit Client (ump::EndpointsListener& l)
|
|
: listener (l) {}
|
|
|
|
void deviceAdded (const String&) override
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void deviceRemoved (const String& id) override
|
|
{
|
|
listeners.call ([&] (auto& c) { c.deviceRemoved (id); });
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void deviceUpdated (const String&) override
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void bleDeviceAdded (const String&) override
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void bleDeviceDisconnected (const String&) override
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
updateCachedEndpoints();
|
|
listener.endpointsChanged();
|
|
}
|
|
|
|
void updateCachedEndpoints()
|
|
{
|
|
std::vector<ump::EndpointAndStaticInfo> buffer;
|
|
inputDeviceWatcher->getAvailableDevices (buffer);
|
|
outputDeviceWatcher->getAvailableDevices (buffer);
|
|
|
|
cachedEndpoints.clear();
|
|
|
|
for (auto& item : buffer)
|
|
cachedEndpoints.emplace (item.id, item);
|
|
|
|
// If this is hit, we got an identical ID for an input and output device
|
|
jassert (cachedEndpoints.size() == buffer.size());
|
|
}
|
|
|
|
ump::EndpointsListener& listener;
|
|
std::map<ump::EndpointId, ump::EndpointAndStaticInfo> cachedEndpoints;
|
|
|
|
ComSmartPtr<IMidiInPortStatics> midiInFactory;
|
|
ComSmartPtr<IMidiOutPortStatics> midiOutFactory;
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiInPortStatics>> inputDeviceWatcher;
|
|
std::unique_ptr<MidiIODeviceWatcher<IMidiOutPortStatics>> outputDeviceWatcher;
|
|
std::unique_ptr<BLEDeviceWatcher> bleDeviceWatcher;
|
|
WaitFreeListeners<DeviceRemovalListener> listeners;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Client)
|
|
};
|
|
|
|
template <bool isInput>
|
|
class WinRTIOWrapper : private BLEDeviceWatcherListener,
|
|
private DeviceRemovalListener,
|
|
private AsyncUpdater
|
|
{
|
|
public:
|
|
static std::unique_ptr<WinRTIOWrapper> make (std::shared_ptr<Client> c,
|
|
const ump::EndpointId& deviceIdentifier,
|
|
ump::DisconnectionListener& listener)
|
|
{
|
|
auto result = rawToUniquePtr (new WinRTIOWrapper);
|
|
result->client = c;
|
|
result->onDisconnect = &listener;
|
|
result->deviceInfo = isInput ? c->getWinRTDeviceInfoForInput (deviceIdentifier)
|
|
: c->getWinRTDeviceInfoForOutput (deviceIdentifier);
|
|
|
|
if (result->deviceInfo.deviceID.isEmpty())
|
|
return {};
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Creating JUCE MIDI IO: " << deviceInfo.deviceID);
|
|
|
|
result->client->addDeviceRemovalListener (*result);
|
|
|
|
if (result->deviceInfo.containerID.isNotEmpty())
|
|
{
|
|
result->isBLEDevice = result->client->isBleDevice (result->deviceInfo.containerID);
|
|
result->client->addBleListener (*result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
WinRTMIDIDeviceInfo getDeviceInfo() const { return deviceInfo; }
|
|
bool isBLE() const { return isBLEDevice; }
|
|
|
|
ump::EndpointId getEndpointId() const
|
|
{
|
|
return isInput ? ump::EndpointId::make (ump::IOKind::src, deviceInfo.containerID)
|
|
: ump::EndpointId::make (ump::IOKind::dst, deviceInfo.containerID);
|
|
}
|
|
|
|
~WinRTIOWrapper() override
|
|
{
|
|
client->removeDeviceRemovalListener (*this);
|
|
client->removeBleListener (*this);
|
|
|
|
cancelPendingUpdate();
|
|
}
|
|
|
|
private:
|
|
WinRTIOWrapper() = default;
|
|
|
|
void bleDeviceAdded (const String& containerID) override
|
|
{
|
|
if (containerID == deviceInfo.containerID)
|
|
isBLEDevice = true;
|
|
}
|
|
|
|
void bleDeviceDisconnected (const String& containerID) override
|
|
{
|
|
if (containerID != deviceInfo.containerID)
|
|
triggerAsyncUpdate();
|
|
|
|
JUCE_WINRT_MIDI_LOG ("Disconnecting MIDI port from BLE disconnection: " << deviceInfo.deviceID
|
|
<< " " << deviceInfo.containerID << " " << deviceInfo.name);
|
|
}
|
|
|
|
void deviceRemoved (const String& deviceId) override
|
|
{
|
|
if (deviceId == deviceInfo.deviceID)
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
if (onDisconnect != nullptr)
|
|
onDisconnect->disconnected();
|
|
}
|
|
|
|
std::shared_ptr<Client> client;
|
|
WinRTMIDIDeviceInfo deviceInfo;
|
|
bool isBLEDevice = false;
|
|
ump::DisconnectionListener* onDisconnect = nullptr;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTIOWrapper)
|
|
};
|
|
|
|
template <typename COMType, typename COMFactoryType, typename COMInterfaceType>
|
|
static void openMidiPortThread (String threadName,
|
|
String midiDeviceID,
|
|
const ComSmartPtr<COMFactoryType>& comFactory,
|
|
ComSmartPtr<COMInterfaceType>& comPort)
|
|
{
|
|
std::thread { [&]
|
|
{
|
|
Thread::setCurrentThreadName (threadName);
|
|
|
|
const WinRTWrapper::ScopedHString hDeviceId { midiDeviceID };
|
|
ComSmartPtr<IAsyncOperation<COMType*>> asyncOp;
|
|
const auto hr = comFactory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return;
|
|
|
|
std::promise<ComSmartPtr<COMInterfaceType>> promise;
|
|
auto future = promise.get_future();
|
|
|
|
auto callback = [p = std::move (promise)] (IAsyncOperation<COMType*>* asyncOpPtr, AsyncStatus) mutable
|
|
{
|
|
if (asyncOpPtr == nullptr)
|
|
{
|
|
p.set_value (nullptr);
|
|
return E_ABORT;
|
|
}
|
|
|
|
ComSmartPtr<COMInterfaceType> result;
|
|
const auto hr = asyncOpPtr->GetResults (result.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
p.set_value (nullptr);
|
|
return hr;
|
|
}
|
|
|
|
p.set_value (std::move (result));
|
|
return S_OK;
|
|
};
|
|
|
|
const auto ir = asyncOp->put_Completed (Microsoft::WRL::Callback<IAsyncOperationCompletedHandler<COMType*>> (std::move (callback)).Get());
|
|
|
|
if (FAILED (ir))
|
|
return;
|
|
|
|
if (future.wait_for (std::chrono::milliseconds (2000)) == std::future_status::ready)
|
|
comPort = future.get();
|
|
} }.join();
|
|
}
|
|
|
|
class InputImplNative : public ump::Input::Impl::Native
|
|
{
|
|
public:
|
|
~InputImplNative()
|
|
{
|
|
disconnect();
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override
|
|
{
|
|
return wrapper->getEndpointId();
|
|
}
|
|
|
|
ump::PacketProtocol getProtocol() const override
|
|
{
|
|
return dispatcher.getProtocol();
|
|
}
|
|
|
|
static std::unique_ptr<InputImplNative> open (std::shared_ptr<Client> c,
|
|
ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& identifier,
|
|
ump::PacketProtocol protocol,
|
|
ump::Consumer& consumer)
|
|
{
|
|
auto result = rawToUniquePtr (new InputImplNative (c, protocol, consumer));
|
|
|
|
auto w = WinRTIOWrapper<true>::make (c, identifier, listener);
|
|
|
|
if (w == nullptr)
|
|
return {};
|
|
|
|
result->wrapper = std::move (w);
|
|
|
|
openMidiPortThread<MidiInPort> ("Open WinRT MIDI input port",
|
|
result->wrapper->getDeviceInfo().deviceID,
|
|
result->client->getMidiInFactory(),
|
|
result->port);
|
|
|
|
if (result->port == nullptr)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Timed out waiting for midi input port creation");
|
|
return {};
|
|
}
|
|
|
|
result->startTime = Time::getMillisecondCounterHiRes();
|
|
result->start();
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
InputImplNative (std::shared_ptr<Client> c, ump::PacketProtocol p, ump::Consumer& cb)
|
|
: client (c), consumer (cb), dispatcher { 0, p, 4096 }
|
|
{
|
|
}
|
|
|
|
void start()
|
|
{
|
|
if (midiInMessageToken.value != 0)
|
|
{
|
|
JUCE_WINRT_MIDI_LOG ("Input already started");
|
|
return;
|
|
}
|
|
|
|
const auto hr = port->add_MessageReceived (
|
|
Microsoft::WRL::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;
|
|
}
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (port != nullptr && midiInMessageToken.value != 0)
|
|
port->remove_MessageReceived (midiInMessageToken);
|
|
|
|
midiInMessageToken = {};
|
|
}
|
|
|
|
void disconnect()
|
|
{
|
|
stop();
|
|
|
|
if (port != nullptr && wrapper->isBLE())
|
|
port->Release();
|
|
|
|
port = nullptr;
|
|
}
|
|
|
|
HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args)
|
|
{
|
|
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<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;
|
|
|
|
const Span bytes { unalignedPointerCast<const std::byte*> (bufferData), numBytes };
|
|
const auto time = convertTimeStamp (timespan.Duration);
|
|
dispatcher.dispatch (bytes, time, [this] (const ump::View& view, double timestamp)
|
|
{
|
|
const ump::Iterator b { view.data(), view.size() };
|
|
const auto e = std::next (b);
|
|
consumer.consume (b, e, timestamp);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
std::shared_ptr<Client> client;
|
|
ComSmartPtr<IMidiInPort> port;
|
|
ump::Consumer& consumer;
|
|
EventRegistrationToken midiInMessageToken { 0 };
|
|
double startTime = 0;
|
|
ump::BytestreamToUMPDispatcher dispatcher;
|
|
std::unique_ptr<WinRTIOWrapper<true>> wrapper;
|
|
};
|
|
|
|
class OutputImplNative : public ump::Output::Impl::Native
|
|
{
|
|
public:
|
|
~OutputImplNative()
|
|
{
|
|
disconnect();
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override
|
|
{
|
|
return wrapper->getEndpointId();
|
|
}
|
|
|
|
bool send (ump::Iterator b, ump::Iterator e) override
|
|
{
|
|
for (const auto& view : makeRange (b, e))
|
|
{
|
|
toBytestream.convert (view, 0, [&] (ump::BytesOnGroup bytesView, double)
|
|
{
|
|
sendBytestream (bytesView.bytes);
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static std::unique_ptr<OutputImplNative> open (std::shared_ptr<Client> c,
|
|
ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& identifier)
|
|
{
|
|
auto result = rawToUniquePtr (new OutputImplNative { c });
|
|
|
|
auto w = WinRTIOWrapper<false>::make (c, identifier, listener);
|
|
|
|
if (w == nullptr)
|
|
return {};
|
|
|
|
result->wrapper = std::move (w);
|
|
|
|
openMidiPortThread<IMidiOutPort> ("Open WinRT MIDI output port",
|
|
result->wrapper->getDeviceInfo().deviceID,
|
|
result->client->getMidiOutFactory(),
|
|
result->port);
|
|
|
|
if (result->port == nullptr)
|
|
return {};
|
|
|
|
auto* wrtWrapper = WinRTWrapper::getInstanceWithoutCreating();
|
|
|
|
if (wrtWrapper == nullptr)
|
|
return {};
|
|
|
|
auto bufferFactory = wrtWrapper->getWRLFactory<IBufferFactory> (&RuntimeClass_Windows_Storage_Streams_Buffer[0]);
|
|
|
|
if (bufferFactory == nullptr)
|
|
return {};
|
|
|
|
auto hr = bufferFactory->Create (static_cast<UINT32> (65536), result->buffer.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return {};
|
|
|
|
hr = result->buffer->QueryInterface (result->bufferByteAccess.resetAndGetPointerAddress());
|
|
|
|
if (FAILED (hr))
|
|
return {};
|
|
|
|
hr = result->bufferByteAccess->Buffer (&result->bufferData);
|
|
|
|
if (FAILED (hr))
|
|
return {};
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
explicit OutputImplNative (std::shared_ptr<Client> c)
|
|
: client (std::move (c))
|
|
{
|
|
}
|
|
|
|
void disconnect()
|
|
{
|
|
if (port != nullptr && wrapper->isBLE())
|
|
port->Release();
|
|
|
|
port = nullptr;
|
|
}
|
|
|
|
void sendBytestream (Span<const std::byte> message)
|
|
{
|
|
if (port == nullptr)
|
|
return;
|
|
|
|
auto numBytes = message.size();
|
|
auto hr = buffer->put_Length ((UINT32) numBytes);
|
|
|
|
if (FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
memcpy_s (bufferData, numBytes, message.data(), numBytes);
|
|
port->SendBuffer (buffer);
|
|
}
|
|
|
|
std::shared_ptr<Client> client;
|
|
ComSmartPtr<IMidiOutPort> port;
|
|
ump::ToBytestreamConverter toBytestream { 4096 };
|
|
ComSmartPtr<IBuffer> buffer;
|
|
ComSmartPtr<IBufferByteAccess> bufferByteAccess;
|
|
uint8_t* bufferData = nullptr;
|
|
std::unique_ptr<WinRTIOWrapper<false>> wrapper;
|
|
};
|
|
|
|
class SessionImplNative : public ump::Session::Impl::Native
|
|
{
|
|
public:
|
|
SessionImplNative (std::shared_ptr<Client> c, const String& x) : client (c), name (x) {}
|
|
|
|
String getName() const override
|
|
{
|
|
return name;
|
|
}
|
|
|
|
std::unique_ptr<ump::Input::Impl::Native> connectInput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& id,
|
|
ump::PacketProtocol protocol,
|
|
ump::Consumer& consumer) override
|
|
{
|
|
return InputImplNative::open (client, listener, id, protocol, consumer);
|
|
}
|
|
|
|
std::unique_ptr<ump::Output::Impl::Native> connectOutput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& id) override
|
|
{
|
|
return OutputImplNative::open (client, listener, id);
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<Client> client;
|
|
String name;
|
|
};
|
|
|
|
class EndpointsImplNative : public ump::Endpoints::Impl::Native
|
|
{
|
|
public:
|
|
ump::Backend getBackend() const override
|
|
{
|
|
return ump::Backend::winrt;
|
|
}
|
|
|
|
bool isVirtualMidiUmpServiceActive() const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool isVirtualMidiBytestreamServiceActive() const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void getEndpoints (std::vector<ump::EndpointId>& x) const override
|
|
{
|
|
client->getEndpoints (x);
|
|
}
|
|
|
|
std::optional<ump::Endpoint> getEndpoint (const ump::EndpointId& x) const override
|
|
{
|
|
return client->getEndpoint (x);
|
|
}
|
|
|
|
std::optional<ump::StaticDeviceInfo> getStaticDeviceInfo (const ump::EndpointId& x) const override
|
|
{
|
|
return client->getStaticDeviceInfo (x);
|
|
}
|
|
|
|
std::unique_ptr<ump::Session::Impl::Native> makeSession (const String& x) override
|
|
{
|
|
return std::make_unique<SessionImplNative> (client, x);
|
|
}
|
|
|
|
static std::unique_ptr<EndpointsImplNative> make (ump::EndpointsListener& l)
|
|
{
|
|
return rawToUniquePtr (new EndpointsImplNative { l });
|
|
}
|
|
|
|
private:
|
|
explicit EndpointsImplNative (ump::EndpointsListener& l)
|
|
: client (Client::make (l)) {}
|
|
|
|
std::shared_ptr<Client> client;
|
|
};
|
|
};
|
|
#endif
|
|
|
|
struct Win32
|
|
{
|
|
struct ITraits
|
|
{
|
|
using Ptr = HMIDIIN;
|
|
using Caps = MIDIINCAPS;
|
|
static inline const auto getNum = midiInGetNumDevs;
|
|
static inline const auto getCaps = midiInGetDevCaps;
|
|
static inline const auto message = midiInMessage;
|
|
static inline const auto isInput = true;
|
|
};
|
|
|
|
struct OTraits
|
|
{
|
|
using Ptr = HMIDIOUT;
|
|
using Caps = MIDIOUTCAPS;
|
|
static inline const auto getNum = midiOutGetNumDevs;
|
|
static inline const auto getCaps = midiOutGetDevCaps;
|
|
static inline const auto message = midiOutMessage;
|
|
static inline const auto isInput = false;
|
|
};
|
|
|
|
template <typename Traits>
|
|
static void getAvailableDevices (const Traits&, std::vector<ump::EndpointAndStaticInfo>& result)
|
|
{
|
|
const auto deviceCaps = [&]
|
|
{
|
|
std::vector<typename Traits::Caps> devices;
|
|
|
|
const auto end = Traits::getNum();
|
|
|
|
for (auto i = (decltype (end)) 0; i < end; ++i)
|
|
if (typename Traits::Caps mc{}; Traits::getCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR)
|
|
devices.push_back (mc);
|
|
|
|
return devices;
|
|
}();
|
|
|
|
StringArray deviceIDs, deviceNames;
|
|
|
|
for (const auto [i, device] : enumerate (deviceCaps))
|
|
{
|
|
const auto name = device.szPname;
|
|
const auto identifier = std::invoke ([index = i]() -> String
|
|
{
|
|
ULONG size = 0;
|
|
|
|
if (Traits::message ((typename Traits::Ptr) index, DRV_QUERYDEVICEINTERFACESIZE, (DWORD_PTR) &size, 0) != MMSYSERR_NOERROR)
|
|
return {};
|
|
|
|
WCHAR interfaceName[512]{};
|
|
|
|
if (! isPositiveAndBelow (size, std::size (interfaceName))
|
|
|| Traits::message ((typename Traits::Ptr) index, DRV_QUERYDEVICEINTERFACE, (DWORD_PTR) interfaceName, sizeof (interfaceName)) != MMSYSERR_NOERROR)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
return interfaceName;
|
|
});
|
|
|
|
deviceNames.add (name);
|
|
deviceIDs.add (identifier.isNotEmpty() ? identifier : name);
|
|
}
|
|
|
|
for (auto* list : { &deviceIDs, &deviceNames })
|
|
list->appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 (""));
|
|
|
|
constexpr auto direction = Traits::isInput ? ump::BlockDirection::sender : ump::BlockDirection::receiver;
|
|
|
|
for (auto [index, id] : enumerate (deviceIDs, int{}))
|
|
{
|
|
const auto fullInfo = ump::IOHelpers::makeProxyEndpoint ({ deviceNames[index], id }, direction);
|
|
result.push_back (fullInfo);
|
|
}
|
|
}
|
|
|
|
class MidiHeader
|
|
{
|
|
public:
|
|
void prepare (HMIDIIN device)
|
|
{
|
|
hdr = {};
|
|
hdr.lpData = data.data();
|
|
hdr.dwBufferLength = (DWORD) data.size();
|
|
|
|
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);
|
|
}
|
|
|
|
private:
|
|
MIDIHDR hdr{};
|
|
std::array<char, 256> data{};
|
|
};
|
|
|
|
template <typename Device, auto openInternal>
|
|
class DeviceCache
|
|
{
|
|
public:
|
|
std::shared_ptr<Device> open (const ump::EndpointId& id)
|
|
{
|
|
const std::scoped_lock lock { mutex };
|
|
|
|
if (const auto iter = devices.find (id); iter != devices.end())
|
|
if (auto strong = iter->second.lock())
|
|
return strong;
|
|
|
|
std::unique_ptr uniqueDevice = openInternal (id);
|
|
std::shared_ptr sharedDevice { std::move (uniqueDevice) };
|
|
devices.emplace (id, sharedDevice);
|
|
return sharedDevice;
|
|
}
|
|
|
|
private:
|
|
std::mutex mutex;
|
|
std::map<ump::EndpointId, std::weak_ptr<Device>> devices;
|
|
};
|
|
|
|
// A device can only be opened by one client at a time. In order to allow multiple inputs
|
|
// to open the same device in JUCE, we share the device between all inputs that are
|
|
// currently using it.
|
|
class InputDevice : private AsyncUpdater
|
|
{
|
|
public:
|
|
static std::shared_ptr<InputDevice> open (const ump::EndpointId& id)
|
|
{
|
|
static DeviceCache<InputDevice, &InputDevice::openInternal> devices;
|
|
return devices.open (id);
|
|
}
|
|
|
|
~InputDevice() override
|
|
{
|
|
allInputs().remove (*this);
|
|
|
|
if (deviceHandle == nullptr)
|
|
return;
|
|
|
|
unprepareAllHeaders();
|
|
|
|
midiInReset (deviceHandle);
|
|
midiInStop (deviceHandle);
|
|
|
|
for (int count = 5; --count >= 0;)
|
|
{
|
|
if (midiInClose (deviceHandle) == MMSYSERR_NOERROR)
|
|
break;
|
|
|
|
Sleep (20);
|
|
}
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const
|
|
{
|
|
return endpointId;
|
|
}
|
|
|
|
void addConsumer (ump::Consumer& c)
|
|
{
|
|
consumers.add (c);
|
|
}
|
|
|
|
void removeConsumer (ump::Consumer& c)
|
|
{
|
|
consumers.add (c);
|
|
}
|
|
|
|
void addDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.add (&l);
|
|
}
|
|
|
|
void removeDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.remove (&l);
|
|
}
|
|
|
|
private:
|
|
static std::unique_ptr<InputDevice> openInternal (const ump::EndpointId& id)
|
|
{
|
|
std::vector<ump::EndpointAndStaticInfo> endpoints;
|
|
getAvailableDevices (ITraits{}, endpoints);
|
|
const auto iter = std::find_if (endpoints.begin(),
|
|
endpoints.end(),
|
|
[&] (const auto& x) { return x.id == id; });
|
|
|
|
if (iter == endpoints.end())
|
|
return {};
|
|
|
|
const auto deviceID = std::distance (endpoints.begin(), iter);
|
|
auto result = rawToUniquePtr (new InputDevice { id });
|
|
|
|
HMIDIIN handle{};
|
|
const auto err = midiInOpen (&handle,
|
|
(UINT) deviceID,
|
|
(DWORD_PTR) &midiInCallback,
|
|
(DWORD_PTR) result.get(),
|
|
CALLBACK_FUNCTION);
|
|
|
|
if (err != MMSYSERR_NOERROR)
|
|
return {};
|
|
|
|
result->deviceHandle = handle;
|
|
|
|
for (auto& header : result->headers)
|
|
{
|
|
header.prepare (handle);
|
|
header.write (handle);
|
|
}
|
|
|
|
if (midiInStart (handle) != MMSYSERR_NOERROR)
|
|
return {};
|
|
|
|
return result;
|
|
}
|
|
|
|
explicit InputDevice (const ump::EndpointId& x)
|
|
: endpointId (x)
|
|
{
|
|
allInputs().add (*this);
|
|
}
|
|
|
|
void handleMessage (const uint8* bytes, uint32 timeStamp)
|
|
{
|
|
if (bytes[0] >= 0x80)
|
|
{
|
|
auto len = MidiMessage::getMessageLengthFromFirstByte (bytes[0]);
|
|
auto time = convertTimeStamp (timeStamp);
|
|
dispatcher.dispatch ({ unalignedPointerCast<const std::byte*> (bytes), (size_t) len }, time, [this] (const ump::View& view, double timestamp)
|
|
{
|
|
const ump::Iterator b { view.data(), view.size() };
|
|
const auto e = std::next (b);
|
|
consumers.call ([&] (ump::Consumer& c) { c.consume (b, e, timestamp); });
|
|
});
|
|
|
|
writeFinishedBlocks();
|
|
}
|
|
}
|
|
|
|
void handleSysEx (MIDIHDR* hdr, uint32 timeStamp)
|
|
{
|
|
if (hdr->dwBytesRecorded > 0)
|
|
{
|
|
auto time = convertTimeStamp (timeStamp);
|
|
dispatcher.dispatch ({ unalignedPointerCast<const std::byte*> (hdr->lpData), (size_t) hdr->dwBytesRecorded }, time, [this] (const ump::View& view, double timestamp)
|
|
{
|
|
const ump::Iterator b { view.data(), view.size() };
|
|
const auto e = std::next (b);
|
|
consumers.call ([&] (ump::Consumer& c) { c.consume (b, e, timestamp); });
|
|
});
|
|
|
|
writeFinishedBlocks();
|
|
}
|
|
}
|
|
|
|
void disconnected()
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
disconnectListeners.call ([] (auto& x) { x.disconnected(); });
|
|
}
|
|
|
|
void writeFinishedBlocks()
|
|
{
|
|
for (auto& header : headers)
|
|
header.writeIfFinished (deviceHandle);
|
|
}
|
|
|
|
void unprepareAllHeaders()
|
|
{
|
|
for (auto& header : headers)
|
|
header.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;
|
|
}
|
|
|
|
static void CALLBACK midiInCallback (HMIDIIN,
|
|
UINT uMsg,
|
|
DWORD_PTR dwInstance,
|
|
DWORD_PTR midiMessage,
|
|
DWORD_PTR timeStamp)
|
|
{
|
|
auto* collector = reinterpret_cast<InputDevice*> (dwInstance);
|
|
|
|
allInputs().call ([&] (auto& l)
|
|
{
|
|
if (collector != &l)
|
|
return;
|
|
|
|
switch (uMsg)
|
|
{
|
|
case MIM_DATA:
|
|
l.handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp);
|
|
break;
|
|
|
|
case MIM_LONGDATA:
|
|
l.handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp);
|
|
break;
|
|
|
|
case MIM_CLOSE:
|
|
l.disconnected();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
static WaitFreeListeners<InputDevice>& allInputs()
|
|
{
|
|
static WaitFreeListeners<InputDevice> result;
|
|
return result;
|
|
}
|
|
|
|
ump::EndpointId endpointId;
|
|
HMIDIIN deviceHandle = nullptr;
|
|
std::array<MidiHeader, 32> headers;
|
|
double startTime = Time::getMillisecondCounterHiRes();
|
|
WaitFreeListeners<ump::Consumer> consumers;
|
|
ListenerList<ump::DisconnectionListener> disconnectListeners;
|
|
// The shared input always converts to plain MIDI 1.0. Clients that want MIDI 2.0 have
|
|
// their own converters.
|
|
ump::BytestreamToUMPDispatcher dispatcher { 0, ump::PacketProtocol::MIDI_1_0, 4096 };
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InputDevice)
|
|
};
|
|
|
|
class OutputDevice : private AsyncUpdater
|
|
{
|
|
public:
|
|
~OutputDevice() override
|
|
{
|
|
allOutputs().remove (*this);
|
|
|
|
cancelPendingUpdate();
|
|
|
|
if (handle != nullptr)
|
|
midiOutClose (handle);
|
|
}
|
|
|
|
static std::shared_ptr<OutputDevice> open (const ump::EndpointId& id)
|
|
{
|
|
static DeviceCache<OutputDevice, &OutputDevice::openInternal> devices;
|
|
return devices.open (id);
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const
|
|
{
|
|
return endpointId;
|
|
}
|
|
|
|
bool send (ump::Iterator b, ump::Iterator e)
|
|
{
|
|
for (const auto& view : makeRange (b, e))
|
|
{
|
|
toBytestream.convert (view, 0, [&] (ump::BytesOnGroup bytesView, double)
|
|
{
|
|
sendBytestream (bytesView.bytes);
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void addDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.add (&l);
|
|
}
|
|
|
|
void removeDisconnectListener (ump::DisconnectionListener& l)
|
|
{
|
|
disconnectListeners.remove (&l);
|
|
}
|
|
|
|
private:
|
|
static std::unique_ptr<OutputDevice> openInternal (const ump::EndpointId& id)
|
|
{
|
|
std::vector<ump::EndpointAndStaticInfo> endpoints;
|
|
getAvailableDevices (OTraits{}, endpoints);
|
|
const auto iter = std::find_if (endpoints.begin(),
|
|
endpoints.end(),
|
|
[&] (const auto& x) { return x.id == id; });
|
|
|
|
if (iter == endpoints.end())
|
|
return {};
|
|
|
|
const auto deviceID = std::distance (endpoints.begin(), iter);
|
|
|
|
for (auto i = 0; i < 4; ++i)
|
|
{
|
|
auto result = rawToUniquePtr (new OutputDevice (id));
|
|
|
|
HMIDIOUT h = nullptr;
|
|
auto res = midiOutOpen (&h,
|
|
(UINT) deviceID,
|
|
(DWORD_PTR) &midiOutCallback,
|
|
(DWORD_PTR) result.get(),
|
|
CALLBACK_FUNCTION);
|
|
|
|
switch (res)
|
|
{
|
|
case MMSYSERR_NOERROR:
|
|
result->handle = h;
|
|
return result;
|
|
|
|
case MMSYSERR_ALLOCATED:
|
|
Sleep (100);
|
|
break;
|
|
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
explicit OutputDevice (const ump::EndpointId& x)
|
|
: endpointId (x)
|
|
{
|
|
allOutputs().add (*this);
|
|
}
|
|
|
|
void sendBytestream (Span<const std::byte> message)
|
|
{
|
|
if (message.empty())
|
|
return;
|
|
|
|
if (message.size() > 3 || message[0] == std::byte { 0xf0 })
|
|
{
|
|
MIDIHDR h = {};
|
|
|
|
h.lpData = (char*) message.data();
|
|
h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.size();
|
|
|
|
if (midiOutPrepareHeader (handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR)
|
|
{
|
|
auto res = midiOutLongMsg (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 (handle, &h, sizeof (MIDIHDR));
|
|
|
|
if (res == MIDIERR_STILLPLAYING)
|
|
Sleep (2);
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const auto msg = ByteOrder::makeInt (0 < message.size() ? (uint8_t) message[0] : 0,
|
|
1 < message.size() ? (uint8_t) message[1] : 0,
|
|
2 < message.size() ? (uint8_t) message[2] : 0,
|
|
0);
|
|
|
|
for (int i = 0; i < 50; ++i)
|
|
{
|
|
if (midiOutShortMsg (handle, msg) != MIDIERR_NOTREADY)
|
|
break;
|
|
|
|
Sleep (1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void disconnected()
|
|
{
|
|
triggerAsyncUpdate();
|
|
}
|
|
|
|
void handleAsyncUpdate() override
|
|
{
|
|
disconnectListeners.call ([] (auto& x) { x.disconnected(); });
|
|
}
|
|
|
|
static void CALLBACK midiOutCallback (HMIDIOUT,
|
|
UINT wMsg,
|
|
DWORD_PTR dwInstance,
|
|
DWORD_PTR,
|
|
DWORD_PTR)
|
|
{
|
|
auto* collector = reinterpret_cast<OutputDevice*> (dwInstance);
|
|
|
|
allOutputs().call ([&] (auto& l)
|
|
{
|
|
if (collector != &l)
|
|
return;
|
|
|
|
switch (wMsg)
|
|
{
|
|
case MOM_CLOSE:
|
|
l.disconnected();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
static WaitFreeListeners<OutputDevice>& allOutputs()
|
|
{
|
|
static WaitFreeListeners<OutputDevice> result;
|
|
return result;
|
|
}
|
|
|
|
ump::EndpointId endpointId;
|
|
HMIDIOUT handle = nullptr;
|
|
ListenerList<ump::DisconnectionListener> disconnectListeners;
|
|
ump::ToBytestreamConverter toBytestream { 4096 };
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OutputDevice)
|
|
};
|
|
|
|
class InputImplNative : public ump::Input::Impl::Native,
|
|
private ump::Consumer
|
|
{
|
|
public:
|
|
InputImplNative (std::shared_ptr<InputDevice> d,
|
|
ump::DisconnectionListener& l,
|
|
ump::PacketProtocol p,
|
|
Consumer& c)
|
|
: device (d), listener (l), consumer (c), converter (p)
|
|
{
|
|
device->addConsumer (*this);
|
|
device->addDisconnectListener (listener);
|
|
}
|
|
|
|
~InputImplNative() override
|
|
{
|
|
device->removeDisconnectListener (listener);
|
|
device->removeConsumer (*this);
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override
|
|
{
|
|
return device->getEndpointId();
|
|
}
|
|
|
|
ump::PacketProtocol getProtocol() const override
|
|
{
|
|
return converter.getProtocol();
|
|
}
|
|
|
|
private:
|
|
void consume (ump::Iterator b, ump::Iterator e, double time) override
|
|
{
|
|
converter.convert (b, e, [&] (ump::View v)
|
|
{
|
|
const ump::Iterator iter { v.data(), v.size() };
|
|
consumer.consume (iter, std::next (iter), time);
|
|
});
|
|
}
|
|
|
|
std::shared_ptr<InputDevice> device;
|
|
ump::DisconnectionListener& listener;
|
|
Consumer& consumer;
|
|
ump::GenericUMPConverter converter;
|
|
};
|
|
|
|
class SessionImplNative : public ump::Session::Impl::Native
|
|
{
|
|
public:
|
|
explicit SessionImplNative (const String& x) : name (x) {}
|
|
|
|
String getName() const override
|
|
{
|
|
return name;
|
|
}
|
|
|
|
std::unique_ptr<ump::Input::Impl::Native> connectInput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& endpoint,
|
|
ump::PacketProtocol protocol,
|
|
ump::Consumer& consumer) override
|
|
{
|
|
auto device = InputDevice::open (endpoint);
|
|
|
|
if (device == nullptr)
|
|
return {};
|
|
|
|
return std::make_unique<InputImplNative> (device, listener, protocol, consumer);
|
|
}
|
|
|
|
std::unique_ptr<ump::Output::Impl::Native> connectOutput (ump::DisconnectionListener& listener,
|
|
const ump::EndpointId& endpoint) override
|
|
{
|
|
class Result : public ump::Output::Impl::Native
|
|
{
|
|
public:
|
|
Result (std::shared_ptr<OutputDevice> d, ump::DisconnectionListener& l)
|
|
: device (d), listener (l)
|
|
{
|
|
device->addDisconnectListener (listener);
|
|
}
|
|
|
|
~Result() override
|
|
{
|
|
device->removeDisconnectListener (listener);
|
|
}
|
|
|
|
ump::EndpointId getEndpointId() const override { return device->getEndpointId(); }
|
|
bool send (ump::Iterator b, ump::Iterator e) override { return device->send (b, e); }
|
|
|
|
private:
|
|
std::shared_ptr<OutputDevice> device;
|
|
ump::DisconnectionListener& listener;
|
|
};
|
|
|
|
auto device = OutputDevice::open (endpoint);
|
|
|
|
if (device == nullptr)
|
|
return {};
|
|
|
|
return std::make_unique<Result> (device, listener);
|
|
}
|
|
|
|
private:
|
|
String name;
|
|
};
|
|
|
|
class EndpointsImplNative : public ump::Endpoints::Impl::Native
|
|
{
|
|
public:
|
|
ump::Backend getBackend() const override
|
|
{
|
|
return ump::Backend::winmm;
|
|
}
|
|
|
|
bool isVirtualMidiUmpServiceActive() const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool isVirtualMidiBytestreamServiceActive() const override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void getEndpoints (std::vector<ump::EndpointId>& storage) const override
|
|
{
|
|
std::transform (cachedEndpoints.begin(),
|
|
cachedEndpoints.end(),
|
|
std::back_inserter (storage),
|
|
[] (auto& p) { return p.first; });
|
|
}
|
|
|
|
std::optional<ump::Endpoint> getEndpoint (const ump::EndpointId& x) const override
|
|
{
|
|
const auto iter = cachedEndpoints.find (x);
|
|
|
|
if (iter != cachedEndpoints.end())
|
|
return iter->second.endpoint;
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<ump::StaticDeviceInfo> getStaticDeviceInfo (const ump::EndpointId& x) const override
|
|
{
|
|
const auto iter = cachedEndpoints.find (x);
|
|
|
|
if (iter != cachedEndpoints.end())
|
|
return iter->second.info;
|
|
|
|
return {};
|
|
}
|
|
|
|
std::unique_ptr<ump::Session::Impl::Native> makeSession (const String& x) override
|
|
{
|
|
return std::make_unique<SessionImplNative> (x);
|
|
}
|
|
|
|
static std::unique_ptr<EndpointsImplNative> make (ump::EndpointsListener& listener)
|
|
{
|
|
return rawToUniquePtr (new EndpointsImplNative { listener });
|
|
}
|
|
|
|
private:
|
|
explicit EndpointsImplNative (ump::EndpointsListener& l)
|
|
: listener (l)
|
|
{
|
|
updateCachedEndpoints();
|
|
}
|
|
|
|
void updateCachedEndpoints()
|
|
{
|
|
JUCE_ASSERT_MESSAGE_THREAD;
|
|
|
|
std::vector<ump::EndpointAndStaticInfo> buffer;
|
|
getAvailableDevices (ITraits{}, buffer);
|
|
getAvailableDevices (OTraits{}, buffer);
|
|
|
|
cachedEndpoints.clear();
|
|
|
|
for (auto& item : buffer)
|
|
cachedEndpoints.emplace (item.id, item);
|
|
|
|
// If this is hit, we got an identical ID for an input and output device
|
|
jassert (cachedEndpoints.size() == buffer.size());
|
|
}
|
|
|
|
ump::EndpointsListener& listener;
|
|
std::map<ump::EndpointId, ump::EndpointAndStaticInfo> cachedEndpoints;
|
|
DeviceChangeDetector detector { L"JuceMidiDeviceDetector_", [&]
|
|
{
|
|
updateCachedEndpoints();
|
|
listener.endpointsChanged();
|
|
} };
|
|
};
|
|
};
|
|
};
|
|
|
|
auto ump::Endpoints::Impl::Native::make (EndpointsListener& l) -> std::unique_ptr<Native>
|
|
{
|
|
#if JUCE_USE_WINDOWS_MIDI_SERVICES
|
|
if (auto ptr = MidiServices::makeEndpoints (l))
|
|
return ptr;
|
|
#endif
|
|
|
|
#if JUCE_USE_WINRT_MIDI
|
|
if (auto winrtSession = WindowsMidiHelpers::Winrt::EndpointsImplNative::make (l))
|
|
return winrtSession;
|
|
#endif
|
|
|
|
#if JUCE_FORCE_WINRT_MIDI
|
|
return nullptr;
|
|
#else
|
|
return WindowsMidiHelpers::Win32::EndpointsImplNative::make (l);
|
|
#endif
|
|
}
|
|
|
|
} // namespace juce
|