1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-13 00:04:19 +00:00
JUCE/modules/juce_audio_basics/mpe/juce_MPEZoneLayout.cpp
reuk f4ba4c1ad9
MPEZoneLayout: Correctly handle 14-bit pitch-bend ranges
Previously, the MPEZoneLayout could only handle pitch-bend range
adjustments that ended with the MSB. If the final controller message was
the LSB, this resulted in the range being set as a 14-bit value, with a
value 128 times higher than intended.
2026-01-07 17:30:23 +00:00

473 lines
17 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
{
MPEZoneLayout::MPEZoneLayout (MPEZone lower, MPEZone upper)
: lowerZone (lower), upperZone (upper)
{
}
MPEZoneLayout::MPEZoneLayout (MPEZone zone)
: lowerZone (zone.isLowerZone() ? zone : MPEZone()),
upperZone (! zone.isLowerZone() ? zone : MPEZone())
{
}
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
: lowerZone (other.lowerZone),
upperZone (other.upperZone)
{
}
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other)
{
lowerZone = other.lowerZone;
upperZone = other.upperZone;
sendLayoutChangeMessage();
return *this;
}
void MPEZoneLayout::sendLayoutChangeMessage()
{
listeners.call ([this] (Listener& l) { l.zoneLayoutChanged (*this); });
}
//==============================================================================
void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
checkAndLimitZoneParameters (0, 15, numMemberChannels);
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
if (isLower)
lowerZone = { MPEZone::Type::lower, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
else
upperZone = { MPEZone::Type::upper, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
if (numMemberChannels > 0)
{
auto totalChannels = lowerZone.numMemberChannels + upperZone.numMemberChannels;
if (totalChannels >= 15)
{
if (isLower)
upperZone.numMemberChannels = 14 - numMemberChannels;
else
lowerZone.numMemberChannels = 14 - numMemberChannels;
}
}
sendLayoutChangeMessage();
}
void MPEZoneLayout::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::clearAllZones()
{
lowerZone = { MPEZone::Type::lower, 0 };
upperZone = { MPEZone::Type::upper, 0 };
sendLayoutChangeMessage();
}
//==============================================================================
void MPEZoneLayout::processNextMidiEvent (const MidiMessage& message)
{
if (! message.isController())
return;
if (auto parsed = rpnDetector.tryParse (message.getChannel(),
message.getControllerNumber(),
message.getControllerValue()))
{
processRpnMessage (*parsed);
}
}
void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn)
{
if (rpn.parameterNumber == MPEMessages::zoneLayoutMessagesRpnNumber)
processZoneLayoutRpnMessage (rpn);
else if (rpn.parameterNumber == 0)
processPitchbendRangeRpnMessage (rpn);
}
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
{
if (rpn.value < 16)
{
if (rpn.channel == 1)
setLowerZone (rpn.value);
else if (rpn.channel == 16)
setUpperZone (rpn.value);
}
}
void MPEZoneLayout::updateMasterPitchbend (MPEZone& zone, int value)
{
if (zone.masterPitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange);
zone.masterPitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::updatePerNotePitchbendRange (MPEZone& zone, int value)
{
if (zone.perNotePitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange);
zone.perNotePitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn)
{
// When the range is specified using both MSB and LSB, then MSB corresponds to whole semitones
// and LSB corresponds to cents.
const auto range = rpn.is14BitValue
? std::div (rpn.value, 128)
: div_t { rpn.value, 0 };
// If this is hit, the requested pitchbend range is not a whole number of semitones.
// This isn't currently supported by JUCE - adding support would require
// public API updates.
jassert (range.rem == 0);
if (rpn.channel == 1)
{
updateMasterPitchbend (lowerZone, range.quot);
}
else if (rpn.channel == 16)
{
updateMasterPitchbend (upperZone, range.quot);
}
else
{
if (lowerZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (lowerZone, range.quot);
else if (upperZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (upperZone, range.quot);
}
}
void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
{
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
}
//==============================================================================
void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept
{
listeners.add (listenerToAdd);
}
void MPEZoneLayout::removeListener (Listener* const listenerToRemove) noexcept
{
listeners.remove (listenerToRemove);
}
//==============================================================================
void MPEZoneLayout::checkAndLimitZoneParameters (int minValue, int maxValue,
int& valueToCheckAndLimit) noexcept
{
if (valueToCheckAndLimit < minValue || valueToCheckAndLimit > maxValue)
{
// if you hit this, one of the parameters you supplied for this zone
// was not within the allowed range!
// we fit this back into the allowed range here to maintain a valid
// state for the zone, but probably the resulting zone is not what you
// wanted it to be!
jassertfalse;
valueToCheckAndLimit = jlimit (minValue, maxValue, valueToCheckAndLimit);
}
}
//==============================================================================
bool MPEZone::operator== (const MPEZone& other) const
{
const auto tie = [] (auto& x)
{
return std::tie (x.zoneType,
x.numMemberChannels,
x.perNotePitchbendRange,
x.masterPitchbendRange);
};
return tie (*this) == tie (other);
}
bool MPEZone::operator!= (const MPEZone& other) const
{
return ! operator== (other);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEZoneLayoutTests final : public UnitTest
{
public:
MPEZoneLayoutTests()
: UnitTest ("MPEZoneLayout class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("initialisation");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("adding zones");
{
MPEZoneLayout layout;
layout.setLowerZone (7);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
layout.setUpperZone (7);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setLowerZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setUpperZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 3);
layout.setLowerZone (15);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 15);
}
beginTest ("clear all zones");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
layout.setLowerZone (7);
layout.setUpperZone (2);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
layout.clearAllZones();
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process MIDI buffers");
{
MPEZoneLayout layout;
MidiBuffer buffer;
buffer = MPEMessages::setLowerZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
buffer = MPEMessages::setUpperZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
{
buffer = MPEMessages::setLowerZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 4);
buffer = MPEMessages::setLowerZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 33);
expectEquals (layout.getLowerZone().masterPitchbendRange, 44);
}
{
buffer = MPEMessages::setUpperZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 4);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
buffer = MPEMessages::setUpperZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().perNotePitchbendRange, 33);
expectEquals (layout.getUpperZone().masterPitchbendRange, 44);
}
buffer = MPEMessages::clearAllZones();
layout.processNextMidiBuffer (buffer);
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process individual MIDI messages");
{
MPEZoneLayout layout;
layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2
layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg
layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3
layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 48);
expectEquals (layout.getLowerZone().masterPitchbendRange, 2);
const auto masterPitchBend = 0x0c;
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x00 });
layout.processNextMidiEvent ({ 0xb0, 0x06, masterPitchBend });
expectEquals (layout.getLowerZone().masterPitchbendRange, masterPitchBend);
const auto newPitchBend = 0x0d;
layout.processNextMidiEvent ({ 0xb0, 0x06, newPitchBend });
expectEquals (layout.getLowerZone().masterPitchbendRange, newPitchBend);
}
beginTest ("process 14-bit pitch bend sensitivity");
{
MPEZoneLayout layout;
layout.setLowerZone (15);
expect (layout.getLowerZone().isActive());
constexpr auto masterPitchBendA = 0x60;
// LSB first
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x00 }); // RPN part 1
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // PRN part 2
layout.processNextMidiEvent ({ 0xb0, 0x26, 0x00 }); // pitch bend cents
layout.processNextMidiEvent ({ 0xb0, 0x06, masterPitchBendA }); // pitch bend semis
expectEquals (layout.getLowerZone().masterPitchbendRange, masterPitchBendA);
constexpr auto masterPitchBendB = 0x50;
// MSB first
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x00 }); // RPN part 1
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // PRN part 2
layout.processNextMidiEvent ({ 0xb0, 0x06, masterPitchBendB }); // pitch bend semis
layout.processNextMidiEvent ({ 0xb0, 0x26, 0x00 }); // pitch bend cents
expectEquals (layout.getLowerZone().masterPitchbendRange, masterPitchBendB);
}
}
};
static MPEZoneLayoutTests MPEZoneLayoutUnitTests;
#endif
} // namespace juce