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,376 @@
/*
==============================================================================
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.
==============================================================================
*/
/*
Utilities for converting sequences of bytes to and from
C++ struct types.
*/
namespace juce::midi_ci::detail::Marshalling
{
template <uint8_t> struct IntForNumBytes;
template <> struct IntForNumBytes<1> { using Type = uint8_t; };
template <> struct IntForNumBytes<2> { using Type = uint16_t; };
template <> struct IntForNumBytes<4> { using Type = uint32_t; };
template <uint8_t NumBytes> using IntForNumBytesT = typename IntForNumBytes<NumBytes>::Type;
//==============================================================================
/*
Reads a sequence of bytes representing a MIDI-CI message, and populates
structs with the information contained in the message.
*/
class Reader
{
public:
/* Constructs a reader that will parse the provided buffer, using the most
recent known MIDI-CI version.
*/
explicit Reader (Span<const std::byte> b)
: Reader (b, static_cast<uint8_t> (MessageMeta::implementationVersion)) {}
/* Constructs a reader for the provided MIDI-CI version that will parse
the provided buffer. Fields introduced in later versions will be ignored,
and so left with their default values.
*/
Reader (Span<const std::byte> b, int v)
: bytes (b), version (v) {}
std::optional<int> getVersion() const { return version; }
/* Attempts to interpret the byte sequence passed to the constructor
as a sequence of structs 'T'.
Returns true if parsing succeeds, otherwise returns false.
*/
template <typename... T>
bool operator() (T&&... t)
{
return (doArchiveChecked (std::forward<T> (t)) && ...);
}
private:
template <typename T>
bool doArchiveChecked (T&& t)
{
if (failed)
return false;
doArchive (t);
return ! failed;
}
void doArchive (ChannelInGroup& x)
{
if (const auto popped = popBytes (1))
{
const auto p = *popped;
x = ChannelInGroup (p[0] & std::byte { 0x7f });
return;
}
failed = true;
}
// If we're trying to parse into a constant, then we should check that the next byte(s)
// match that constant.
void doArchive (const std::byte& x)
{
std::byte temp{};
if (! doArchiveChecked (temp))
return;
failed |= x != temp;
}
void doArchive (std::byte& x)
{
if (const auto popped = popBytes (1))
{
const auto p = *popped;
x = p[0] & std::byte { 0x7f };
return;
}
failed = true;
}
void doArchive (const uint16_t& x)
{
uint16_t temp{};
if (! doArchiveChecked (temp))
return;
failed |= temp != x;
}
void doArchive (uint16_t& x)
{
if (const auto popped = popBytes (2))
{
const auto p = *popped;
x = (uint16_t) (((uint16_t) p[0] & 0x7f) << 0x00)
| (uint16_t) (((uint16_t) p[1] & 0x7f) << 0x07);
return;
}
failed = true;
}
void doArchive (const uint32_t& x)
{
uint32_t temp{};
if (! doArchiveChecked (temp))
return;
failed |= temp != x;
}
void doArchive (uint32_t& x)
{
if (const auto popped = popBytes (4))
{
const auto p = *popped;
x = (((uint32_t) p[0] & 0x7f) << 0x00)
| (((uint32_t) p[1] & 0x7f) << 0x07)
| (((uint32_t) p[2] & 0x7f) << 0x0e)
| (((uint32_t) p[3] & 0x7f) << 0x15);
return;
}
failed = true;
}
template <uint8_t NumBytes, bool B>
void doArchive (MessageMeta::SpanWithSizeBytes<NumBytes, Span<const std::byte>, B> x)
{
IntForNumBytesT<NumBytes> numBytes{};
// Read the number of bytes in the field
if (! doArchiveChecked (numBytes))
return;
// Attempt to pop that many bytes
if (const auto popped = popBytes (numBytes))
{
x.span = *popped;
return;
}
failed = true;
}
template <uint8_t NumBytes, size_t N>
void doArchive (MessageMeta::SpanWithSizeBytes<NumBytes, Span<const std::array<std::byte, N>>> x)
{
IntForNumBytesT<NumBytes> numItems{};
// Read the number of items in the field
if (! doArchiveChecked (numItems))
return;
if (const auto popped = popBytes (numItems * N))
{
x.span = Span (unalignedPointerCast<const std::array<std::byte, N>*> (popped->data()), numItems);
return;
}
failed = true;
}
template <size_t N>
void doArchive (Span<const std::byte, N>& x)
{
if (const auto popped = popBytes (bytes.size()))
{
x = *popped;
return;
}
failed = true;
}
template <size_t N>
void doArchive (std::array<std::byte, N>& x)
{
if (const auto popped = popBytes (x.size()))
{
const auto p = *popped;
std::transform (p.begin(), p.end(), x.begin(), [] (std::byte b)
{
return b & std::byte { 0x7f };
});
return;
}
failed = true;
}
template <typename T>
void doArchive (T& t)
{
juce::detail::doLoad (*this, t);
}
template <typename T>
void doArchive (Named<T> named)
{
doArchiveChecked (named.value);
}
std::optional<Span<const std::byte>> popBytes (size_t num)
{
if (bytes.size() < num)
return {};
const Span result { bytes.data(), num };
bytes = Span { bytes.data() + num, bytes.size() - num };
return result;
}
Span<const std::byte> bytes; /* Bytes making up a CI message. */
int version{}; /* The version to assume when parsing the message, specified in the message header. */
bool failed = false;
};
//==============================================================================
/*
Converts one or more structs into a byte sequence suitable for transmission
as a MIDI-CI message.
*/
class Writer
{
public:
/* Constructs a writer that will write into the provided buffer. */
explicit Writer (std::vector<std::byte>& b)
: Writer (b, static_cast<uint8_t> (MessageMeta::implementationVersion)) {}
/* Constructs a writer that will write a MIDI-CI message of the requested
version to the provided buffer.
Fields introduced in later MIDI-CI versions will be ignored.
*/
Writer (std::vector<std::byte>& b, int v)
: bytes (b), version (v) {}
std::optional<int> getVersion() const { return version; }
/* Formats the information contained in the provided structs into a
MIDI-CI message, and returns a bool indicating success or failure.
*/
template <typename... T>
bool operator() (const T&... t)
{
return (doArchiveChecked (t) && ...);
}
private:
template <typename T>
bool doArchiveChecked (T&& t)
{
if (failed)
return false;
doArchive (t);
return ! failed;
}
void doArchive (ChannelInGroup x)
{
doArchiveChecked (std::byte (x));
}
void doArchive (std::byte x)
{
bytes.push_back (x);
}
void doArchive (uint16_t x)
{
bytes.insert (bytes.end(), { (std::byte) ((x >> 0x00) & 0x7f),
(std::byte) ((x >> 0x07) & 0x7f) });
}
void doArchive (uint32_t x)
{
bytes.insert (bytes.end(), { (std::byte) ((x >> 0x00) & 0x7f),
(std::byte) ((x >> 0x07) & 0x7f),
(std::byte) ((x >> 0x0e) & 0x7f),
(std::byte) ((x >> 0x15) & 0x7f) });
}
template <uint8_t NumBytes, typename T, bool B>
void doArchive (MessageMeta::SpanWithSizeBytes<NumBytes, T, B> x)
{
if (x.span.size() >= (1 << (7 * NumBytes)))
{
// Unable to express the size of the field in the requested number of bytes
jassertfalse;
failed = true;
return;
}
// Write the number of bytes, followed by the bytes themselves.
const auto numBytes = (IntForNumBytesT<NumBytes>) x.span.size();
doArchiveChecked (numBytes);
doArchiveChecked (x.span);
}
template <typename T, size_t N>
void doArchive (Span<const T, N> x)
{
failed = ! std::all_of (x.begin(), x.end(), [&] (const auto& item)
{
return doArchiveChecked (item);
});
}
template <size_t N>
void doArchive (const std::array<std::byte, N>& x)
{
bytes.insert (bytes.end(), x.begin(), x.end());
}
template <typename T>
void doArchive (const T& t)
{
juce::detail::doSave (*this, t);
}
template <typename T>
void doArchive (Named<T> named)
{
doArchiveChecked (named.value);
}
std::vector<std::byte>& bytes; /* The buffer that will hold the completed message. */
int version{}; /* The version to assume when writing the message, specified in the message header. */
bool failed = false;
};
} // namespace juce::midi_ci::detail::Marshalling
@@ -0,0 +1,623 @@
/*
==============================================================================
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 containing metadata about MIDI-CI message types, such as
replies corresponding to inquiries, and serialization functions.
@tags{Audio}
*/
namespace juce::midi_ci::detail::MessageMeta
{
//==============================================================================
/* The maximum CI version that can be parsed and generated by this implementation. */
static constexpr std::byte implementationVersion { 0x02 };
/* Wraps a pointer to a Span. Used to indicate to CI readers/writers that a particular field is
of variable length, starting with a 16-bit or 32-bit byte count.
*/
template <uint8_t NumBytes, typename T, bool isJson = false>
struct SpanWithSizeBytes
{
T& span;
};
/* Creates a SpanWithSizeBytes with an appropriate template argument. */
template <uint8_t NumBytes, typename T>
static constexpr auto makeSpanWithSizeBytes ( Span<T>& span) { return SpanWithSizeBytes<NumBytes, Span<T>> { span }; }
template <uint8_t NumBytes, typename T>
static constexpr auto makeSpanWithSizeBytes (const Span<T>& span) { return SpanWithSizeBytes<NumBytes, const Span<T>> { span }; }
template <uint8_t NumBytes, typename T>
static constexpr auto makeJsonWithSizeBytes ( Span<T>& span) { return SpanWithSizeBytes<NumBytes, Span<T>, true> { span }; }
template <uint8_t NumBytes, typename T>
static constexpr auto makeJsonWithSizeBytes (const Span<T>& span) { return SpanWithSizeBytes<NumBytes, const Span<T>, true> { span }; }
template <uint8_t SubID2, typename R = void>
struct Metadata
{
static constexpr std::byte subID2 { SubID2 };
using Reply = R;
};
template <typename T>
struct Meta;
template <>
struct Meta<Message::DiscoveryResponse> : Metadata<0x71> {};
template <>
struct Meta<Message::Discovery> : Metadata<0x70, Message::DiscoveryResponse> {};
template <>
struct Meta<Message::EndpointInquiryResponse> : Metadata<0x73> {};
template <>
struct Meta<Message::EndpointInquiry> : Metadata<0x72, Message::EndpointInquiryResponse> {};
template <>
struct Meta<Message::InvalidateMUID> : Metadata<0x7e> {};
template <>
struct Meta<Message::ACK> : Metadata<0x7d> {};
template <>
struct Meta<Message::NAK> : Metadata<0x7f> {};
template <>
struct Meta<Message::ProfileInquiryResponse> : Metadata<0x21> {};
template <>
struct Meta<Message::ProfileInquiry> : Metadata<0x20, Message::ProfileInquiryResponse> {};
template <>
struct Meta<Message::ProfileAdded> : Metadata<0x26> {};
template <>
struct Meta<Message::ProfileRemoved> : Metadata<0x27> {};
template <>
struct Meta<Message::ProfileDetailsResponse> : Metadata<0x29> {};
template <>
struct Meta<Message::ProfileDetails> : Metadata<0x28, Message::ProfileDetailsResponse> {};
template <>
struct Meta<Message::ProfileOn> : Metadata<0x22> {};
template <>
struct Meta<Message::ProfileOff> : Metadata<0x23> {};
template <>
struct Meta<Message::ProfileEnabledReport> : Metadata<0x24> {};
template <>
struct Meta<Message::ProfileDisabledReport> : Metadata<0x25> {};
template <>
struct Meta<Message::ProfileSpecificData> : Metadata<0x2f> {};
template <>
struct Meta<Message::PropertyExchangeCapabilitiesResponse> : Metadata<0x31> {};
template <>
struct Meta<Message::PropertyExchangeCapabilities> : Metadata<0x30, Message::PropertyExchangeCapabilitiesResponse> {};
template <>
struct Meta<Message::StaticSizePropertyExchange> {};
template <>
struct Meta<Message::DynamicSizePropertyExchange> {};
template <>
struct Meta<Message::PropertyGetDataResponse> : Meta<Message::DynamicSizePropertyExchange>, Metadata<0x35> {};
template <>
struct Meta<Message::PropertyGetData> : Meta<Message::StaticSizePropertyExchange>, Metadata<0x34, Message::PropertyGetDataResponse> {};
template <>
struct Meta<Message::PropertySetDataResponse> : Meta<Message::StaticSizePropertyExchange>, Metadata<0x37> {};
template <>
struct Meta<Message::PropertySetData> : Meta<Message::DynamicSizePropertyExchange>, Metadata<0x36, Message::PropertySetDataResponse> {};
template <>
struct Meta<Message::PropertySubscribeResponse> : Meta<Message::DynamicSizePropertyExchange>, Metadata<0x39> {};
template <>
struct Meta<Message::PropertySubscribe> : Meta<Message::DynamicSizePropertyExchange>, Metadata<0x38, Message::PropertySubscribeResponse> {};
template <>
struct Meta<Message::PropertyNotify> : Meta<Message::DynamicSizePropertyExchange>, Metadata<0x3f> {};
template <>
struct Meta<Message::ProcessInquiryResponse> : Metadata<0x41> {};
template <>
struct Meta<Message::ProcessInquiry> : Metadata<0x40, Message::ProcessInquiryResponse> {};
template <>
struct Meta<Message::ProcessMidiMessageReportResponse> : Metadata<0x43> {};
template <>
struct Meta<Message::ProcessMidiMessageReport> : Metadata<0x42, Message::ProcessMidiMessageReportResponse> {};
template <>
struct Meta<Message::ProcessEndMidiMessageReport> : Metadata<0x44> {};
} // namespace juce::midi_ci::detail::MessageMeta
#ifndef DOXYGEN
namespace juce
{
struct VersionBase
{
static constexpr auto marshallingVersion = (int) juce::midi_ci::detail::MessageMeta::implementationVersion;
};
template <uint8_t NumBytes, typename T, bool isJson>
struct SerialisationTraits<midi_ci::detail::MessageMeta::SpanWithSizeBytes<NumBytes, T, isJson>>
{
static constexpr auto marshallingVersion = std::nullopt;
template <typename This>
static auto getSize (This& t)
{
if constexpr (NumBytes == 1)
return (uint8_t) t.size();
else if constexpr (NumBytes == 2)
return (uint16_t) t.size();
else if constexpr (NumBytes == 4)
return (uint32_t) t.size();
else if constexpr (NumBytes == 8)
return (uint64_t) t.size();
else
static_assert (detail::delayStaticAssert<T>, "NumBytes is not a power of two");
}
template <typename Archive, typename This>
static auto load (Archive&, This&)
{
}
template <typename Archive, typename This>
static auto save (Archive& archive, const This& t)
{
auto size = getSize (t.span);
archive (serialisationSize (size));
for (const auto& element : t.span)
archive (element);
}
};
template <>
struct SerialisationTraits<ci::MUID> : VersionBase
{
template <typename Archive>
static auto load (Archive& archive, ci::MUID& t)
{
uint32_t muid{};
auto result = archive (muid);
t = ci::MUID::makeUnchecked (muid);
return result;
}
template <typename Archive>
static auto save (Archive& archive, const ci::MUID& t)
{
return archive (t.get());
}
};
template <>
struct SerialisationTraits<ci::Message::Header> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
const std::byte universalSystemExclusive { 0x7e }, subID { 0x0d };
return archive (universalSystemExclusive,
t.deviceID,
subID,
t.category,
t.version,
t.source,
t.destination);
}
};
template <>
struct SerialisationTraits<ci::Message::Generic> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
return archive (t.header, t.data);
}
};
template <>
struct SerialisationTraits<ci::Message::DiscoveryResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("device", t.device),
named ("capabilities", t.capabilities),
named ("maximumSysexSize", t.maximumSysexSize));
if (0x02 <= archive.getVersion())
archive (named ("outputPathID", t.outputPathID), named ("functionBlock", t.functionBlock));
}
};
template <>
struct SerialisationTraits<ci::Message::Discovery> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("device", t.device),
named ("capabilities", t.capabilities),
named ("maximumSysexSize", t.maximumSysexSize));
if (0x02 <= archive.getVersion())
archive (named ("outputPathID", t.outputPathID));
}
};
template <>
struct SerialisationTraits<ci::Message::EndpointInquiryResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("status", t.status),
named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data)));
}
};
template <>
struct SerialisationTraits<ci::Message::EndpointInquiry> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("status", t.status));
}
};
template <>
struct SerialisationTraits<ci::Message::InvalidateMUID> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("target", t.target));
}
};
template <>
struct SerialisationTraits<ci::Message::ACK> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("originalCategory", t.originalCategory),
named ("statusCode", t.statusCode),
named ("statusData", t.statusData),
named ("details", t.details),
named ("messageText", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.messageText)));
}
};
template <>
struct SerialisationTraits<ci::Message::NAK> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
if (0x02 <= archive.getVersion())
{
archive (named ("originalCategory", t.originalCategory),
named ("statusCode", t.statusCode),
named ("statusData", t.statusData),
named ("details", t.details),
named ("messageText", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.messageText)));
}
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileInquiryResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("enabledProfiles", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.enabledProfiles)),
named ("disabledProfiles", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.disabledProfiles)));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileInquiry> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive&, This&)
{
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileAdded> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileRemoved> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileDetailsResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile),
named ("target", t.target),
named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data)));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileDetails> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile),
named ("target", t.target));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileOn> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
if (0x02 <= archive.getVersion())
archive (named ("numChannels", t.numChannels));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileOff> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
if (0x02 <= archive.getVersion())
{
uint16_t reserved{};
archive (named ("reserved", reserved));
}
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileEnabledReport> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
if (0x02 <= archive.getVersion())
archive (named ("numChannels", t.numChannels));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileDisabledReport> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile));
if (0x02 <= archive.getVersion())
archive (named ("numChannels", t.numChannels));
}
};
template <>
struct SerialisationTraits<ci::Message::ProfileSpecificData> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("profile", t.profile), named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<4> (t.data)));
}
};
template <>
struct SerialisationTraits<ci::Message::PropertyExchangeCapabilitiesResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("numRequests", t.numSimultaneousRequestsSupported));
if (0x02 <= archive.getVersion())
archive (named ("major", t.majorVersion), named ("minor", t.minorVersion));
}
};
template <>
struct SerialisationTraits<ci::Message::PropertyExchangeCapabilities> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("numRequests", t.numSimultaneousRequestsSupported));
if (0x02 <= archive.getVersion())
archive (named ("major", t.majorVersion), named ("minor", t.minorVersion));
}
};
template <>
struct SerialisationTraits<ci::Message::StaticSizePropertyExchange> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
const uint16_t chunkNum = 1, dataLength = 0;
archive (named ("requestID", t.requestID),
named ("header", midi_ci::detail::MessageMeta::makeJsonWithSizeBytes<2> (t.header)),
named ("numChunks", chunkNum),
named ("thisChunk", chunkNum),
named ("length", dataLength));
}
};
template <>
struct SerialisationTraits<ci::Message::DynamicSizePropertyExchange> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (named ("requestID", t.requestID),
named ("header", midi_ci::detail::MessageMeta::makeJsonWithSizeBytes<2> (t.header)),
named ("numChunks", t.totalNumChunks),
named ("thisChunk", t.thisChunkNum),
named ("data", midi_ci::detail::MessageMeta::makeSpanWithSizeBytes<2> (t.data)));
}
};
template <>
struct SerialisationTraits<ci::Message::PropertyGetDataResponse> : SerialisationTraits<ci::Message::DynamicSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertyGetData> : SerialisationTraits<ci::Message::StaticSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertySetDataResponse> : SerialisationTraits<ci::Message::StaticSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertySetData> : SerialisationTraits<ci::Message::DynamicSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertySubscribeResponse> : SerialisationTraits<ci::Message::DynamicSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertySubscribe> : SerialisationTraits<ci::Message::DynamicSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::PropertyNotify> : SerialisationTraits<ci::Message::DynamicSizePropertyExchange> {};
template <>
struct SerialisationTraits<ci::Message::ProcessInquiryResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
archive (t.supportedFeatures);
}
};
template <>
struct SerialisationTraits<ci::Message::ProcessInquiry> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive&, This&)
{
}
};
template <>
struct SerialisationTraits<ci::Message::ProcessMidiMessageReportResponse> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
std::byte reserved{};
archive (named ("messageDataControl", t.messageDataControl),
named ("requestedMessages", t.requestedMessages),
named ("reserved", reserved),
named ("channelControllerMessages", t.channelControllerMessages),
named ("noteDataMessages", t.noteDataMessages));
}
};
template <>
struct SerialisationTraits<ci::Message::ProcessMidiMessageReport> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive& archive, This& t)
{
std::byte reserved{};
archive (named ("messageDataControl", t.messageDataControl),
named ("requestedMessages", t.requestedMessages),
named ("reserved", reserved),
named ("channelControllerMessages", t.channelControllerMessages),
named ("noteDataMessages", t.noteDataMessages));
}
};
template <>
struct SerialisationTraits<ci::Message::ProcessEndMidiMessageReport> : VersionBase
{
template <typename Archive, typename This>
static auto serialise (Archive&, This&)
{
}
};
} // namespace juce
#endif // ifndef DOXYGEN
@@ -0,0 +1,245 @@
/*
==============================================================================
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::detail::MessageTypeUtils
{
//==============================================================================
/*
An interface used for types that want to operate on parsed MIDI-CI messages.
@tags{Audio}
*/
struct MessageVisitor
{
MessageVisitor() = default;
MessageVisitor (const MessageVisitor&) = default;
MessageVisitor (MessageVisitor&&) = default;
MessageVisitor& operator= (const MessageVisitor&) = default;
MessageVisitor& operator= (MessageVisitor&&) = default;
virtual ~MessageVisitor() = default;
virtual void visit (const std::monostate&) const {}
virtual void visit (const Message::Discovery&) const {}
virtual void visit (const Message::EndpointInquiry&) const {}
virtual void visit (const Message::ProfileInquiry&) const {}
virtual void visit (const Message::ProfileDetails&) const {}
virtual void visit (const Message::PropertyExchangeCapabilities&) const {}
virtual void visit (const Message::PropertyGetData&) const {}
virtual void visit (const Message::PropertySetData&) const {}
virtual void visit (const Message::PropertySubscribe&) const {}
virtual void visit (const Message::ProcessInquiry&) const {}
virtual void visit (const Message::ProcessMidiMessageReport&) const {}
virtual void visit (const Message::DiscoveryResponse&) const {}
virtual void visit (const Message::EndpointInquiryResponse&) const {}
virtual void visit (const Message::InvalidateMUID&) const {}
virtual void visit (const Message::ACK&) const {}
virtual void visit (const Message::NAK&) const {}
virtual void visit (const Message::ProfileInquiryResponse&) const {}
virtual void visit (const Message::ProfileAdded&) const {}
virtual void visit (const Message::ProfileRemoved&) const {}
virtual void visit (const Message::ProfileDetailsResponse&) const {}
virtual void visit (const Message::ProfileOn&) const {}
virtual void visit (const Message::ProfileOff&) const {}
virtual void visit (const Message::ProfileEnabledReport&) const {}
virtual void visit (const Message::ProfileDisabledReport&) const {}
virtual void visit (const Message::ProfileSpecificData&) const {}
virtual void visit (const Message::PropertyExchangeCapabilitiesResponse&) const {}
virtual void visit (const Message::PropertyGetDataResponse&) const {}
virtual void visit (const Message::PropertySetDataResponse&) const {}
virtual void visit (const Message::PropertySubscribeResponse&) const {}
virtual void visit (const Message::PropertyNotify&) const {}
virtual void visit (const Message::ProcessInquiryResponse&) const {}
virtual void visit (const Message::ProcessMidiMessageReportResponse&) const {}
virtual void visit (const Message::ProcessEndMidiMessageReport&) const {}
};
using ParseFn = Message::Parsed::Body (*) (Message::Generic, Parser::Status* status);
using VisitFn = void (*) (const Message::Parsed&, const MessageVisitor&);
/* These return the Universal System Exclusive Sub-ID#2 for a particular message type. */
template <typename Specific>
static constexpr auto getParserFor (std::in_place_type_t<Specific>)
{
return [] (Message::Generic message, Parser::Status* status) -> Message::Parsed::Body
{
// Parse messages using the version specified in the header of the message
if (Specific parsed; Marshalling::Reader { message.data, static_cast<uint8_t> (message.header.version) } (parsed))
{
return parsed;
}
if (status != nullptr)
*status = Parser::Status::malformed;
return std::monostate{};
};
}
template <typename Specific>
static constexpr auto getVisitorFor (std::in_place_type_t<Specific>)
{
return [] (const Message::Parsed& parsed, const MessageVisitor& visitor)
{
if (auto* body = std::get_if<Specific> (&parsed.body))
visitor.visit (*body);
};
}
template <typename... Ts>
struct LookupTables
{
constexpr LookupTables()
{
for (auto& x : parsers)
{
x = [] (Message::Generic, Parser::Status* status) -> Message::Parsed::Body
{
if (status != nullptr)
*status = Parser::Status::unrecognisedMessage;
return std::monostate{};
};
}
for (auto& x : visitors)
{
x = [] (const Message::Parsed&, const MessageVisitor& visitor)
{
visitor.visit (std::monostate{});
};
}
(registerTag (std::in_place_type<Ts>), ...);
}
template <typename T>
constexpr void registerTag (std::in_place_type_t<T> tag)
{
constexpr auto category = MessageMeta::Meta<T>::subID2;
parsers[uint8_t (category)] = getParserFor (tag);
visitors[uint8_t (category)] = getVisitorFor (tag);
}
ParseFn parsers[std::numeric_limits<uint8_t>::max()]{};
VisitFn visitors[std::numeric_limits<uint8_t>::max()]{};
};
template <typename Body>
static void send (BufferOutput& output, uint8_t group, const Message::Header& header, const Body& body)
{
output.getOutputBuffer().clear();
Marshalling::Writer { output.getOutputBuffer() } (header, body);
output.send (group);
}
template <typename Body>
static void send (BufferOutput& output, uint8_t group, MUID targetMuid, ChannelInGroup cig, const Body& body)
{
Message::Header header
{
cig,
MessageMeta::Meta<Body>::subID2,
MessageMeta::implementationVersion,
output.getMuid(),
targetMuid,
};
send (output, group, header, body);
}
template <typename Body>
static void send (ResponderOutput& output, const Body& body)
{
send (output, output.getIncomingGroup(), output.getReplyHeader (MessageMeta::Meta<Body>::subID2), body);
}
static void sendNAK (ResponderOutput& output, std::byte statusCode)
{
const auto header = output.getReplyHeader (MessageMeta::Meta<Message::NAK>::subID2);
const Message::NAK body { output.getIncomingHeader().category,
statusCode,
std::byte { 0x00 },
{}, // No additional details
{} }; // No message text
send (output, output.getIncomingGroup(), header, body);
}
class BaseCaseDelegate : public ResponderDelegate
{
public:
bool tryRespond (ResponderOutput& output, const Message::Parsed&) override
{
sendNAK (output, {});
return true;
}
};
static constexpr auto getTables()
{
return LookupTables<Message::Discovery,
Message::DiscoveryResponse,
Message::InvalidateMUID,
Message::EndpointInquiry,
Message::EndpointInquiryResponse,
Message::ACK,
Message::NAK,
Message::ProfileInquiry,
Message::ProfileInquiryResponse,
Message::ProfileAdded,
Message::ProfileRemoved,
Message::ProfileDetails,
Message::ProfileDetailsResponse,
Message::ProfileOn,
Message::ProfileOff,
Message::ProfileEnabledReport,
Message::ProfileDisabledReport,
Message::ProfileSpecificData,
Message::PropertyExchangeCapabilities,
Message::PropertyExchangeCapabilitiesResponse,
Message::PropertyGetData,
Message::PropertyGetDataResponse,
Message::PropertySetData,
Message::PropertySetDataResponse,
Message::PropertySubscribe,
Message::PropertySubscribeResponse,
Message::PropertyNotify,
Message::ProcessInquiry,
Message::ProcessInquiryResponse,
Message::ProcessMidiMessageReport,
Message::ProcessMidiMessageReportResponse,
Message::ProcessEndMidiMessageReport>{};
}
static void visit (const Message::Parsed& msg, const MessageVisitor& visitor)
{
constexpr auto tables = getTables();
const auto fn = tables.visitors[(uint8_t) msg.header.category];
fn (msg, visitor);
}
} // namespace juce::midi_ci::detail::MessageTypeUtils
@@ -0,0 +1,153 @@
/*
==============================================================================
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::detail
{
PropertyDataMessageChunker::PropertyDataMessageChunker (std::vector<std::byte>& storageIn,
int chunkSizeIn,
const std::byte messageKindIn,
const std::byte requestIdIn,
Span<const std::byte> headerIn,
MUID sourceIn,
MUID destIn,
InputStream& bodyIn)
: header (headerIn),
storage (&storageIn),
body (&bodyIn),
source (sourceIn),
dest (destIn),
chunkSize (chunkSizeIn),
messageKind (messageKindIn),
requestId (requestIdIn)
{
if (hasRoomForBody())
{
populateStorage();
}
else
{
// Header too large! There's no way to fit this message into the requested chunk size.
jassertfalse;
*this = PropertyDataMessageChunker();
}
}
PropertyDataMessageChunker& PropertyDataMessageChunker::operator++() noexcept
{
if (*this != PropertyDataMessageChunker())
{
if (body->isExhausted())
{
*this = PropertyDataMessageChunker();
}
else
{
++thisChunk;
populateStorage();
}
}
return *this;
}
Span<const std::byte> PropertyDataMessageChunker::operator*() const noexcept
{
// The end of the stream was reached, no point dereferencing the iterator now!
jassert (storage != nullptr && (int) storage->size() <= chunkSize);
return *storage;
}
Span<const std::byte> PropertyDataMessageChunker::getHeaderForBlock() const
{
return thisChunk == 1 ? header : Span<const std::byte>{};
}
int PropertyDataMessageChunker::getRoomForBody() const
{
return chunkSize - (int) (getHeaderForBlock().size() + 22);
}
bool PropertyDataMessageChunker::hasRoomForBody() const
{
const auto bodyRoom = getRoomForBody();
return (0 < bodyRoom)
|| (0 == bodyRoom && body->getNumBytesRemaining() == 0);
}
void PropertyDataMessageChunker::populateStorage() const
{
storage->clear();
storage->resize ((size_t) getRoomForBody());
// Read body data into buffer
const auto numBytesRead = (uint16_t) jmax (ssize_t (0), body->read (storage->data(), storage->size()));
const auto [numChunks, thisChunkNum] = [&]() -> std::tuple<uint16_t, uint16_t>
{
if (body->isExhausted() || body->getNumBytesRemaining() == 0)
return std::tuple (thisChunk, thisChunk);
const auto totalLength = body->getTotalLength();
if (totalLength < 0)
return std::tuple ((uint16_t) 0, thisChunk); // 0 means "unknown number"
const auto roomForBody = getRoomForBody();
if (roomForBody != 0)
return std::tuple ((uint16_t) ((totalLength + roomForBody - 1) / roomForBody), thisChunk);
// During construction, the input stream reported that it had no data remaining, so no
// space was reserved for body content.
// Now, the input stream reports that it has data remaining, but there's nowhere
// to fit it in the message!
jassertfalse;
return std::tuple (thisChunk, (uint16_t) 0); // 0 means "data potentially unusable"
}();
// Now we know how many bytes we managed to read, write the header at the end of the buffer
const auto headerForBlock = getHeaderForBlock();
detail::Marshalling::Writer writer { *storage };
writer (Message::Header { ChannelInGroup::wholeBlock,
messageKind,
detail::MessageMeta::implementationVersion,
source,
dest },
requestId,
detail::MessageMeta::makeSpanWithSizeBytes<2> (headerForBlock),
numChunks,
thisChunkNum,
numBytesRead);
// Finally, swap the header to the beginning of the buffer
std::rotate (storage->begin(), storage->begin() + getRoomForBody(), storage->end());
// ...and bring the storage buffer down to size, if we didn't manage to fill it
const auto room = (size_t) getRoomForBody();
storage->resize (storage->size() + numBytesRead - room);
}
} // namespace juce::midi_ci::detail
@@ -0,0 +1,97 @@
/*
==============================================================================
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::detail
{
/*
Breaks up a large property exchange message into chunks of the requested size.
Note that the header *must* fit inside the first block, so you must ensure
that the header is small enough to fit inside the requested chunk size.
*/
class PropertyDataMessageChunker
{
auto tie() const { return std::tie (storage, body, source, dest, chunkSize, messageKind, requestId); }
public:
/* Constructs a chunker instance.
@param storageIn backing storage where each chunk will be written
@param chunkSizeIn the maximum size of each chunk
@param messageKindIn the subID2 byte identifying the type of message in each chunk
@param requestIdIn the id that should be included in all messages that are part of the same property exchange transaction
@param headerIn the header bytes of the message. This is always JSON encoded as 7-bit ASCII text, see the MIDI-CI spec for full details
@param sourceIn the MUID of the device sending the chunked messages
@param destIn the MUID of the recipient of the chunked messages
@param bodyIn a stream that can supply the data payload for this chunk sequence. All payload bytes *must* be 7-bit (MSB not set).
*/
PropertyDataMessageChunker (std::vector<std::byte>& storageIn,
int chunkSizeIn,
const std::byte messageKindIn,
const std::byte requestIdIn,
Span<const std::byte> headerIn,
MUID sourceIn,
MUID destIn,
InputStream& bodyIn);
/* Returns true if this chunker hasn't finished producing chunks. */
explicit operator bool() const { return *this != PropertyDataMessageChunker(); }
/* Allowing foreach usage. */
auto begin() const { return *this; }
/* Allow foreach usage. */
auto end() const { return PropertyDataMessageChunker{}; }
/* Writes the bytes of the next chunk, if any, into the storage buffer. */
PropertyDataMessageChunker& operator++() noexcept;
/* Checks whether the state of this chunker matches the state of another chunker, enabling foreach usage. */
bool operator== (const PropertyDataMessageChunker& other) const noexcept { return tie() == other.tie(); }
bool operator!= (const PropertyDataMessageChunker& other) const noexcept { return tie() != other.tie(); }
/* Returns a span over the valid bytes in the output buffer. */
Span<const std::byte> operator*() const noexcept;
private:
PropertyDataMessageChunker() = default;
Span<const std::byte> getHeaderForBlock() const;
int getRoomForBody() const;
bool hasRoomForBody() const;
void populateStorage() const;
Span<const std::byte> header;
std::vector<std::byte>* storage{};
InputStream* body{};
MUID source = MUID::makeUnchecked (0), dest = MUID::makeUnchecked (0);
int chunkSize{};
uint16_t thisChunk { 0x01 };
std::byte messageKind{};
std::byte requestId{};
};
} // namespace juce::midi_ci::detail
@@ -0,0 +1,55 @@
/*
==============================================================================
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::detail
{
struct PropertyHostUtils
{
PropertyHostUtils() = delete;
static void send (BufferOutput& output,
uint8_t group,
std::byte subID2,
MUID targetMuid,
std::byte requestID,
Span<const std::byte> header,
Span<const std::byte> body,
int chunkSize)
{
MemoryInputStream stream (body.data(), body.size(), false);
const detail::PropertyDataMessageChunker chunker { output.getOutputBuffer(),
std::min (chunkSize, 1 << 16),
subID2,
requestID,
header,
output.getMuid(),
targetMuid,
stream };
std::for_each (chunker.begin(), chunker.end(), [&] (auto) { output.send (group); });
}
};
} // namespace juce::midi_ci::detail
@@ -0,0 +1,507 @@
/*
==============================================================================
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::detail
{
Parser::Status Responder::processCompleteMessage (BufferOutput& output,
ump::BytesOnGroup message,
Span<ResponderDelegate* const> listeners)
{
auto status = Parser::Status::noError;
const auto parsed = Parser::parse (output.getMuid(), message.bytes, &status);
if (! parsed.has_value())
return Parser::Status::malformed;
class Output : public ResponderOutput
{
public:
Output (BufferOutput& o, Message::Header h, uint8_t g)
: innerOutput (o), header (h), group (g) {}
MUID getMuid() const override { return innerOutput.getMuid(); }
Message::Header getIncomingHeader() const override { return header; }
uint8_t getIncomingGroup() const override { return group; }
std::vector<std::byte>& getOutputBuffer() override { return innerOutput.getOutputBuffer(); }
void send (uint8_t g) override { innerOutput.send (g); }
private:
BufferOutput& innerOutput;
Message::Header header;
uint8_t group{};
};
Output responderOutput { output, parsed->header, message.group };
if (status != Parser::Status::noError)
{
switch (status)
{
case Parser::Status::collidingMUID:
{
const Message::Header header { ChannelInGroup::wholeBlock,
MessageMeta::Meta<Message::InvalidateMUID>::subID2,
MessageMeta::implementationVersion,
output.getMuid(),
MUID::getBroadcast() };
const Message::InvalidateMUID body { output.getMuid() };
MessageTypeUtils::send (responderOutput, responderOutput.getIncomingGroup(), header, body);
break;
}
case Parser::Status::unrecognisedMessage:
MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x01 });
break;
case Parser::Status::reservedVersion:
MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x02 });
break;
case Parser::Status::malformed:
MessageTypeUtils::sendNAK (responderOutput, std::byte { 0x41 });
break;
case Parser::Status::mismatchedMUID:
case Parser::Status::noError:
break;
}
return status;
}
for (auto* listener : listeners)
if (listener != nullptr && listener->tryRespond (responderOutput, *parsed))
return Parser::Status::noError;
MessageTypeUtils::BaseCaseDelegate base;
if (base.tryRespond (responderOutput, *parsed))
return Parser::Status::noError;
return Parser::Status::unrecognisedMessage;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class ResponderTests : public UnitTest
{
public:
ResponderTests() : UnitTest ("Responder", UnitTestCategories::midi) {}
void runTest() override
{
auto random = getRandom();
std::vector<std::byte> outgoing;
const auto makeOutput = [&]
{
struct Output : public BufferOutput
{
Output (Random& r, std::vector<std::byte>& b)
: muid (MUID::makeRandom (r)), buf (b) {}
MUID getMuid() const override { return muid; }
std::vector<std::byte>& getOutputBuffer() override { return buf; }
void send (uint8_t) override { sent.push_back (buf); }
MUID muid;
std::vector<std::byte>& buf;
std::vector<std::vector<std::byte>> sent;
};
return Output { random, outgoing };
};
beginTest ("An endpoint message with a matching MUID provokes an endpoint response");
{
constexpr auto version = MessageMeta::implementationVersion;
auto output = makeOutput();
const auto initialMUID = output.getMuid();
const auto bytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* endpoint message */ 0x72,
/* version */ version,
/* source MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* status, product instance ID */ 0x00);
const Message::Parsed expectedInput { Message::Header { ChannelInGroup::wholeBlock,
std::byte { 0x72 },
version,
MUID::makeUnchecked (0x80c101),
initialMUID },
Message::EndpointInquiry { std::byte { 0x00 } } };
EndpointResponderListener listener;
processCompleteMessage (output, { 0, bytes }, listener);
expect (listener == SilentResponderListener (expectedInput));
const auto expectedOutputBytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* endpoint reply */ 0x73,
/* version */ version,
/* source MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* destination MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* status */ 0x00,
/* 16-bit length of following data */ 0x04,
/* ... */ 0x00,
/* info */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00);
expect (rangesEqual (output.sent.front(), expectedOutputBytes));
}
beginTest ("An endpoint message directed at a different MUID does not provoke a response");
{
const auto destMUID = MUID::makeRandom (random);
constexpr auto version = MessageMeta::implementationVersion;
const auto bytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* endpoint message */ 0x72,
/* version */ version,
/* source MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* destination MUID */ (destMUID.get() >> 0x00) & 0x7f,
/* ... */ (destMUID.get() >> 0x07) & 0x7f,
/* ... */ (destMUID.get() >> 0x0e) & 0x7f,
/* ... */ (destMUID.get() >> 0x15) & 0x7f,
/* status, product instance ID */ 0x00);
auto output = makeOutput();
EndpointResponderListener listener;
processCompleteMessage (output, { 0, bytes }, listener);
expect (listener == SilentResponderListener());
expect (output.sent.empty());
}
beginTest ("If the listener fails to compose an endpoint response, a NAK is emitted");
{
auto output = makeOutput();
const auto initialMUID = output.getMuid();
SilentResponderListener listener;
constexpr auto version = MessageMeta::implementationVersion;
const auto bytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* endpoint message */ 0x72,
/* version */ version,
/* source MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* status, product instance ID */ 0x00);
processCompleteMessage (output, { 0, bytes }, listener);
const auto expectedOutputBytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* nak */ 0x7f,
/* version */ version,
/* source MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* destination MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* original transaction sub-id #2 */ 0x72,
/* nak status code */ 0x00,
/* nak status data */ 0x00,
/* details */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* message text length */ 0x00,
/* ... */ 0x00);
expect (rangesEqual (output.sent.front(), expectedOutputBytes));
}
beginTest ("If a message is sent with reserved bits set in the Message Format Version, a NAK is emitted");
{
auto output = makeOutput();
const auto initialMUID = output.getMuid();
const auto bytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* endpoint message */ 0x72,
/* version, reserved bit set */ 0x12,
/* source MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* destination MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* status, product instance ID */ 0x00);
SilentResponderListener listener;
processCompleteMessage (output, { 0, bytes }, listener);
expect (listener == SilentResponderListener{});
const auto expectedOutputBytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* nak */ 0x7f,
/* version */ MessageMeta::implementationVersion,
/* source MUID */ (initialMUID.get() >> 0x00) & 0x7f,
/* ... */ (initialMUID.get() >> 0x07) & 0x7f,
/* ... */ (initialMUID.get() >> 0x0e) & 0x7f,
/* ... */ (initialMUID.get() >> 0x15) & 0x7f,
/* destination MUID */ 0x01,
/* ... */ 0x02,
/* ... */ 0x03,
/* ... */ 0x04,
/* original transaction sub-id #2 */ 0x72,
/* nak status code */ 0x02,
/* nak status data */ 0x00,
/* details */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* message text length */ 0x00,
/* ... */ 0x00);
expect (rangesEqual (output.sent.front(), expectedOutputBytes));
}
beginTest ("If the message body is malformed, a NAK with a status of 0x41 is emitted");
{
const auto sourceMUID = MUID::makeRandom (random);
Message::Header header;
header.deviceID = ChannelInGroup::wholeBlock;
header.category = std::byte { 0x7e };
header.version = MessageMeta::implementationVersion;
header.source = sourceMUID;
header.destination = MUID::getBroadcast();
Message::InvalidateMUID invalidate;
invalidate.target = MUID::makeRandom (random);
std::vector<std::byte> message;
Marshalling::Writer { message } (header, invalidate);
// Remove a byte from the end of the message
message.pop_back();
auto output = makeOutput();
const auto ourMUID = output.getMuid();
SilentResponderListener listener;
processCompleteMessage (output, { 0, message }, listener);
const auto expectedOutputBytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* nak */ 0x7f,
/* version */ MessageMeta::implementationVersion,
/* source MUID */ (ourMUID.get() >> 0x00) & 0x7f,
/* ... */ (ourMUID.get() >> 0x07) & 0x7f,
/* ... */ (ourMUID.get() >> 0x0e) & 0x7f,
/* ... */ (ourMUID.get() >> 0x15) & 0x7f,
/* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x07) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x0e) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x15) & 0x7f,
/* original transaction sub-id #2 */ 0x7e,
/* nak status code */ 0x41,
/* nak status data */ 0x00,
/* details */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* message text length */ 0x00,
/* ... */ 0x00);
expect (rangesEqual (output.sent.front(), expectedOutputBytes));
}
beginTest ("If an unrecognised message is received, a NAK with a status of 0x01 is emitted");
{
const auto sourceMUID = MUID::makeRandom (random);
Message::Header header;
header.deviceID = ChannelInGroup::wholeBlock;
header.category = std::byte { 0x50 }; // reserved
header.version = MessageMeta::implementationVersion;
header.source = sourceMUID;
header.destination = MUID::getBroadcast();
std::vector<std::byte> message;
Marshalling::Writer { message } (header);
message.emplace_back();
auto output = makeOutput();
const auto ourMUID = output.getMuid();
SilentResponderListener listener;
processCompleteMessage (output, { 0, message }, listener);
const auto expectedOutputBytes = makeByteArray (0x7e,
/* to function block */ 0x7f,
/* midi CI */ 0x0d,
/* nak */ 0x7f,
/* version */ MessageMeta::implementationVersion,
/* source MUID */ (ourMUID.get() >> 0x00) & 0x7f,
/* ... */ (ourMUID.get() >> 0x07) & 0x7f,
/* ... */ (ourMUID.get() >> 0x0e) & 0x7f,
/* ... */ (ourMUID.get() >> 0x15) & 0x7f,
/* destination MUID */ (sourceMUID.get() >> 0x00) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x07) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x0e) & 0x7f,
/* ... */ (sourceMUID.get() >> 0x15) & 0x7f,
/* original transaction sub-id #2 */ 0x50,
/* nak status code */ 0x01,
/* nak status data */ 0x00,
/* details */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* ... */ 0x00,
/* message text length */ 0x00,
/* ... */ 0x00);
expect (rangesEqual (output.sent.front(), expectedOutputBytes));
}
}
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)... };
}
struct SilentResponderListener : public ResponderDelegate
{
SilentResponderListener() = default;
explicit SilentResponderListener (const Message::Parsed& p) : parsed (p) {}
bool tryRespond (ResponderOutput&, const Message::Parsed& p) override
{
parsed = p;
return false;
}
// Returning false indicates that the message was not handled
bool operator== (const SilentResponderListener& other) const { return parsed == other.parsed; }
bool operator!= (const SilentResponderListener& other) const { return ! operator== (other); }
std::optional<Message::Parsed> parsed;
};
struct EndpointResponderListener : public SilentResponderListener
{
bool tryRespond (ResponderOutput& output, const Message::Parsed& message) override
{
parsed = message;
if (std::holds_alternative<Message::EndpointInquiry> (message.body))
{
std::array<std::byte, 4> data{};
Message::EndpointInquiryResponse response;
response.status = std::byte{};
response.data = data;
MessageTypeUtils::send (output, output.getIncomingGroup(), output.getReplyHeader (std::byte { 0x73 }), response);
return true;
}
return SilentResponderListener::tryRespond (output, message);
}
using SilentResponderListener::operator==, SilentResponderListener::operator!=;
};
struct OutputCallback
{
void operator() (Span<const std::byte> bytes)
{
output = std::vector<std::byte> (bytes.begin(), bytes.end());
}
std::vector<std::byte> output;
};
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));
}
static Parser::Status processCompleteMessage (BufferOutput& output,
ump::BytesOnGroup message,
ResponderDelegate& listener)
{
ResponderDelegate* const listeners[] { &listener };
return Responder::processCompleteMessage (output, message, listeners);
}
};
static ResponderTests responderTests;
#endif
} // namespace juce::midi_ci::detail
@@ -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::detail
{
/*
Parses individual messages, and additionally gives ResponderDelegates a chance to formulate
a response to any message that would normally necessitate a reply.
*/
struct Responder
{
Responder() = delete;
/* Parses the message, then calls tryParse on each ResponderDelegate in
turn until one returns true, indicating that the message has been
handled. Most 'inquiry' messages should emit one or more reply messages.
These replies will be written to the provided BufferOutput.
If none of the provided delegates are able to handle the message, then
a generic NAK will be written to the BufferOutput.
*/
static Parser::Status processCompleteMessage (BufferOutput& output,
ump::BytesOnGroup message,
Span<ResponderDelegate* const> delegates);
};
} // namespace juce::midi_ci