mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
516 lines
24 KiB
C++
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
|