build(deps): vendor JUCE 7.0.12

This commit is contained in:
2026-09-08 14:55:21 +02:00
parent 5b99f51bd1
commit d6705ab29f
3591 changed files with 1218267 additions and 0 deletions
@@ -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
+313
View File
@@ -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
+83
View File
@@ -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