1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/modules/juce_midi_ci/detail/juce_CIResponder.cpp
2024-04-16 11:39:35 +01:00

516 lines
24 KiB
C++

/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
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