build(deps): vendor JUCE 7.0.12
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Identifies a channel or set of channels in a multi-group MIDI endpoint.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ChannelAddress
|
||||
{
|
||||
private:
|
||||
uint8_t group{}; ///< A group within a MIDI endpoint, where 0 <= group && group < 16
|
||||
ChannelInGroup channel{}; ///< A set of channels related to specified group
|
||||
|
||||
auto tie() const { return std::tie (group, channel); }
|
||||
|
||||
public:
|
||||
/** Returns a copy of this object with the specified group. */
|
||||
[[nodiscard]] ChannelAddress withGroup (int g) const
|
||||
{
|
||||
jassert (isPositiveAndBelow (g, 16));
|
||||
return withMember (*this, &ChannelAddress::group, (uint8_t) g);
|
||||
}
|
||||
|
||||
/** Returns a copy of this object with the specified channel. */
|
||||
[[nodiscard]] ChannelAddress withChannel (ChannelInGroup c) const
|
||||
{
|
||||
return withMember (*this, &ChannelAddress::channel, c);
|
||||
}
|
||||
|
||||
/** Returns the group. */
|
||||
[[nodiscard]] uint8_t getGroup() const { return group; }
|
||||
|
||||
/** Returns the channel in the group. */
|
||||
[[nodiscard]] ChannelInGroup getChannel() const { return channel; }
|
||||
|
||||
/** Returns true if this address refers to all channels in the function
|
||||
block containing the specified group.
|
||||
*/
|
||||
bool isBlock() const { return channel == ChannelInGroup::wholeBlock; }
|
||||
|
||||
/** Returns true if this address refers to all channels in the specified
|
||||
group.
|
||||
*/
|
||||
bool isGroup() const { return channel == ChannelInGroup::wholeGroup; }
|
||||
|
||||
/** Returns true if this address refers to a single channel. */
|
||||
bool isSingleChannel() const { return ! isBlock() && ! isGroup(); }
|
||||
|
||||
bool operator< (const ChannelAddress& other) const { return tie() < other.tie(); }
|
||||
bool operator<= (const ChannelAddress& other) const { return tie() <= other.tie(); }
|
||||
bool operator> (const ChannelAddress& other) const { return tie() > other.tie(); }
|
||||
bool operator>= (const ChannelAddress& other) const { return tie() >= other.tie(); }
|
||||
bool operator== (const ChannelAddress& other) const { return tie() == other.tie(); }
|
||||
bool operator!= (const ChannelAddress& other) const { return ! operator== (other); }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Instances of this type are responsible for parsing and interpreting incoming
|
||||
MIDI-CI messages, and for sending MIDI-CI messages to other devices.
|
||||
|
||||
Each Device can act both as a target for messages, and as a source of
|
||||
messages intended to inspect/configure other devices.
|
||||
|
||||
The member functions of Device are generally used to inspect other
|
||||
devices. Member functions starting with 'send' are used to send or request
|
||||
information from other devices; registered DeviceListeners will be notified
|
||||
when the Device receives a response, and then member functions named
|
||||
matching 'get.*ForMuid' can be used to retrieve the result of the inquiry.
|
||||
|
||||
If the Device does not have local profiles or properties, then responses
|
||||
to all incoming messages will be generated automatically using the
|
||||
information supplied during construction.
|
||||
|
||||
If the Device has profiles or properties, then you should implement a
|
||||
ProfileDelegate and/or a PropertyDelegate as appropriate, and pass this
|
||||
delegate during construction. Each Delegate will receive callbacks when a
|
||||
remote device makes a request of the local device, such as
|
||||
enabling/disabling a profile, or setting/getting property data.
|
||||
|
||||
Sometimes the local device must send notifications when
|
||||
updating its profile or property state, for example when profiles are
|
||||
added, or when a subscribed property is changed. Methods to send these
|
||||
notifications are found on the ProfileHost and PropertyHost classes.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class Device : public DeviceMessageHandler
|
||||
{
|
||||
public:
|
||||
using Features = DeviceFeatures;
|
||||
using Listener = DeviceListener;
|
||||
using Options = DeviceOptions;
|
||||
|
||||
/** Constructs a device using the provided options. */
|
||||
explicit Device (const Options& opt);
|
||||
|
||||
Device (Device&&) noexcept;
|
||||
Device& operator= (Device&&) noexcept;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (Device)
|
||||
|
||||
/** Destructor, sends a message to invalidate this device's MUID. */
|
||||
~Device() override;
|
||||
|
||||
//==============================================================================
|
||||
/** To be called with any message that should be processed by the device.
|
||||
This should only be passed complete CI messages - you might find the Extractor
|
||||
class useful for parsing a stream of Universal MIDI Packets and extracting the
|
||||
CI messages.
|
||||
Note that this function does *not* synchronise with any other member function of this
|
||||
class. This means that you must not call this directly from the MIDI input thread if there's
|
||||
any chance of other member functions being called on the same instance simultaneously from
|
||||
other threads.
|
||||
It's probably easiest to send all messages onto the main thread and to limit interactions
|
||||
with the Device to that thread.
|
||||
*/
|
||||
void processMessage (ump::BytesOnGroup) override;
|
||||
|
||||
//==============================================================================
|
||||
/** Sends an inquiry message.
|
||||
|
||||
You can use DeviceListener::deviceAdded to be notified when new devices are discovered.
|
||||
|
||||
This will clear the internal cache of discovered devices, and repopulate it as discovery
|
||||
response messages are received.
|
||||
*/
|
||||
void sendDiscovery();
|
||||
|
||||
/** Sends an endpoint inquiry message.
|
||||
|
||||
Check the MIDI-CI spec for an explanation of the different endpoint message status codes.
|
||||
|
||||
Received responses will be sent to DeviceListener::endpointReceived. Responses are not
|
||||
cached by the Device; if you need to cache endpoint responses, you can keep your own
|
||||
map of MUID->response, update it in endpointReceived, and remove entries in
|
||||
DeviceListener::deviceRemoved.
|
||||
*/
|
||||
void sendEndpointInquiry (MUID destination, Message::EndpointInquiry endpoint);
|
||||
|
||||
//==============================================================================
|
||||
/** Sends a profile inquiry to a particular device.
|
||||
|
||||
DeviceListener::profileStateReceived will be called when the device replies.
|
||||
*/
|
||||
void sendProfileInquiry (MUID muid, ChannelInGroup address);
|
||||
|
||||
/** Sends a profile details inquiry to a particular device.
|
||||
|
||||
DeviceListener::profileDetailsReceived will be called when the device replies.
|
||||
*/
|
||||
void sendProfileDetailsInquiry (MUID muid, ChannelInGroup address, Profile profile, std::byte target);
|
||||
|
||||
/** Sends profile data to a particular device. */
|
||||
void sendProfileSpecificData (MUID muid, ChannelInGroup address, Profile profile, Span<const std::byte>);
|
||||
|
||||
/** Sets a profile on or off. Pass 0 or less to disable the profile, or a positive number to enable it.
|
||||
|
||||
This also goes for group/block profiles. If the request is addressed to a group/block, then
|
||||
a positive number will cause a "profile on" message to be sent, and a non-positive number
|
||||
will cause a "profile off" message to be sent. The channel count of the sent message will
|
||||
always be zero for messages addressed to groups/blocks.regardless of the value of the
|
||||
numChannels argument.
|
||||
*/
|
||||
void sendProfileEnablement (MUID muid, ChannelInGroup address, Profile profile, int numChannels);
|
||||
|
||||
//==============================================================================
|
||||
/** Sends a property inquiry to a particular device.
|
||||
If the device supports properties, this will also automatically request the ResourceList
|
||||
property, and then the ChannelList and DeviceInfo properties if they are present in the
|
||||
ResourceList.
|
||||
*/
|
||||
void sendPropertyCapabilitiesInquiry (MUID destination);
|
||||
|
||||
/** Initiates an inquiry to fetch a property from a particular device.
|
||||
|
||||
@param m the MUID of the device to query
|
||||
@param header specifies the resource to query, along with format/encoding options
|
||||
@param onResult called when the transaction completes; not called if the transaction fails to start
|
||||
@returns a key uniquely identifying this request, if the transaction begins successfully, or nullopt otherwise
|
||||
*/
|
||||
std::optional<RequestKey> sendPropertyGetInquiry (MUID m,
|
||||
const PropertyRequestHeader& header,
|
||||
std::function<void (const PropertyExchangeResult&)> onResult);
|
||||
|
||||
/** Initiates an inquiry to set a property on a particular device.
|
||||
|
||||
@param m the MUID of the device to query
|
||||
@param header specifies the resource to query, along with format/encoding options
|
||||
@param body the unencoded body content of the message
|
||||
@param onResult called when the transaction completes; not called if the transaction fails to start
|
||||
@returns a key uniquely identifying this request, if the transaction begins successfully, or nullopt otherwise
|
||||
*/
|
||||
std::optional<RequestKey> sendPropertySetInquiry (MUID m,
|
||||
const PropertyRequestHeader& header,
|
||||
Span<const std::byte> body,
|
||||
std::function<void (const PropertyExchangeResult&)> onResult);
|
||||
|
||||
/** Cancels a request started with sendPropertyGetInquiry() or sendPropertySetInquiry().
|
||||
|
||||
This sends a property notify message indicating that the responder no longer needs to
|
||||
process the initial request.
|
||||
*/
|
||||
void abortPropertyRequest (RequestKey);
|
||||
|
||||
/** Returns the request id corresponding to a particular request.
|
||||
|
||||
If the request could not be found (it never started, or already finished), then this
|
||||
returns nullopt.
|
||||
*/
|
||||
std::optional<RequestID> getIdForRequestKey (RequestKey) const;
|
||||
|
||||
/** Returns all the ongoing requests. */
|
||||
std::vector<RequestKey> getOngoingRequests() const;
|
||||
|
||||
/** Attempts to begin a subscription with the provided attributes.
|
||||
|
||||
Once the subscription is no longer required, cancel it by passing the SubscriptionKey to endSubscription().
|
||||
*/
|
||||
SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header);
|
||||
|
||||
/** Ends a previously-started subscription. */
|
||||
void endSubscription (SubscriptionKey);
|
||||
|
||||
/** Returns all the subscriptions that have been initiated by this device. */
|
||||
std::vector<SubscriptionKey> getOngoingSubscriptions() const;
|
||||
|
||||
/** If the provided subscription has started successfully, this returns the subscribeId assigned
|
||||
to the subscription by the remote device.
|
||||
*/
|
||||
std::optional<String> getSubscribeIdForKey (SubscriptionKey key) const;
|
||||
|
||||
/** If the provided subscription has not been cancelled, this returns the name of the
|
||||
subscribed resource.
|
||||
*/
|
||||
std::optional<String> getResourceForKey (SubscriptionKey key) const;
|
||||
|
||||
/** Sends any cached messages that need retrying.
|
||||
|
||||
@returns true if there are no more messages to send, or false otherwise
|
||||
*/
|
||||
bool sendPendingMessages();
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a listener that will be notified when particular events occur.
|
||||
|
||||
Check the members of the Listener class to see the kinds of events that are reported.
|
||||
To receive notifications through Listener::propertySubscriptionReceived(), you must
|
||||
first request a subscription using sendPropertySubscriptionStart().
|
||||
|
||||
@see Listener, removeListener()
|
||||
*/
|
||||
void addListener (Listener& l);
|
||||
|
||||
/** Removes a listener that was previously added with addListener(). */
|
||||
void removeListener (Listener& l);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the MUID currently associated with this device.
|
||||
|
||||
This may change, e.g. if another device reports that it shares the same MUID.
|
||||
*/
|
||||
MUID getMuid() const;
|
||||
|
||||
/** Returns the configuration of this device. */
|
||||
Options getOptions() const;
|
||||
|
||||
/** Returns a list of all MUIDs that have been discovered by this device. */
|
||||
std::vector<MUID> getDiscoveredMuids() const;
|
||||
|
||||
/** If you set withProfileConfigurationSupported when constructing this device, this will return
|
||||
a pointer to an object that can be used to query the states of the profiles for this device.
|
||||
*/
|
||||
const ProfileHost* getProfileHost() const;
|
||||
|
||||
/** If you set withProfileConfigurationSupported when constructing this device, this will return
|
||||
a pointer to an object that can be used to modify the states of the profiles for this device.
|
||||
*/
|
||||
ProfileHost* getProfileHost();
|
||||
|
||||
/** If you set withPropertyExchangeSupported when constructing this device, this will return
|
||||
a pointer to an object that can be used to query the states of the properties for this device.
|
||||
*/
|
||||
const PropertyHost* getPropertyHost() const;
|
||||
|
||||
/** If you set withPropertyExchangeSupported when constructing this device, this will return
|
||||
a pointer to an object that can be used to modify the states of the properties for this device.
|
||||
*/
|
||||
PropertyHost* getPropertyHost();
|
||||
|
||||
//==============================================================================
|
||||
/** Returns basic attributes about another device that was discovered.
|
||||
|
||||
If there's no record of the provided device, this will return nullopt.
|
||||
*/
|
||||
std::optional<Message::Discovery> getDiscoveryInfoForMuid (MUID m) const;
|
||||
|
||||
/** Returns the states of the profiles on a particular channel of a device.
|
||||
|
||||
If the state is unknown, returns nullptr.
|
||||
|
||||
Devices don't report profile capabilities unless asked; you can request capabilities
|
||||
using inquireProfile().
|
||||
*/
|
||||
const ChannelProfileStates* getProfileStateForMuid (MUID m, ChannelAddress address) const;
|
||||
|
||||
/** Returns the number of simultaneous property exchange requests supported by a particular
|
||||
device.
|
||||
|
||||
If there's no record of this device's property capabilities (including the case where
|
||||
the device doesn't support property exchange at all) this will return nullopt.
|
||||
|
||||
Devices don't report property capabilities unless asked; you can request capabilities
|
||||
using inquirePropertyCapabilities().
|
||||
*/
|
||||
std::optional<int> getNumPropertyExchangeRequestsSupportedForMuid (MUID m) const;
|
||||
|
||||
/** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a
|
||||
particular device, this function will return that device's ResourceList if available, or
|
||||
a null var otherwise.
|
||||
*/
|
||||
var getResourceListForMuid (MUID x) const;
|
||||
|
||||
/** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a
|
||||
particular device, this function will return that device's DeviceInfo if available, or
|
||||
a null var otherwise.
|
||||
*/
|
||||
var getDeviceInfoForMuid (MUID x) const;
|
||||
|
||||
/** After DeviceListener::propertyExchangeCapabilitiesReceived() has been received for a
|
||||
particular device, this function will return that device's ChannelList if available, or
|
||||
a null var otherwise.
|
||||
*/
|
||||
var getChannelListForMuid (MUID x) const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Flags indicating the features that are supported by a given CI device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class DeviceFeatures
|
||||
{
|
||||
public:
|
||||
/** Constructs a DeviceFeatures object with no flags enabled. */
|
||||
DeviceFeatures() = default;
|
||||
|
||||
/** Constructs a DeviceFeatures object, taking flag values from the "Capability Inquiry
|
||||
Category Supported" byte in a CI Discovery message.
|
||||
*/
|
||||
explicit DeviceFeatures (std::byte f) : flags ((uint8_t) f) {}
|
||||
|
||||
/** Returns a new DeviceFeatures instance with profile configuration marked as supported. */
|
||||
[[nodiscard]] DeviceFeatures withProfileConfigurationSupported (bool x = true) const { return withFlag (profileConfiguration, x); }
|
||||
/** Returns a new DeviceFeatures instance with property exchange marked as supported. */
|
||||
[[nodiscard]] DeviceFeatures withPropertyExchangeSupported (bool x = true) const { return withFlag (propertyExchange, x); }
|
||||
/** Returns a new DeviceFeatures instance with process inquiry marked as supported. */
|
||||
[[nodiscard]] DeviceFeatures withProcessInquirySupported (bool x = true) const { return withFlag (processInquiry, x); }
|
||||
|
||||
/** @see withProfileConfigurationSupported() */
|
||||
[[nodiscard]] bool isProfileConfigurationSupported () const { return getFlag (profileConfiguration); }
|
||||
/** @see withPropertyExchangeSupported() */
|
||||
[[nodiscard]] bool isPropertyExchangeSupported () const { return getFlag (propertyExchange); }
|
||||
/** @see withProcessInquirySupported() */
|
||||
[[nodiscard]] bool isProcessInquirySupported () const { return getFlag (processInquiry); }
|
||||
|
||||
/** Returns the feature flags formatted into a bitfield suitable for use as the "Capability
|
||||
Inquiry Category Supported" byte in a CI Discovery message.
|
||||
*/
|
||||
std::byte getSupportedCapabilities() const { return std::byte { flags }; }
|
||||
|
||||
/** Returns true if this and other both have the same flags set. */
|
||||
bool operator== (const DeviceFeatures& other) const { return flags == other.flags; }
|
||||
/** Returns true if any flags in this and other differ. */
|
||||
bool operator!= (const DeviceFeatures& other) const { return ! operator== (other); }
|
||||
|
||||
private:
|
||||
enum Flags
|
||||
{
|
||||
profileConfiguration = 1 << 2,
|
||||
propertyExchange = 1 << 3,
|
||||
processInquiry = 1 << 4,
|
||||
};
|
||||
|
||||
[[nodiscard]] DeviceFeatures withFlag (Flags f, bool value) const
|
||||
{
|
||||
return withMember (*this, &DeviceFeatures::flags, (uint8_t) (value ? (flags | f) : (flags & ~f)));
|
||||
}
|
||||
|
||||
bool getFlag (Flags f) const
|
||||
{
|
||||
return (flags & f) != 0;
|
||||
}
|
||||
|
||||
uint8_t flags = 0;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Contains information relating to a subscription update. Check the header's
|
||||
subscription kind to find out whether the payload is a full update, a
|
||||
partial update, or empty (as is the case for a notification or
|
||||
subscription-end request).
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySubscriptionData
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
Span<const std::byte> body;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
An interface that receives callbacks when certain messages are received by a Device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct DeviceListener
|
||||
{
|
||||
DeviceListener() = default;
|
||||
virtual ~DeviceListener() = default;
|
||||
DeviceListener (const DeviceListener&) = default;
|
||||
DeviceListener (DeviceListener&&) = default;
|
||||
DeviceListener& operator= (const DeviceListener&) = default;
|
||||
DeviceListener& operator= (DeviceListener&&) = default;
|
||||
|
||||
//==============================================================================
|
||||
/** Called to indicate that a device with the provided MUID was discovered.
|
||||
To find out more about the device, use Device::getDiscoveryInfoForMuid().
|
||||
*/
|
||||
virtual void deviceAdded ([[maybe_unused]] MUID x) {}
|
||||
|
||||
/** Called to indicate that a device's MUID was invalidated.
|
||||
If you were previously storing your own information about this device, you should forget
|
||||
that information here.
|
||||
*/
|
||||
virtual void deviceRemoved ([[maybe_unused]] MUID x) {}
|
||||
|
||||
/** Called to indicate that endpoint information was received for the given device.
|
||||
See the MIDI-CI spec for an explanation of the different status codes.
|
||||
*/
|
||||
virtual void endpointReceived ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] Message::EndpointInquiryResponse response) {}
|
||||
|
||||
|
||||
/** Called to indicate that a NAK message was received.
|
||||
This is useful e.g. to display a diagnostic to the user, or to cache the failed request
|
||||
details and retry the request at a later date.
|
||||
|
||||
The message field of the NAK is 7-bit text. You can convert it to a string using
|
||||
Encodings::stringFrom7BitText().
|
||||
*/
|
||||
virtual void messageNotAcknowledged ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] Message::NAK) {}
|
||||
|
||||
//==============================================================================
|
||||
/** Called to indicate that another device reported its enabled and disabled profiles on a
|
||||
particular channel.
|
||||
|
||||
@see Device::getProfileStateForMuid()
|
||||
*/
|
||||
virtual void profileStateReceived ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ChannelInGroup destination) {}
|
||||
|
||||
/** Called to indicate that a profile was added or removed. */
|
||||
virtual void profilePresenceChanged ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ChannelInGroup destination,
|
||||
[[maybe_unused]] Profile profile,
|
||||
[[maybe_unused]] bool exists) {}
|
||||
|
||||
/** Called to indicate that a profile was enabled or disabled.
|
||||
A channel count of 0 indicates that the profile was disabled.
|
||||
*/
|
||||
virtual void profileEnablementChanged ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ChannelInGroup destination,
|
||||
[[maybe_unused]] Profile profile,
|
||||
[[maybe_unused]] int numChannels) {}
|
||||
|
||||
/** Called to indicate that details about a profile were received. */
|
||||
virtual void profileDetailsReceived ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ChannelInGroup destination,
|
||||
[[maybe_unused]] Profile profile,
|
||||
[[maybe_unused]] std::byte target,
|
||||
[[maybe_unused]] Span<const std::byte> data) {}
|
||||
|
||||
/** Called to indicate that data for a profile were received.
|
||||
|
||||
Note that this function may be called either when a remote device attempts to send data to
|
||||
one of the local Device's profiles, or when a profile on a remote device produces some data.
|
||||
|
||||
Each profile will specify its own mechanism for distinguishing between the two cases if
|
||||
necessary.
|
||||
*/
|
||||
virtual void profileSpecificDataReceived ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ChannelInGroup destination,
|
||||
[[maybe_unused]] Profile profile,
|
||||
[[maybe_unused]] Span<const std::byte> data) {}
|
||||
|
||||
//==============================================================================
|
||||
/** Called to indicate that another device reported its property exchange capabilities.
|
||||
|
||||
@see Device::getPropertyExchangeCapabilitiesResponseForMuid()
|
||||
*/
|
||||
virtual void propertyExchangeCapabilitiesReceived ([[maybe_unused]] MUID x) {}
|
||||
|
||||
/** Called to indicate that a subscription update was received.
|
||||
This only receives messages with responder commands (partial, full, notify, end).
|
||||
|
||||
To start a subscription, use Device::sendPropertySubscriptionStart().
|
||||
*/
|
||||
virtual void propertySubscriptionDataReceived ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] const PropertySubscriptionData& data) {}
|
||||
|
||||
/** Called when a remote device updates a subscription by accepting or terminating it.
|
||||
|
||||
If the subscription was accepted, the subscribeId will be non-null. Otherwise, a null
|
||||
subscribeId indicates that the subscription was terminated.
|
||||
*/
|
||||
virtual void propertySubscriptionChanged ([[maybe_unused]] SubscriptionKey subscription,
|
||||
[[maybe_unused]] const std::optional<String>& subscribeId) {}
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
An interface that will receive a callback every time a Device wishes to send a new MIDI-CI
|
||||
message.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct DeviceMessageHandler
|
||||
{
|
||||
DeviceMessageHandler() = default;
|
||||
virtual ~DeviceMessageHandler() = default;
|
||||
DeviceMessageHandler (const DeviceMessageHandler&) = default;
|
||||
DeviceMessageHandler (DeviceMessageHandler&&) = default;
|
||||
DeviceMessageHandler& operator= (const DeviceMessageHandler&) = default;
|
||||
DeviceMessageHandler& operator= (DeviceMessageHandler&&) = default;
|
||||
|
||||
/** Called with the bytes of a MIDI-CI message, along with the message's group.
|
||||
|
||||
To send the message on, format the message appropriately (either into bytestream sysex
|
||||
or into multiple UMP sysex packets).
|
||||
*/
|
||||
virtual void processMessage (ump::BytesOnGroup) = 0;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Configuration options for a Device.
|
||||
|
||||
The options set here will remain constant over the lifetime of a Device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class DeviceOptions
|
||||
{
|
||||
public:
|
||||
static constexpr auto beginValidAscii = 32; // inclusive
|
||||
static constexpr auto endValidAscii = 127; // exclusive
|
||||
|
||||
/** Creates a random product instance ID.
|
||||
This isn't really recommended - it's probably better to have a unique ID that remains
|
||||
persistent after a restart.
|
||||
*/
|
||||
static std::array<char, 16> makeProductInstanceId (Random& random)
|
||||
{
|
||||
std::array<char, 16> result{};
|
||||
|
||||
for (auto& c : result)
|
||||
c = (char) random.nextInt ({ beginValidAscii, endValidAscii });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** One or more DeviceMessageHandlers that should receive callbacks with any messages that the
|
||||
device wishes to send.
|
||||
Referenced DeviceMessageHandlers *must* outlive any Device constructed from these options.
|
||||
*/
|
||||
[[nodiscard]] DeviceOptions withOutputs (std::vector<DeviceMessageHandler*> x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::outputs, x);
|
||||
}
|
||||
|
||||
/** The function block layout of this device. */
|
||||
[[nodiscard]] DeviceOptions withFunctionBlock (FunctionBlock x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::functionBlock, x);
|
||||
}
|
||||
|
||||
/** Basic information about the device used to determine manufacturer, model, etc.
|
||||
In order to populate this correctly, you'll need to register with the MIDI association -
|
||||
otherwise you might accidentally end up using IDs that are already assigned to other
|
||||
companies/individuals.
|
||||
*/
|
||||
[[nodiscard]] DeviceOptions withDeviceInfo (const ump::DeviceInfo& x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::deviceInfo, x);
|
||||
}
|
||||
|
||||
/** The features that you want to enable on the device.
|
||||
|
||||
If you enable property exchange, you may wish to supply a PropertyDelegate using
|
||||
withPropertyDelegate().
|
||||
If you enable profile configuration, you may wish to supply a ProfileDelegate using
|
||||
withProfileDelegate().
|
||||
Process inquiry is not currently supported.
|
||||
*/
|
||||
[[nodiscard]] DeviceOptions withFeatures (DeviceFeatures x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::features, x);
|
||||
}
|
||||
|
||||
/** The maximum size of sysex messages to accept and to produce. */
|
||||
[[nodiscard]] DeviceOptions withMaxSysExSize (size_t x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::maxSysExSize, x);
|
||||
}
|
||||
|
||||
/** Specifies a profile delegate that can be used to respond to particular profile events.
|
||||
The referenced ProfileDelegate *must* outlive the Device.
|
||||
*/
|
||||
[[nodiscard]] DeviceOptions withProfileDelegate (ProfileDelegate* x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::profileDelegate, x);
|
||||
}
|
||||
|
||||
/** Specifies a property delegate that can be used to respond to particular property events.
|
||||
The referenced PropertyDelegate *must* outlive the Device.
|
||||
*/
|
||||
[[nodiscard]] DeviceOptions withPropertyDelegate (PropertyDelegate* x) const
|
||||
{
|
||||
return withMember (*this, &DeviceOptions::propertyDelegate, x);
|
||||
}
|
||||
|
||||
/** Specifies a product instance ID that will be returned in endpoint response messages. */
|
||||
[[nodiscard]] DeviceOptions withProductInstanceId (const std::array<char, 16>& x) const
|
||||
{
|
||||
const auto null = std::find (x.begin(), x.end(), 0);
|
||||
|
||||
if (! std::all_of (x.begin(), null, [] (char c) { return beginValidAscii <= c && c < endValidAscii; }))
|
||||
{
|
||||
// The product instance ID must be made up of ASCII characters
|
||||
jassertfalse;
|
||||
return *this;
|
||||
}
|
||||
|
||||
if (std::any_of (null, x.end(), [] (auto c) { return c != 0; }))
|
||||
{
|
||||
// All characters after the null terminator must be 0
|
||||
jassertfalse;
|
||||
return *this;
|
||||
}
|
||||
|
||||
return withMember (*this, &DeviceOptions::productInstanceId, x);
|
||||
}
|
||||
|
||||
/** @see withOutputs() */
|
||||
[[nodiscard]] const auto& getOutputs() const { return outputs; }
|
||||
/** @see withFunctionBlock() */
|
||||
[[nodiscard]] const auto& getFunctionBlock() const { return functionBlock; }
|
||||
/** @see withDeviceInfo() */
|
||||
[[nodiscard]] const auto& getDeviceInfo() const { return deviceInfo; }
|
||||
/** @see withFeatures() */
|
||||
[[nodiscard]] const auto& getFeatures() const { return features; }
|
||||
/** @see withMaxSysExSize() */
|
||||
[[nodiscard]] const auto& getMaxSysExSize() const { return maxSysExSize; }
|
||||
/** @see withProductInstanceId() */
|
||||
[[nodiscard]] const auto& getProductInstanceId() const { return productInstanceId; }
|
||||
/** @see withProfileDelegate() */
|
||||
[[nodiscard]] const auto& getProfileDelegate() const { return profileDelegate; }
|
||||
/** @see withPropertyDelegate() */
|
||||
[[nodiscard]] const auto& getPropertyDelegate() const { return propertyDelegate; }
|
||||
|
||||
private:
|
||||
std::vector<DeviceMessageHandler*> outputs;
|
||||
FunctionBlock functionBlock;
|
||||
ump::DeviceInfo deviceInfo;
|
||||
DeviceFeatures features;
|
||||
size_t maxSysExSize = 512;
|
||||
std::array<char, 16> productInstanceId{};
|
||||
ProfileDelegate* profileDelegate = nullptr;
|
||||
PropertyDelegate* propertyDelegate = nullptr;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
#define JUCE_ENCODINGS X(ascii, "ASCII") X(mcoded7, "Mcoded7") X(zlibAndMcoded7, "zlib+Mcoded7")
|
||||
|
||||
/**
|
||||
Identifies different encodings that may be used by property exchange messages.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
enum class Encoding
|
||||
{
|
||||
#define X(name, unused) name,
|
||||
JUCE_ENCODINGS
|
||||
#undef X
|
||||
};
|
||||
|
||||
/**
|
||||
Utility functions for working with the Encoding enum.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct EncodingUtils
|
||||
{
|
||||
EncodingUtils() = delete;
|
||||
|
||||
/** Converts an Encoding to a human-readable string. */
|
||||
static const char* toString (Encoding e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
#define X(name, string) case Encoding::name: return string;
|
||||
JUCE_ENCODINGS
|
||||
#undef X
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/** Converts an encoding string from a property exchange JSON header to
|
||||
an Encoding.
|
||||
*/
|
||||
static std::optional<Encoding> toEncoding (const char* str)
|
||||
{
|
||||
#define X(name, string) if (std::string_view (str) == std::string_view (string)) return Encoding::name;
|
||||
JUCE_ENCODINGS
|
||||
#undef X
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
#undef JUCE_ENCODINGS
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
|
||||
#ifndef DOXYGEN
|
||||
|
||||
namespace juce
|
||||
{
|
||||
template <>
|
||||
struct SerialisationTraits<midi_ci::Encoding>
|
||||
{
|
||||
static constexpr auto marshallingVersion = std::nullopt;
|
||||
|
||||
template <typename Archive>
|
||||
void load (Archive& archive, midi_ci::Encoding& t)
|
||||
{
|
||||
String encoding;
|
||||
archive (encoding);
|
||||
t = midi_ci::EncodingUtils::toEncoding (encoding.toRawUTF8()).value_or (midi_ci::Encoding{});
|
||||
}
|
||||
|
||||
template <typename Archive>
|
||||
void save (Archive& archive, const midi_ci::Encoding& t)
|
||||
{
|
||||
archive (midi_ci::EncodingUtils::toString (t));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
||||
#endif // ifndef DOXYGEN
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
String Encodings::stringFrom7BitText (Span<const std::byte> bytes)
|
||||
{
|
||||
std::vector<CharPointer_UTF16::CharType> chars;
|
||||
|
||||
while (! bytes.empty())
|
||||
{
|
||||
const auto front = (uint8_t) bytes.front();
|
||||
|
||||
if ((front < 0x20 || 0x80 <= front) && front != 0x0a)
|
||||
{
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (front == '\\')
|
||||
{
|
||||
bytes = Span (bytes.data() + 1, bytes.size() - 1);
|
||||
|
||||
if (bytes.empty())
|
||||
return {};
|
||||
|
||||
const auto kind = (uint8_t) bytes.front();
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case '"': chars.push_back ('"'); break;
|
||||
case '\\': chars.push_back ('\\'); break;
|
||||
case '/': chars.push_back ('/'); break;
|
||||
case 'b': chars.push_back ('\b'); break;
|
||||
case 'f': chars.push_back ('\f'); break;
|
||||
case 'n': chars.push_back ('\n'); break;
|
||||
case 'r': chars.push_back ('\r'); break;
|
||||
case 't': chars.push_back ('\t'); break;
|
||||
|
||||
case 'u':
|
||||
{
|
||||
bytes = Span (bytes.data() + 1, bytes.size() - 1);
|
||||
|
||||
if (bytes.size() < 4)
|
||||
return {};
|
||||
|
||||
std::string byteStr (reinterpret_cast<const char*> (bytes.data()), 4);
|
||||
const auto unit = [&]() -> std::optional<CharPointer_UTF16::CharType>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (CharPointer_UTF16::CharType) std::stoi (byteStr, {}, 16);
|
||||
}
|
||||
catch (...) {}
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
}();
|
||||
|
||||
if (! unit.has_value())
|
||||
return {};
|
||||
|
||||
chars.push_back (*unit);
|
||||
bytes = Span (bytes.data() + 4, bytes.size() - 4);
|
||||
continue;
|
||||
}
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
bytes = Span (bytes.data() + 1, bytes.size() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
chars.push_back (front);
|
||||
bytes = Span (bytes.data() + 1, bytes.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
chars.push_back ({});
|
||||
return String { CharPointer_UTF16 { chars.data() } };
|
||||
}
|
||||
|
||||
std::vector<std::byte> Encodings::stringTo7BitText (const String& text)
|
||||
{
|
||||
std::vector<std::byte> result;
|
||||
|
||||
for (const auto character : text)
|
||||
{
|
||||
if (character == 0x0a || (0x20 <= character && character < 0x80))
|
||||
{
|
||||
result.emplace_back (std::byte (character));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Suspiciously low ASCII value encountered!
|
||||
jassert (character >= 0x80);
|
||||
|
||||
CharPointer_UTF16::CharType points[2]{};
|
||||
CharPointer_UTF16 asUTF16 { points };
|
||||
asUTF16.write (character);
|
||||
|
||||
std::for_each (points, asUTF16.getAddress(), [&] (CharPointer_UTF16::CharType unit)
|
||||
{
|
||||
const auto str = String::toHexString (unit);
|
||||
|
||||
result.insert (result.end(), { std::byte { '\\' }, std::byte { 'u' } });
|
||||
|
||||
for (const auto c : str)
|
||||
result.push_back ((std::byte) c);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::byte> Encodings::toMcoded7 (Span<const std::byte> bytes)
|
||||
{
|
||||
std::vector<std::byte> result;
|
||||
result.reserve ((bytes.size() * 8) + 6 / 7);
|
||||
|
||||
for (size_t index = 0; index < bytes.size(); index += 7)
|
||||
{
|
||||
std::array<std::byte, 7> slice{};
|
||||
const auto sliceSize = std::min ((size_t) 7, bytes.size() - index);
|
||||
std::copy (bytes.begin() + index, bytes.begin() + index + sliceSize, slice.begin());
|
||||
|
||||
result.push_back ((slice[0] & std::byte { 0x80 }) >> 1
|
||||
| (slice[1] & std::byte { 0x80 }) >> 2
|
||||
| (slice[2] & std::byte { 0x80 }) >> 3
|
||||
| (slice[3] & std::byte { 0x80 }) >> 4
|
||||
| (slice[4] & std::byte { 0x80 }) >> 5
|
||||
| (slice[5] & std::byte { 0x80 }) >> 6
|
||||
| (slice[6] & std::byte { 0x80 }) >> 7);
|
||||
std::transform (slice.begin(),
|
||||
std::next (slice.begin(), (ptrdiff_t) sliceSize),
|
||||
std::back_inserter (result),
|
||||
[] (const std::byte b) { return b & std::byte { 0x7f }; });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::byte> Encodings::fromMcoded7 (Span<const std::byte> bytes)
|
||||
{
|
||||
std::vector<std::byte> result;
|
||||
result.reserve ((bytes.size() * 7) + 7 / 8);
|
||||
|
||||
for (size_t index = 0; index < bytes.size(); index += 8)
|
||||
{
|
||||
const auto sliceSize = std::min ((size_t) 7, bytes.size() - index - 1);
|
||||
|
||||
for (size_t i = 0; i < sliceSize; ++i)
|
||||
{
|
||||
const auto highBit = (bytes[index] & std::byte { (uint8_t) (1 << (6 - i)) }) << (i + 1);
|
||||
result.push_back (highBit | bytes[index + 1 + i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::byte>> Encodings::tryEncode (Span<const std::byte> bytes, Encoding mutualEncoding)
|
||||
{
|
||||
switch (mutualEncoding)
|
||||
{
|
||||
case Encoding::ascii:
|
||||
{
|
||||
if (std::all_of (bytes.begin(), bytes.end(), [] (auto b) { return (b & std::byte { 0x80 }) == std::byte{}; }))
|
||||
return std::vector<std::byte> (bytes.begin(), bytes.end());
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
case Encoding::mcoded7:
|
||||
return toMcoded7 (bytes);
|
||||
|
||||
case Encoding::zlibAndMcoded7:
|
||||
{
|
||||
MemoryOutputStream memoryStream;
|
||||
GZIPCompressorOutputStream (memoryStream).write (bytes.data(), bytes.size());
|
||||
return toMcoded7 (Span (static_cast<const std::byte*> (memoryStream.getData()), memoryStream.getDataSize()));
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown encoding!
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::byte> Encodings::decode (Span<const std::byte> bytes, Encoding mutualEncoding)
|
||||
{
|
||||
if (mutualEncoding == Encoding::ascii)
|
||||
{
|
||||
// All values must be 7-bit!
|
||||
jassert (std::none_of (bytes.begin(), bytes.end(), [] (const auto& b) { return (b & std::byte { 0x80 }) != std::byte{}; }));
|
||||
return std::vector<std::byte> (bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
if (mutualEncoding == Encoding::mcoded7)
|
||||
return fromMcoded7 (bytes);
|
||||
|
||||
if (mutualEncoding == Encoding::zlibAndMcoded7)
|
||||
{
|
||||
const auto mcoded = fromMcoded7 (bytes);
|
||||
MemoryInputStream memoryStream (mcoded.data(), mcoded.size(), false);
|
||||
|
||||
GZIPDecompressorInputStream zipStream (memoryStream);
|
||||
|
||||
const size_t chunkSize = 1 << 8;
|
||||
|
||||
std::vector<std::byte> result;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const auto previousSize = result.size();
|
||||
result.resize (previousSize + chunkSize);
|
||||
const auto read = zipStream.read (result.data() + previousSize, chunkSize);
|
||||
|
||||
if (read < 0)
|
||||
{
|
||||
// Decompression failed!
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
result.resize ((size_t) read + previousSize);
|
||||
|
||||
if (read == 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown encoding!
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class EncodingsTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
EncodingsTests() : UnitTest ("Encodings", UnitTestCategories::midi) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("7-bit text encoding");
|
||||
{
|
||||
{
|
||||
const auto converted = Encodings::stringTo7BitText (juce::CharPointer_UTF8 ("Accepted Beat \xe2\x99\xaa"));
|
||||
const auto expected = makeByteArray ('A', 'c', 'c', 'e', 'p', 't', 'e', 'd', ' ', 'B', 'e', 'a', 't', ' ', '\\', 'u', '2', '6', '6', 'a');
|
||||
expect (std::equal (converted.begin(), converted.end(), expected.begin(), expected.end()));
|
||||
}
|
||||
|
||||
{
|
||||
const auto converted = Encodings::stringTo7BitText (juce::CharPointer_UTF8 ("\xe6\xae\x8b\xe3\x82\x8a\xe3\x82\x8f\xe3\x81\x9a\xe3\x81\x8b""5\xe3\x83\x90\xe3\x82\xa4\xe3\x83\x88"));
|
||||
const auto expected = makeByteArray ('\\', 'u', '6', 'b', '8', 'b',
|
||||
'\\', 'u', '3', '0', '8', 'a',
|
||||
'\\', 'u', '3', '0', '8', 'f',
|
||||
'\\', 'u', '3', '0', '5', 'a',
|
||||
'\\', 'u', '3', '0', '4', 'b',
|
||||
'5',
|
||||
'\\', 'u', '3', '0', 'd', '0',
|
||||
'\\', 'u', '3', '0', 'a', '4',
|
||||
'\\', 'u', '3', '0', 'c', '8');
|
||||
expect (std::equal (converted.begin(), converted.end(), expected.begin(), expected.end()));
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("7-bit text decoding");
|
||||
{
|
||||
{
|
||||
const auto converted = Encodings::stringFrom7BitText (makeByteArray ('A', 'c', 'c', 'e', 'p', 't', 'e', 'd', ' ', 'B', 'e', 'a', 't', ' ', '\\', 'u', '2', '6', '6', 'a'));
|
||||
const String expected = juce::CharPointer_UTF8 ("Accepted Beat \xe2\x99\xaa");
|
||||
expect (converted == expected);
|
||||
}
|
||||
|
||||
{
|
||||
const auto converted = Encodings::stringFrom7BitText (makeByteArray ('\\', 'u', '6', 'b', '8', 'b',
|
||||
'\\', 'u', '3', '0', '8', 'a',
|
||||
'\\', 'u', '3', '0', '8', 'f',
|
||||
'\\', 'u', '3', '0', '5', 'a',
|
||||
'\\', 'u', '3', '0', '4', 'b',
|
||||
'5',
|
||||
'\\', 'u', '3', '0', 'd', '0',
|
||||
'\\', 'u', '3', '0', 'a', '4',
|
||||
'\\', 'u', '3', '0', 'c', '8'));
|
||||
const String expected = juce::CharPointer_UTF8 ("\xe6\xae\x8b\xe3\x82\x8a\xe3\x82\x8f\xe3\x81\x9a\xe3\x81\x8b""5\xe3\x83\x90\xe3\x82\xa4\xe3\x83\x88");
|
||||
expect (converted == expected);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Mcoded7 encoding");
|
||||
{
|
||||
{
|
||||
const auto converted = Encodings::toMcoded7 (makeByteArray (0x81, 0x82, 0x83));
|
||||
const auto expected = makeByteArray (0x70, 0x01, 0x02, 0x03);
|
||||
expect (rangesEqual (converted, expected));
|
||||
}
|
||||
|
||||
{
|
||||
const auto converted = Encodings::toMcoded7 (makeByteArray (0x01, 0x82, 0x03, 0x04, 0x85, 0x06, 0x87, 0x08));
|
||||
const auto expected = makeByteArray (0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x08);
|
||||
expect (rangesEqual (converted, expected));
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Mcoded7 decoding");
|
||||
{
|
||||
{
|
||||
const auto converted = Encodings::fromMcoded7 (makeByteArray (0x70, 0x01, 0x02, 0x03));
|
||||
const auto expected = makeByteArray (0x81, 0x82, 0x83);
|
||||
expect (rangesEqual (converted, expected));
|
||||
}
|
||||
|
||||
{
|
||||
const auto converted = Encodings::fromMcoded7 (makeByteArray (0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x08));
|
||||
const auto expected = makeByteArray (0x01, 0x82, 0x03, 0x04, 0x85, 0x06, 0x87, 0x08);
|
||||
expect (rangesEqual (converted, expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static bool deepEqual (const std::optional<var>& a, const std::optional<var>& b)
|
||||
{
|
||||
if (a.has_value() && b.has_value())
|
||||
return JSONUtils::deepEqual (*a, *b);
|
||||
|
||||
return a == b;
|
||||
}
|
||||
|
||||
template <typename A, typename B>
|
||||
static bool rangesEqual (A&& a, B&& b)
|
||||
{
|
||||
using std::begin, std::end;
|
||||
return std::equal (begin (a), end (a), begin (b), end (b));
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
static std::array<std::byte, sizeof... (Ts)> makeByteArray (Ts&&... ts)
|
||||
{
|
||||
jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits<uint8_t>::max()) && ...));
|
||||
return { std::byte (ts)... };
|
||||
}
|
||||
};
|
||||
|
||||
static EncodingsTests encodingsTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Utility functions for working with data formats used by property exchange
|
||||
messages.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Encodings
|
||||
{
|
||||
/** Text in ACK and NAK messages can't be utf-8 or ASCII because each byte only has 7 usable bits.
|
||||
The encoding rules are in section 5.10.4 of the CI spec.
|
||||
*/
|
||||
static String stringFrom7BitText (Span<const std::byte> bytes);
|
||||
|
||||
/** Text in ACK and NAK messages can't be utf-8 or ASCII because each byte only has 7 usable bits.
|
||||
The encoding rules are in section 5.10.4 of the CI spec.
|
||||
*/
|
||||
static std::vector<std::byte> stringTo7BitText (const String& text);
|
||||
|
||||
/** Converts a list of bytes representing a 7-bit ASCII string to JSON. */
|
||||
static var jsonFrom7BitText (Span<const std::byte> bytes)
|
||||
{
|
||||
return JSON::parse (stringFrom7BitText (bytes));
|
||||
}
|
||||
|
||||
/** Converts a JSON object to a list of bytes in 7-bit ASCII format. */
|
||||
static std::vector<std::byte> jsonTo7BitText (const var& v)
|
||||
{
|
||||
return stringTo7BitText (JSON::toString (v, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none)));
|
||||
}
|
||||
|
||||
/** Each group of seven stored bytes is transmitted as eight bytes.
|
||||
First, the sign bits of the seven bytes are sent, followed by the low-order 7 bits of each byte.
|
||||
*/
|
||||
static std::vector<std::byte> toMcoded7 (Span<const std::byte> bytes);
|
||||
|
||||
/** Each group of seven stored bytes is transmitted as eight bytes.
|
||||
First, the sign bits of the seven bytes are sent, followed by the low-order 7 bits of each byte.
|
||||
*/
|
||||
static std::vector<std::byte> fromMcoded7 (Span<const std::byte> bytes);
|
||||
|
||||
/** Attempts to encode the provided byte span using the specified encoding.
|
||||
|
||||
The ASCII encoding does not make any changes to the input stream, but
|
||||
encoding will fail if any byte has its most significant bit set.
|
||||
*/
|
||||
static std::optional<std::vector<std::byte>> tryEncode (Span<const std::byte> bytes,
|
||||
Encoding mutualEncoding);
|
||||
|
||||
/** Decodes the provided byte span using the specified encoding.
|
||||
|
||||
All bytes of the input must be 7-bit values, i.e. all most-significant bits
|
||||
are unset.
|
||||
*/
|
||||
static std::vector<std::byte> decode (Span<const std::byte> bytes, Encoding mutualEncoding);
|
||||
|
||||
Encodings() = delete;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Contains information about a MIDI 2.0 function block.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct FunctionBlock
|
||||
{
|
||||
std::byte identifier { 0x7f }; ///< 0x7f == no function block
|
||||
uint8_t firstGroup = 0; ///< The first group that is part of the block, 0-based
|
||||
uint8_t numGroups = 1; ///< The number of groups contained in the block
|
||||
|
||||
bool operator== (const FunctionBlock& other) const
|
||||
{
|
||||
const auto tie = [] (auto& x) { return std::tie (x.identifier, x.firstGroup, x.numGroups); };
|
||||
return tie (*this) == tie (other);
|
||||
}
|
||||
|
||||
bool operator!= (const FunctionBlock& other) const { return ! operator== (other); }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,777 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Byte values representing different addresses within a group.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
enum class ChannelInGroup : uint8_t
|
||||
{
|
||||
channel0 = 0x0,
|
||||
channel1 = 0x1,
|
||||
channel2 = 0x2,
|
||||
channel3 = 0x3,
|
||||
channel4 = 0x4,
|
||||
channel5 = 0x5,
|
||||
channel6 = 0x6,
|
||||
channel7 = 0x7,
|
||||
channel8 = 0x8,
|
||||
channel9 = 0x9,
|
||||
channelA = 0xA,
|
||||
channelB = 0xB,
|
||||
channelC = 0xC,
|
||||
channelD = 0xD,
|
||||
channelE = 0xE,
|
||||
channelF = 0xF,
|
||||
wholeGroup = 0x7e, ///< Refers to all channels in the UMP group
|
||||
wholeBlock = 0x7f, ///< Refers to all channels in the function block that contains the UMP group
|
||||
};
|
||||
|
||||
/**
|
||||
Utility functions for working with the ChannelInGroup enum.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ChannelInGroupUtils
|
||||
{
|
||||
ChannelInGroupUtils() = delete;
|
||||
|
||||
/** Converts a ChannelInGroup to a descriptive string. */
|
||||
static String toString (ChannelInGroup c)
|
||||
{
|
||||
if (c == ChannelInGroup::wholeGroup)
|
||||
return "Group";
|
||||
|
||||
if (c == ChannelInGroup::wholeBlock)
|
||||
return "Function Block";
|
||||
|
||||
const auto underlying = (std::underlying_type_t<ChannelInGroup>) c;
|
||||
return "Channel " + String (underlying + 1);
|
||||
}
|
||||
};
|
||||
|
||||
using Profile = std::array<std::byte, 5>;
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Namespace containing structs representing different kinds of MIDI-CI message.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
namespace Message
|
||||
{
|
||||
/** Wraps a span, providing equality operators that compare the span
|
||||
contents elementwise.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
template <typename T>
|
||||
struct ComparableRange
|
||||
{
|
||||
T& data;
|
||||
|
||||
bool operator== (const ComparableRange& other) const
|
||||
{
|
||||
return std::equal (data.begin(), data.end(), other.data.begin(), other.data.end());
|
||||
}
|
||||
|
||||
bool operator!= (const ComparableRange& other) const { return ! operator== (other); }
|
||||
};
|
||||
|
||||
template <typename T> static constexpr auto makeComparableRange ( T& t) { return ComparableRange< T> { t }; }
|
||||
template <typename T> static constexpr auto makeComparableRange (const T& t) { return ComparableRange<const T> { t }; }
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Holds fields that can be found at the beginning of every MIDI CI message.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Header
|
||||
{
|
||||
ChannelInGroup deviceID{};
|
||||
std::byte category{};
|
||||
std::byte version{};
|
||||
MUID source = MUID::makeUnchecked (0);
|
||||
MUID destination = MUID::makeUnchecked (0);
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (deviceID, category, version, source, destination);
|
||||
}
|
||||
|
||||
bool operator== (const Header& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const Header& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/**
|
||||
Groups together a CI message header, and some number of trailing bytes.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Generic
|
||||
{
|
||||
Header header;
|
||||
Span<const std::byte> data;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct DiscoveryResponse
|
||||
{
|
||||
ump::DeviceInfo device;
|
||||
std::byte capabilities{};
|
||||
uint32_t maximumSysexSize{};
|
||||
std::byte outputPathID{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::byte functionBlock{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (device, capabilities, maximumSysexSize, outputPathID, functionBlock);
|
||||
}
|
||||
|
||||
bool operator== (const DiscoveryResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const DiscoveryResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Discovery
|
||||
{
|
||||
ump::DeviceInfo device;
|
||||
std::byte capabilities{};
|
||||
uint32_t maximumSysexSize{};
|
||||
std::byte outputPathID{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (device, capabilities, maximumSysexSize, outputPathID);
|
||||
}
|
||||
|
||||
bool operator== (const Discovery& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const Discovery& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct EndpointInquiryResponse
|
||||
{
|
||||
std::byte status;
|
||||
Span<const std::byte> data;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (status, makeComparableRange (data));
|
||||
}
|
||||
|
||||
bool operator== (const EndpointInquiryResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const EndpointInquiryResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct EndpointInquiry
|
||||
{
|
||||
std::byte status;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (status);
|
||||
}
|
||||
|
||||
bool operator== (const EndpointInquiry& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const EndpointInquiry& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct InvalidateMUID
|
||||
{
|
||||
MUID target = MUID::makeUnchecked (0);
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (target);
|
||||
}
|
||||
|
||||
bool operator== (const InvalidateMUID& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const InvalidateMUID& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ACK
|
||||
{
|
||||
std::byte originalCategory{};
|
||||
std::byte statusCode{};
|
||||
std::byte statusData{};
|
||||
std::array<std::byte, 5> details{};
|
||||
Span<const std::byte> messageText{};
|
||||
|
||||
/** Convenience function that returns the message's text as a String. */
|
||||
String getMessageTextAsString() const
|
||||
{
|
||||
return Encodings::stringFrom7BitText (messageText);
|
||||
}
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (originalCategory, statusCode, statusData, details, makeComparableRange (messageText));
|
||||
}
|
||||
|
||||
bool operator== (const ACK& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ACK& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct NAK
|
||||
{
|
||||
std::byte originalCategory{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::byte statusCode{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::byte statusData{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::array<std::byte, 5> details{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
Span<const std::byte> messageText{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
/** Convenience function that returns the message's text as a String. */
|
||||
String getMessageTextAsString() const
|
||||
{
|
||||
return Encodings::stringFrom7BitText (messageText);
|
||||
}
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (originalCategory, statusCode, statusData, details, makeComparableRange (messageText));
|
||||
}
|
||||
|
||||
bool operator== (const NAK& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const NAK& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileInquiryResponse
|
||||
{
|
||||
Span<const Profile> enabledProfiles;
|
||||
Span<const Profile> disabledProfiles;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (makeComparableRange (enabledProfiles), makeComparableRange (disabledProfiles));
|
||||
}
|
||||
|
||||
bool operator== (const ProfileInquiryResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileInquiryResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileInquiry
|
||||
{
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple<>();
|
||||
}
|
||||
|
||||
bool operator== (const ProfileInquiry& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileInquiry& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileAdded
|
||||
{
|
||||
Profile profile{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileAdded& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileAdded& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileRemoved
|
||||
{
|
||||
Profile profile{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileRemoved& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileRemoved& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileDetailsResponse
|
||||
{
|
||||
Profile profile{};
|
||||
std::byte target{};
|
||||
Span<const std::byte> data;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, target, makeComparableRange (data));
|
||||
}
|
||||
|
||||
bool operator== (const ProfileDetailsResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileDetailsResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileDetails
|
||||
{
|
||||
Profile profile{};
|
||||
std::byte target{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, target);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileDetails& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileDetails& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileOn
|
||||
{
|
||||
Profile profile{};
|
||||
uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, numChannels);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileOn& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileOn& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileOff
|
||||
{
|
||||
Profile profile{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileOff& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileOff& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileEnabledReport
|
||||
{
|
||||
Profile profile{};
|
||||
uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, numChannels);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileEnabledReport& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileEnabledReport& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileDisabledReport
|
||||
{
|
||||
Profile profile{};
|
||||
uint16_t numChannels{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, numChannels);
|
||||
}
|
||||
|
||||
bool operator== (const ProfileDisabledReport& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileDisabledReport& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileSpecificData
|
||||
{
|
||||
Profile profile{};
|
||||
Span<const std::byte> data;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (profile, makeComparableRange (data));
|
||||
}
|
||||
|
||||
bool operator== (const ProfileSpecificData& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileSpecificData& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyExchangeCapabilitiesResponse
|
||||
{
|
||||
std::byte numSimultaneousRequestsSupported{};
|
||||
std::byte majorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::byte minorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (numSimultaneousRequestsSupported, majorVersion, minorVersion);
|
||||
}
|
||||
|
||||
bool operator== (const PropertyExchangeCapabilitiesResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertyExchangeCapabilitiesResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyExchangeCapabilities
|
||||
{
|
||||
std::byte numSimultaneousRequestsSupported{};
|
||||
std::byte majorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
std::byte minorVersion{}; /**< Only valid if the message header specifies version 0x02 or greater. */
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (numSimultaneousRequestsSupported, majorVersion, minorVersion);
|
||||
}
|
||||
|
||||
bool operator== (const PropertyExchangeCapabilities& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertyExchangeCapabilities& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** A property-exchange message that has no payload, and must therefore
|
||||
be contained in a single chunk.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct StaticSizePropertyExchange
|
||||
{
|
||||
std::byte requestID{};
|
||||
Span<const std::byte> header;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (requestID, makeComparableRange (header));
|
||||
}
|
||||
};
|
||||
|
||||
/** A property-exchange message that may form part of a multi-chunk
|
||||
message sequence.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct DynamicSizePropertyExchange
|
||||
{
|
||||
std::byte requestID{};
|
||||
Span<const std::byte> header;
|
||||
uint16_t totalNumChunks{};
|
||||
uint16_t thisChunkNum{};
|
||||
Span<const std::byte> data;
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (requestID,
|
||||
makeComparableRange (header),
|
||||
totalNumChunks,
|
||||
thisChunkNum,
|
||||
makeComparableRange (data));
|
||||
}
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyGetDataResponse : public DynamicSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertyGetDataResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertyGetDataResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyGetData : public StaticSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertyGetData& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertyGetData& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySetDataResponse : public StaticSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertySetDataResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertySetDataResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySetData : public DynamicSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertySetData& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertySetData& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySubscribeResponse : public DynamicSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertySubscribeResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertySubscribeResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySubscribe : public DynamicSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertySubscribe& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertySubscribe& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyNotify : public DynamicSizePropertyExchange
|
||||
{
|
||||
bool operator== (const PropertyNotify& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const PropertyNotify& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProcessInquiryResponse
|
||||
{
|
||||
std::byte supportedFeatures{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (supportedFeatures);
|
||||
}
|
||||
|
||||
bool operator== (const ProcessInquiryResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProcessInquiryResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProcessInquiry
|
||||
{
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple<>();
|
||||
}
|
||||
|
||||
bool operator== (const ProcessInquiry& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProcessInquiry& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProcessMidiMessageReportResponse
|
||||
{
|
||||
std::byte messageDataControl{};
|
||||
std::byte requestedMessages{};
|
||||
std::byte channelControllerMessages{};
|
||||
std::byte noteDataMessages{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (messageDataControl, requestedMessages, channelControllerMessages, noteDataMessages);
|
||||
}
|
||||
|
||||
bool operator== (const ProcessMidiMessageReportResponse& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProcessMidiMessageReportResponse& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProcessMidiMessageReport
|
||||
{
|
||||
std::byte messageDataControl{};
|
||||
std::byte requestedMessages{};
|
||||
std::byte channelControllerMessages{};
|
||||
std::byte noteDataMessages{};
|
||||
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple (messageDataControl, requestedMessages, channelControllerMessages, noteDataMessages);
|
||||
}
|
||||
|
||||
bool operator== (const ProcessMidiMessageReport& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProcessMidiMessageReport& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/** See the MIDI-CI specification.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProcessEndMidiMessageReport
|
||||
{
|
||||
auto tie() const
|
||||
{
|
||||
return std::tuple<>();
|
||||
}
|
||||
|
||||
bool operator== (const ProcessEndMidiMessageReport& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProcessEndMidiMessageReport& x) const { return ! operator== (x); }
|
||||
};
|
||||
|
||||
/**
|
||||
A message with a header and optional body.
|
||||
|
||||
The body may be set to std::monostate to indicate some kind of failure, such as a malformed
|
||||
incoming message.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Parsed
|
||||
{
|
||||
using Body = std::variant<std::monostate,
|
||||
Discovery,
|
||||
DiscoveryResponse,
|
||||
InvalidateMUID,
|
||||
EndpointInquiry,
|
||||
EndpointInquiryResponse,
|
||||
ACK,
|
||||
NAK,
|
||||
ProfileInquiry,
|
||||
ProfileInquiryResponse,
|
||||
ProfileAdded,
|
||||
ProfileRemoved,
|
||||
ProfileDetails,
|
||||
ProfileDetailsResponse,
|
||||
ProfileOn,
|
||||
ProfileOff,
|
||||
ProfileEnabledReport,
|
||||
ProfileDisabledReport,
|
||||
ProfileSpecificData,
|
||||
PropertyExchangeCapabilities,
|
||||
PropertyExchangeCapabilitiesResponse,
|
||||
PropertyGetData,
|
||||
PropertyGetDataResponse,
|
||||
PropertySetData,
|
||||
PropertySetDataResponse,
|
||||
PropertySubscribe,
|
||||
PropertySubscribeResponse,
|
||||
PropertyNotify,
|
||||
ProcessInquiry,
|
||||
ProcessInquiryResponse,
|
||||
ProcessMidiMessageReport,
|
||||
ProcessMidiMessageReportResponse,
|
||||
ProcessEndMidiMessageReport>;
|
||||
|
||||
Header header;
|
||||
Body body;
|
||||
|
||||
bool operator== (const Parsed& other) const
|
||||
{
|
||||
const auto tie = [] (const auto& x) { return std::tie (x.header, x.body); };
|
||||
return tie (*this) == tie (other);
|
||||
}
|
||||
|
||||
bool operator!= (const Parsed& other) const { return ! operator== (other); }
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
A 28-bit ID that uniquely identifies a device taking part in a series of
|
||||
MIDI-CI transactions.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class MUID
|
||||
{
|
||||
constexpr explicit MUID (uint32_t v) : value (v) {}
|
||||
|
||||
// 0x0fffff00 to 0x0ffffffe are reserved, 0x0fffffff is 'broadcast'
|
||||
static constexpr uint32_t userMuidEnd = 0x0fffff00;
|
||||
static constexpr uint32_t mask = 0x0fffffff;
|
||||
uint32_t value{};
|
||||
|
||||
public:
|
||||
/** Returns the ID as a plain integer. */
|
||||
constexpr uint32_t get() const { return value; }
|
||||
|
||||
/** Converts the provided integer to a MUID without validation that it
|
||||
is within the allowed range.
|
||||
*/
|
||||
static MUID makeUnchecked (uint32_t v)
|
||||
{
|
||||
// If this is hit, the MUID has too many bits set!
|
||||
jassert ((v & mask) == v);
|
||||
return MUID (v);
|
||||
}
|
||||
|
||||
/** Returns a MUID if the provided value is within the valid range for
|
||||
MUID values; otherwise returns nullopt.
|
||||
*/
|
||||
static std::optional<MUID> make (uint32_t v)
|
||||
{
|
||||
if ((v & mask) == v)
|
||||
return makeUnchecked (v);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Makes a random MUID using the provided random engine. */
|
||||
static MUID makeRandom (Random& r)
|
||||
{
|
||||
return makeUnchecked ((uint32_t) r.nextInt (userMuidEnd));
|
||||
}
|
||||
|
||||
bool operator== (const MUID other) const { return value == other.value; }
|
||||
bool operator!= (const MUID other) const { return value != other.value; }
|
||||
bool operator< (const MUID other) const { return value < other.value; }
|
||||
|
||||
/** Returns the special MUID representing the broadcast address. */
|
||||
static constexpr MUID getBroadcast() { return MUID { mask }; }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
std::optional<Message::Parsed> Parser::parse (Span<const std::byte> message, Status* status)
|
||||
{
|
||||
const auto setStatus = [&] (Status s)
|
||||
{
|
||||
if (status != nullptr)
|
||||
*status = s;
|
||||
};
|
||||
|
||||
setStatus (Status::noError);
|
||||
|
||||
Message::Generic generic;
|
||||
|
||||
if (! detail::Marshalling::Reader { message } (generic))
|
||||
{
|
||||
// Got a full sysex message, but it didn't contain a well-formed header.
|
||||
setStatus (Status::malformed);
|
||||
return {};
|
||||
}
|
||||
|
||||
if ((generic.header.version & std::byte { 0x70 }) != std::byte{})
|
||||
{
|
||||
setStatus (Status::reservedVersion);
|
||||
return Message::Parsed { generic.header, std::monostate{} };
|
||||
}
|
||||
|
||||
const auto index = (uint8_t) generic.header.category;
|
||||
constexpr auto tables = detail::MessageTypeUtils::getTables();
|
||||
const auto processFunction = tables.parsers[index];
|
||||
return Message::Parsed { generic.header, processFunction (generic, status) };
|
||||
}
|
||||
|
||||
std::optional<Message::Parsed> Parser::parse (const MUID ourMUID,
|
||||
Span<const std::byte> message,
|
||||
Status* status)
|
||||
{
|
||||
const auto setStatus = [&] (Status s)
|
||||
{
|
||||
if (status != nullptr)
|
||||
*status = s;
|
||||
};
|
||||
|
||||
setStatus (Status::noError);
|
||||
|
||||
if (const auto parsed = parse (message, status))
|
||||
{
|
||||
if (parsed->header.destination != MUID::getBroadcast() && parsed->header.destination != ourMUID)
|
||||
setStatus (Status::mismatchedMUID);
|
||||
else if (parsed->header.source == ourMUID)
|
||||
setStatus (Status::collidingMUID);
|
||||
else if ((parsed->header.version & std::byte { 0x70 }) != std::byte{})
|
||||
setStatus (Status::reservedVersion);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
class DescriptionVisitor : public detail::MessageTypeUtils::MessageVisitor
|
||||
{
|
||||
public:
|
||||
DescriptionVisitor (const Message::Parsed* m, String* str) : msg (m), result (str) {}
|
||||
|
||||
void visit (const std::monostate&) const override {}
|
||||
void visit (const Message::Discovery& body) const override { visitImpl (body); }
|
||||
void visit (const Message::DiscoveryResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::InvalidateMUID& body) const override { visitImpl (body); }
|
||||
void visit (const Message::EndpointInquiry& body) const override { visitImpl (body); }
|
||||
void visit (const Message::EndpointInquiryResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ACK& body) const override { visitImpl (body); }
|
||||
void visit (const Message::NAK& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileInquiry& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileInquiryResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileAdded& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileRemoved& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileDetails& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileDetailsResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileOn& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileOff& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileEnabledReport& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileDisabledReport& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileSpecificData& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyExchangeCapabilitiesResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyGetData& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyGetDataResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySetData& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySetDataResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySubscribeResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyNotify& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProcessInquiry& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProcessInquiryResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProcessMidiMessageReport& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProcessMidiMessageReportResponse& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProcessEndMidiMessageReport& body) const override { visitImpl (body); }
|
||||
|
||||
private:
|
||||
static const char* getDescription (const Message::Discovery&) { return "Discovery"; }
|
||||
static const char* getDescription (const Message::DiscoveryResponse&) { return "Discovery Response"; }
|
||||
static const char* getDescription (const Message::InvalidateMUID&) { return "Invalidate MUID"; }
|
||||
static const char* getDescription (const Message::EndpointInquiry&) { return "Endpoint"; }
|
||||
static const char* getDescription (const Message::EndpointInquiryResponse&) { return "Endpoint Response"; }
|
||||
static const char* getDescription (const Message::ACK&) { return "ACK"; }
|
||||
static const char* getDescription (const Message::NAK&) { return "NAK"; }
|
||||
static const char* getDescription (const Message::ProfileInquiry&) { return "Profile Inquiry"; }
|
||||
static const char* getDescription (const Message::ProfileInquiryResponse&) { return "Profile Inquiry Response"; }
|
||||
static const char* getDescription (const Message::ProfileAdded&) { return "Profile Added"; }
|
||||
static const char* getDescription (const Message::ProfileRemoved&) { return "Profile Removed"; }
|
||||
static const char* getDescription (const Message::ProfileDetails&) { return "Profile Details"; }
|
||||
static const char* getDescription (const Message::ProfileDetailsResponse&) { return "Profile Details Response"; }
|
||||
static const char* getDescription (const Message::ProfileOn&) { return "Profile On"; }
|
||||
static const char* getDescription (const Message::ProfileOff&) { return "Profile Off"; }
|
||||
static const char* getDescription (const Message::ProfileEnabledReport&) { return "Profile Enabled Report"; }
|
||||
static const char* getDescription (const Message::ProfileDisabledReport&) { return "Profile Disabled Report"; }
|
||||
static const char* getDescription (const Message::ProfileSpecificData&) { return "Profile Specific Data"; }
|
||||
static const char* getDescription (const Message::PropertyExchangeCapabilities&) { return "Property Exchange Capabilities"; }
|
||||
static const char* getDescription (const Message::PropertyExchangeCapabilitiesResponse&) { return "Property Exchange Capabilities Response"; }
|
||||
static const char* getDescription (const Message::PropertyGetData&) { return "Property Get Data"; }
|
||||
static const char* getDescription (const Message::PropertyGetDataResponse&) { return "Property Get Data Response"; }
|
||||
static const char* getDescription (const Message::PropertySetData&) { return "Property Set Data"; }
|
||||
static const char* getDescription (const Message::PropertySetDataResponse&) { return "Property Set Data Response"; }
|
||||
static const char* getDescription (const Message::PropertySubscribe&) { return "Property Subscribe"; }
|
||||
static const char* getDescription (const Message::PropertySubscribeResponse&) { return "Property Subscribe Response"; }
|
||||
static const char* getDescription (const Message::PropertyNotify&) { return "Property Notify"; }
|
||||
static const char* getDescription (const Message::ProcessInquiry&) { return "Process Inquiry"; }
|
||||
static const char* getDescription (const Message::ProcessInquiryResponse&) { return "Process Inquiry Response"; }
|
||||
static const char* getDescription (const Message::ProcessMidiMessageReport&) { return "Process Midi Message Report"; }
|
||||
static const char* getDescription (const Message::ProcessMidiMessageReportResponse&) { return "Process Midi Message Report Response"; }
|
||||
static const char* getDescription (const Message::ProcessEndMidiMessageReport&) { return "Process End Midi Message Report"; }
|
||||
|
||||
template <typename Body>
|
||||
void visitImpl (const Body& body) const
|
||||
{
|
||||
const auto opts = ToVarOptions{}.withExplicitVersion ((int) msg->header.version)
|
||||
.withVersionIncluded (false);
|
||||
auto json = ToVar::convert (body, opts);
|
||||
|
||||
if (auto* obj = json->getDynamicObject(); obj != nullptr && obj->hasProperty ("header"))
|
||||
{
|
||||
const auto header = obj->getProperty ("header");
|
||||
const auto bytes = [&]() -> std::vector<std::byte>
|
||||
{
|
||||
const auto* arr = header.getArray();
|
||||
|
||||
if (arr == nullptr)
|
||||
return {};
|
||||
|
||||
std::vector<std::byte> vec;
|
||||
vec.reserve ((size_t) arr->size());
|
||||
|
||||
for (const auto& i : *arr)
|
||||
vec.push_back ((std::byte) (int) i);
|
||||
|
||||
return vec;
|
||||
}();
|
||||
|
||||
obj->setProperty ("header", Encodings::jsonFrom7BitText (bytes));
|
||||
}
|
||||
|
||||
if (json.has_value())
|
||||
*result = String (getDescription (body)) + ": " + JSON::toString (*json, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none));
|
||||
}
|
||||
|
||||
const Message::Parsed* msg = nullptr;
|
||||
String* result = nullptr;
|
||||
};
|
||||
|
||||
String Parser::getMessageDescription (const Message::Parsed& message)
|
||||
{
|
||||
String result { "!! Unrecognised !!" };
|
||||
detail::MessageTypeUtils::visit (message, DescriptionVisitor { &message, &result });
|
||||
return result;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class ParserTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
ParserTests() : UnitTest ("Parser", UnitTestCategories::midi) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
auto random = getRandom();
|
||||
|
||||
beginTest ("Sending an empty message does nothing");
|
||||
{
|
||||
const auto parsed = Parser::parse (MUID::makeRandom (random), {});
|
||||
expect (parsed == std::nullopt);
|
||||
}
|
||||
|
||||
beginTest ("Sending a garbage message does nothing");
|
||||
{
|
||||
const std::vector<std::byte> bytes (128, std::byte { 0x70 });
|
||||
const auto parsed = Parser::parse (MUID::makeRandom (random), bytes);
|
||||
expect (parsed == std::nullopt);
|
||||
}
|
||||
|
||||
beginTest ("Sending a message with truncated body produces a malformed status");
|
||||
{
|
||||
constexpr auto version1 = 0x01;
|
||||
const auto truncatedV1 = makeByteArray (0x7e,
|
||||
/* to function block */ 0x7f,
|
||||
/* midi CI */ 0x0d,
|
||||
/* discovery message */ 0x70,
|
||||
/* version */ version1,
|
||||
/* source MUID */ 0x01,
|
||||
/* ... */ 0x02,
|
||||
/* ... */ 0x03,
|
||||
/* ... */ 0x04,
|
||||
/* broadcast MUID */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* manufacturer */ 0x10,
|
||||
/* ... */ 0x11,
|
||||
/* ... */ 0x12,
|
||||
/* family */ 0x20,
|
||||
/* ... */ 0x21,
|
||||
/* model */ 0x30,
|
||||
/* ... */ 0x31,
|
||||
/* revision */ 0x40,
|
||||
/* ... */ 0x41,
|
||||
/* ... */ 0x42,
|
||||
/* ... */ 0x43,
|
||||
/* CI category supported */ 0x7f,
|
||||
/* max sysex size */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f);
|
||||
/* Missing final byte for a version 1 message */
|
||||
Parser::Status status{};
|
||||
const auto parsedV1 = Parser::parse (MUID::makeRandom (random), truncatedV1, &status);
|
||||
|
||||
expect (status == Parser::Status::malformed);
|
||||
expect (parsedV1 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
|
||||
std::byte { 0x70 },
|
||||
std::byte { version1 },
|
||||
MUID::makeUnchecked (0x80c101),
|
||||
MUID::getBroadcast() },
|
||||
std::monostate{} });
|
||||
|
||||
constexpr auto version2 = 0x02;
|
||||
const auto truncatedV2 = makeByteArray (0x7e,
|
||||
/* to function block */ 0x7f,
|
||||
/* midi CI */ 0x0d,
|
||||
/* discovery message */ 0x70,
|
||||
/* version */ version2,
|
||||
/* source MUID */ 0x01,
|
||||
/* ... */ 0x02,
|
||||
/* ... */ 0x03,
|
||||
/* ... */ 0x04,
|
||||
/* broadcast MUID */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* manufacturer */ 0x10,
|
||||
/* ... */ 0x11,
|
||||
/* ... */ 0x12,
|
||||
/* family */ 0x20,
|
||||
/* ... */ 0x21,
|
||||
/* model */ 0x30,
|
||||
/* ... */ 0x31,
|
||||
/* revision */ 0x40,
|
||||
/* ... */ 0x41,
|
||||
/* ... */ 0x42,
|
||||
/* ... */ 0x43,
|
||||
/* CI category supported */ 0x7f,
|
||||
/* max sysex size */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f);
|
||||
/* Missing final byte for a version 2 message */
|
||||
const auto parsedV2 = Parser::parse (MUID::makeRandom (random), truncatedV2);
|
||||
|
||||
expect (status == Parser::Status::malformed);
|
||||
expect (parsedV2 == Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
|
||||
std::byte { 0x70 },
|
||||
std::byte { version2 },
|
||||
MUID::makeUnchecked (0x80c101),
|
||||
MUID::getBroadcast() },
|
||||
std::monostate{} });
|
||||
}
|
||||
|
||||
const auto getExpectedDiscoveryInput = [] (uint8_t version, uint8_t outputPathID)
|
||||
{
|
||||
return Message::Parsed { Message::Header { ChannelInGroup::wholeBlock,
|
||||
std::byte { 0x70 },
|
||||
std::byte { version },
|
||||
MUID::makeUnchecked (0x80c101),
|
||||
MUID::getBroadcast() },
|
||||
Message::Discovery { { { std::byte { 0x10 }, std::byte { 0x11 }, std::byte { 0x12 } },
|
||||
{ std::byte { 0x20 }, std::byte { 0x21 } },
|
||||
{ std::byte { 0x30 }, std::byte { 0x31 } },
|
||||
{ std::byte { 0x40 }, std::byte { 0x41 }, std::byte { 0x42 }, std::byte { 0x43 } } },
|
||||
std::byte { 0x7f },
|
||||
0xfffffff,
|
||||
std::byte { outputPathID } } };
|
||||
};
|
||||
|
||||
beginTest ("Sending a V1 discovery message notifies the input listener");
|
||||
{
|
||||
const auto initialMUID = MUID::makeRandom (random);
|
||||
constexpr uint8_t version = 0x01;
|
||||
|
||||
const auto bytes = makeByteArray (0x7e,
|
||||
/* to function block */ 0x7f,
|
||||
/* midi CI */ 0x0d,
|
||||
/* discovery message */ 0x70,
|
||||
/* version */ version,
|
||||
/* source MUID */ 0x01,
|
||||
/* ... */ 0x02,
|
||||
/* ... */ 0x03,
|
||||
/* ... */ 0x04,
|
||||
/* broadcast MUID */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* manufacturer */ 0x10,
|
||||
/* ... */ 0x11,
|
||||
/* ... */ 0x12,
|
||||
/* family */ 0x20,
|
||||
/* ... */ 0x21,
|
||||
/* model */ 0x30,
|
||||
/* ... */ 0x31,
|
||||
/* revision */ 0x40,
|
||||
/* ... */ 0x41,
|
||||
/* ... */ 0x42,
|
||||
/* ... */ 0x43,
|
||||
/* CI category supported */ 0x7f,
|
||||
/* max sysex size */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f);
|
||||
const auto parsed = Parser::parse (initialMUID, bytes);
|
||||
|
||||
expect (parsed == getExpectedDiscoveryInput (version, 0));
|
||||
}
|
||||
|
||||
beginTest ("Sending a V2 discovery message notifies the input listener");
|
||||
{
|
||||
constexpr uint8_t outputPathID = 5;
|
||||
const auto initialMUID = MUID::makeRandom (random);
|
||||
constexpr uint8_t version = 0x02;
|
||||
|
||||
const auto bytes = makeByteArray (0x7e,
|
||||
/* to function block */ 0x7f,
|
||||
/* midi CI */ 0x0d,
|
||||
/* discovery message */ 0x70,
|
||||
/* version */ version,
|
||||
/* source MUID */ 0x01,
|
||||
/* ... */ 0x02,
|
||||
/* ... */ 0x03,
|
||||
/* ... */ 0x04,
|
||||
/* broadcast MUID */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* manufacturer */ 0x10,
|
||||
/* ... */ 0x11,
|
||||
/* ... */ 0x12,
|
||||
/* family */ 0x20,
|
||||
/* ... */ 0x21,
|
||||
/* model */ 0x30,
|
||||
/* ... */ 0x31,
|
||||
/* revision */ 0x40,
|
||||
/* ... */ 0x41,
|
||||
/* ... */ 0x42,
|
||||
/* ... */ 0x43,
|
||||
/* CI category supported */ 0x7f,
|
||||
/* max sysex size */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* output path ID */ outputPathID);
|
||||
const auto parsed = Parser::parse (initialMUID, bytes);
|
||||
|
||||
expect (parsed == getExpectedDiscoveryInput (version, outputPathID));
|
||||
}
|
||||
|
||||
beginTest ("Sending a discovery message with a future version notifies the input listener and ignores trailing fields");
|
||||
{
|
||||
constexpr uint8_t outputPathID = 10;
|
||||
const auto initialMUID = MUID::makeRandom (random);
|
||||
constexpr auto version = (uint8_t) detail::MessageMeta::implementationVersion + 1;
|
||||
|
||||
const auto bytes = makeByteArray (0x7e,
|
||||
/* to function block */ 0x7f,
|
||||
/* midi CI */ 0x0d,
|
||||
/* discovery message */ 0x70,
|
||||
/* version */ version,
|
||||
/* source MUID */ 0x01,
|
||||
/* ... */ 0x02,
|
||||
/* ... */ 0x03,
|
||||
/* ... */ 0x04,
|
||||
/* broadcast MUID */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* manufacturer */ 0x10,
|
||||
/* ... */ 0x11,
|
||||
/* ... */ 0x12,
|
||||
/* family */ 0x20,
|
||||
/* ... */ 0x21,
|
||||
/* model */ 0x30,
|
||||
/* ... */ 0x31,
|
||||
/* revision */ 0x40,
|
||||
/* ... */ 0x41,
|
||||
/* ... */ 0x42,
|
||||
/* ... */ 0x43,
|
||||
/* CI category supported */ 0x7f,
|
||||
/* max sysex size */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* ... */ 0x7f,
|
||||
/* output path ID */ outputPathID,
|
||||
/* extra bytes */ 0x00,
|
||||
/* ... */ 0x00,
|
||||
/* ... */ 0x00,
|
||||
/* ... */ 0x00);
|
||||
const auto parsed = Parser::parse (initialMUID, bytes);
|
||||
|
||||
expect (parsed == getExpectedDiscoveryInput (version, outputPathID));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename... Ts>
|
||||
static std::array<std::byte, sizeof... (Ts)> makeByteArray (Ts&&... ts)
|
||||
{
|
||||
jassert (((0 <= (int) ts && (int) ts <= std::numeric_limits<uint8_t>::max()) && ...));
|
||||
return { std::byte (ts)... };
|
||||
}
|
||||
};
|
||||
|
||||
static ParserTests parserTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Parses CI messages.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class Parser
|
||||
{
|
||||
public:
|
||||
Parser() = delete;
|
||||
|
||||
enum class Status
|
||||
{
|
||||
noError, ///< Parsing was successful
|
||||
mismatchedMUID, ///< The message destination MUID doesn't match the provided MUID
|
||||
collidingMUID, ///< The message source MUID matches the provided MUID
|
||||
unrecognisedMessage, ///< The message ID doesn't correspond to a known message
|
||||
reservedVersion, ///< The MIDI CI version uses an unrecognised major version
|
||||
malformed, ///< The message (whole message, or just body) could not be parsed
|
||||
};
|
||||
|
||||
/** Parses the provided message;
|
||||
|
||||
Call this with a full CI message. Don't include any "extra" bytes such as
|
||||
the leading/trailing 0xf0/0xf7 for messages that were originally in bytestream midi format,
|
||||
or the packet-header bytes from UMP-formatted sysex messages.
|
||||
|
||||
Returns nullopt if the message doesn't need to be acknowledged by the entity with the provided MUID,
|
||||
or if the message is malformed.
|
||||
Otherwise, returns a parsed header, and optionally a body.
|
||||
If the body is std::monostate, then something went wrong while parsing. For example, the body
|
||||
may be malformed, or the CI version might be unrecognised.
|
||||
*/
|
||||
static std::optional<Message::Parsed> parse (MUID ourMUID, Span<const std::byte> message, Status* = nullptr);
|
||||
|
||||
/** Parses the provided message;
|
||||
|
||||
Call this with a full CI message. Don't include any "extra" bytes such as
|
||||
the leading/trailing 0xf0/0xf7 for messages that were originally in bytestream midi format,
|
||||
or the packet-header bytes from UMP-formatted sysex messages.
|
||||
|
||||
Returns nullopt if the message is malformed.
|
||||
Otherwise, returns a parsed header, and optionally a body.
|
||||
If the body is std::monostate, then something went wrong while parsing. For example, the body
|
||||
may be malformed, or the CI version might be unrecognised.
|
||||
*/
|
||||
static std::optional<Message::Parsed> parse (Span<const std::byte> message, Status* = nullptr);
|
||||
|
||||
/** Returns a human-readable string describing the message. */
|
||||
static String getMessageDescription (const Message::Parsed& message);
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Holds a profile ID, and the address of a group/channel.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ProfileAtAddress
|
||||
{
|
||||
auto tie() const { return std::tie (profile, address); }
|
||||
|
||||
public:
|
||||
Profile profile; ///< The id of a MIDI-CI profile
|
||||
ChannelAddress address; ///< A group and channel
|
||||
|
||||
bool operator== (const ProfileAtAddress& x) const { return tie() == x.tie(); }
|
||||
bool operator!= (const ProfileAtAddress& x) const { return tie() != x.tie(); }
|
||||
|
||||
bool operator< (const ProfileAtAddress& x) const { return tie() < x.tie(); }
|
||||
bool operator<= (const ProfileAtAddress& x) const { return tie() <= x.tie(); }
|
||||
bool operator> (const ProfileAtAddress& x) const { return tie() > x.tie(); }
|
||||
bool operator>= (const ProfileAtAddress& x) const { return tie() >= x.tie(); }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
An interface with methods that can be overridden to customise how a Device
|
||||
implementing profiles responds to profile inquiries.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileDelegate
|
||||
{
|
||||
ProfileDelegate() = default;
|
||||
virtual ~ProfileDelegate() = default;
|
||||
ProfileDelegate (const ProfileDelegate&) = default;
|
||||
ProfileDelegate (ProfileDelegate&&) = default;
|
||||
ProfileDelegate& operator= (const ProfileDelegate&) = default;
|
||||
ProfileDelegate& operator= (ProfileDelegate&&) = default;
|
||||
|
||||
/** Called when a remote device requests that a profile is enabled or disabled.
|
||||
|
||||
Old MIDI-CI implementations on remote devices may request that a profile
|
||||
is enabled with zero channels active - in this situation, it is
|
||||
recommended that you use ProfileHost::enableProfile to enable the
|
||||
default number of channels for that profile.
|
||||
|
||||
Additionally, profiles for entire groups or function blocks may be enabled with zero
|
||||
active channels. In this case, the profile should be enabled on the entire group or
|
||||
function block.
|
||||
*/
|
||||
virtual void profileEnablementRequested ([[maybe_unused]] MUID x,
|
||||
[[maybe_unused]] ProfileAtAddress profileAtAddress,
|
||||
[[maybe_unused]] int numChannels,
|
||||
[[maybe_unused]] bool enabled) = 0;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
class ProfileHost::Visitor : public detail::MessageTypeUtils::MessageVisitor
|
||||
{
|
||||
public:
|
||||
Visitor (ProfileHost* h, ResponderOutput* o, bool* b)
|
||||
: host (h), output (o), handled (b) {}
|
||||
|
||||
void visit (const Message::ProfileInquiry& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileDetails& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileOn& body) const override { visitImpl (body); }
|
||||
void visit (const Message::ProfileOff& body) const override { visitImpl (body); }
|
||||
using MessageVisitor::visit;
|
||||
|
||||
static auto getNumChannels (Message::Header header, Message::ProfileOn p)
|
||||
{
|
||||
return (uint8_t) header.version >= 2 ? p.numChannels : 1;
|
||||
}
|
||||
|
||||
static auto getNumChannels (Message::Header, Message::ProfileOff) { return 0; }
|
||||
|
||||
private:
|
||||
template <typename Body>
|
||||
void visitImpl (const Body& body) const { *handled = messageReceived (body); }
|
||||
|
||||
bool messageReceived (const Message::ProfileInquiry&) const
|
||||
{
|
||||
host->isResponder = true;
|
||||
|
||||
if ((uint8_t) output->getIncomingHeader().deviceID < 16
|
||||
|| output->getIncomingHeader().deviceID == ChannelInGroup::wholeGroup)
|
||||
{
|
||||
if (const auto* state = host->getProfileStates().groupStates[output->getIncomingGroup()].getStateForDestination (output->getIncomingHeader().deviceID))
|
||||
{
|
||||
const auto active = state->getActive();
|
||||
const auto inactive = state->getInactive();
|
||||
detail::MessageTypeUtils::send (*output, Message::ProfileInquiryResponse { active, inactive });
|
||||
}
|
||||
}
|
||||
else if (output->getIncomingHeader().deviceID == ChannelInGroup::wholeBlock)
|
||||
{
|
||||
auto header = output->getReplyHeader (detail::MessageMeta::Meta<Message::ProfileInquiryResponse>::subID2);
|
||||
|
||||
const auto sendIfNonEmpty = [&] (const auto group, const auto& state)
|
||||
{
|
||||
if (! state.empty())
|
||||
{
|
||||
const auto active = state.getActive();
|
||||
const auto inactive = state.getInactive();
|
||||
detail::MessageTypeUtils::send (*output, (uint8_t) group, header, Message::ProfileInquiryResponse { active, inactive });
|
||||
}
|
||||
};
|
||||
|
||||
for (auto groupNum = 0; groupNum < host->functionBlock.numGroups; ++groupNum)
|
||||
{
|
||||
const auto group = host->functionBlock.firstGroup + groupNum;
|
||||
const auto& groupState = host->getProfileStates().groupStates[(size_t) group];
|
||||
|
||||
for (size_t channel = 0; channel < groupState.channelStates.size(); ++channel)
|
||||
{
|
||||
header.deviceID = ChannelInGroup (channel);
|
||||
sendIfNonEmpty (group, groupState.channelStates[channel]);
|
||||
}
|
||||
}
|
||||
|
||||
header.deviceID = ChannelInGroup::wholeGroup;
|
||||
|
||||
for (auto i = 0; i < host->functionBlock.numGroups; ++i)
|
||||
{
|
||||
const auto group = host->functionBlock.firstGroup + i;
|
||||
const auto& groupState = host->getProfileStates().groupStates[(size_t) group];
|
||||
sendIfNonEmpty (group, groupState.groupState);
|
||||
}
|
||||
|
||||
// Always send the block response to indicate that no further replies will follow
|
||||
header.deviceID = ChannelInGroup::wholeBlock;
|
||||
const auto state = host->getProfileStates().blockState;
|
||||
const auto active = state.getActive();
|
||||
const auto inactive = state.getInactive();
|
||||
detail::MessageTypeUtils::send (*output, output->getIncomingGroup(), header, Message::ProfileInquiryResponse { active, inactive });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::ProfileDetails& body) const
|
||||
{
|
||||
if (body.target == std::byte{})
|
||||
{
|
||||
const auto address = ChannelAddress{}.withGroup (output->getIncomingGroup())
|
||||
.withChannel (output->getIncomingHeader().deviceID);
|
||||
const ProfileAtAddress profileAtAddress { body.profile, address };
|
||||
const auto state = host->getState (profileAtAddress);
|
||||
std::vector<std::byte> extraData;
|
||||
detail::Marshalling::Writer { extraData } (state.active, state.supported);
|
||||
detail::MessageTypeUtils::send (*output, Message::ProfileDetailsResponse { body.profile, body.target, extraData });
|
||||
}
|
||||
else
|
||||
{
|
||||
detail::MessageTypeUtils::sendNAK (*output, std::byte { 0x04 });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Body>
|
||||
bool profileEnablementReceived (const Body& request) const
|
||||
{
|
||||
const auto destination = ChannelAddress{}.withGroup (output->getIncomingGroup())
|
||||
.withChannel (output->getIncomingHeader().deviceID);
|
||||
if (auto* state = host->states.getStateForDestination (destination))
|
||||
{
|
||||
const auto previousState = state->get (request.profile);
|
||||
|
||||
if (previousState.isSupported())
|
||||
{
|
||||
const ProfileAtAddress profileAtAddress { request.profile, destination };
|
||||
|
||||
{
|
||||
const ScopedValueSetter scope { host->currentEnablementMessage,
|
||||
std::optional<ProfileAtAddress> (profileAtAddress) };
|
||||
host->delegate.profileEnablementRequested (output->getIncomingHeader().source,
|
||||
profileAtAddress,
|
||||
getNumChannels (output->getIncomingHeader(), request),
|
||||
std::is_same_v<Message::ProfileOn, Body>);
|
||||
}
|
||||
|
||||
const auto currentState = host->getState (profileAtAddress);
|
||||
|
||||
const auto sendResponse = [&] (auto response)
|
||||
{
|
||||
const Message::Header header
|
||||
{
|
||||
profileAtAddress.address.getChannel(),
|
||||
detail::MessageMeta::Meta<decltype (response)>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
output->getMuid(),
|
||||
MUID::getBroadcast(),
|
||||
};
|
||||
|
||||
detail::MessageTypeUtils::send (*output, profileAtAddress.address.getGroup(), header, response);
|
||||
};
|
||||
|
||||
const auto numIndividualChannels = (std::is_same_v<Message::ProfileOn, Body> ? currentState : previousState).active;
|
||||
|
||||
const auto numChannelsToSend = destination.isSingleChannel()
|
||||
? numIndividualChannels
|
||||
: uint16_t{};
|
||||
|
||||
if (currentState.isActive())
|
||||
sendResponse (Message::ProfileEnabledReport { profileAtAddress.profile, numChannelsToSend });
|
||||
else
|
||||
sendResponse (Message::ProfileDisabledReport { profileAtAddress.profile, numChannelsToSend });
|
||||
|
||||
host->isResponder = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
detail::MessageTypeUtils::sendNAK (*output, {});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::ProfileOn& request) const
|
||||
{
|
||||
return profileEnablementReceived (request);
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::ProfileOff& request) const
|
||||
{
|
||||
return profileEnablementReceived (request);
|
||||
}
|
||||
|
||||
ProfileHost* host = nullptr;
|
||||
ResponderOutput* output = nullptr;
|
||||
bool* handled = nullptr;
|
||||
};
|
||||
|
||||
void ProfileHost::setProfileEnablement (ProfileAtAddress profileAtAddress, int numChannels)
|
||||
{
|
||||
if (numChannels > 0)
|
||||
enableProfileImpl (profileAtAddress, numChannels);
|
||||
else
|
||||
disableProfileImpl (profileAtAddress);
|
||||
}
|
||||
|
||||
void ProfileHost::addProfile (ProfileAtAddress profileAtAddress, int maxNumChannels)
|
||||
{
|
||||
auto* state = states.getStateForDestination (profileAtAddress.address);
|
||||
|
||||
if (state == nullptr || state->get (profileAtAddress.profile).isSupported())
|
||||
return;
|
||||
|
||||
// There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense!
|
||||
jassert (maxNumChannels <= 256);
|
||||
|
||||
state->set (profileAtAddress.profile, { (uint16_t) jmax (1, maxNumChannels), 0 });
|
||||
|
||||
if (! isResponder || profileAtAddress == currentEnablementMessage)
|
||||
return;
|
||||
|
||||
const Message::Header header
|
||||
{
|
||||
profileAtAddress.address.getChannel(),
|
||||
detail::MessageMeta::Meta<Message::ProfileAdded>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
output.getMuid(),
|
||||
MUID::getBroadcast(),
|
||||
};
|
||||
|
||||
detail::MessageTypeUtils::send (output,
|
||||
profileAtAddress.address.getGroup(),
|
||||
header,
|
||||
Message::ProfileAdded { profileAtAddress.profile });
|
||||
}
|
||||
|
||||
void ProfileHost::removeProfile (ProfileAtAddress profileAtAddress)
|
||||
{
|
||||
auto* state = states.getStateForDestination (profileAtAddress.address);
|
||||
|
||||
if (state == nullptr)
|
||||
return;
|
||||
|
||||
setProfileEnablement (profileAtAddress, 0);
|
||||
|
||||
if (! state->get (profileAtAddress.profile).isSupported())
|
||||
return;
|
||||
|
||||
state->erase (profileAtAddress.profile);
|
||||
|
||||
if (! isResponder || profileAtAddress == currentEnablementMessage)
|
||||
return;
|
||||
|
||||
const Message::Header header
|
||||
{
|
||||
profileAtAddress.address.getChannel(),
|
||||
detail::MessageMeta::Meta<Message::ProfileRemoved>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
output.getMuid(),
|
||||
MUID::getBroadcast(),
|
||||
};
|
||||
|
||||
detail::MessageTypeUtils::send (output,
|
||||
profileAtAddress.address.getGroup(),
|
||||
header,
|
||||
Message::ProfileRemoved { profileAtAddress.profile });
|
||||
}
|
||||
|
||||
void ProfileHost::enableProfileImpl (ProfileAtAddress profileAtAddress, int numChannels)
|
||||
{
|
||||
auto* state = states.getStateForDestination (profileAtAddress.address);
|
||||
|
||||
if (state == nullptr)
|
||||
return;
|
||||
|
||||
const auto old = state->get (profileAtAddress.profile);
|
||||
|
||||
if (! old.isSupported())
|
||||
return;
|
||||
|
||||
// There are only 256 channels on a UMP endpoint, so requesting more probably doesn't make sense!
|
||||
jassert (numChannels <= 256);
|
||||
|
||||
const auto enabledChannels = jmax ((uint16_t) 1, jmin (old.supported, (uint16_t) numChannels));
|
||||
state->set (profileAtAddress.profile, { old.supported, enabledChannels });
|
||||
|
||||
if (! isResponder || profileAtAddress == currentEnablementMessage)
|
||||
return;
|
||||
|
||||
const Message::Header header
|
||||
{
|
||||
profileAtAddress.address.getChannel(),
|
||||
detail::MessageMeta::Meta<Message::ProfileEnabledReport>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
output.getMuid(),
|
||||
MUID::getBroadcast(),
|
||||
};
|
||||
|
||||
const auto numChannelsToSend = profileAtAddress.address.isSingleChannel() ? enabledChannels : uint16_t{};
|
||||
|
||||
detail::MessageTypeUtils::send (output,
|
||||
profileAtAddress.address.getGroup(),
|
||||
header,
|
||||
Message::ProfileEnabledReport { profileAtAddress.profile, numChannelsToSend });
|
||||
}
|
||||
|
||||
void ProfileHost::disableProfileImpl (ProfileAtAddress profileAtAddress)
|
||||
{
|
||||
auto* state = states.getStateForDestination (profileAtAddress.address);
|
||||
|
||||
if (state == nullptr)
|
||||
return;
|
||||
|
||||
const auto old = state->get (profileAtAddress.profile);
|
||||
|
||||
if (! old.isActive())
|
||||
return;
|
||||
|
||||
state->set (profileAtAddress.profile, { old.supported, 0 });
|
||||
|
||||
if (! isResponder || profileAtAddress == currentEnablementMessage)
|
||||
return;
|
||||
|
||||
const Message::Header header
|
||||
{
|
||||
profileAtAddress.address.getChannel(),
|
||||
detail::MessageMeta::Meta<Message::ProfileDisabledReport>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
output.getMuid(),
|
||||
MUID::getBroadcast(),
|
||||
};
|
||||
|
||||
const auto numChannelsToSend = profileAtAddress.address.isSingleChannel() ? old.active : uint16_t{};
|
||||
|
||||
detail::MessageTypeUtils::send (output,
|
||||
profileAtAddress.address.getGroup(),
|
||||
header,
|
||||
Message::ProfileDisabledReport { profileAtAddress.profile, numChannelsToSend });
|
||||
}
|
||||
|
||||
bool ProfileHost::tryRespond (ResponderOutput& responderOutput, const Message::Parsed& message)
|
||||
{
|
||||
bool result = false;
|
||||
detail::MessageTypeUtils::visit (message, Visitor { this, &responderOutput, &result });
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Acting as a ResponderListener, instances of this class can formulate
|
||||
appropriate replies to profile transactions initiated by remote devices.
|
||||
|
||||
ProfileHost instances also contains methods to inform remote devices about
|
||||
changes to local profile state.
|
||||
|
||||
Stores the current state of profiles on the local device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ProfileHost final : public ResponderDelegate
|
||||
{
|
||||
public:
|
||||
/** @internal
|
||||
|
||||
Rather than constructing one of these objects yourself, you should configure
|
||||
a Device with profile support, and then use Device::getProfileHost()
|
||||
to retrieve a profile host that has been set up to work with that device.
|
||||
*/
|
||||
ProfileHost (FunctionBlock fb, ProfileDelegate& d, BufferOutput& o)
|
||||
: functionBlock (fb), delegate (d), output (o) {}
|
||||
|
||||
/** Adds support for a profile on the specified group/channel with a
|
||||
maximum number of channels that may be activated.
|
||||
*/
|
||||
void addProfile (ProfileAtAddress, int maxNumChannels = 1);
|
||||
|
||||
/** Removes support for a profile on the specified group/channel.
|
||||
*/
|
||||
void removeProfile (ProfileAtAddress);
|
||||
|
||||
/** Activates or deactivates a profile on the specified group/channel.
|
||||
|
||||
The profile should previously have been added with addProfile().
|
||||
A positive value of numChannels will enable the profile, and a non-positive value
|
||||
will disable it. This includes group and function-block profiles; passing any positive
|
||||
value will enable the profile on the entire group or block.
|
||||
*/
|
||||
void setProfileEnablement (ProfileAtAddress, int numChannels);
|
||||
|
||||
/** Returns the profile states (supported/active) for all groups and channels.
|
||||
*/
|
||||
const BlockProfileStates& getProfileStates() const { return states; }
|
||||
|
||||
/** Returns the number of supported and active channels for the given
|
||||
profile on the specified group/channel.
|
||||
|
||||
If the supported channels is 0, then the profile is not supported
|
||||
on the group/channel.
|
||||
|
||||
If the active channels is 0, then the profile is inactive on the
|
||||
group/channel.
|
||||
*/
|
||||
SupportedAndActive getState (ProfileAtAddress profileAtAddress) const
|
||||
{
|
||||
if (auto* state = states.getStateForDestination (profileAtAddress.address))
|
||||
return state->get (profileAtAddress.profile);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
bool tryRespond (ResponderOutput&, const Message::Parsed&) override;
|
||||
|
||||
private:
|
||||
class Visitor;
|
||||
|
||||
void enableProfileImpl (ProfileAtAddress, int);
|
||||
void disableProfileImpl (ProfileAtAddress);
|
||||
|
||||
template <typename Body>
|
||||
bool profileEnablementReceived (ResponderOutput&, const Body&);
|
||||
|
||||
FunctionBlock functionBlock;
|
||||
ProfileDelegate& delegate;
|
||||
BufferOutput& output;
|
||||
BlockProfileStates states;
|
||||
bool isResponder = false;
|
||||
std::optional<ProfileAtAddress> currentEnablementMessage;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
SupportedAndActive ChannelProfileStates::get (const Profile& profile) const
|
||||
{
|
||||
const auto iter = std::lower_bound (entries.begin(), entries.end(), profile);
|
||||
|
||||
if (iter != entries.end() && iter->profile == profile)
|
||||
return iter->state;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<Profile> ChannelProfileStates::getActive() const
|
||||
{
|
||||
std::vector<Profile> result;
|
||||
|
||||
for (const auto& item : entries)
|
||||
if (item.state.isActive())
|
||||
result.push_back (item.profile);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Profile> ChannelProfileStates::getInactive() const
|
||||
{
|
||||
std::vector<Profile> result;
|
||||
|
||||
for (const auto& item : entries)
|
||||
if (item.state.isSupported())
|
||||
result.push_back (item.profile);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChannelProfileStates::set (const Profile& profile, SupportedAndActive state)
|
||||
{
|
||||
const auto iter = std::lower_bound (entries.begin(), entries.end(), profile);
|
||||
|
||||
if (iter != entries.end() && iter->profile == profile)
|
||||
{
|
||||
if (state != SupportedAndActive{})
|
||||
iter->state = state;
|
||||
else
|
||||
entries.erase (iter);
|
||||
}
|
||||
else if (state != SupportedAndActive{})
|
||||
{
|
||||
entries.insert (iter, { profile, state });
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelProfileStates::erase (const Profile& profile)
|
||||
{
|
||||
const auto iter = std::lower_bound (entries.begin(), entries.end(), profile);
|
||||
|
||||
if (iter != entries.end() && iter->profile == profile)
|
||||
entries.erase (iter);
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Holds a profile ID, along with the number of supported and active channels
|
||||
corresponding to that profile.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct ProfileStateEntry
|
||||
{
|
||||
Profile profile; ///< A MIDI-CI profile ID
|
||||
SupportedAndActive state; ///< The number of channels corresponding to the profile
|
||||
|
||||
bool operator< (const Profile& other) const { return profile < other; }
|
||||
bool operator< (const ProfileStateEntry& other) const { return profile < other.profile; }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Holds the number of channels that are supported and activated for all profiles
|
||||
at a particular channel address.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ChannelProfileStates
|
||||
{
|
||||
public:
|
||||
using Entry = ProfileStateEntry;
|
||||
|
||||
/** Returns the number of channels that are supported and active for the
|
||||
given profile.
|
||||
*/
|
||||
SupportedAndActive get (const Profile& profile) const;
|
||||
|
||||
/** Returns all profiles that are active at this address. */
|
||||
std::vector<Profile> getActive() const;
|
||||
|
||||
/** Returns all profiles that are supported but inactive at this address. */
|
||||
std::vector<Profile> getInactive() const;
|
||||
|
||||
/** Sets the number of channels that are supported/active for a given profile. */
|
||||
void set (const Profile& profile, SupportedAndActive state);
|
||||
|
||||
/** Removes the record of a particular profile, equivalent to removing support. */
|
||||
void erase (const Profile& profile);
|
||||
|
||||
/** Gets a const iterator over all profiles, for range-for compatibility. */
|
||||
auto begin() const { return entries.begin(); }
|
||||
|
||||
/** Gets a const iterator over all profiles, for range-for compatibility. */
|
||||
auto end() const { return entries.end(); }
|
||||
|
||||
/** Returns true if no profiles are supported. */
|
||||
auto empty() const { return entries.empty(); }
|
||||
|
||||
/** Returns the number of profiles that are supported at this address. */
|
||||
auto size() const { return entries.size(); }
|
||||
|
||||
private:
|
||||
std::vector<Entry> entries;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Contains profile states for each channel in a group, along with the state
|
||||
of profiles that apply to the group itself.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class GroupProfileStates
|
||||
{
|
||||
template <typename This>
|
||||
static auto getStateForDestinationImpl (This& t, ChannelInGroup destination) -> decltype (&t.groupState)
|
||||
{
|
||||
if (destination == ChannelInGroup::wholeGroup)
|
||||
return &t.groupState;
|
||||
|
||||
if (const auto index = (size_t) destination; index < t.channelStates.size())
|
||||
return &t.channelStates[index];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
public:
|
||||
/** Returns the profile state for the group or a contained channel as appropriate.
|
||||
Returns nullptr if ChannelInGroup refers to a whole function block.
|
||||
*/
|
||||
auto* getStateForDestination (ChannelInGroup d) { return getStateForDestinationImpl (*this, d); }
|
||||
|
||||
/** Returns the profile state for the group or a contained channel as appropriate.
|
||||
Returns nullptr if ChannelInGroup refers to a whole function block.
|
||||
*/
|
||||
auto* getStateForDestination (ChannelInGroup d) const { return getStateForDestinationImpl (*this, d); }
|
||||
|
||||
std::array<ChannelProfileStates, 16> channelStates; ///< Profile states for each channel in the group
|
||||
ChannelProfileStates groupState; ///< Profile states for the group itself
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Contains profile states for each group and channel in a function block, along with the state
|
||||
of profiles that apply to the function block itself.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class BlockProfileStates
|
||||
{
|
||||
template <typename This>
|
||||
static auto getStateForDestinationImpl (This& t, ChannelAddress address) -> decltype (&t.blockState)
|
||||
{
|
||||
if (address.isBlock())
|
||||
return &t.blockState;
|
||||
|
||||
if (const auto index = (size_t) address.getGroup(); index < t.groupStates.size())
|
||||
return t.groupStates[index].getStateForDestination (address.getChannel());
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
public:
|
||||
/** Returns the profile state for the function block, group, or channel as appropriate.
|
||||
Returns nullptr if the address refers to a non-existent channel or group.
|
||||
*/
|
||||
auto* getStateForDestination (ChannelAddress address) { return getStateForDestinationImpl (*this, address); }
|
||||
|
||||
/** Returns the profile state for the function block, group, or channel as appropriate.
|
||||
Returns nullptr if the address refers to a non-existent channel or group.
|
||||
*/
|
||||
auto* getStateForDestination (ChannelAddress address) const { return getStateForDestinationImpl (*this, address); }
|
||||
|
||||
std::array<GroupProfileStates, 16> groupStates; ///< Profile states for each group in the function block
|
||||
ChannelProfileStates blockState; ///< Profile states for the whole function block
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
struct PropertyDelegateDetail
|
||||
{
|
||||
/*
|
||||
Note: We don't use ToVar and FromVar here, because we want to omit fields that are using
|
||||
their default values.
|
||||
*/
|
||||
|
||||
template <typename Target>
|
||||
static Target parseTargetHeader (const var& v,
|
||||
const std::map<Identifier, void (*) (Target&, const var&)>& parsers)
|
||||
{
|
||||
Target target;
|
||||
|
||||
if (auto* obj = v.getDynamicObject())
|
||||
{
|
||||
for (const auto& pair : obj->getProperties())
|
||||
{
|
||||
const auto parserIter = parsers.find (pair.name);
|
||||
|
||||
if (parserIter != parsers.end())
|
||||
parserIter->second (target, pair.value);
|
||||
else
|
||||
target.extended[pair.name] = pair.value;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
static auto getParsersForPropertyReplyHeader()
|
||||
{
|
||||
using Target = PropertyReplyHeader;
|
||||
std::map<Identifier, void (*) (Target& header, const var& v)> map;
|
||||
|
||||
map.emplace ("status", [] (Target& header, const var& v) { header.status = v; });
|
||||
map.emplace ("message", [] (Target& header, const var& v) { header.message = v; });
|
||||
map.emplace ("cacheTime", [] (Target& header, const var& v) { header.cacheTime = v; });
|
||||
map.emplace ("mediaType", [] (Target& header, const var& v) { header.mediaType = v; });
|
||||
map.emplace ("mutualEncoding", [] (Target& header, const var& v)
|
||||
{
|
||||
header.mutualEncoding = EncodingUtils::toEncoding (v.toString().toRawUTF8()).value_or (Encoding::ascii);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
template <typename Target>
|
||||
static auto getParsersForGenericPropertyRequestHeader()
|
||||
{
|
||||
std::map<Identifier, void (*) (Target& header, const var& v)> map;
|
||||
|
||||
map.emplace ("resource", [] (Target& header, const var& v) { header.resource = v; });
|
||||
map.emplace ("resId", [] (Target& header, const var& v) { header.resId = v; });
|
||||
map.emplace ("mediaType", [] (Target& header, const var& v) { header.mediaType = v; });
|
||||
map.emplace ("mutualEncoding", [] (Target& header, const var& v)
|
||||
{
|
||||
header.mutualEncoding = EncodingUtils::toEncoding (v.toString().toRawUTF8()).value_or (Encoding::ascii);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
static auto getParsersForPropertyRequestHeader()
|
||||
{
|
||||
auto map = getParsersForGenericPropertyRequestHeader<PropertyRequestHeader>();
|
||||
map.emplace ("setPartial", [] (PropertyRequestHeader& header, const var& v) { header.setPartial = v; });
|
||||
map.emplace ("offset", [] (PropertyRequestHeader& header, const var& v)
|
||||
{
|
||||
if (! header.pagination.has_value())
|
||||
header.pagination = Pagination{};
|
||||
|
||||
header.pagination->offset = v;
|
||||
});
|
||||
map.emplace ("limit", [] (PropertyRequestHeader& header, const var& v)
|
||||
{
|
||||
if (! header.pagination.has_value())
|
||||
header.pagination = Pagination{};
|
||||
|
||||
header.pagination->limit = v;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
static auto getParsersForPropertySubscriptionHeader()
|
||||
{
|
||||
auto map = getParsersForGenericPropertyRequestHeader<PropertySubscriptionHeader>();
|
||||
|
||||
map.emplace ("subscribeId", [] (PropertySubscriptionHeader& header, const var& v) { header.subscribeId = v; });
|
||||
map.emplace ("command", [] (PropertySubscriptionHeader& header, const var& v)
|
||||
{
|
||||
header.command = [&]
|
||||
{
|
||||
if (v == "start")
|
||||
return PropertySubscriptionCommand::start;
|
||||
|
||||
if (v == "partial")
|
||||
return PropertySubscriptionCommand::partial;
|
||||
|
||||
if (v == "full")
|
||||
return PropertySubscriptionCommand::full;
|
||||
|
||||
if (v == "notify")
|
||||
return PropertySubscriptionCommand::notify;
|
||||
|
||||
if (v == "end")
|
||||
return PropertySubscriptionCommand::end;
|
||||
|
||||
return PropertySubscriptionCommand::notify;
|
||||
}();
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
static auto getSetPartial (const PropertySubscriptionHeader&) { return false; }
|
||||
static auto getSetPartial (const PropertyRequestHeader& h) { return h.setPartial; }
|
||||
static auto getSetPartial (const PropertyReplyHeader&) { return false; }
|
||||
|
||||
static auto getPagination (const PropertySubscriptionHeader&) { return std::optional<Pagination>{}; }
|
||||
static auto getPagination (const PropertyRequestHeader& h) { return h.pagination; }
|
||||
static auto getPagination (const PropertyReplyHeader&) { return std::optional<Pagination>{}; }
|
||||
|
||||
static auto getCacheTime (const PropertySubscriptionHeader&) { return 0; }
|
||||
static auto getCacheTime (const PropertyRequestHeader&) { return 0; }
|
||||
static auto getCacheTime (const PropertyReplyHeader& h) { return h.cacheTime; }
|
||||
|
||||
static auto getMessage (const PropertySubscriptionHeader&) { return String{}; }
|
||||
static auto getMessage (const PropertyRequestHeader&) { return String{}; }
|
||||
static auto getMessage (const PropertyReplyHeader& h) { return h.message; }
|
||||
|
||||
static auto getResource (const PropertySubscriptionHeader& h) { return h.resource; }
|
||||
static auto getResource (const PropertyRequestHeader& h) { return h.resource; }
|
||||
static auto getResource (const PropertyReplyHeader&) { return String{}; }
|
||||
|
||||
static auto getResId (const PropertySubscriptionHeader& h) { return h.resId; }
|
||||
static auto getResId (const PropertyRequestHeader& h) { return h.resId; }
|
||||
static auto getResId (const PropertyReplyHeader&) { return String{}; }
|
||||
|
||||
static auto getCommand (const PropertySubscriptionHeader& h) { return h.command; }
|
||||
static auto getCommand (const PropertyRequestHeader&) { return PropertySubscriptionCommand{}; }
|
||||
static auto getCommand (const PropertyReplyHeader&) { return PropertySubscriptionCommand{}; }
|
||||
|
||||
static auto getSubscribeId (const PropertySubscriptionHeader& h) { return h.subscribeId; }
|
||||
static auto getSubscribeId (const PropertyRequestHeader&) { return String{}; }
|
||||
static auto getSubscribeId (const PropertyReplyHeader&) { return String{}; }
|
||||
|
||||
static std::optional<int> getStatus (const PropertySubscriptionHeader&) { return {}; }
|
||||
static std::optional<int> getStatus (const PropertyRequestHeader&) { return {}; }
|
||||
static std::optional<int> getStatus (const PropertyReplyHeader& h) { return h.status; }
|
||||
|
||||
template <typename T>
|
||||
static auto toFieldsFromHeader (const T& t)
|
||||
{
|
||||
auto fields = t.extended;
|
||||
|
||||
// Status shall always be included if it is present in the header
|
||||
if (const auto status = getStatus (t))
|
||||
fields["status"] = *status;
|
||||
|
||||
if (getResource (t) != getResource (T()))
|
||||
fields["resource"] = getResource (t);
|
||||
|
||||
if (getCommand (t) != getCommand (T()))
|
||||
fields["command"] = PropertySubscriptionCommandUtils::toString (getCommand (t));
|
||||
|
||||
if (getSubscribeId (t) != getSubscribeId (T()))
|
||||
fields["subscribeId"] = getSubscribeId (t);
|
||||
|
||||
if (getResId (t) != getResId (T()))
|
||||
fields["resId"] = getResId (t);
|
||||
|
||||
if (t.mutualEncoding != T().mutualEncoding)
|
||||
fields["mutualEncoding"] = EncodingUtils::toString (t.mutualEncoding);
|
||||
|
||||
if (t.mediaType != T().mediaType)
|
||||
fields["mediaType"] = t.mediaType;
|
||||
|
||||
if (getSetPartial (t))
|
||||
fields["setPartial"] = true;
|
||||
|
||||
if (getCacheTime (t) != getCacheTime (T()))
|
||||
fields["cacheTime"] = getCacheTime (t);
|
||||
|
||||
if (getMessage (t) != getMessage (T()))
|
||||
fields["message"] = getMessage (t);
|
||||
|
||||
if (const auto pagination = getPagination (t))
|
||||
{
|
||||
fields["offset"] = pagination->offset;
|
||||
fields["limit"] = pagination->limit;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
PropertySubscriptionHeader PropertySubscriptionHeader::parseCondensed (const var& v)
|
||||
{
|
||||
return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertySubscriptionHeader());
|
||||
}
|
||||
|
||||
var PropertySubscriptionHeader::toVarCondensed() const
|
||||
{
|
||||
return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "command");
|
||||
}
|
||||
|
||||
PropertyRequestHeader PropertyRequestHeader::parseCondensed (const var& v)
|
||||
{
|
||||
return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertyRequestHeader());
|
||||
}
|
||||
|
||||
var PropertyRequestHeader::toVarCondensed() const
|
||||
{
|
||||
return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "resource");
|
||||
}
|
||||
|
||||
PropertyReplyHeader PropertyReplyHeader::parseCondensed (const var& v)
|
||||
{
|
||||
return PropertyDelegateDetail::parseTargetHeader (v, PropertyDelegateDetail::getParsersForPropertyReplyHeader());
|
||||
}
|
||||
|
||||
var PropertyReplyHeader::toVarCondensed() const
|
||||
{
|
||||
return JSONUtils::makeObjectWithKeyFirst (PropertyDelegateDetail::toFieldsFromHeader (*this), "status");
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
#define JUCE_SUBSCRIPTION_COMMANDS X(start) X(partial) X(full) X(notify) X(end)
|
||||
|
||||
/**
|
||||
Kinds of command that may be sent as part of a subscription update.
|
||||
|
||||
Check the Property Exchange specification to find the meaning of the
|
||||
different kinds.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
enum class PropertySubscriptionCommand
|
||||
{
|
||||
#define X(str) str,
|
||||
JUCE_SUBSCRIPTION_COMMANDS
|
||||
#undef X
|
||||
};
|
||||
|
||||
/**
|
||||
Functions to use with PropertySubscriptionCommand.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySubscriptionCommandUtils
|
||||
{
|
||||
PropertySubscriptionCommandUtils() = delete;
|
||||
|
||||
/** Converts a command to a human-readable string. */
|
||||
static const char* toString (PropertySubscriptionCommand x)
|
||||
{
|
||||
switch (x)
|
||||
{
|
||||
#define X(str) case PropertySubscriptionCommand::str: return #str;
|
||||
JUCE_SUBSCRIPTION_COMMANDS
|
||||
#undef X
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/** Converts a command string from a property exchange JSON header to
|
||||
an PropertySubscriptionCommand.
|
||||
*/
|
||||
static std::optional<PropertySubscriptionCommand> toCommand (const char* str)
|
||||
{
|
||||
#define X(name) if (std::string_view (str) == std::string_view (#name)) return PropertySubscriptionCommand::name;
|
||||
JUCE_SUBSCRIPTION_COMMANDS
|
||||
#undef X
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
#undef JUCE_SUBSCRIPTION_COMMANDS
|
||||
|
||||
/**
|
||||
A struct containing data members that correspond to common fields in a
|
||||
property subscription header.
|
||||
|
||||
Check the Property Exchange specification to find the meaning of the
|
||||
different fields.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertySubscriptionHeader
|
||||
{
|
||||
String resource;
|
||||
String resId;
|
||||
Encoding mutualEncoding = Encoding::ascii;
|
||||
String mediaType = "application/json";
|
||||
PropertySubscriptionCommand command { -1 };
|
||||
String subscribeId;
|
||||
std::map<Identifier, var> extended;
|
||||
|
||||
/** Converts a JSON object to a PropertyRequestHeader.
|
||||
|
||||
Unspecified fields will use their default values.
|
||||
*/
|
||||
static PropertySubscriptionHeader parseCondensed (const var&);
|
||||
|
||||
/** Converts a PropertySubscriptionHeader to a JSON object suitable for use as
|
||||
a MIDI-CI message header after conversion to 7-bit ASCII.
|
||||
*/
|
||||
var toVarCondensed() const;
|
||||
};
|
||||
|
||||
/**
|
||||
Contains information about the pagination of a request.
|
||||
|
||||
Check the Property Exchange specification to find the meaning of the
|
||||
different fields.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Pagination
|
||||
{
|
||||
int offset = 0;
|
||||
int limit = 1;
|
||||
};
|
||||
|
||||
/**
|
||||
A struct containing data members that correspond to common fields in a
|
||||
property request header.
|
||||
|
||||
Check the Property Exchange specification to find the meaning of the
|
||||
different fields.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyRequestHeader
|
||||
{
|
||||
String resource;
|
||||
String resId;
|
||||
Encoding mutualEncoding = Encoding::ascii;
|
||||
String mediaType = "application/json";
|
||||
bool setPartial = false;
|
||||
std::optional<Pagination> pagination;
|
||||
std::map<Identifier, var> extended;
|
||||
|
||||
/** Converts a JSON object to a PropertyRequestHeader.
|
||||
|
||||
Unspecified fields will use their default values.
|
||||
*/
|
||||
static PropertyRequestHeader parseCondensed (const var&);
|
||||
|
||||
/** Converts a PropertyRequestHeader to a JSON object suitable for use as
|
||||
a MIDI-CI message header after conversion to 7-bit ASCII.
|
||||
*/
|
||||
var toVarCondensed() const;
|
||||
};
|
||||
|
||||
/**
|
||||
Bundles together a property request header and a data payload.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyRequestData
|
||||
{
|
||||
PropertyRequestHeader header;
|
||||
Span<const std::byte> body;
|
||||
};
|
||||
|
||||
/**
|
||||
A struct containing data members that correspond to common fields in a
|
||||
reply to a property exchange request.
|
||||
|
||||
Check the Property Exchange specification to find the meaning of the
|
||||
different fields.
|
||||
|
||||
For extended attributes that don't correspond to any of the defined data
|
||||
members, use the 'extended' map.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyReplyHeader
|
||||
{
|
||||
int status = 200;
|
||||
String message;
|
||||
Encoding mutualEncoding = Encoding::ascii;
|
||||
int cacheTime = 0;
|
||||
String mediaType = "application/json";
|
||||
std::map<Identifier, var> extended;
|
||||
|
||||
/** Converts a JSON object to a PropertyReplyHeader.
|
||||
|
||||
Unspecified fields will use their default values.
|
||||
*/
|
||||
static PropertyReplyHeader parseCondensed (const var&);
|
||||
|
||||
/** Converts a PropertyReplyHeader to a JSON object suitable for use as
|
||||
a MIDI-CI message header after conversion to 7-bit ASCII.
|
||||
*/
|
||||
var toVarCondensed() const;
|
||||
};
|
||||
|
||||
/**
|
||||
Bundles together a property reply header and a data payload.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyReplyData
|
||||
{
|
||||
PropertyReplyHeader header;
|
||||
std::vector<std::byte> body;
|
||||
};
|
||||
|
||||
/**
|
||||
An interface with methods that can be overridden to customise how a Device
|
||||
implementing properties responds to property inquiries.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct PropertyDelegate
|
||||
{
|
||||
PropertyDelegate() = default;
|
||||
virtual ~PropertyDelegate() = default;
|
||||
PropertyDelegate (const PropertyDelegate&) = default;
|
||||
PropertyDelegate (PropertyDelegate&&) = default;
|
||||
PropertyDelegate& operator= (const PropertyDelegate&) = default;
|
||||
PropertyDelegate& operator= (PropertyDelegate&&) = default;
|
||||
|
||||
/** Returns the max number of simultaneous property exchange messages that can be processed. */
|
||||
virtual uint8_t getNumSimultaneousRequestsSupported() const { return 127; }
|
||||
|
||||
/** Returns a header/body containing the requested data.
|
||||
To report an error, you can return a failure status code in the header and leave the body empty.
|
||||
*/
|
||||
virtual PropertyReplyData propertyGetDataRequested (MUID, const PropertyRequestHeader&) = 0;
|
||||
|
||||
/** Returns a header that describes the result of the set operation. */
|
||||
virtual PropertyReplyHeader propertySetDataRequested (MUID, const PropertyRequestData&) = 0;
|
||||
|
||||
/** Returns true to allow the subscription, or false otherwise. */
|
||||
virtual bool subscriptionStartRequested (MUID, const PropertySubscriptionHeader&) = 0;
|
||||
|
||||
/** Called with the corresponding subscription token after a subscription has started. */
|
||||
virtual void subscriptionDidStart (MUID, const String& subId, const PropertySubscriptionHeader&) = 0;
|
||||
|
||||
/** Called when a device requests for an ongoing subscription to end. */
|
||||
virtual void subscriptionWillEnd (MUID, const Subscription& sub) = 0;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
|
||||
#ifndef DOXYGEN
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
template <>
|
||||
struct SerialisationTraits<midi_ci::PropertySubscriptionCommand>
|
||||
{
|
||||
static constexpr auto marshallingVersion = std::nullopt;
|
||||
|
||||
template <typename Archive>
|
||||
void load (Archive& archive, midi_ci::PropertySubscriptionCommand& t)
|
||||
{
|
||||
String command;
|
||||
archive (command);
|
||||
t = midi_ci::PropertySubscriptionCommandUtils::toCommand (command.toRawUTF8()).value_or (midi_ci::PropertySubscriptionCommand{});
|
||||
}
|
||||
|
||||
template <typename Archive>
|
||||
void save (Archive& archive, const midi_ci::PropertySubscriptionCommand& t)
|
||||
{
|
||||
archive (midi_ci::PropertySubscriptionCommandUtils::toString (t));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
||||
#endif // ifndef DOXYGEN
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
class PropertyExchangeCache
|
||||
{
|
||||
public:
|
||||
PropertyExchangeCache() = default;
|
||||
|
||||
struct OwningResult
|
||||
{
|
||||
explicit OwningResult (PropertyExchangeResult::Error e)
|
||||
: result (e) {}
|
||||
|
||||
OwningResult (var header, std::vector<std::byte> body)
|
||||
: backingStorage (std::move (body)),
|
||||
result (header, backingStorage) {}
|
||||
|
||||
OwningResult (OwningResult&&) noexcept = default;
|
||||
OwningResult& operator= (OwningResult&&) noexcept = default;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (OwningResult)
|
||||
|
||||
std::vector<std::byte> backingStorage;
|
||||
PropertyExchangeResult result;
|
||||
};
|
||||
|
||||
std::optional<OwningResult> addChunk (Message::DynamicSizePropertyExchange chunk)
|
||||
{
|
||||
jassert (chunk.thisChunkNum == lastChunk + 1 || chunk.thisChunkNum == 0);
|
||||
lastChunk = chunk.thisChunkNum;
|
||||
headerStorage.reserve (headerStorage.size() + chunk.header.size());
|
||||
std::transform (chunk.header.begin(),
|
||||
chunk.header.end(),
|
||||
std::back_inserter (headerStorage),
|
||||
[] (std::byte b) { return char (b); });
|
||||
bodyStorage.insert (bodyStorage.end(), chunk.data.begin(), chunk.data.end());
|
||||
|
||||
if (chunk.thisChunkNum != 0 && chunk.thisChunkNum != chunk.totalNumChunks)
|
||||
return {};
|
||||
|
||||
const auto headerJson = JSON::parse (String (headerStorage.data(), headerStorage.size()));
|
||||
|
||||
terminate();
|
||||
const auto encodingString = headerJson.getProperty ("mutualEncoding", "ASCII").toString();
|
||||
|
||||
if (chunk.thisChunkNum != chunk.totalNumChunks)
|
||||
return std::optional<OwningResult> { std::in_place, PropertyExchangeResult::Error::partial };
|
||||
|
||||
const int status = headerJson.getProperty ("status", 200);
|
||||
|
||||
if (status == 343)
|
||||
return std::optional<OwningResult> { std::in_place, PropertyExchangeResult::Error::tooManyTransactions };
|
||||
|
||||
return std::optional<OwningResult> { std::in_place,
|
||||
headerJson,
|
||||
Encodings::decode (bodyStorage, EncodingUtils::toEncoding (encodingString.toRawUTF8()).value_or (Encoding::ascii)) };
|
||||
}
|
||||
|
||||
std::optional<OwningResult> notify (Span<const std::byte> header)
|
||||
{
|
||||
const auto headerJson = JSON::parse (String (reinterpret_cast<const char*> (header.data()), header.size()));
|
||||
|
||||
if (! headerJson.isObject())
|
||||
return {};
|
||||
|
||||
const auto status = headerJson.getProperty ("status", {});
|
||||
|
||||
if (! status.isInt() || (int) status == 100)
|
||||
return {};
|
||||
|
||||
terminate();
|
||||
return std::optional<OwningResult> { std::in_place, PropertyExchangeResult::Error::notify };
|
||||
}
|
||||
|
||||
bool terminate()
|
||||
{
|
||||
return std::exchange (ongoing, false);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<char> headerStorage;
|
||||
std::vector<std::byte> bodyStorage;
|
||||
uint16_t lastChunk = 0;
|
||||
bool ongoing = true;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class PropertyExchangeCacheArray
|
||||
{
|
||||
public:
|
||||
PropertyExchangeCacheArray() = default;
|
||||
|
||||
Token64 primeCacheForRequestId (uint8_t id, std::function<void (const PropertyExchangeResult&)> onDone)
|
||||
{
|
||||
jassert (id < caches.size());
|
||||
|
||||
++lastKey;
|
||||
|
||||
auto& entry = caches[id];
|
||||
|
||||
if (entry.has_value())
|
||||
{
|
||||
// Trying to start a new message with the same id as another in-progress message
|
||||
jassertfalse;
|
||||
ids.erase (entry->key);
|
||||
}
|
||||
|
||||
const auto& item = entry.emplace (id, std::move (onDone), Token64 { lastKey });
|
||||
ids.emplace (item.key, id);
|
||||
return item.key;
|
||||
}
|
||||
|
||||
bool terminate (Token64 key)
|
||||
{
|
||||
const auto iter = ids.find (key);
|
||||
|
||||
// If the key isn't found, then the transaction must have completed already
|
||||
if (iter == ids.end())
|
||||
return false;
|
||||
|
||||
// We're about to terminate this transaction, so we don't need to retain this record
|
||||
auto index = iter->second;
|
||||
ids.erase (iter);
|
||||
|
||||
auto& entry = caches[index];
|
||||
|
||||
// If the entry is null, something's gone wrong. The ids map should only contain elements for
|
||||
// non-null cache entries.
|
||||
if (! entry.has_value())
|
||||
{
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto result = entry->cache.terminate();
|
||||
entry.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk)
|
||||
{
|
||||
updateCache (b, [&] (PropertyExchangeCache& c) { return c.addChunk (chunk); });
|
||||
}
|
||||
|
||||
void notify (RequestID b, Span<const std::byte> header)
|
||||
{
|
||||
updateCache (b, [&] (PropertyExchangeCache& c) { return c.notify (header); });
|
||||
}
|
||||
|
||||
std::optional<Token64> getKeyForId (RequestID id) const
|
||||
{
|
||||
if (auto& c = caches[id.asInt()])
|
||||
return c->key;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool hasTransaction (RequestID id) const
|
||||
{
|
||||
return getKeyForId (id).has_value();
|
||||
}
|
||||
|
||||
std::optional<RequestID> getIdForKey (Token64 key) const
|
||||
{
|
||||
const auto iter = ids.find (key);
|
||||
return iter != ids.end() ? RequestID::create (iter->second) : std::nullopt;
|
||||
}
|
||||
|
||||
auto countOngoingTransactions() const
|
||||
{
|
||||
jassert (ids.size() == (size_t) std::count_if (caches.begin(), caches.end(), [] (auto& c) { return c.has_value(); }));
|
||||
|
||||
return (int) ids.size();
|
||||
}
|
||||
|
||||
auto getOngoingTransactions() const
|
||||
{
|
||||
jassert (ids.size() == (size_t) std::count_if (caches.begin(), caches.end(), [] (auto& c) { return c.has_value(); }));
|
||||
|
||||
std::vector<Token64> result (ids.size());
|
||||
std::transform (ids.begin(), ids.end(), result.begin(), [] (const auto& p) { return Token64 { p.first }; });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<RequestID> findUnusedId (uint8_t maxSimultaneousTransactions) const
|
||||
{
|
||||
if (countOngoingTransactions() >= maxSimultaneousTransactions)
|
||||
return {};
|
||||
|
||||
return RequestID::create ((uint8_t) std::distance (caches.begin(), std::find (caches.begin(), caches.end(), std::nullopt)));
|
||||
}
|
||||
|
||||
// Instances must stay at the same location to ensure that references captured in the
|
||||
// ErasedScopeGuard returned from primeCacheForRequestId do not dangle.
|
||||
JUCE_DECLARE_NON_COPYABLE (PropertyExchangeCacheArray)
|
||||
JUCE_DECLARE_NON_MOVEABLE (PropertyExchangeCacheArray)
|
||||
|
||||
private:
|
||||
static constexpr auto numCaches = 128;
|
||||
|
||||
class Transaction
|
||||
{
|
||||
public:
|
||||
Transaction (uint8_t i, std::function<void (const PropertyExchangeResult&)> onSuccess, Token64 k)
|
||||
: onFinish (std::move (onSuccess)), key (k), id (i) {}
|
||||
|
||||
PropertyExchangeCache cache;
|
||||
std::function<void (const PropertyExchangeResult&)> onFinish;
|
||||
Token64 key{};
|
||||
uint8_t id = 0;
|
||||
};
|
||||
|
||||
template <typename WithCache>
|
||||
void updateCache (RequestID b, WithCache&& withCache)
|
||||
{
|
||||
if (auto& entry = caches[b.asInt()])
|
||||
{
|
||||
if (const auto result = withCache (entry->cache))
|
||||
{
|
||||
const auto tmp = std::move (*entry);
|
||||
ids.erase (tmp.key);
|
||||
entry.reset();
|
||||
NullCheckedInvocation::invoke (tmp.onFinish, result->result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::optional<Transaction>, numCaches> caches;
|
||||
std::map<Token64, uint8_t> ids;
|
||||
uint64_t lastKey = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class InitiatorPropertyExchangeCache::Impl
|
||||
{
|
||||
public:
|
||||
std::optional<Token64> primeCache (uint8_t maxSimultaneousRequests,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone)
|
||||
{
|
||||
const auto id = array.findUnusedId (maxSimultaneousRequests);
|
||||
|
||||
return id.has_value() ? std::optional<Token64> (array.primeCacheForRequestId (id->asInt(), std::move (onDone)))
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
bool terminate (Token64 token)
|
||||
{
|
||||
return array.terminate (token);
|
||||
}
|
||||
|
||||
std::optional<Token64> getTokenForRequestId (RequestID id) const
|
||||
{
|
||||
return array.getKeyForId (id);
|
||||
}
|
||||
|
||||
std::optional<RequestID> getRequestIdForToken (Token64 token) const
|
||||
{
|
||||
return array.getIdForKey (token);
|
||||
}
|
||||
|
||||
void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); }
|
||||
void notify (RequestID b, Span<const std::byte> header) { array.notify (b, header); }
|
||||
auto getOngoingTransactions() const { return array.getOngoingTransactions(); }
|
||||
|
||||
private:
|
||||
PropertyExchangeCacheArray array;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache() : pimpl (std::make_unique<Impl>()) {}
|
||||
InitiatorPropertyExchangeCache::InitiatorPropertyExchangeCache (InitiatorPropertyExchangeCache&&) noexcept = default;
|
||||
InitiatorPropertyExchangeCache& InitiatorPropertyExchangeCache::operator= (InitiatorPropertyExchangeCache&&) noexcept = default;
|
||||
InitiatorPropertyExchangeCache::~InitiatorPropertyExchangeCache() = default;
|
||||
|
||||
std::optional<Token64> InitiatorPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone)
|
||||
{
|
||||
return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone));
|
||||
}
|
||||
|
||||
bool InitiatorPropertyExchangeCache::terminate (Token64 token) { return pimpl->terminate (token); }
|
||||
std::optional<Token64> InitiatorPropertyExchangeCache::getTokenForRequestId (RequestID id) const { return pimpl->getTokenForRequestId (id); }
|
||||
std::optional<RequestID> InitiatorPropertyExchangeCache::getRequestIdForToken (Token64 token) const { return pimpl->getRequestIdForToken (token); }
|
||||
void InitiatorPropertyExchangeCache::addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); }
|
||||
void InitiatorPropertyExchangeCache::notify (RequestID b, Span<const std::byte> header) { pimpl->notify (b, header); }
|
||||
std::vector<Token64> InitiatorPropertyExchangeCache::getOngoingTransactions() const { return pimpl->getOngoingTransactions(); }
|
||||
|
||||
//==============================================================================
|
||||
class ResponderPropertyExchangeCache::Impl
|
||||
{
|
||||
public:
|
||||
void primeCache (uint8_t maxSimultaneousTransactions,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone,
|
||||
RequestID id)
|
||||
{
|
||||
if (array.hasTransaction (id))
|
||||
return;
|
||||
|
||||
if (array.countOngoingTransactions() >= maxSimultaneousTransactions)
|
||||
NullCheckedInvocation::invoke (onDone, PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
|
||||
else
|
||||
array.primeCacheForRequestId (id.asInt(), std::move (onDone));
|
||||
}
|
||||
|
||||
void addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { array.addChunk (b, chunk); }
|
||||
void notify (RequestID b, Span<const std::byte> header) { array.notify (b, header); }
|
||||
int countOngoingTransactions() const { return array.countOngoingTransactions(); }
|
||||
|
||||
private:
|
||||
PropertyExchangeCacheArray array;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ResponderPropertyExchangeCache::ResponderPropertyExchangeCache() : pimpl (std::make_unique<Impl>()) {}
|
||||
ResponderPropertyExchangeCache::ResponderPropertyExchangeCache (ResponderPropertyExchangeCache&&) noexcept = default;
|
||||
ResponderPropertyExchangeCache& ResponderPropertyExchangeCache::operator= (ResponderPropertyExchangeCache&&) noexcept = default;
|
||||
ResponderPropertyExchangeCache::~ResponderPropertyExchangeCache() = default;
|
||||
|
||||
void ResponderPropertyExchangeCache::primeCache (uint8_t maxSimultaneousTransactions,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone,
|
||||
RequestID id)
|
||||
{
|
||||
return pimpl->primeCache (maxSimultaneousTransactions, std::move (onDone), id);
|
||||
}
|
||||
|
||||
void ResponderPropertyExchangeCache::addChunk (RequestID b, const Message::DynamicSizePropertyExchange& chunk) { pimpl->addChunk (b, chunk); }
|
||||
void ResponderPropertyExchangeCache::notify (RequestID b, Span<const std::byte> header) { pimpl->notify (b, header); }
|
||||
int ResponderPropertyExchangeCache::countOngoingTransactions() const { return pimpl->countOngoingTransactions(); }
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
A strongly-typed identifier for a 7-bit request ID with a nullable state.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class RequestID
|
||||
{
|
||||
public:
|
||||
/** Constructs a RequestID if the provided value is valid, i.e. its most significant bit is
|
||||
not set. Otherwise, returns nullopt.
|
||||
*/
|
||||
static std::optional<RequestID> create (uint8_t v)
|
||||
{
|
||||
if (v < 128)
|
||||
return RequestID { v };
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Constructs a RequestID if the provided value is valid, i.e. its most significant bit is
|
||||
not set. Otherwise, returns nullopt.
|
||||
*/
|
||||
static std::optional<RequestID> create (std::byte value)
|
||||
{
|
||||
return create (static_cast<uint8_t> (value));
|
||||
}
|
||||
|
||||
/** Returns the byte corresponding to this ID. */
|
||||
std::byte asByte() const
|
||||
{
|
||||
return std::byte { value };
|
||||
}
|
||||
|
||||
/** Returns the int value of this ID. */
|
||||
uint8_t asInt() const
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Equality operator. */
|
||||
bool operator== (RequestID other) const
|
||||
{
|
||||
return value == other.value;
|
||||
}
|
||||
|
||||
/** Inequality operator. */
|
||||
bool operator!= (RequestID other) const
|
||||
{
|
||||
return ! operator== (other);
|
||||
}
|
||||
|
||||
private:
|
||||
/* Constructs a non-null request ID.
|
||||
|
||||
The argument must not have its most significant bit set.
|
||||
*/
|
||||
explicit RequestID (uint8_t index)
|
||||
: value (index)
|
||||
{
|
||||
// IDs must only use the lowest 7 bits
|
||||
jassert (value < 128);
|
||||
}
|
||||
|
||||
uint8_t value{};
|
||||
};
|
||||
|
||||
/** A strongly-typed 64-bit identifier. */
|
||||
enum class Token64 : uint64_t {};
|
||||
|
||||
/** Compares Token64 instances. */
|
||||
constexpr bool operator< (Token64 a, Token64 b)
|
||||
{
|
||||
return toUnderlyingType (a) < toUnderlyingType (b);
|
||||
}
|
||||
|
||||
/**
|
||||
Accumulates message chunks that have been sent by another device in response
|
||||
to a transaction initiated by a local device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class InitiatorPropertyExchangeCache
|
||||
{
|
||||
public:
|
||||
InitiatorPropertyExchangeCache();
|
||||
~InitiatorPropertyExchangeCache();
|
||||
|
||||
InitiatorPropertyExchangeCache (InitiatorPropertyExchangeCache&&) noexcept;
|
||||
InitiatorPropertyExchangeCache& operator= (InitiatorPropertyExchangeCache&&) noexcept;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (InitiatorPropertyExchangeCache)
|
||||
|
||||
/** Picks an unused request ID, and prepares the cache for that ID to accumulate message chunks.
|
||||
|
||||
Incoming chunks added with addChunk are generated by another device acting as a responder.
|
||||
*/
|
||||
std::optional<Token64> primeCache (uint8_t maxSimultaneousRequests,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone);
|
||||
|
||||
/** Terminates/cancels an ongoing transaction.
|
||||
|
||||
Returns true if the termination had an effect (i.e. the transaction was still ongoing), or
|
||||
false otherwise (the transaction already ended or never started).
|
||||
*/
|
||||
bool terminate (Token64);
|
||||
|
||||
/** If there's a transaction ongoing with the given request id, returns the token uniquely
|
||||
identifying that transaction, otherwise returns nullopt.
|
||||
*/
|
||||
std::optional<Token64> getTokenForRequestId (RequestID) const;
|
||||
|
||||
/** If the token refers to an ongoing transaction, returns the request id of that transaction.
|
||||
Otherwise, returns an invalid request id.
|
||||
*/
|
||||
std::optional<RequestID> getRequestIdForToken (Token64) const;
|
||||
|
||||
/** Adds a message chunk for the provided transaction id. */
|
||||
void addChunk (RequestID, const Message::DynamicSizePropertyExchange& chunk);
|
||||
|
||||
/** Updates the transaction state based on the contents of the provided notification. */
|
||||
void notify (RequestID, Span<const std::byte> header);
|
||||
|
||||
/** Returns all ongoing transactions. */
|
||||
std::vector<Token64> getOngoingTransactions() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Accumulates message chunks that form a request initiated by a remote device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ResponderPropertyExchangeCache
|
||||
{
|
||||
public:
|
||||
ResponderPropertyExchangeCache();
|
||||
~ResponderPropertyExchangeCache();
|
||||
|
||||
ResponderPropertyExchangeCache (ResponderPropertyExchangeCache&&) noexcept;
|
||||
ResponderPropertyExchangeCache& operator= (ResponderPropertyExchangeCache&&) noexcept;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (ResponderPropertyExchangeCache)
|
||||
|
||||
/** Prepares the cache for the given requestID to accumulate message chunks.
|
||||
|
||||
Incoming chunks added with addChunk are generated by another device acting as an initiator.
|
||||
*/
|
||||
void primeCache (uint8_t maxSimultaneousTransactions,
|
||||
std::function<void (const PropertyExchangeResult&)> onDone,
|
||||
RequestID id);
|
||||
|
||||
/** Adds a message chunk for the provided transaction id. */
|
||||
void addChunk (RequestID, const Message::DynamicSizePropertyExchange& chunk);
|
||||
|
||||
/** Updates the transaction state based on the contents of the provided notification. */
|
||||
void notify (RequestID, Span<const std::byte> header);
|
||||
|
||||
/** Returns the number of transactions that have been started but not finished. */
|
||||
int countOngoingTransactions() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
An interface for objects that provide resources for property exchange
|
||||
transactions.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class CacheProvider
|
||||
{
|
||||
public:
|
||||
virtual ~CacheProvider() = default;
|
||||
|
||||
/** Returns a set containing all of the MUIDs currently known to the provider. */
|
||||
virtual std::set<MUID> getDiscoveredMuids() const = 0;
|
||||
|
||||
/** Returns a property exchange cache for accumulating replies to transactions
|
||||
we initiated.
|
||||
*/
|
||||
virtual InitiatorPropertyExchangeCache* getCacheForMuidAsInitiator (MUID m) = 0;
|
||||
|
||||
/** Returns a property exchange cache for accumulating requests initiated
|
||||
by other devices.
|
||||
*/
|
||||
virtual ResponderPropertyExchangeCache* getCacheForMuidAsResponder (MUID m) = 0;
|
||||
|
||||
/** Returns the maximum sysex size supported by the device with the
|
||||
given MUID.
|
||||
*/
|
||||
virtual int getMaxSysexSizeForMuid (MUID m) const = 0;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Contains data returned by a responder in response to a request.
|
||||
|
||||
PropertyExchangeResult::kind indicates whether the transaction resulted in
|
||||
a well-formed message; however, it's possible that the message is a
|
||||
well-formed message indicating an error in the responder, so it's important
|
||||
to check the 'status' field of the header before attempting to do anything
|
||||
with the payload.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class PropertyExchangeResult
|
||||
{
|
||||
public:
|
||||
enum class Error
|
||||
{
|
||||
partial, ///< Got a response, but the responder terminated it before
|
||||
///< sending a well-formed message.
|
||||
|
||||
notify, ///< Got a notify message terminating the transaction.
|
||||
tooManyTransactions, ///< Unable to send the request because doing so would
|
||||
///< exceed the number of simultaneous inquiries that were declared.
|
||||
///< @see PropertyDelegate::getNumSimultaneousRequestsSupported().
|
||||
};
|
||||
|
||||
/** Creates a result denoting an error state. */
|
||||
explicit PropertyExchangeResult (Error errorIn)
|
||||
: PropertyExchangeResult (errorIn, {}, {}) {}
|
||||
|
||||
/** Creates a result denoting a successful transmission. */
|
||||
PropertyExchangeResult (var headerIn, Span<const std::byte> bodyIn)
|
||||
: PropertyExchangeResult (std::nullopt, headerIn, bodyIn) {}
|
||||
|
||||
/** Returns the result kind, either nullopt for a successful transmission, or
|
||||
an error code if something went wrong.
|
||||
*/
|
||||
std::optional<Error> getError() const { return error; }
|
||||
|
||||
/** Parses the header as a subscription header.
|
||||
|
||||
This may only be called for messages of kind 'full'.
|
||||
*/
|
||||
PropertySubscriptionHeader getHeaderAsSubscriptionHeader() const
|
||||
{
|
||||
jassert (header != var());
|
||||
return PropertySubscriptionHeader::parseCondensed (header);
|
||||
}
|
||||
|
||||
/** Parses the header as a request header.
|
||||
|
||||
This may only be called for messages of kind 'full'.
|
||||
*/
|
||||
PropertyRequestHeader getHeaderAsRequestHeader() const
|
||||
{
|
||||
jassert (header != var());
|
||||
return PropertyRequestHeader::parseCondensed (header);
|
||||
}
|
||||
|
||||
/** Parses the header as a reply header.
|
||||
|
||||
This may only be called for messages of kind 'full'.
|
||||
*/
|
||||
PropertyReplyHeader getHeaderAsReplyHeader() const
|
||||
{
|
||||
jassert (header != var());
|
||||
return PropertyReplyHeader::parseCondensed (header);
|
||||
}
|
||||
|
||||
/** When getKind returns 'full', this is the message payload.
|
||||
|
||||
Note that this is not stored internally; if you need to keep this data
|
||||
around and reference it in the future, you should copy it into a
|
||||
vector or some other suitable container.
|
||||
*/
|
||||
Span<const std::byte> getBody() const { return body; }
|
||||
|
||||
private:
|
||||
PropertyExchangeResult (std::optional<Error> errorIn, var headerIn, Span<const std::byte> bodyIn)
|
||||
: error (errorIn), header (headerIn), body (bodyIn) {}
|
||||
|
||||
std::optional<Error> error;
|
||||
var header;
|
||||
Span<const std::byte> body;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
class PropertyHost::Visitor : public detail::MessageTypeUtils::MessageVisitor
|
||||
{
|
||||
public:
|
||||
Visitor (PropertyHost* h, ResponderOutput* o, bool* b)
|
||||
: host (h), output (o), handled (b) {}
|
||||
|
||||
void visit (const Message::PropertyExchangeCapabilities& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertyGetData& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySetData& body) const override { visitImpl (body); }
|
||||
void visit (const Message::PropertySubscribe& body) const override { visitImpl (body); }
|
||||
using MessageVisitor::visit;
|
||||
|
||||
private:
|
||||
template <typename Body>
|
||||
void visitImpl (const Body& body) const { *handled = messageReceived (body); }
|
||||
|
||||
bool messageReceived (const Message::PropertyExchangeCapabilities&) const
|
||||
{
|
||||
detail::MessageTypeUtils::send (*output, Message::PropertyExchangeCapabilitiesResponse { std::byte { host->delegate.getNumSimultaneousRequestsSupported() },
|
||||
{},
|
||||
{} });
|
||||
return true;
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::PropertyGetData& data) const
|
||||
{
|
||||
// This should always be a single message, so no need to accumulate chunks
|
||||
const auto reply = host->delegate.propertyGetDataRequested (output->getIncomingHeader().source,
|
||||
PropertyRequestHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header)));
|
||||
|
||||
const auto encoded = Encodings::tryEncode (reply.body, reply.header.mutualEncoding);
|
||||
|
||||
if (! encoded.has_value())
|
||||
{
|
||||
// If this is hit, the data that was supplied isn't valid for the encoding that was specified
|
||||
jassertfalse;
|
||||
return false;
|
||||
}
|
||||
|
||||
detail::PropertyHostUtils::send (*output,
|
||||
output->getIncomingGroup(),
|
||||
detail::MessageMeta::Meta<Message::PropertyGetDataResponse>::subID2,
|
||||
output->getIncomingHeader().source,
|
||||
data.requestID,
|
||||
Encodings::jsonTo7BitText (reply.header.toVarCondensed()),
|
||||
*encoded,
|
||||
host->cacheProvider.getMaxSysexSizeForMuid (output->getIncomingHeader().source));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::PropertySetData& data) const
|
||||
{
|
||||
auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source);
|
||||
|
||||
if (caches == nullptr)
|
||||
return false;
|
||||
|
||||
const auto source = output->getIncomingHeader().source;
|
||||
const auto dest = output->getIncomingHeader().destination;
|
||||
const auto group = output->getIncomingGroup();
|
||||
const auto request = RequestID::create (data.requestID);
|
||||
|
||||
if (! request.has_value())
|
||||
return false;
|
||||
|
||||
caches->primeCache (host->delegate.getNumSimultaneousRequestsSupported(), [hostPtr = host, source, dest, group, request] (const PropertyExchangeResult& result)
|
||||
{
|
||||
const auto send = [&] (const PropertyReplyHeader& header)
|
||||
{
|
||||
detail::MessageTypeUtils::send (hostPtr->output,
|
||||
group,
|
||||
Message::Header { ChannelInGroup::wholeBlock,
|
||||
detail::MessageMeta::Meta<Message::PropertySetDataResponse>::subID2,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
dest,
|
||||
source },
|
||||
Message::PropertySetDataResponse { { request->asByte(), Encodings::jsonTo7BitText (header.toVarCondensed()) } });
|
||||
};
|
||||
|
||||
const auto sendStatus = [&] (int status, StringRef message)
|
||||
{
|
||||
PropertyReplyHeader header;
|
||||
header.status = status;
|
||||
header.message = message;
|
||||
send (header);
|
||||
};
|
||||
|
||||
if (const auto error = result.getError())
|
||||
{
|
||||
switch (*error)
|
||||
{
|
||||
case PropertyExchangeResult::Error::tooManyTransactions:
|
||||
sendStatus (343, TRANS ("The device has initiated too many simultaneous requests"));
|
||||
break;
|
||||
|
||||
case PropertyExchangeResult::Error::partial:
|
||||
sendStatus (400, TRANS ("Request was incomplete"));
|
||||
break;
|
||||
|
||||
case PropertyExchangeResult::Error::notify:
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
send (hostPtr->delegate.propertySetDataRequested (source, { result.getHeaderAsRequestHeader(), result.getBody() }));
|
||||
}, *request);
|
||||
|
||||
caches->addChunk (*request, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool messageReceived (const Message::PropertySubscribe& data) const
|
||||
{
|
||||
auto* caches = host->cacheProvider.getCacheForMuidAsResponder (output->getIncomingHeader().source);
|
||||
|
||||
if (caches == nullptr)
|
||||
return false;
|
||||
|
||||
if (data.header.empty() || data.thisChunkNum != 1 || data.totalNumChunks != 1)
|
||||
return false;
|
||||
|
||||
const auto subHeader = PropertySubscriptionHeader::parseCondensed (Encodings::jsonFrom7BitText (data.header));
|
||||
const auto tryNotifyInitiator = subHeader.command == PropertySubscriptionCommand::start
|
||||
|| subHeader.command == PropertySubscriptionCommand::end;
|
||||
|
||||
if (! tryNotifyInitiator)
|
||||
return false;
|
||||
|
||||
const auto source = output->getIncomingHeader().source;
|
||||
|
||||
const auto sendResponse = [&] (const PropertyReplyHeader& header)
|
||||
{
|
||||
detail::PropertyHostUtils::send (*output,
|
||||
output->getIncomingGroup(),
|
||||
detail::MessageMeta::Meta<Message::PropertySubscribeResponse>::subID2,
|
||||
source,
|
||||
data.requestID,
|
||||
Encodings::jsonTo7BitText (header.toVarCondensed()),
|
||||
{},
|
||||
host->cacheProvider.getMaxSysexSizeForMuid (source));
|
||||
};
|
||||
|
||||
if (subHeader.command == PropertySubscriptionCommand::start)
|
||||
{
|
||||
if (host->delegate.subscriptionStartRequested (source, subHeader))
|
||||
{
|
||||
auto& currentSubscribeIds = host->registry[source];
|
||||
const auto newToken = findUnusedSubscribeId (currentSubscribeIds);
|
||||
[[maybe_unused]] const auto pair = currentSubscribeIds.emplace (newToken, subHeader.resource);
|
||||
jassert (pair.second);
|
||||
const auto subscribeId = subscribeIdFromUid (newToken);
|
||||
host->delegate.subscriptionDidStart (source, subscribeId, subHeader);
|
||||
|
||||
PropertyReplyHeader header;
|
||||
header.extended["subscribeId"] = subscribeId;
|
||||
sendResponse (header);
|
||||
}
|
||||
else
|
||||
{
|
||||
PropertyReplyHeader header;
|
||||
header.status = 405;
|
||||
sendResponse (header);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subHeader.command == PropertySubscriptionCommand::end)
|
||||
{
|
||||
const auto token = uidFromSubscribeId (subHeader.subscribeId);
|
||||
auto& currentSubscribeIds = host->registry[source];
|
||||
const auto iter = currentSubscribeIds.find (token);
|
||||
|
||||
if (iter != currentSubscribeIds.end())
|
||||
{
|
||||
host->delegate.subscriptionWillEnd (source, { subHeader.subscribeId, iter->second });
|
||||
currentSubscribeIds.erase (iter);
|
||||
|
||||
sendResponse ({});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
PropertyHost* host = nullptr;
|
||||
ResponderOutput* output = nullptr;
|
||||
bool* handled = nullptr;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
std::set<Subscription> PropertyHost::findSubscriptionsForDevice (MUID device) const
|
||||
{
|
||||
const auto iter = registry.find (device);
|
||||
|
||||
if (iter == registry.end())
|
||||
return {};
|
||||
|
||||
std::set<Subscription> result;
|
||||
|
||||
for (const auto& [subId, resource] : iter->second)
|
||||
{
|
||||
[[maybe_unused]] const auto pair = result.insert ({ subscribeIdFromUid (subId), resource });
|
||||
jassert (pair.second);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int PropertyHost::countOngoingTransactions() const
|
||||
{
|
||||
const auto muids = cacheProvider.getDiscoveredMuids();
|
||||
|
||||
return std::accumulate (muids.begin(), muids.end(), 0, [&] (auto acc, const auto& m)
|
||||
{
|
||||
if (auto* cache = cacheProvider.getCacheForMuidAsResponder (m))
|
||||
return acc + cache->countOngoingTransactions();
|
||||
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
bool PropertyHost::tryRespond (ResponderOutput& responderOutput, const Message::Parsed& message)
|
||||
{
|
||||
bool result = false;
|
||||
detail::MessageTypeUtils::visit (message, Visitor { this, &responderOutput, &result });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<RequestKey> PropertyHost::sendSubscriptionUpdate (MUID device,
|
||||
const PropertySubscriptionHeader& header,
|
||||
Span<const std::byte> body,
|
||||
std::function<void (const PropertyExchangeResult&)> cb)
|
||||
{
|
||||
const auto deviceIter = registry.find (device);
|
||||
|
||||
if (deviceIter == registry.end())
|
||||
{
|
||||
// That device doesn't have any active subscriptions
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto uid = uidFromSubscribeId (header.subscribeId);
|
||||
const auto subIter = deviceIter->second.find (uid);
|
||||
|
||||
if (subIter == deviceIter->second.end())
|
||||
{
|
||||
// That subscribeId isn't currently in use by that device
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto resource = subIter->second;
|
||||
|
||||
if (header.resource != resource)
|
||||
{
|
||||
// That subscribeId corresponds to a different resource
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (header.command == PropertySubscriptionCommand::start)
|
||||
{
|
||||
// This function is intended to update ongoing subscriptions. To start a new subscription,
|
||||
// use CIDevice.
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* caches = cacheProvider.getCacheForMuidAsInitiator (device);
|
||||
|
||||
if (caches == nullptr)
|
||||
return {};
|
||||
|
||||
auto wrappedCallback = [&]() -> std::function<void (const PropertyExchangeResult&)>
|
||||
{
|
||||
if (header.command != PropertySubscriptionCommand::end)
|
||||
return cb;
|
||||
|
||||
return [this, device, uid, resource, cb] (const PropertyExchangeResult& result)
|
||||
{
|
||||
if (! result.getError().has_value())
|
||||
{
|
||||
delegate.subscriptionWillEnd (device, { subscribeIdFromUid (uid), resource });
|
||||
registry[device].erase (uid);
|
||||
}
|
||||
|
||||
NullCheckedInvocation::invoke (cb, result);
|
||||
};
|
||||
}();
|
||||
|
||||
const auto encoded = Encodings::tryEncode (body, header.mutualEncoding);
|
||||
|
||||
if (! encoded.has_value())
|
||||
{
|
||||
// The data could not be encoded successfully
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto primed = caches->primeCache (delegate.getNumSimultaneousRequestsSupported(),
|
||||
std::move (wrappedCallback));
|
||||
|
||||
if (! primed.has_value())
|
||||
return {};
|
||||
|
||||
const auto id = caches->getRequestIdForToken (*primed);
|
||||
|
||||
if (! id.has_value())
|
||||
return {};
|
||||
|
||||
detail::PropertyHostUtils::send (output,
|
||||
functionBlock.firstGroup,
|
||||
detail::MessageMeta::Meta<Message::PropertySubscribe>::subID2,
|
||||
device,
|
||||
id->asByte(),
|
||||
Encodings::jsonTo7BitText (header.toVarCondensed()),
|
||||
*encoded,
|
||||
cacheProvider.getMaxSysexSizeForMuid (device));
|
||||
|
||||
return RequestKey { device, *primed };
|
||||
}
|
||||
|
||||
void PropertyHost::terminateSubscription (MUID device, const String& subscribeId)
|
||||
{
|
||||
const auto deviceIter = registry.find (device);
|
||||
|
||||
if (deviceIter == registry.end())
|
||||
{
|
||||
// That device doesn't have any active subscriptions
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto uid = uidFromSubscribeId (subscribeId);
|
||||
const auto subIter = deviceIter->second.find (uid);
|
||||
|
||||
if (subIter == deviceIter->second.end())
|
||||
{
|
||||
// That subscribeId isn't currently in use by that device
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::end;
|
||||
header.subscribeId = subscribeId;
|
||||
header.resource = subIter->second;
|
||||
|
||||
sendSubscriptionUpdate (device, header, {}, nullptr);
|
||||
}
|
||||
|
||||
PropertyHost::SubscriptionToken PropertyHost::uidFromSubscribeId (String id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// from_chars would be better once we no longer need to support older macOS
|
||||
return { (size_t) std::stoull (id.toStdString(), {}, 36) };
|
||||
}
|
||||
catch (...) {}
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
String PropertyHost::subscribeIdFromUid (SubscriptionToken uid)
|
||||
{
|
||||
const auto str = std::to_string (uid.uid);
|
||||
jassert (str.size() <= 8);
|
||||
return str;
|
||||
}
|
||||
|
||||
PropertyHost::SubscriptionToken PropertyHost::findUnusedSubscribeId (const std::map<SubscriptionToken, String>& used)
|
||||
{
|
||||
return ! used.empty() ? SubscriptionToken { std::prev (used.end())->first.uid + 1 } : SubscriptionToken { 0 };
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
A key used to uniquely identify ongoing transactions initiated by a ci::Device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class RequestKey
|
||||
{
|
||||
auto tie() const { return std::tuple (m, v); }
|
||||
|
||||
public:
|
||||
/** Constructor. */
|
||||
RequestKey (MUID muid, Token64 key) : m (muid), v (key) {}
|
||||
|
||||
/** Returns the muid of the device to which we are subscribed. */
|
||||
MUID getMuid() const { return m; }
|
||||
|
||||
/** Returns an identifier unique to this subscription. */
|
||||
Token64 getKey() const { return v; }
|
||||
|
||||
/** Equality operator. */
|
||||
bool operator== (const RequestKey& other) const { return tie() == other.tie(); }
|
||||
|
||||
/** Inequality operator. */
|
||||
bool operator!= (const RequestKey& other) const { return tie() != other.tie(); }
|
||||
|
||||
/** Less-than operator. */
|
||||
bool operator< (const RequestKey& other) const { return tie() < other.tie(); }
|
||||
|
||||
private:
|
||||
MUID m;
|
||||
Token64 v{};
|
||||
};
|
||||
|
||||
/**
|
||||
Acting as a ResponderListener, instances of this class can formulate
|
||||
appropriate replies to property transactions initiated by remote devices.
|
||||
|
||||
PropertyHost instances also contain methods to inform remote devices about
|
||||
changes to local property state.
|
||||
|
||||
Keeps track of property subscriptions requested by remote devices.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class PropertyHost final : public ResponderDelegate
|
||||
{
|
||||
public:
|
||||
/** @internal
|
||||
|
||||
Rather than constructing one of these objects yourself, you should configure
|
||||
a Device with property exchange support, and then use Device::getPropertyHost()
|
||||
to retrieve a property host that has been set up to work with that device.
|
||||
*/
|
||||
PropertyHost (FunctionBlock fb, PropertyDelegate& d, BufferOutput& o, CacheProvider& p)
|
||||
: functionBlock (fb), delegate (d), output (o), cacheProvider (p) {}
|
||||
|
||||
/** Sends a "Subscription" message from a device, when acting as a
|
||||
subscription responder. You should call this for all registered
|
||||
subscribers whenever the subscribed property is modified in a way that
|
||||
remote devices don't know about (if a remote device requests a
|
||||
property update, there's no need to send a subscription update after
|
||||
changing the property accordingly).
|
||||
|
||||
You should *not* attempt to start a new subscription on another device
|
||||
using this function. Valid subscription commands are "full", "partial",
|
||||
and "notify". Check the property exchange specification for the intended
|
||||
use of these commands.
|
||||
|
||||
To terminate a subscription that was initiated by a remote device,
|
||||
use terminateSubscription().
|
||||
|
||||
The provided callback will be called once the remote device has confirmed
|
||||
receipt of the subscription update. If the state of your application
|
||||
changes such that you no longer need to respond/wait for confirmation,
|
||||
you can pass the request key to Device::abortPropertyRequest().
|
||||
*/
|
||||
std::optional<RequestKey> sendSubscriptionUpdate (MUID device,
|
||||
const PropertySubscriptionHeader& header,
|
||||
Span<const std::byte> body,
|
||||
std::function<void (const PropertyExchangeResult&)> callback);
|
||||
|
||||
/** Terminates a subscription that was started by a remote device.
|
||||
|
||||
This may be useful if your application has properties that can be
|
||||
added and removed - you can terminate subscriptions to subscribed
|
||||
properties before removing those properties.
|
||||
*/
|
||||
void terminateSubscription (MUID device, const String& subscribeId);
|
||||
|
||||
/** Returns a set of subscribed resources.
|
||||
|
||||
This set contains all active subscriptionIDs for the given device,
|
||||
along with the resources to which those subscriptionIDs refer.
|
||||
*/
|
||||
std::set<Subscription> findSubscriptionsForDevice (MUID device) const;
|
||||
|
||||
/** Returns the number of transactions that have been initiated by other devices, but not yet
|
||||
completed, normally because the request has been split into several messages.
|
||||
*/
|
||||
int countOngoingTransactions() const;
|
||||
|
||||
/** @internal */
|
||||
bool tryRespond (ResponderOutput&, const Message::Parsed&) override;
|
||||
|
||||
private:
|
||||
class Visitor;
|
||||
|
||||
struct SubscriptionToken
|
||||
{
|
||||
size_t uid{};
|
||||
|
||||
bool operator< (const SubscriptionToken& other) const { return uid < other.uid; }
|
||||
bool operator== (const SubscriptionToken& other) const { return uid == other.uid; }
|
||||
bool operator!= (const SubscriptionToken& other) const { return uid != other.uid; }
|
||||
};
|
||||
|
||||
static SubscriptionToken uidFromSubscribeId (String id);
|
||||
static String subscribeIdFromUid (SubscriptionToken uid);
|
||||
static SubscriptionToken findUnusedSubscribeId (const std::map<SubscriptionToken, String>& used);
|
||||
|
||||
FunctionBlock functionBlock;
|
||||
PropertyDelegate& delegate;
|
||||
BufferOutput& output;
|
||||
CacheProvider& cacheProvider;
|
||||
|
||||
std::map<MUID, std::map<SubscriptionToken, String>> registry;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
An interface for types that implement responses for certain message types.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ResponderDelegate
|
||||
{
|
||||
public:
|
||||
ResponderDelegate() = default;
|
||||
virtual ~ResponderDelegate() = default;
|
||||
|
||||
/** If the message is processed successfully, and a response sent, then
|
||||
this returns true. Otherwise, returns false, allowing other ResponderDelegates
|
||||
to attempt to handle the message if necessary.
|
||||
*/
|
||||
virtual bool tryRespond (ResponderOutput& output, const Message::Parsed& message) = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (ResponderDelegate)
|
||||
JUCE_DECLARE_NON_MOVEABLE (ResponderDelegate)
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
ChannelAddress ResponderOutput::getChannelAddress() const
|
||||
{
|
||||
return ChannelAddress{}.withGroup (getIncomingGroup())
|
||||
.withChannel (getIncomingHeader().deviceID);
|
||||
}
|
||||
|
||||
Message::Header ResponderOutput::getReplyHeader (std::byte replySubID) const
|
||||
{
|
||||
return { getIncomingHeader().deviceID,
|
||||
replySubID,
|
||||
detail::MessageMeta::implementationVersion,
|
||||
getMuid(),
|
||||
getIncomingHeader().source };
|
||||
}
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Represents a destination into which MIDI-CI messages can be written.
|
||||
|
||||
Each message should be written into the output buffer. Then, send() will
|
||||
send the current contents of the buffer to the specified group.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class BufferOutput
|
||||
{
|
||||
public:
|
||||
BufferOutput() = default;
|
||||
virtual ~BufferOutput() = default;
|
||||
|
||||
/** Returns the MUID of the responder. */
|
||||
virtual MUID getMuid() const = 0;
|
||||
|
||||
/** Returns the buffer into which replies should be written. */
|
||||
virtual std::vector<std::byte>& getOutputBuffer() = 0;
|
||||
|
||||
/** Sends the current contents of the buffer to the provided group. */
|
||||
virtual void send (uint8_t group) = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (BufferOutput)
|
||||
JUCE_DECLARE_NON_MOVEABLE (BufferOutput)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A buffer output that additionally provides information about an incoming message, so that
|
||||
an appropriate reply can be constructed for that message.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class ResponderOutput : public BufferOutput
|
||||
{
|
||||
public:
|
||||
/** Returns the header of the message that was received. */
|
||||
virtual Message::Header getIncomingHeader() const = 0;
|
||||
|
||||
/** Returns the group of the message that was received. */
|
||||
virtual uint8_t getIncomingGroup() const = 0;
|
||||
|
||||
/** Returns the channel to which the incoming message was addressed. */
|
||||
ChannelAddress getChannelAddress() const;
|
||||
|
||||
/** Returns a default header that can be used for outgoing replies.
|
||||
|
||||
This always sets the destination MUID equal to the source MUID of the incoming header,
|
||||
so it's not suitable for broadcast messages.
|
||||
*/
|
||||
Message::Header getReplyHeader (std::byte replySubID) const;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Matches a subscription ID to a resource name.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct Subscription
|
||||
{
|
||||
String subscribeId;
|
||||
String resource;
|
||||
|
||||
bool operator< (const Subscription& other) const { return subscribeId < other.subscribeId; }
|
||||
bool operator<= (const Subscription& other) const { return subscribeId <= other.subscribeId; }
|
||||
bool operator> (const Subscription& other) const { return subscribeId > other.subscribeId; }
|
||||
bool operator>= (const Subscription& other) const { return subscribeId >= other.subscribeId; }
|
||||
|
||||
bool operator== (const Subscription& other) const
|
||||
{
|
||||
const auto tie = [] (const auto& x) { return std::tie (x.subscribeId, x.resource); };
|
||||
return tie (*this) == tie (other);
|
||||
}
|
||||
|
||||
bool operator!= (const Subscription& other) const { return ! operator== (other); }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,812 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
struct RequestRetryQueueEntry
|
||||
{
|
||||
PropertySubscriptionHeader msg;
|
||||
Token64 key{}; ///< A unique identifier for this message
|
||||
bool inFlight = false; ///< True if the message has been sent and we're waiting for a reply, false otherwise
|
||||
};
|
||||
|
||||
/*
|
||||
A queue to store pending property exchange messages.
|
||||
|
||||
A property exchange message may fail to send because the initiator doesn't have enough vacant
|
||||
property exchange IDs.
|
||||
Similarly, if the responder doesn't have enough vacant IDs, then it may tell us to retry the
|
||||
request.
|
||||
|
||||
We store messages that we're planning to send, and mark them as in-flight once we've attempted
|
||||
to send them.
|
||||
We always try to send the first not-in-flight message in the queue.
|
||||
If the responder informs us that the message was actioned, or there was an unrecoverable error,
|
||||
then we can remove the message from the queue. We can also remove the message if the user
|
||||
decides that the message is no longer important.
|
||||
Otherwise, if the message wasn't sent successfully, we leave the message at its current
|
||||
position in the queue, and mark it as not-in-flight again.
|
||||
*/
|
||||
class RequestRetryQueue
|
||||
{
|
||||
using Entry = RequestRetryQueueEntry;
|
||||
|
||||
private:
|
||||
auto getIter (Token64 k)
|
||||
{
|
||||
const auto iter = std::lower_bound (entries.begin(),
|
||||
entries.end(),
|
||||
(uint64_t) k,
|
||||
[] (const Entry& e, uint64_t v) { return (uint64_t) e.key < v; });
|
||||
return iter != entries.end() && iter->key == k ? iter : entries.end();
|
||||
}
|
||||
|
||||
public:
|
||||
/* Add a new message at the end of the queue, and return the entry for that message. */
|
||||
Entry* add (PropertySubscriptionHeader msg)
|
||||
{
|
||||
const auto key = ++lastKey;
|
||||
|
||||
entries.push_back (Entry { std::move (msg), Token64 { key }, false });
|
||||
return &entries.back();
|
||||
}
|
||||
|
||||
/* Erase the entry for a given key. */
|
||||
std::optional<Entry> erase (Token64 k)
|
||||
{
|
||||
const auto iter = getIter (k);
|
||||
|
||||
if (iter == entries.end())
|
||||
return {};
|
||||
|
||||
auto result = std::move (*iter);
|
||||
entries.erase (iter);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Find the next entry that should be sent, and return it after marking it as in-flight. */
|
||||
const Entry* markNextInFlight()
|
||||
{
|
||||
const auto iter = std::find_if (entries.begin(), entries.end(), [] (const Entry& e) { return ! e.inFlight; });
|
||||
|
||||
if (iter == entries.end())
|
||||
return nullptr;
|
||||
|
||||
iter->inFlight = true;
|
||||
return &*iter;
|
||||
}
|
||||
|
||||
void markNotInFlight (Token64 k)
|
||||
{
|
||||
const auto iter = getIter (k);
|
||||
|
||||
if (iter != entries.end())
|
||||
iter->inFlight = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Entry> entries;
|
||||
uint64_t lastKey = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
Info about a particular subscription.
|
||||
|
||||
You can think of this as a subscription agreement as identified by a subscribeId, but this
|
||||
also holds state that is necessary to negotiate the subscribeId.
|
||||
*/
|
||||
struct SubscriptionState
|
||||
{
|
||||
// If we're waiting to send this subscription request, this is monostate
|
||||
// If the request has been sent, but we haven't received a reply, this is the id of the request
|
||||
// If the subscription started successfully, this is the subscribeId for the subscription
|
||||
std::variant<std::monostate, Token64, String> state;
|
||||
String resource;
|
||||
};
|
||||
|
||||
/**
|
||||
Info about all the subscriptions requested of a particular device/MUID.
|
||||
This keeps track of the order in which subscription requests are made, so that requests can
|
||||
be re-tried in order if the initial sending of a request fails.
|
||||
*/
|
||||
class DeviceSubscriptionStates
|
||||
{
|
||||
public:
|
||||
Token64 postToQueue (const PropertySubscriptionHeader& header)
|
||||
{
|
||||
return queue.add (header)->key;
|
||||
}
|
||||
|
||||
Token64 beginSubscription (const PropertySubscriptionHeader& header)
|
||||
{
|
||||
jassert (header.command == PropertySubscriptionCommand::start);
|
||||
|
||||
auto headerCopy = header;
|
||||
headerCopy.command = PropertySubscriptionCommand::start;
|
||||
|
||||
const auto key = postToQueue (headerCopy);
|
||||
stateForSubscription[key].resource = headerCopy.resource;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
std::optional<SubscriptionState> endSubscription (Token64 key)
|
||||
{
|
||||
queue.erase (key);
|
||||
|
||||
const auto iter = stateForSubscription.find (key);
|
||||
|
||||
if (iter == stateForSubscription.end())
|
||||
return {};
|
||||
|
||||
auto subInfo = iter->second;
|
||||
stateForSubscription.erase (iter);
|
||||
|
||||
return { std::move (subInfo) };
|
||||
}
|
||||
|
||||
std::vector<Token64> endSubscription (String subscribeId)
|
||||
{
|
||||
std::vector<Token64> ended;
|
||||
|
||||
for (auto it = stateForSubscription.begin(); it != stateForSubscription.end();)
|
||||
{
|
||||
if (const auto* id = std::get_if<String> (&it->second.state))
|
||||
{
|
||||
if (*id == subscribeId)
|
||||
{
|
||||
ended.push_back (it->first);
|
||||
queue.erase (it->first);
|
||||
it = stateForSubscription.erase (it);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
return ended;
|
||||
}
|
||||
|
||||
void endAll()
|
||||
{
|
||||
for (auto& item : stateForSubscription)
|
||||
queue.erase (item.first);
|
||||
|
||||
stateForSubscription.clear();
|
||||
}
|
||||
|
||||
void resetKey (Token64 key)
|
||||
{
|
||||
const auto iter = stateForSubscription.find (key);
|
||||
|
||||
if (iter != stateForSubscription.end())
|
||||
iter->second.state = std::monostate{};
|
||||
|
||||
queue.markNotInFlight (key);
|
||||
}
|
||||
|
||||
void setRequestIdForKey (Token64 key, Token64 request)
|
||||
{
|
||||
const auto iter = stateForSubscription.find (key);
|
||||
|
||||
if (iter != stateForSubscription.end())
|
||||
iter->second.state = request;
|
||||
}
|
||||
|
||||
void setSubscribeIdForKey (Token64 key, String subscribeId)
|
||||
{
|
||||
const auto iter = stateForSubscription.find (key);
|
||||
|
||||
if (iter != stateForSubscription.end())
|
||||
iter->second.state = subscribeId;
|
||||
|
||||
queue.erase (key);
|
||||
}
|
||||
|
||||
auto* markNextInFlight()
|
||||
{
|
||||
return queue.markNextInFlight();
|
||||
}
|
||||
|
||||
std::optional<SubscriptionState> getInfoForSubscriptionKey (Token64 key) const
|
||||
{
|
||||
const auto iter = stateForSubscription.find (key);
|
||||
|
||||
if (iter != stateForSubscription.end())
|
||||
return iter->second;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
auto begin() const { return stateForSubscription.begin(); }
|
||||
auto end() const { return stateForSubscription.end(); }
|
||||
|
||||
private:
|
||||
RequestRetryQueue queue;
|
||||
std::map<Token64, SubscriptionState> stateForSubscription;
|
||||
};
|
||||
|
||||
class SubscriptionManager::Impl : public std::enable_shared_from_this<Impl>,
|
||||
private DeviceListener
|
||||
{
|
||||
public:
|
||||
explicit Impl (SubscriptionManagerDelegate& d)
|
||||
: delegate (d) {}
|
||||
|
||||
SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header)
|
||||
{
|
||||
const auto key = infoForMuid[m].beginSubscription (header);
|
||||
sendPendingMessages();
|
||||
return SubscriptionKey { m, key };
|
||||
}
|
||||
|
||||
void endSubscription (SubscriptionKey key)
|
||||
{
|
||||
const auto iter = infoForMuid.find (key.getMuid());
|
||||
|
||||
if (iter == infoForMuid.end())
|
||||
return;
|
||||
|
||||
const auto ended = iter->second.endSubscription (key.getKey());
|
||||
|
||||
if (! ended.has_value())
|
||||
return;
|
||||
|
||||
if (auto* request = std::get_if<Token64> (&ended->state))
|
||||
{
|
||||
delegate.abortPropertyRequest ({ key.getMuid(), *request });
|
||||
}
|
||||
else if (auto* subscribeId = std::get_if<String> (&ended->state))
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::end;
|
||||
header.subscribeId = *subscribeId;
|
||||
iter->second.postToQueue (header);
|
||||
sendPendingMessages();
|
||||
}
|
||||
}
|
||||
|
||||
void endSubscriptionFromResponder (MUID m, String sub)
|
||||
{
|
||||
const auto iter = infoForMuid.find (m);
|
||||
|
||||
if (iter != infoForMuid.end())
|
||||
for (const auto& ended : iter->second.endSubscription (sub))
|
||||
delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
|
||||
}
|
||||
|
||||
void endSubscriptionsFromResponder (MUID m)
|
||||
{
|
||||
const auto iter = infoForMuid.find (m);
|
||||
|
||||
if (iter == infoForMuid.end())
|
||||
return;
|
||||
|
||||
std::vector<Token64> tokens;
|
||||
std::transform (iter->second.begin(),
|
||||
iter->second.end(),
|
||||
std::back_inserter (tokens),
|
||||
[] (const auto& p) { return p.first; });
|
||||
|
||||
iter->second.endAll();
|
||||
|
||||
for (const auto& ended : tokens)
|
||||
delegate.propertySubscriptionChanged ({ m, ended }, std::nullopt);
|
||||
}
|
||||
|
||||
std::vector<SubscriptionKey> getOngoingSubscriptions() const
|
||||
{
|
||||
std::vector<SubscriptionKey> result;
|
||||
|
||||
for (const auto& pair : infoForMuid)
|
||||
for (const auto& info : pair.second)
|
||||
result.emplace_back (pair.first, info.first);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<SubscriptionState> getInfoForSubscriptionKey (SubscriptionKey key) const
|
||||
{
|
||||
const auto iter = infoForMuid.find (key.getMuid());
|
||||
|
||||
if (iter != infoForMuid.end())
|
||||
return iter->second.getInfoForSubscriptionKey (key.getKey());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool sendPendingMessages()
|
||||
{
|
||||
// Note: not using any_of here because we don't want the early-exit behaviour
|
||||
bool result = true;
|
||||
|
||||
for (auto& pair : infoForMuid)
|
||||
result &= sendPendingMessages (pair.first, pair.second);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void handleReply (SubscriptionKey subscriptionKey, PropertySubscriptionCommand command, const PropertyExchangeResult& r)
|
||||
{
|
||||
const auto iter = infoForMuid.find (subscriptionKey.getMuid());
|
||||
|
||||
if (iter == infoForMuid.end())
|
||||
return;
|
||||
|
||||
auto& second = iter->second;
|
||||
|
||||
if (const auto error = r.getError())
|
||||
{
|
||||
// If the responder requested a retry, keep the message in the queue so that
|
||||
// it can be re-sent
|
||||
if (*error == PropertyExchangeResult::Error::tooManyTransactions)
|
||||
{
|
||||
second.resetKey (subscriptionKey.getKey());
|
||||
return;
|
||||
}
|
||||
|
||||
// We tried to begin or end a subscription, but the responder said no!
|
||||
// If the responder declined to start a subscription, we can just
|
||||
// mark the subscription as ended.
|
||||
// If the responder declined to end a subscription, that's a bit trickier.
|
||||
// Hopefully this won't happen in practice, because all the options to resolve are pretty bad:
|
||||
// - One option is to ignore the failure. The remote device can carry on sending us updates.
|
||||
// This might be a bit dangerous if we repeatedly subscribe and then fail to unsubscribe, as this
|
||||
// would result in lots of redundant subscription messages that could clog the connection.
|
||||
// - Another option is to store the subscription-end request and to attempt to send it again later.
|
||||
// This also has the potential to clog up the connection, depending on how frequently we attempt
|
||||
// to re-send failed messages. Given that unsubscribing has already failed once, there's no
|
||||
// guarantee that any future attempts will succeed, so we might end up in a loop, sending the
|
||||
// same message over and over.
|
||||
// On balance, I think the former option is best for now. If this ends up being an issue in
|
||||
// practice, perhaps we could add a mechanism to do exponential back-off, but that would
|
||||
// add complexity that isn't necessarily required.
|
||||
jassert (*error != PropertyExchangeResult::Error::notify);
|
||||
|
||||
// If we failed to begin a subscription, then the subscription never started,
|
||||
// and we should remove it from the set of ongoing subscriptions.
|
||||
second.endSubscription (subscriptionKey.getKey());
|
||||
|
||||
// We only need to alert the delegate if the subscription failed to start.
|
||||
// If the subscription fails to end, we'll treat the subscription as ended anyway.
|
||||
if (command == PropertySubscriptionCommand::start)
|
||||
delegate.propertySubscriptionChanged (subscriptionKey, std::nullopt);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == PropertySubscriptionCommand::start)
|
||||
{
|
||||
second.setSubscribeIdForKey (subscriptionKey.getKey(), r.getHeaderAsSubscriptionHeader().subscribeId);
|
||||
delegate.propertySubscriptionChanged (subscriptionKey, r.getHeaderAsSubscriptionHeader().subscribeId);
|
||||
}
|
||||
}
|
||||
|
||||
bool sendPendingMessages (MUID m, DeviceSubscriptionStates& info)
|
||||
{
|
||||
while (auto* entry = info.markNextInFlight())
|
||||
{
|
||||
const auto requestKind = entry->msg.command;
|
||||
const SubscriptionKey subscriptionKey { m, entry->key };
|
||||
|
||||
auto cb = [weak = weak_from_this(), requestKind, subscriptionKey] (const PropertyExchangeResult& r)
|
||||
{
|
||||
if (const auto locked = weak.lock())
|
||||
locked->handleReply (subscriptionKey, requestKind, r);
|
||||
};
|
||||
|
||||
if (const auto request = delegate.sendPropertySubscribe (m, entry->msg, std::move (cb)))
|
||||
{
|
||||
if (entry->msg.command == PropertySubscriptionCommand::start)
|
||||
info.setRequestIdForKey (entry->key, request->getKey());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid ID to use, so we must have exhausted all message slots.
|
||||
// There's no point trying to send the rest of the messages that are queued for this
|
||||
// MUID, so give up. It's probably a good idea to try again in a bit.
|
||||
info.resetKey (entry->key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SubscriptionManagerDelegate& delegate;
|
||||
std::map<MUID, DeviceSubscriptionStates> infoForMuid;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
SubscriptionManager::SubscriptionManager (SubscriptionManagerDelegate& delegate)
|
||||
: pimpl (std::make_shared<Impl> (delegate)) {}
|
||||
|
||||
SubscriptionKey SubscriptionManager::beginSubscription (MUID m, const PropertySubscriptionHeader& header)
|
||||
{
|
||||
return pimpl->beginSubscription (m, header);
|
||||
}
|
||||
|
||||
void SubscriptionManager::endSubscription (SubscriptionKey key)
|
||||
{
|
||||
pimpl->endSubscription (key);
|
||||
}
|
||||
|
||||
void SubscriptionManager::endSubscriptionFromResponder (MUID m, String sub)
|
||||
{
|
||||
pimpl->endSubscriptionFromResponder (m, sub);
|
||||
}
|
||||
|
||||
void SubscriptionManager::endSubscriptionsFromResponder (MUID m)
|
||||
{
|
||||
pimpl->endSubscriptionsFromResponder (m);
|
||||
}
|
||||
|
||||
std::vector<SubscriptionKey> SubscriptionManager::getOngoingSubscriptions() const
|
||||
{
|
||||
return pimpl->getOngoingSubscriptions();
|
||||
}
|
||||
|
||||
std::optional<String> SubscriptionManager::getSubscribeIdForKey (SubscriptionKey key) const
|
||||
{
|
||||
if (const auto info = pimpl->getInfoForSubscriptionKey (key))
|
||||
if (const auto* subscribeId = std::get_if<String> (&info->state))
|
||||
return *subscribeId;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<String> SubscriptionManager::getResourceForKey (SubscriptionKey key) const
|
||||
{
|
||||
if (const auto info = pimpl->getInfoForSubscriptionKey (key))
|
||||
return info->resource;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SubscriptionManager::sendPendingMessages()
|
||||
{
|
||||
return pimpl->sendPendingMessages();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class SubscriptionTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
SubscriptionTests() : UnitTest ("Subscription", UnitTestCategories::midi) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
auto random = getRandom();
|
||||
|
||||
class Delegate : public SubscriptionManagerDelegate
|
||||
{
|
||||
public:
|
||||
std::optional<RequestKey> sendPropertySubscribe (MUID m,
|
||||
const PropertySubscriptionHeader&,
|
||||
std::function<void (const PropertyExchangeResult&)> cb) override
|
||||
{
|
||||
++sendCount;
|
||||
|
||||
if (! sendShouldSucceed)
|
||||
return {};
|
||||
|
||||
const RequestKey key { m, Token64 { ++lastKey } };
|
||||
callbacks[key] = std::move (cb);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
void abortPropertyRequest (RequestKey k) override
|
||||
{
|
||||
++abortCount;
|
||||
callbacks.erase (k);
|
||||
}
|
||||
|
||||
void propertySubscriptionChanged (SubscriptionKey, const std::optional<String>&) override
|
||||
{
|
||||
subChanged = true;
|
||||
}
|
||||
|
||||
void setSendShouldSucceed (bool b) { sendShouldSucceed = b; }
|
||||
|
||||
void sendResult (RequestKey key, const PropertyExchangeResult& r)
|
||||
{
|
||||
const auto iter = callbacks.find (key);
|
||||
|
||||
if (iter != callbacks.end())
|
||||
NullCheckedInvocation::invoke (iter->second, r);
|
||||
|
||||
callbacks.erase (key);
|
||||
}
|
||||
|
||||
std::vector<RequestKey> getOngoingRequests() const
|
||||
{
|
||||
std::vector<RequestKey> result;
|
||||
result.reserve (callbacks.size());
|
||||
std::transform (callbacks.begin(), callbacks.end(), std::back_inserter (result), [] (const auto& p) { return p.first; });
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t getAndClearSendCount() { return std::exchange (sendCount, 0); }
|
||||
uint64_t getAndClearAbortCount() { return std::exchange (abortCount, 0); }
|
||||
bool getAndClearSubChanged() { return std::exchange (subChanged, false); }
|
||||
|
||||
private:
|
||||
std::map<RequestKey, std::function<void (const PropertyExchangeResult&)>> callbacks;
|
||||
uint64_t sendCount = 0, abortCount = 0;
|
||||
uint64_t lastKey = 0;
|
||||
bool sendShouldSucceed = true, subChanged = false;
|
||||
};
|
||||
|
||||
Delegate delegate;
|
||||
SubscriptionManager manager { delegate };
|
||||
|
||||
const auto inquiryMUID = MUID::makeRandom (random);
|
||||
|
||||
beginTest ("Beginning a subscription and ending it before the remote device replies aborts the request");
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::start;
|
||||
header.resource = "X-CustomProp";
|
||||
|
||||
const auto a = manager.beginSubscription (inquiryMUID, header);
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
|
||||
// Sending a subscription request uses a request slot
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
const auto request = delegate.getOngoingRequests().back();
|
||||
|
||||
// subscription id is empty until the responder confirms the subscription
|
||||
expect (manager.getResourceForKey (a) == header.resource);
|
||||
|
||||
manager.endSubscription (a);
|
||||
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearAbortCount() == 1);
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
|
||||
const auto successHeader = []
|
||||
{
|
||||
auto ptr = std::make_unique<DynamicObject>();
|
||||
ptr->setProperty ("status", 200);
|
||||
ptr->setProperty ("subscribeId", "anId");
|
||||
return var { ptr.release() };
|
||||
}();
|
||||
|
||||
delegate.sendResult (request, PropertyExchangeResult { successHeader, {} });
|
||||
|
||||
// Already ended, the confirmation shouldn't do anything
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
// There shouldn't be any queued messages.
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
}
|
||||
|
||||
beginTest ("Starting a new subscription while the device is waiting for a previous subscription to be confirmed queues further requests");
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::start;
|
||||
header.resource = "X-CustomProp";
|
||||
|
||||
delegate.setSendShouldSucceed (false);
|
||||
const auto a = manager.beginSubscription (inquiryMUID, header);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
|
||||
expect (! manager.sendPendingMessages());
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
|
||||
delegate.setSendShouldSucceed (true);
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
|
||||
delegate.setSendShouldSucceed (false);
|
||||
const auto b = manager.beginSubscription (inquiryMUID, header);
|
||||
const auto c = manager.beginSubscription (inquiryMUID, header);
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a, b, c });
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
|
||||
// subscription id is empty until the responder confirms the subscription
|
||||
expect (manager.getResourceForKey (a) == header.resource);
|
||||
expect (manager.getResourceForKey (b) == header.resource);
|
||||
expect (manager.getResourceForKey (c) == header.resource);
|
||||
|
||||
expect (delegate.getAndClearSendCount() == 2);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
delegate.setSendShouldSucceed (true);
|
||||
|
||||
// The device has sent a subscription start for a, but not for c,
|
||||
// so it should send a notify to end subscription a, but shouldn't emit any
|
||||
// messages related to subscription c.
|
||||
manager.endSubscription (a);
|
||||
manager.endSubscription (c);
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { b });
|
||||
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 1);
|
||||
|
||||
// There should still be requests related to subscription b pending
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
// Now, we should send a terminate request for subscription b
|
||||
manager.endSubscription (b);
|
||||
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 1);
|
||||
|
||||
// The manager never received any replies, so it shouldn't have notified listeners about
|
||||
// changed subscriptions
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
}
|
||||
|
||||
beginTest ("If the device receives a retry or notify in response to a subscription start request, the subscription is retried or terminated as necessary");
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::start;
|
||||
header.resource = "X-CustomProp";
|
||||
|
||||
const auto a = manager.beginSubscription (inquiryMUID, header);
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
|
||||
|
||||
// The subscription is still active from the perspective of the manager, but the
|
||||
// first request is over and should be retried
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
expect (manager.sendPendingMessages());
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
|
||||
|
||||
// The request was terminated by the responder, so the delegate should get a sub-changed message
|
||||
expect (delegate.getAndClearSubChanged());
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
}
|
||||
|
||||
beginTest ("If the device receives a retry or notify in response to a subscription end request, the subscription is retried as necessary");
|
||||
{
|
||||
PropertySubscriptionHeader header;
|
||||
header.command = PropertySubscriptionCommand::start;
|
||||
header.resource = "X-CustomProp";
|
||||
|
||||
const auto a = manager.beginSubscription (inquiryMUID, header);
|
||||
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (manager.getResourceForKey (a) == header.resource);
|
||||
expect (! manager.getSubscribeIdForKey (a).has_value());
|
||||
|
||||
const auto subscriptionResponseHeader = []
|
||||
{
|
||||
auto ptr = std::make_unique<DynamicObject>();
|
||||
ptr->setProperty ("status", 200);
|
||||
ptr->setProperty ("subscribeId", "newId");
|
||||
return ptr.release();
|
||||
}();
|
||||
|
||||
// Accept the subscription
|
||||
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { subscriptionResponseHeader, {} });
|
||||
|
||||
// The subscription is still active from the perspective of the device, but the
|
||||
// request is over and should be retried
|
||||
expect (manager.getOngoingSubscriptions() == std::vector { a });
|
||||
expect (delegate.getAndClearSubChanged());
|
||||
// Now that the subscription was accepted, the subscription id should be non-empty
|
||||
expect (manager.getResourceForKey (a) == header.resource);
|
||||
expect (manager.getSubscribeIdForKey (a) == "newId");
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
manager.endSubscription (a);
|
||||
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
// The responder is busy, can't process the subscription end
|
||||
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::tooManyTransactions });
|
||||
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (delegate.getOngoingRequests().size() == 1);
|
||||
expect (delegate.getAndClearSendCount() == 1);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
// The responder told us to immediately terminate our request to end the subscription!
|
||||
// It's unclear how this should behave, so we'll just ignore the failure and assume
|
||||
// the subscription is really over.
|
||||
delegate.sendResult (delegate.getOngoingRequests().back(), PropertyExchangeResult { PropertyExchangeResult::Error::notify });
|
||||
|
||||
expect (manager.getOngoingSubscriptions().empty());
|
||||
expect (delegate.getOngoingRequests().empty());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
expect (manager.sendPendingMessages());
|
||||
expect (delegate.getAndClearSendCount() == 0);
|
||||
expect (delegate.getAndClearAbortCount() == 0);
|
||||
|
||||
expect (! delegate.getAndClearSubChanged());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static SubscriptionTests subscriptionTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
A key used to uniquely identify ongoing property subscriptions initiated by a ci::Device.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class SubscriptionKey
|
||||
{
|
||||
auto tie() const { return std::tuple (m, v); }
|
||||
|
||||
public:
|
||||
/** Constructor */
|
||||
SubscriptionKey() = default;
|
||||
|
||||
/** Constructor */
|
||||
SubscriptionKey (MUID muid, Token64 key) : m (muid), v (key) {}
|
||||
|
||||
/** Returns the muid of the device to which we are subscribed. */
|
||||
MUID getMuid() const { return m; }
|
||||
|
||||
/** Returns an identifier unique to this subscription. */
|
||||
Token64 getKey() const { return v; }
|
||||
|
||||
/** Equality operator. */
|
||||
bool operator== (const SubscriptionKey& other) const { return tie() == other.tie(); }
|
||||
|
||||
/** Inequality operator. */
|
||||
bool operator!= (const SubscriptionKey& other) const { return tie() != other.tie(); }
|
||||
|
||||
/** Less-than operator. */
|
||||
bool operator< (const SubscriptionKey& other) const { return tie() < other.tie(); }
|
||||
|
||||
private:
|
||||
MUID m = MUID::getBroadcast();
|
||||
Token64 v{};
|
||||
};
|
||||
|
||||
/**
|
||||
Functions used by a SubscriptionManager to negotiate subscriptions.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct SubscriptionManagerDelegate
|
||||
{
|
||||
virtual ~SubscriptionManagerDelegate() = default;
|
||||
|
||||
/** Called when the manager wants to send an update. */
|
||||
virtual std::optional<RequestKey> sendPropertySubscribe (MUID m,
|
||||
const PropertySubscriptionHeader& header,
|
||||
std::function<void (const PropertyExchangeResult&)> onResult) = 0;
|
||||
|
||||
/** Called by the manager to cancel a previous request. */
|
||||
virtual void abortPropertyRequest (RequestKey) = 0;
|
||||
|
||||
/** Called by the manager when the remote device provides a subscribeId, or when it
|
||||
terminates a subscription.
|
||||
*/
|
||||
virtual void propertySubscriptionChanged (SubscriptionKey, const std::optional<String>&) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
Manages subscriptions to properties on remote devices.
|
||||
|
||||
Occasionally, sending a subscription-begin request may fail, in which case the request will be
|
||||
cached. Cached requests will be sent during a future call to sendPendingMessages().
|
||||
|
||||
To use this:
|
||||
- pass a SubscriptionManagerDelegate (such as a ci::Device) to the constructor
|
||||
- call sendPendingMessages() periodically, e.g. in a timer callback
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class SubscriptionManager
|
||||
{
|
||||
public:
|
||||
/** Constructor.
|
||||
|
||||
The delegate functions will be called when necessary to start and cancel property requests.
|
||||
*/
|
||||
explicit SubscriptionManager (SubscriptionManagerDelegate& delegate);
|
||||
|
||||
/** Attempts to begin a subscription using the provided details.
|
||||
|
||||
@returns a token that uniquely identifies this subscription. This token can be passed to
|
||||
endSubscription to terminate an ongoing subscription.
|
||||
*/
|
||||
SubscriptionKey beginSubscription (MUID m, const PropertySubscriptionHeader& header);
|
||||
|
||||
/** Ends an ongoing subscription by us.
|
||||
|
||||
If the subscription begin request hasn't been sent yet, then this will just cancel the cached request.
|
||||
|
||||
If a subscription begin request has been sent, but no response has been received, this will
|
||||
send a notification cancelling the initial request via SubscriptionManagerDelegate::abortPropertyRequest().
|
||||
|
||||
If the subscription has started successfully, then this will send a subscription end request
|
||||
via SubscriptionManagerDelegate::sendPropertySubscribe().
|
||||
*/
|
||||
void endSubscription (SubscriptionKey);
|
||||
|
||||
/** Ends an ongoing subscription as requested from the remote device.
|
||||
|
||||
Unlike the other overload of endSubscription, this won't notify the delegate. It will only
|
||||
update the internal record of active subscriptions.
|
||||
|
||||
Calls Delegate::propertySubscriptionChanged().
|
||||
*/
|
||||
void endSubscriptionFromResponder (MUID, String);
|
||||
|
||||
/** Ends all ongoing subscriptions as requested from a remote device.
|
||||
|
||||
Calls Delegate::propertySubscriptionChanged().
|
||||
*/
|
||||
void endSubscriptionsFromResponder (MUID);
|
||||
|
||||
/** Returns all of the subscriptions that have been initiated by this manager. */
|
||||
std::vector<SubscriptionKey> getOngoingSubscriptions() const;
|
||||
|
||||
/** If the provided subscription has started successfully, this returns the subscribeId assigned
|
||||
to the subscription by the remote device.
|
||||
*/
|
||||
std::optional<String> getSubscribeIdForKey (SubscriptionKey key) const;
|
||||
|
||||
/** If the provided subscription has not been cancelled, this returns the name of the
|
||||
subscribed resource.
|
||||
*/
|
||||
std::optional<String> getResourceForKey (SubscriptionKey key) const;
|
||||
|
||||
/** Sends any cached messages that need retrying.
|
||||
|
||||
@returns true if there are no more messages to send, or false otherwise
|
||||
*/
|
||||
bool sendPendingMessages();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> pimpl;
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce::midi_ci
|
||||
{
|
||||
|
||||
/**
|
||||
Holds the maximum number of channels that may be activated for a MIDI-CI
|
||||
profile, along with the number of channels that are currently active.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct SupportedAndActive
|
||||
{
|
||||
uint16_t supported{}; ///< The maximum number of member channels for a profile.
|
||||
///< 0 indicates that the profile is unsupported.
|
||||
///< For group/block profiles, 1/0 indicates that the
|
||||
///< profile is supported/unsupported respectively.
|
||||
|
||||
uint16_t active{}; ///< The number of member channels currently active for a profile.
|
||||
///< 0 indicates that the profile is inactive.
|
||||
///< For group/block profiles, 1/0 indicates that the
|
||||
///< profile is supported/unsupported respectively.
|
||||
|
||||
/** Returns true if supported is non-zero. */
|
||||
bool isSupported() const { return supported != 0; }
|
||||
|
||||
/** Returns true if active is non-zero. */
|
||||
bool isActive() const { return active != 0; }
|
||||
|
||||
bool operator== (const SupportedAndActive& other) const
|
||||
{
|
||||
const auto tie = [] (auto& x) { return std::tie (x.supported, x.active); };
|
||||
return tie (*this) == tie (other);
|
||||
}
|
||||
|
||||
bool operator!= (const SupportedAndActive& other) const { return ! operator== (other); }
|
||||
};
|
||||
|
||||
} // namespace juce::midi_ci
|
||||
Reference in New Issue
Block a user