mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
435 lines
16 KiB
C++
435 lines
16 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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.
|
|
|
|
The code included in this file is provided under the terms of the ISC license
|
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
|
without fee is hereby granted provided that the above copyright notice and
|
|
this permission notice appear in all copies.
|
|
|
|
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
|
|
{
|
|
|
|
bool MidiRPNDetector::parseControllerMessage (int midiChannel,
|
|
int controllerNumber,
|
|
int controllerValue,
|
|
MidiRPNMessage& result) noexcept
|
|
{
|
|
auto parsed = tryParse (midiChannel, controllerNumber, controllerValue);
|
|
|
|
if (! parsed.has_value())
|
|
return false;
|
|
|
|
result = *parsed;
|
|
return true;
|
|
}
|
|
|
|
std::optional<MidiRPNMessage> MidiRPNDetector::tryParse (int midiChannel,
|
|
int controllerNumber,
|
|
int controllerValue)
|
|
{
|
|
jassert (midiChannel > 0 && midiChannel <= 16);
|
|
jassert (controllerNumber >= 0 && controllerNumber < 128);
|
|
jassert (controllerValue >= 0 && controllerValue < 128);
|
|
|
|
return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue);
|
|
}
|
|
|
|
void MidiRPNDetector::reset() noexcept
|
|
{
|
|
for (auto& state : states)
|
|
{
|
|
state.parameterMSB = 0xff;
|
|
state.parameterLSB = 0xff;
|
|
state.resetValue();
|
|
state.isNRPN = false;
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
std::optional<MidiRPNMessage> MidiRPNDetector::ChannelState::handleController (int channel,
|
|
int controllerNumber,
|
|
int value) noexcept
|
|
{
|
|
switch (controllerNumber)
|
|
{
|
|
case 0x62: parameterLSB = uint8 (value); resetValue(); isNRPN = true; break;
|
|
case 0x63: parameterMSB = uint8 (value); resetValue(); isNRPN = true; break;
|
|
|
|
case 0x64: parameterLSB = uint8 (value); resetValue(); isNRPN = false; break;
|
|
case 0x65: parameterMSB = uint8 (value); resetValue(); isNRPN = false; break;
|
|
|
|
case 0x06: valueMSB = uint8 (value); valueLSB = 0xff; return sendIfReady (channel);
|
|
case 0x26: valueLSB = uint8 (value); return sendIfReady (channel);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void MidiRPNDetector::ChannelState::resetValue() noexcept
|
|
{
|
|
valueMSB = 0xff;
|
|
valueLSB = 0xff;
|
|
}
|
|
|
|
//==============================================================================
|
|
std::optional<MidiRPNMessage> MidiRPNDetector::ChannelState::sendIfReady (int channel) noexcept
|
|
{
|
|
if (parameterMSB >= 0x80 || parameterLSB >= 0x80 || valueMSB >= 0x80)
|
|
return {};
|
|
|
|
MidiRPNMessage result{};
|
|
result.channel = channel;
|
|
result.parameterNumber = (parameterMSB << 7) + parameterLSB;
|
|
result.isNRPN = isNRPN;
|
|
|
|
if (valueLSB < 0x80)
|
|
{
|
|
result.value = (valueMSB << 7) + valueLSB;
|
|
result.is14BitValue = true;
|
|
}
|
|
else
|
|
{
|
|
result.value = valueMSB;
|
|
result.is14BitValue = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
//==============================================================================
|
|
MidiBuffer MidiRPNGenerator::generate (MidiRPNMessage message)
|
|
{
|
|
return generate (message.channel,
|
|
message.parameterNumber,
|
|
message.value,
|
|
message.isNRPN,
|
|
message.is14BitValue);
|
|
}
|
|
|
|
MidiBuffer MidiRPNGenerator::generate (int midiChannel,
|
|
int parameterNumber,
|
|
int value,
|
|
bool isNRPN,
|
|
bool use14BitValue)
|
|
{
|
|
jassert (midiChannel > 0 && midiChannel <= 16);
|
|
jassert (parameterNumber >= 0 && parameterNumber < 16384);
|
|
jassert (value >= 0 && value < (use14BitValue ? 16384 : 128));
|
|
|
|
auto parameterLSB = uint8 (parameterNumber & 0x0000007f);
|
|
auto parameterMSB = uint8 (parameterNumber >> 7);
|
|
|
|
uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00;
|
|
uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value);
|
|
|
|
auto channelByte = uint8 (0xb0 + midiChannel - 1);
|
|
|
|
MidiBuffer buffer;
|
|
|
|
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x62 : 0x64, parameterLSB), 0);
|
|
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x63 : 0x65, parameterMSB), 0);
|
|
|
|
buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0);
|
|
|
|
// According to the MIDI spec, whenever a MSB is received, the corresponding LSB will
|
|
// be reset. Therefore, the LSB should be sent after the MSB.
|
|
if (use14BitValue)
|
|
buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0);
|
|
|
|
return buffer;
|
|
}
|
|
|
|
|
|
//==============================================================================
|
|
//==============================================================================
|
|
#if JUCE_UNIT_TESTS
|
|
|
|
class MidiRPNDetectorTests : public UnitTest
|
|
{
|
|
public:
|
|
MidiRPNDetectorTests()
|
|
: UnitTest ("MidiRPNDetector class", UnitTestCategories::midi)
|
|
{}
|
|
|
|
void runTest() override
|
|
{
|
|
// From the MIDI 1.0 spec:
|
|
// If 128 steps of resolution is sufficient the second byte (LSB) of the data value can be
|
|
// omitted. If both the MSB and LSB are sent initially, a subsequent fine adjustment only
|
|
// requires the sending of the LSB. The MSB does not have to be retransmitted. If a
|
|
// subsequent major adjustment is necessary the MSB must be transmitted again. When an MSB
|
|
// is received, the receiver should set its concept of the LSB to zero.
|
|
|
|
beginTest ("Individual MSB is parsed as 7-bit");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (2, 101, 0));
|
|
expect (! detector.tryParse (2, 100, 7));
|
|
|
|
auto parsed = detector.tryParse (2, 6, 42);
|
|
expect (parsed.has_value());
|
|
|
|
expectEquals (parsed->channel, 2);
|
|
expectEquals (parsed->parameterNumber, 7);
|
|
expectEquals (parsed->value, 42);
|
|
expect (! parsed->isNRPN);
|
|
expect (! parsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("LSB without preceding MSB is ignored");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (2, 101, 0));
|
|
expect (! detector.tryParse (2, 100, 7));
|
|
expect (! detector.tryParse (2, 38, 42));
|
|
}
|
|
|
|
beginTest ("LSB following MSB is parsed as 14-bit");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (1, 101, 2));
|
|
expect (! detector.tryParse (1, 100, 44));
|
|
|
|
expect (detector.tryParse (1, 6, 1).has_value());
|
|
|
|
auto lsbParsed = detector.tryParse (1, 38, 94);
|
|
expect (lsbParsed.has_value());
|
|
|
|
expectEquals (lsbParsed->channel, 1);
|
|
expectEquals (lsbParsed->parameterNumber, 300);
|
|
expectEquals (lsbParsed->value, 222);
|
|
expect (! lsbParsed->isNRPN);
|
|
expect (lsbParsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("Multiple LSB following MSB re-use the MSB");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (1, 101, 2));
|
|
expect (! detector.tryParse (1, 100, 43));
|
|
|
|
expect (detector.tryParse (1, 6, 1).has_value());
|
|
|
|
expect (detector.tryParse (1, 38, 94).has_value());
|
|
expect (detector.tryParse (1, 38, 95).has_value());
|
|
expect (detector.tryParse (1, 38, 96).has_value());
|
|
|
|
auto lsbParsed = detector.tryParse (1, 38, 97);
|
|
expect (lsbParsed.has_value());
|
|
|
|
expectEquals (lsbParsed->channel, 1);
|
|
expectEquals (lsbParsed->parameterNumber, 299);
|
|
expectEquals (lsbParsed->value, 225);
|
|
expect (! lsbParsed->isNRPN);
|
|
expect (lsbParsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("Sending a new MSB resets the LSB");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (1, 101, 3));
|
|
expect (! detector.tryParse (1, 100, 43));
|
|
|
|
expect (detector.tryParse (1, 6, 1).has_value());
|
|
expect (detector.tryParse (1, 38, 94).has_value());
|
|
|
|
auto newMsb = detector.tryParse (1, 6, 2);
|
|
expect (newMsb.has_value());
|
|
|
|
expectEquals (newMsb->channel, 1);
|
|
expectEquals (newMsb->parameterNumber, 427);
|
|
expectEquals (newMsb->value, 2);
|
|
expect (! newMsb->isNRPN);
|
|
expect (! newMsb->is14BitValue);
|
|
}
|
|
|
|
beginTest ("RPNs on multiple channels simultaneously");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (1, 100, 44));
|
|
expect (! detector.tryParse (2, 101, 0));
|
|
expect (! detector.tryParse (1, 101, 2));
|
|
expect (! detector.tryParse (2, 100, 7));
|
|
expect (detector.tryParse (1, 6, 1).has_value());
|
|
|
|
auto channelTwo = detector.tryParse (2, 6, 42);
|
|
expect (channelTwo.has_value());
|
|
|
|
expectEquals (channelTwo->channel, 2);
|
|
expectEquals (channelTwo->parameterNumber, 7);
|
|
expectEquals (channelTwo->value, 42);
|
|
expect (! channelTwo->isNRPN);
|
|
expect (! channelTwo->is14BitValue);
|
|
|
|
auto channelOne = detector.tryParse (1, 38, 94);
|
|
expect (channelOne.has_value());
|
|
|
|
expectEquals (channelOne->channel, 1);
|
|
expectEquals (channelOne->parameterNumber, 300);
|
|
expectEquals (channelOne->value, 222);
|
|
expect (! channelOne->isNRPN);
|
|
expect (channelOne->is14BitValue);
|
|
}
|
|
|
|
beginTest ("14-bit RPN with value within 7-bit range");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (16, 100, 0));
|
|
expect (! detector.tryParse (16, 101, 0));
|
|
expect (detector.tryParse (16, 6, 0).has_value());
|
|
|
|
auto parsed = detector.tryParse (16, 38, 3);
|
|
expect (parsed.has_value());
|
|
|
|
expectEquals (parsed->channel, 16);
|
|
expectEquals (parsed->parameterNumber, 0);
|
|
expectEquals (parsed->value, 3);
|
|
expect (! parsed->isNRPN);
|
|
expect (parsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("invalid RPN (wrong order)");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (2, 6, 42));
|
|
expect (! detector.tryParse (2, 101, 0));
|
|
expect (! detector.tryParse (2, 100, 7));
|
|
}
|
|
|
|
beginTest ("14-bit RPN interspersed with unrelated CC messages");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (16, 3, 80));
|
|
expect (! detector.tryParse (16, 100, 0));
|
|
expect (! detector.tryParse (16, 4, 81));
|
|
expect (! detector.tryParse (16, 101, 0));
|
|
expect (! detector.tryParse (16, 5, 82));
|
|
expect (! detector.tryParse (16, 5, 83));
|
|
expect (detector.tryParse (16, 6, 0).has_value());
|
|
expect (! detector.tryParse (16, 4, 84).has_value());
|
|
expect (! detector.tryParse (16, 3, 85).has_value());
|
|
|
|
auto parsed = detector.tryParse (16, 38, 3);
|
|
expect (parsed.has_value());
|
|
|
|
expectEquals (parsed->channel, 16);
|
|
expectEquals (parsed->parameterNumber, 0);
|
|
expectEquals (parsed->value, 3);
|
|
expect (! parsed->isNRPN);
|
|
expect (parsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("14-bit NRPN");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (1, 98, 44));
|
|
expect (! detector.tryParse (1, 99 , 2));
|
|
expect (detector.tryParse (1, 6, 1).has_value());
|
|
|
|
auto parsed = detector.tryParse (1, 38, 94);
|
|
expect (parsed.has_value());
|
|
|
|
expectEquals (parsed->channel, 1);
|
|
expectEquals (parsed->parameterNumber, 300);
|
|
expectEquals (parsed->value, 222);
|
|
expect (parsed->isNRPN);
|
|
expect (parsed->is14BitValue);
|
|
}
|
|
|
|
beginTest ("reset");
|
|
{
|
|
MidiRPNDetector detector;
|
|
expect (! detector.tryParse (2, 101, 0));
|
|
detector.reset();
|
|
expect (! detector.tryParse (2, 100, 7));
|
|
expect (! detector.tryParse (2, 6, 42));
|
|
}
|
|
}
|
|
};
|
|
|
|
static MidiRPNDetectorTests MidiRPNDetectorUnitTests;
|
|
|
|
//==============================================================================
|
|
class MidiRPNGeneratorTests : public UnitTest
|
|
{
|
|
public:
|
|
MidiRPNGeneratorTests()
|
|
: UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi)
|
|
{}
|
|
|
|
void runTest() override
|
|
{
|
|
beginTest ("generating RPN/NRPN");
|
|
{
|
|
{
|
|
MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true);
|
|
expectContainsRPN (buffer, 1, 23, 1337, true, true);
|
|
}
|
|
{
|
|
MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false);
|
|
expectContainsRPN (buffer, 16, 101, 34, false, false);
|
|
}
|
|
{
|
|
MidiRPNMessage message = { 16, 101, 34, false, false };
|
|
MidiBuffer buffer = MidiRPNGenerator::generate (message);
|
|
expectContainsRPN (buffer, message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
//==============================================================================
|
|
void expectContainsRPN (const MidiBuffer& midiBuffer,
|
|
int channel,
|
|
int parameterNumber,
|
|
int value,
|
|
bool isNRPN,
|
|
bool is14BitValue)
|
|
{
|
|
MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue };
|
|
expectContainsRPN (midiBuffer, expected);
|
|
}
|
|
|
|
//==============================================================================
|
|
void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected)
|
|
{
|
|
std::optional<MidiRPNMessage> result;
|
|
MidiRPNDetector detector;
|
|
|
|
for (const auto metadata : midiBuffer)
|
|
{
|
|
const auto midiMessage = metadata.getMessage();
|
|
|
|
result = detector.tryParse (midiMessage.getChannel(),
|
|
midiMessage.getControllerNumber(),
|
|
midiMessage.getControllerValue());
|
|
}
|
|
|
|
expect (result.has_value());
|
|
expectEquals (result->channel, expected.channel);
|
|
expectEquals (result->parameterNumber, expected.parameterNumber);
|
|
expectEquals (result->value, expected.value);
|
|
expect (result->isNRPN == expected.isNRPN);
|
|
expect (result->is14BitValue == expected.is14BitValue);
|
|
}
|
|
};
|
|
|
|
static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests;
|
|
|
|
#endif
|
|
|
|
} // namespace juce
|