mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-14 00:14:18 +00:00
Updated JUCE's MPE classes to comply with the new MMA-adopted specification
This commit is contained in:
parent
aa01a84ed5
commit
6ca97fc897
17 changed files with 1247 additions and 963 deletions
|
|
@ -4,6 +4,34 @@ JUCE breaking changes
|
|||
Develop
|
||||
=======
|
||||
|
||||
Change
|
||||
------
|
||||
JUCE's MPE classes have been updated to reflect the official specification recently approved
|
||||
by the MIDI Manufacturers Association (MMA).
|
||||
|
||||
Possible Issues
|
||||
---------------
|
||||
The most significant changes have occured in the MPEZoneLayout classes and programs
|
||||
using the higher level MPE classes such as MPEInstrument, MPESynthesiser, MPESynthesiserBase and
|
||||
MPESynthesiserVoice should be unaffected.
|
||||
|
||||
Previously, any MIDI channel from 1 - 15 could be selected to be the master channel of an MPE zone,
|
||||
with a specified number of member channels ascending from the master channel + 1. However, in the
|
||||
new specification this has been simplified so that a device only has a lower and/or an upper zone,
|
||||
where the lower zone has master channel 1 and assigns new member channels ascending from channel 2
|
||||
and the upper zone has master channel 16 and assigns new member channels descending from channel 15.
|
||||
|
||||
Workaround
|
||||
----------
|
||||
Use the MPEZoneLayout::setLowerZone() and MPEZoneLayout::setUpperZone() methods to set zone layouts.
|
||||
|
||||
Any UI that allows users to select and set zones on an MPE instrument should also
|
||||
be updated to reflect the specification changes.
|
||||
|
||||
Rationale
|
||||
---------
|
||||
The MPE classes in JUCE are out of date and should be updated to reflect the new, official MPE standard.
|
||||
|
||||
|
||||
Version 5.2.1
|
||||
=============
|
||||
|
|
@ -30,6 +58,28 @@ app was attempted to be created, while the older instance was still running in b
|
|||
would result in assertions when starting a second instance.
|
||||
|
||||
|
||||
Change
|
||||
------
|
||||
Calling JUCEApplicationBase::quit() on Android will now really quit the app,
|
||||
rather than just placing it in background. Starting with API level 21 (Android 5.0), the
|
||||
app will not appear in recent apps list after calling quit(). Prior to API 21, the app will still
|
||||
appear in recent app lists but when a user chooses the app, a new instance of the app will be started.
|
||||
|
||||
Possible Issues
|
||||
---------------
|
||||
Any code calling JUCEApplicationBase::quit() to place the app in background will close the app instead.
|
||||
|
||||
Workaround
|
||||
----------
|
||||
Use Process::hide().
|
||||
|
||||
Rationale
|
||||
---------
|
||||
The old behaviour JUCEApplicationBase::quit() was confusing JUCE code, as a new instance of JUCE
|
||||
app was attempted to be created, while the older instance was still running in background. This
|
||||
would result in assertions when starting a second instance.
|
||||
|
||||
|
||||
Change
|
||||
------
|
||||
On Windows, release builds will now link to the dynamic C++ runtime by default
|
||||
|
|
|
|||
|
|
@ -67,13 +67,13 @@
|
|||
#include "midi/juce_MidiRPN.cpp"
|
||||
#include "mpe/juce_MPEValue.cpp"
|
||||
#include "mpe/juce_MPENote.cpp"
|
||||
#include "mpe/juce_MPEZone.cpp"
|
||||
#include "mpe/juce_MPEZoneLayout.cpp"
|
||||
#include "mpe/juce_MPEInstrument.cpp"
|
||||
#include "mpe/juce_MPEMessages.cpp"
|
||||
#include "mpe/juce_MPESynthesiserBase.cpp"
|
||||
#include "mpe/juce_MPESynthesiserVoice.cpp"
|
||||
#include "mpe/juce_MPESynthesiser.cpp"
|
||||
#include "mpe/juce_MPEUtils.cpp"
|
||||
#include "sources/juce_BufferingAudioSource.cpp"
|
||||
#include "sources/juce_ChannelRemappingAudioSource.cpp"
|
||||
#include "sources/juce_IIRFilterAudioSource.cpp"
|
||||
|
|
|
|||
|
|
@ -98,13 +98,13 @@
|
|||
#include "midi/juce_MidiRPN.h"
|
||||
#include "mpe/juce_MPEValue.h"
|
||||
#include "mpe/juce_MPENote.h"
|
||||
#include "mpe/juce_MPEZone.h"
|
||||
#include "mpe/juce_MPEZoneLayout.h"
|
||||
#include "mpe/juce_MPEInstrument.h"
|
||||
#include "mpe/juce_MPEMessages.h"
|
||||
#include "mpe/juce_MPESynthesiserBase.h"
|
||||
#include "mpe/juce_MPESynthesiserVoice.h"
|
||||
#include "mpe/juce_MPESynthesiser.h"
|
||||
#include "mpe/juce_MPEUtils.h"
|
||||
#include "sources/juce_AudioSource.h"
|
||||
#include "sources/juce_PositionableAudioSource.h"
|
||||
#include "sources/juce_BufferingAudioSource.h"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ MPEInstrument::MPEInstrument() noexcept
|
|||
{
|
||||
std::fill_n (lastPressureLowerBitReceivedOnChannel, 16, noLSBValueReceived);
|
||||
std::fill_n (lastTimbreLowerBitReceivedOnChannel, 16, noLSBValueReceived);
|
||||
std::fill_n (isNoteChannelSustained, 16, false);
|
||||
std::fill_n (isMemberChannelSustained, 16, false);
|
||||
|
||||
pitchbendDimension.value = &MPENote::pitchbend;
|
||||
pressureDimension.value = &MPENote::pressure;
|
||||
|
|
@ -144,12 +144,12 @@ void MPEInstrument::processNextMidiEvent (const MidiMessage& message)
|
|||
{
|
||||
zoneLayout.processNextMidiEvent (message);
|
||||
|
||||
if (message.isNoteOn (true)) processMidiNoteOnMessage (message);
|
||||
else if (message.isNoteOff (false)) processMidiNoteOffMessage (message);
|
||||
else if (message.isAllNotesOff()) processMidiAllNotesOffMessage (message);
|
||||
else if (message.isPitchWheel()) processMidiPitchWheelMessage (message);
|
||||
else if (message.isChannelPressure()) processMidiChannelPressureMessage (message);
|
||||
else if (message.isController()) processMidiControllerMessage (message);
|
||||
if (message.isNoteOn (true)) processMidiNoteOnMessage (message);
|
||||
else if (message.isNoteOff (false)) processMidiNoteOffMessage (message);
|
||||
else if (message.isResetAllControllers()) processMidiResetAllControllersMessage (message);
|
||||
else if (message.isPitchWheel()) processMidiPitchWheelMessage (message);
|
||||
else if (message.isChannelPressure()) processMidiChannelPressureMessage (message);
|
||||
else if (message.isController()) processMidiControllerMessage (message);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -211,10 +211,10 @@ void MPEInstrument::processMidiControllerMessage (const MidiMessage& message)
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEInstrument::processMidiAllNotesOffMessage (const MidiMessage& message)
|
||||
void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& message)
|
||||
{
|
||||
// in MPE mode, "all notes off" is per-zone and expected on the master channel;
|
||||
// in legacy mode, "all notes off" is per MIDI channel (within the channel range used).
|
||||
// in MPE mode, "reset all controllers" is per-zone and expected on the master channel;
|
||||
// in legacy mode, it is per MIDI channel (within the channel range used).
|
||||
|
||||
if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel()))
|
||||
{
|
||||
|
|
@ -231,13 +231,16 @@ void MPEInstrument::processMidiAllNotesOffMessage (const MidiMessage& message)
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (auto* zone = zoneLayout.getZoneByMasterChannel (message.getChannel()))
|
||||
else if (isMasterChannel (message.getChannel()))
|
||||
{
|
||||
auto zone = (message.getChannel() == 1 ? zoneLayout.getLowerZone()
|
||||
: zoneLayout.getUpperZone());
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
if (zone->isUsingChannelAsNoteChannel (note.midiChannel))
|
||||
if (zone.isUsingChannelAsMemberChannel (note.midiChannel))
|
||||
{
|
||||
note.keyState = MPENote::off;
|
||||
note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number
|
||||
|
|
@ -280,7 +283,7 @@ void MPEInstrument::noteOn (int midiChannel,
|
|||
int midiNoteNumber,
|
||||
MPEValue midiNoteOnVelocity)
|
||||
{
|
||||
if (! isNoteChannel (midiChannel))
|
||||
if (! isMemberChannel (midiChannel))
|
||||
return;
|
||||
|
||||
MPENote newNote (midiChannel,
|
||||
|
|
@ -289,7 +292,7 @@ void MPEInstrument::noteOn (int midiChannel,
|
|||
getInitialValueForNewNote (midiChannel, pitchbendDimension),
|
||||
getInitialValueForNewNote (midiChannel, pressureDimension),
|
||||
getInitialValueForNewNote (midiChannel, timbreDimension),
|
||||
isNoteChannelSustained[midiChannel - 1] ? MPENote::keyDownAndSustained : MPENote::keyDown);
|
||||
isMemberChannelSustained[midiChannel - 1] ? MPENote::keyDownAndSustained : MPENote::keyDown);
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
updateNoteTotalPitchbend (newNote);
|
||||
|
|
@ -312,7 +315,7 @@ void MPEInstrument::noteOff (int midiChannel,
|
|||
int midiNoteNumber,
|
||||
MPEValue midiNoteOffVelocity)
|
||||
{
|
||||
if (notes.isEmpty() || ! isNoteChannel (midiChannel))
|
||||
if (notes.isEmpty() || ! isMemberChannel (midiChannel))
|
||||
return;
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
|
|
@ -375,11 +378,11 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
|
|||
if (notes.isEmpty())
|
||||
return;
|
||||
|
||||
if (auto* zone = zoneLayout.getZoneByMasterChannel (midiChannel))
|
||||
if (isMasterChannel (midiChannel))
|
||||
{
|
||||
updateDimensionMaster (*zone, dimension, value);
|
||||
updateDimensionMaster (midiChannel == 1, dimension, value);
|
||||
}
|
||||
else if (isNoteChannel (midiChannel))
|
||||
else if (isMemberChannel (midiChannel))
|
||||
{
|
||||
if (dimension.trackingMode == allNotesOnChannel)
|
||||
{
|
||||
|
|
@ -400,15 +403,19 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEInstrument::updateDimensionMaster (const MPEZone& zone, MPEDimension& dimension, MPEValue value)
|
||||
void MPEInstrument::updateDimensionMaster (bool isLowerZone, MPEDimension& dimension, MPEValue value)
|
||||
{
|
||||
auto channels = zone.getNoteChannelRange();
|
||||
auto zone = (isLowerZone ? zoneLayout.getLowerZone()
|
||||
: zoneLayout.getUpperZone());
|
||||
|
||||
if (! zone.isActive())
|
||||
return;
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
if (! channels.contains (note.midiChannel))
|
||||
if (! zone.isUsingChannelAsMemberChannel (note.midiChannel))
|
||||
continue;
|
||||
|
||||
if (&dimension == &pitchbendDimension)
|
||||
|
|
@ -457,17 +464,29 @@ void MPEInstrument::updateNoteTotalPitchbend (MPENote& note)
|
|||
}
|
||||
else
|
||||
{
|
||||
if (auto* zone = zoneLayout.getZoneByNoteChannel (note.midiChannel))
|
||||
auto zone = zoneLayout.getLowerZone();
|
||||
|
||||
if (! zone.isUsingChannelAsMemberChannel (note.midiChannel))
|
||||
{
|
||||
auto notePitchbendInSemitones = note.pitchbend.asSignedFloat() * zone->getPerNotePitchbendRange();
|
||||
auto masterPitchbendInSemitones = pitchbendDimension.lastValueReceivedOnChannel[zone->getMasterChannel() - 1].asSignedFloat() * zone->getMasterPitchbendRange();
|
||||
note.totalPitchbendInSemitones = notePitchbendInSemitones + masterPitchbendInSemitones;
|
||||
}
|
||||
else
|
||||
{
|
||||
// oops - this note seems to not belong to any zone!
|
||||
jassertfalse;
|
||||
if (zoneLayout.getUpperZone().isUsingChannelAsMemberChannel (note.midiChannel))
|
||||
{
|
||||
zone = zoneLayout.getUpperZone();
|
||||
}
|
||||
else
|
||||
{
|
||||
// this note doesn't belong to any zone!
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto notePitchbendInSemitones = note.pitchbend.asSignedFloat() * zone.perNotePitchbendRange;
|
||||
|
||||
auto masterPitchbendInSemitones = pitchbendDimension.lastValueReceivedOnChannel[zone.getMasterChannel() - 1]
|
||||
.asSignedFloat()
|
||||
* zone.masterPitchbendRange;
|
||||
|
||||
note.totalPitchbendInSemitones = notePitchbendInSemitones + masterPitchbendInSemitones;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -490,16 +509,17 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
|
|||
// in MPE mode, sustain/sostenuto is per-zone and expected on the master channel;
|
||||
// in legacy mode, sustain/sostenuto is per MIDI channel (within the channel range used).
|
||||
|
||||
auto* affectedZone = zoneLayout.getZoneByMasterChannel (midiChannel);
|
||||
|
||||
if (legacyMode.isEnabled ? (! legacyMode.channelRange.contains (midiChannel)) : (affectedZone == nullptr))
|
||||
if (legacyMode.isEnabled ? (! legacyMode.channelRange.contains (midiChannel)) : (! isMasterChannel (midiChannel)))
|
||||
return;
|
||||
|
||||
auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone()
|
||||
: zoneLayout.getUpperZone());
|
||||
|
||||
for (auto i = notes.size(); --i >= 0;)
|
||||
{
|
||||
auto& note = notes.getReference (i);
|
||||
|
||||
if (legacyMode.isEnabled ? (note.midiChannel == midiChannel) : affectedZone->isUsingChannel (note.midiChannel))
|
||||
if (legacyMode.isEnabled ? (note.midiChannel == midiChannel) : zone.isUsingChannelAsMemberChannel (note.midiChannel))
|
||||
{
|
||||
if (note.keyState == MPENote::keyDown && isDown)
|
||||
note.keyState = MPENote::keyDownAndSustained;
|
||||
|
|
@ -523,20 +543,29 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
|
|||
if (! isSostenuto)
|
||||
{
|
||||
if (legacyMode.isEnabled)
|
||||
isNoteChannelSustained[midiChannel - 1] = isDown;
|
||||
{
|
||||
isMemberChannelSustained[midiChannel - 1] = isDown;
|
||||
}
|
||||
else
|
||||
for (auto i = affectedZone->getFirstNoteChannel(); i <= affectedZone->getLastNoteChannel(); ++i)
|
||||
isNoteChannelSustained[i - 1] = isDown;
|
||||
{
|
||||
if (zone.isLowerZone())
|
||||
for (auto i = zone.getFirstMemberChannel(); i <= zone.getLastMemberChannel(); ++i)
|
||||
isMemberChannelSustained[i - 1] = isDown;
|
||||
else
|
||||
for (auto i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
|
||||
isMemberChannelSustained[i - 1] = isDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEInstrument::isNoteChannel (int midiChannel) const noexcept
|
||||
bool MPEInstrument::isMemberChannel (int midiChannel) noexcept
|
||||
{
|
||||
if (legacyMode.isEnabled)
|
||||
return legacyMode.channelRange.contains (midiChannel);
|
||||
|
||||
return zoneLayout.getZoneByNoteChannel (midiChannel) != nullptr;
|
||||
return zoneLayout.getLowerZone().isUsingChannelAsMemberChannel (midiChannel)
|
||||
|| zoneLayout.getUpperZone().isUsingChannelAsMemberChannel (midiChannel);
|
||||
}
|
||||
|
||||
bool MPEInstrument::isMasterChannel (int midiChannel) const noexcept
|
||||
|
|
@ -544,9 +573,8 @@ bool MPEInstrument::isMasterChannel (int midiChannel) const noexcept
|
|||
if (legacyMode.isEnabled)
|
||||
return false;
|
||||
|
||||
return zoneLayout.getZoneByMasterChannel (midiChannel) != nullptr;
|
||||
return (midiChannel == 1 || midiChannel == 16);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int MPEInstrument::getNumPlayingNotes() const noexcept
|
||||
{
|
||||
|
|
@ -725,13 +753,13 @@ public:
|
|||
MPEInstrumentTests()
|
||||
: UnitTest ("MPEInstrument class", "MIDI/MPE")
|
||||
{
|
||||
// using two MPE zones with the following layout for testing
|
||||
// using lower and upper MPE zones with the following layout for testing
|
||||
//
|
||||
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
||||
// * ...................| * ........................|
|
||||
// * ...................| |........................ *
|
||||
|
||||
testLayout.addZone (MPEZone (2, 5));
|
||||
testLayout.addZone (MPEZone (9, 6));
|
||||
testLayout.setLowerZone (5);
|
||||
testLayout.setUpperZone (6);
|
||||
}
|
||||
|
||||
void runTest() override
|
||||
|
|
@ -739,7 +767,8 @@ public:
|
|||
beginTest ("initial zone layout");
|
||||
{
|
||||
MPEInstrument test;
|
||||
expectEquals (test.getZoneLayout().getNumZones(), 0);
|
||||
expect (! test.getZoneLayout().getLowerZone().isActive());
|
||||
expect (! test.getZoneLayout().getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("get/setZoneLayout");
|
||||
|
|
@ -747,12 +776,14 @@ public:
|
|||
MPEInstrument test;
|
||||
test.setZoneLayout (testLayout);
|
||||
|
||||
MPEZoneLayout newLayout = test.getZoneLayout();
|
||||
expectEquals (newLayout.getNumZones(), 2);
|
||||
expectEquals (newLayout.getZoneByIndex (0)->getMasterChannel(), 2);
|
||||
expectEquals (newLayout.getZoneByIndex (0)->getNumNoteChannels(), 5);
|
||||
expectEquals (newLayout.getZoneByIndex (1)->getMasterChannel(), 9);
|
||||
expectEquals (newLayout.getZoneByIndex (1)->getNumNoteChannels(), 6);
|
||||
auto newLayout = test.getZoneLayout();
|
||||
|
||||
expect (test.getZoneLayout().getLowerZone().isActive());
|
||||
expect (test.getZoneLayout().getUpperZone().isActive());
|
||||
expectEquals (newLayout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (newLayout.getLowerZone().numMemberChannels, 5);
|
||||
expectEquals (newLayout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (newLayout.getUpperZone().numMemberChannels, 6);
|
||||
}
|
||||
|
||||
beginTest ("noteOn / noteOff");
|
||||
|
|
@ -767,16 +798,16 @@ public:
|
|||
test.setZoneLayout (testLayout);
|
||||
|
||||
// note-on on master channel - ignore
|
||||
test.noteOn (9, 60, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
expectEquals (test.noteAddedCallCounter, 0);
|
||||
|
||||
// note-on on any other channel - ignore
|
||||
test.noteOn (1, 60, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
expectEquals (test.noteAddedCallCounter, 0);
|
||||
|
||||
// note-on on note channel - create new note
|
||||
// note-on on any other channel - ignore
|
||||
test.noteOn (7, 60, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
expectEquals (test.noteAddedCallCounter, 0);
|
||||
|
||||
// note-on on member channel - create new note
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 1);
|
||||
expectEquals (test.noteAddedCallCounter, 1);
|
||||
|
|
@ -861,38 +892,37 @@ public:
|
|||
{
|
||||
UnitTestInstrument test;
|
||||
test.setZoneLayout (testLayout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in Zone 1
|
||||
test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in Zone 2
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in lower zone
|
||||
test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in upper zone
|
||||
|
||||
// sustain pedal on per-note channel shouldn't do anything.
|
||||
test.sustainPedal (3, true);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
|
||||
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 0);
|
||||
|
||||
// sustain pedal on non-zone channel shouldn't do anything either.
|
||||
test.sustainPedal (1, true);
|
||||
test.sustainPedal (7, true);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 0);
|
||||
|
||||
// sustain pedal on master channel should sustain notes on *that* zone.
|
||||
test.sustainPedal (2, true);
|
||||
// sustain pedal on master channel should sustain notes on _that_ zone.
|
||||
test.sustainPedal (1, true);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 1);
|
||||
|
||||
// release
|
||||
test.sustainPedal (2, false);
|
||||
test.sustainPedal (1, false);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 2);
|
||||
|
||||
// should also sustain new notes added after the press
|
||||
test.sustainPedal (2, true);
|
||||
test.sustainPedal (1, true);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 3);
|
||||
test.noteOn (4, 51, MPEValue::from7BitInt (100));
|
||||
expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::keyDownAndSustained);
|
||||
|
|
@ -916,7 +946,7 @@ public:
|
|||
expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::sustained);
|
||||
|
||||
// notes should be turned off when pedal is released
|
||||
test.sustainPedal (2, false);
|
||||
test.sustainPedal (1, false);
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
expectEquals (test.noteReleasedCallCounter, 4);
|
||||
}
|
||||
|
|
@ -925,8 +955,8 @@ public:
|
|||
{
|
||||
UnitTestInstrument test;
|
||||
test.setZoneLayout (testLayout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in Zone 1
|
||||
test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in Zone 2
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in lower zone
|
||||
test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in upper zone
|
||||
|
||||
// sostenuto pedal on per-note channel shouldn't do anything.
|
||||
test.sostenutoPedal (3, true);
|
||||
|
|
@ -935,25 +965,25 @@ public:
|
|||
expectEquals (test.noteKeyStateChangedCallCounter, 0);
|
||||
|
||||
// sostenuto pedal on non-zone channel shouldn't do anything either.
|
||||
test.sostenutoPedal (1, true);
|
||||
test.sostenutoPedal (9, true);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 0);
|
||||
|
||||
// sostenuto pedal on master channel should sustain notes on *that* zone.
|
||||
test.sostenutoPedal (2, true);
|
||||
test.sostenutoPedal (1, true);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 1);
|
||||
|
||||
// release
|
||||
test.sostenutoPedal (2, false);
|
||||
test.sostenutoPedal (1, false);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 2);
|
||||
|
||||
// should only sustain notes turned on *before* the press (difference to sustain pedal)
|
||||
test.sostenutoPedal (2, true);
|
||||
test.sostenutoPedal (1, true);
|
||||
expectEquals (test.noteKeyStateChangedCallCounter, 3);
|
||||
test.noteOn (4, 51, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 3);
|
||||
|
|
@ -973,7 +1003,7 @@ public:
|
|||
expectEquals (test.noteKeyStateChangedCallCounter, 4);
|
||||
|
||||
// notes should be turned off when pedal is released
|
||||
test.sustainPedal (2, false);
|
||||
test.sustainPedal (1, false);
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
expectEquals (test.noteReleasedCallCounter, 3);
|
||||
}
|
||||
|
|
@ -987,31 +1017,31 @@ public:
|
|||
test.noteOn (3, 61, MPEValue::from7BitInt (100));
|
||||
|
||||
{
|
||||
MPENote note = test.getMostRecentNote (2);
|
||||
auto note = test.getMostRecentNote (2);
|
||||
expect (! note.isValid());
|
||||
}
|
||||
{
|
||||
MPENote note = test.getMostRecentNote (3);
|
||||
auto note = test.getMostRecentNote (3);
|
||||
expect (note.isValid());
|
||||
expectEquals (int (note.midiChannel), 3);
|
||||
expectEquals (int (note.initialNote), 61);
|
||||
}
|
||||
|
||||
test.sustainPedal (2, true);
|
||||
test.sustainPedal (1, true);
|
||||
test.noteOff (3, 61, MPEValue::from7BitInt (100));
|
||||
|
||||
{
|
||||
MPENote note = test.getMostRecentNote (3);
|
||||
auto note = test.getMostRecentNote (3);
|
||||
expect (note.isValid());
|
||||
expectEquals (int (note.midiChannel), 3);
|
||||
expectEquals (int (note.initialNote), 60);
|
||||
}
|
||||
|
||||
test.sustainPedal (2, false);
|
||||
test.sustainPedal (1, false);
|
||||
test.noteOff (3, 60, MPEValue::from7BitInt (100));
|
||||
|
||||
{
|
||||
MPENote note = test.getMostRecentNote (3);
|
||||
auto note = test.getMostRecentNote (3);
|
||||
expect (! note.isValid());
|
||||
}
|
||||
}
|
||||
|
|
@ -1074,14 +1104,14 @@ public:
|
|||
expectEquals (test.notePressureChangedCallCounter, 1);
|
||||
|
||||
// applying pressure on a master channel should modulate all notes in this zone
|
||||
test.pressure (2, MPEValue::from7BitInt (44));
|
||||
test.pressure (1, MPEValue::from7BitInt (44));
|
||||
expectNote (test.getNote (3, 60), 100, 44, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 44, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.notePressureChangedCallCounter, 3);
|
||||
|
||||
// applying pressure on an unrelated channel should be ignored
|
||||
test.pressure (1, MPEValue::from7BitInt (55));
|
||||
test.pressure (8, MPEValue::from7BitInt (55));
|
||||
expectNote (test.getNote (3, 60), 100, 44, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 44, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
|
|
@ -1179,14 +1209,14 @@ public:
|
|||
// value of per-note pitchbend. Tests covering master pitchbend below.
|
||||
// Note: noteChanged will be called anyway for notes in that zone
|
||||
// because the total pitchbend for those notes has changed
|
||||
test.pitchbend (2, MPEValue::from14BitInt (2222));
|
||||
test.pitchbend (1, MPEValue::from14BitInt (2222));
|
||||
expectNote (test.getNote (3, 60), 100, 0, 1111, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.notePitchbendChangedCallCounter, 3);
|
||||
|
||||
// applying pitchbend on an unrelated channel should do nothing.
|
||||
test.pitchbend (1, MPEValue::from14BitInt (3333));
|
||||
test.pitchbend (8, MPEValue::from14BitInt (3333));
|
||||
expectNote (test.getNote (3, 60), 100, 0, 1111, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
|
|
@ -1231,7 +1261,7 @@ public:
|
|||
// - the first note should not be bent, only the second one.
|
||||
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.sustainPedal (2, true);
|
||||
test.sustainPedal (1, true);
|
||||
test.noteOff (3, 60, MPEValue::from7BitInt (64));
|
||||
expectEquals (test.getNumPlayingNotes(), 1);
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained);
|
||||
|
|
@ -1275,19 +1305,19 @@ public:
|
|||
test.pitchbend (3, MPEValue::from14BitInt (4096));
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -24.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (96);
|
||||
layout.setLowerZone (5, 96);
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (3, MPEValue::from14BitInt (0)); // -max
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (1);
|
||||
layout.setLowerZone (5, 1);
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (3, MPEValue::from14BitInt (16383)); // +max
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (0); // pitchbendrange = 0 --> no pitchbend at all
|
||||
layout.setLowerZone (5, 0); // pitchbendrange = 0 --> no pitchbend at all
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (3, MPEValue::from14BitInt (12345));
|
||||
|
|
@ -1301,25 +1331,25 @@ public:
|
|||
MPEZoneLayout layout = testLayout;
|
||||
test.setZoneLayout (layout); // default should be +/- 2 semitones
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (2, MPEValue::from14BitInt (4096)); //halfway between -max and centre
|
||||
test.pitchbend (1, MPEValue::from14BitInt (4096)); //halfway between -max and centre
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -1.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (96);
|
||||
layout.setLowerZone (5, 48, 96);
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (2, MPEValue::from14BitInt (0)); // -max
|
||||
test.pitchbend (1, MPEValue::from14BitInt (0)); // -max
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (1);
|
||||
layout.setLowerZone (5, 48, 1);
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (2, MPEValue::from14BitInt (16383)); // +max
|
||||
test.pitchbend (1, MPEValue::from14BitInt (16383)); // +max
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01);
|
||||
|
||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (0); // pitchbendrange = 0 --> no pitchbend at all
|
||||
layout.setLowerZone (5, 48, 0); // pitchbendrange = 0 --> no pitchbend at all
|
||||
test.setZoneLayout (layout);
|
||||
test.noteOn (3, 60, MPEValue::from7BitInt (100));
|
||||
test.pitchbend (2, MPEValue::from14BitInt (12345));
|
||||
test.pitchbend (1, MPEValue::from14BitInt (12345));
|
||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01);
|
||||
}
|
||||
{
|
||||
|
|
@ -1329,11 +1359,10 @@ public:
|
|||
UnitTestInstrument test;
|
||||
|
||||
MPEZoneLayout layout = testLayout;
|
||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (12);
|
||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (1);
|
||||
layout.setLowerZone (5, 12, 1);
|
||||
test.setZoneLayout (layout);
|
||||
|
||||
test.pitchbend (2, MPEValue::from14BitInt (4096)); // master pitchbend 0.5 semitones down
|
||||
test.pitchbend (1, MPEValue::from14BitInt (4096)); // master pitchbend 0.5 semitones down
|
||||
test.pitchbend (3, MPEValue::from14BitInt (0)); // per-note pitchbend 12 semitones down
|
||||
// additionally, note should react to both pitchbend messages
|
||||
// correctly even if they arrived before the note-on.
|
||||
|
|
@ -1360,14 +1389,14 @@ public:
|
|||
expectEquals (test.noteTimbreChangedCallCounter, 1);
|
||||
|
||||
// modulating timbre on a master channel should modulate all notes in this zone
|
||||
test.timbre (2, MPEValue::from7BitInt (44));
|
||||
test.timbre (1, MPEValue::from7BitInt (44));
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 44, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 44, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
expectEquals (test.noteTimbreChangedCallCounter, 3);
|
||||
|
||||
// modulating timbre on an unrelated channel should be ignored
|
||||
test.timbre (1, MPEValue::from7BitInt (55));
|
||||
test.timbre (9, MPEValue::from7BitInt (55));
|
||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 44, MPENote::keyDown);
|
||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 44, MPENote::keyDown);
|
||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown);
|
||||
|
|
@ -1727,8 +1756,8 @@ public:
|
|||
MPEInstrument test;
|
||||
|
||||
MidiBuffer buffer;
|
||||
buffer.addEvents (MPEMessages::addZone (MPEZone (2, 5)), 0, -1, 0);
|
||||
buffer.addEvents (MPEMessages::addZone (MPEZone (9, 6)), 0, -1, 0);
|
||||
buffer.addEvents (MPEMessages::setLowerZone (5), 0, -1, 0);
|
||||
buffer.addEvents (MPEMessages::setUpperZone (6), 0, -1, 0);
|
||||
|
||||
MidiBuffer::Iterator iter (buffer);
|
||||
MidiMessage message;
|
||||
|
|
@ -1737,11 +1766,12 @@ public:
|
|||
while (iter.getNextEvent (message, samplePosition))
|
||||
test.processNextMidiEvent (message);
|
||||
|
||||
expectEquals (test.getZoneLayout().getNumZones(), 2);
|
||||
expectEquals (test.getZoneLayout().getZoneByIndex (0)->getMasterChannel(), 2);
|
||||
expectEquals (test.getZoneLayout().getZoneByIndex (0)->getNumNoteChannels(), 5);
|
||||
expectEquals (test.getZoneLayout().getZoneByIndex (1)->getMasterChannel(), 9);
|
||||
expectEquals (test.getZoneLayout().getZoneByIndex (1)->getNumNoteChannels(), 6);
|
||||
expect (test.getZoneLayout().getLowerZone().isActive());
|
||||
expect (test.getZoneLayout().getUpperZone().isActive());
|
||||
expectEquals (test.getZoneLayout().getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (test.getZoneLayout().getLowerZone().numMemberChannels, 5);
|
||||
expectEquals (test.getZoneLayout().getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (test.getZoneLayout().getUpperZone().numMemberChannels, 6);
|
||||
}
|
||||
|
||||
beginTest ("MIDI all notes off");
|
||||
|
|
@ -1755,17 +1785,17 @@ public:
|
|||
expectEquals (test.getNumPlayingNotes(), 4);
|
||||
|
||||
// on note channel: ignore.
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (3));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (3));
|
||||
expectEquals (test.getNumPlayingNotes(), 4);
|
||||
|
||||
// on unused channel: ignore.
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (1));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (9));
|
||||
expectEquals (test.getNumPlayingNotes(), 4);
|
||||
|
||||
// on master channel: release notes in that zone only.
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (2));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (1));
|
||||
expectEquals (test.getNumPlayingNotes(), 2);
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (9));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (16));
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
}
|
||||
|
||||
|
|
@ -1779,13 +1809,13 @@ public:
|
|||
test.noteOn (15, 63, MPEValue::from7BitInt (100));
|
||||
expectEquals (test.getNumPlayingNotes(), 4);
|
||||
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (3));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (3));
|
||||
expectEquals (test.getNumPlayingNotes(), 3);
|
||||
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (15));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (15));
|
||||
expectEquals (test.getNumPlayingNotes(), 1);
|
||||
|
||||
test.processNextMidiEvent (MidiMessage::allNotesOff (4));
|
||||
test.processNextMidiEvent (MidiMessage::allControllersOff (4));
|
||||
expectEquals (test.getNumPlayingNotes(), 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ namespace juce
|
|||
active (playing) notes and the values of their dimensions of expression.
|
||||
|
||||
You can trigger and modulate notes:
|
||||
- by passing MIDI messages with the method processNextMidiEvent;
|
||||
- by directly calling the methods noteOn, noteOff etc.
|
||||
- by passing MIDI messages with the method processNextMidiEvent;
|
||||
- by directly calling the methods noteOn, noteOff etc.
|
||||
|
||||
The class implements the channel and note management logic specified in
|
||||
MPE. If you pass it a message, it will know what notes on what
|
||||
|
|
@ -53,9 +53,9 @@ namespace juce
|
|||
class JUCE_API MPEInstrument
|
||||
{
|
||||
public:
|
||||
|
||||
/** Constructor.
|
||||
This will construct an MPE instrument with initially no MPE zones.
|
||||
|
||||
This will construct an MPE instrument with inactive lower and upper zones.
|
||||
|
||||
In order to process incoming MIDI, call setZoneLayout, define the layout
|
||||
via MIDI RPN messages, or set the instrument to legacy mode.
|
||||
|
|
@ -84,14 +84,16 @@ public:
|
|||
|
||||
/** Returns true if the given MIDI channel (1-16) is a note channel in any
|
||||
of the MPEInstrument's MPE zones; false otherwise.
|
||||
|
||||
When in legacy mode, this will return true if the given channel is
|
||||
contained in the current legacy mode channel range; false otherwise.
|
||||
*/
|
||||
bool isNoteChannel (int midiChannel) const noexcept;
|
||||
bool isMemberChannel (int midiChannel) noexcept;
|
||||
|
||||
/** Returns true if the given MIDI channel (1-16) is a master channel in any
|
||||
of the MPEInstrument's MPE zones; false otherwise.
|
||||
When in legacy mode, this will always return false.
|
||||
/** Returns true if the given MIDI channel (1-16) is a master channel (channel
|
||||
1 or 16).
|
||||
|
||||
In legacy mode, this will always return false.
|
||||
*/
|
||||
bool isMasterChannel (int midiChannel) const noexcept;
|
||||
|
||||
|
|
@ -132,19 +134,23 @@ public:
|
|||
//==============================================================================
|
||||
/** Request a note-on on the given channel, with the given initial note
|
||||
number and velocity.
|
||||
|
||||
If the message arrives on a valid note channel, this will create a
|
||||
new MPENote and call the noteAdded callback.
|
||||
*/
|
||||
virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
|
||||
|
||||
/** Request a note-off. If there is a matching playing note, this will
|
||||
release the note (except if it is sustained by a sustain or sostenuto
|
||||
pedal) and call the noteReleased callback.
|
||||
/** Request a note-off.
|
||||
|
||||
If there is a matching playing note, this will release the note
|
||||
(except if it is sustained by a sustain or sostenuto pedal) and call
|
||||
the noteReleased callback.
|
||||
*/
|
||||
virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
|
||||
|
||||
/** Request a pitchbend on the given channel with the given value (in units
|
||||
of MIDI pitchwheel position).
|
||||
|
||||
Internally, this will determine whether the pitchwheel move is a
|
||||
per-note pitchbend or a master pitchbend (depending on midiChannel),
|
||||
take the correct per-note or master pitchbend range of the affected MPE
|
||||
|
|
@ -153,6 +159,7 @@ public:
|
|||
virtual void pitchbend (int midiChannel, MPEValue pitchbend);
|
||||
|
||||
/** Request a pressure change on the given channel with the given value.
|
||||
|
||||
This will modify the pressure dimension of the note currently held down
|
||||
on this channel (if any). If the channel is a zone master channel,
|
||||
the pressure change will be broadcast to all notes in this zone.
|
||||
|
|
@ -161,59 +168,60 @@ public:
|
|||
|
||||
/** Request a third dimension (timbre) change on the given channel with the
|
||||
given value.
|
||||
|
||||
This will modify the timbre dimension of the note currently held down
|
||||
on this channel (if any). If the channel is a zone master channel,
|
||||
the timbre change will be broadcast to all notes in this zone.
|
||||
*/
|
||||
virtual void timbre (int midiChannel, MPEValue value);
|
||||
|
||||
/** Request a sustain pedal press or release. If midiChannel is a zone's
|
||||
master channel, this will act on all notes in that zone; otherwise,
|
||||
nothing will happen.
|
||||
/** Request a sustain pedal press or release.
|
||||
|
||||
If midiChannel is a zone's master channel, this will act on all notes in
|
||||
that zone; otherwise, nothing will happen.
|
||||
*/
|
||||
virtual void sustainPedal (int midiChannel, bool isDown);
|
||||
|
||||
/** Request a sostenuto pedal press or release. If midiChannel is a zone's
|
||||
master channel, this will act on all notes in that zone; otherwise,
|
||||
nothing will happen.
|
||||
/** Request a sostenuto pedal press or release.
|
||||
|
||||
If midiChannel is a zone's master channel, this will act on all notes in
|
||||
that zone; otherwise, nothing will happen.
|
||||
*/
|
||||
virtual void sostenutoPedal (int midiChannel, bool isDown);
|
||||
|
||||
/** Discard all currently playing notes.
|
||||
|
||||
This will also call the noteReleased listener callback for all of them.
|
||||
*/
|
||||
void releaseAllNotes();
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the number of MPE notes currently played by the
|
||||
instrument.
|
||||
*/
|
||||
/** Returns the number of MPE notes currently played by the instrument. */
|
||||
int getNumPlayingNotes() const noexcept;
|
||||
|
||||
/** Returns the note at the given index. If there is no such note, returns
|
||||
an invalid MPENote. The notes are sorted such that the most recently
|
||||
added note is the last element.
|
||||
/** Returns the note at the given index.
|
||||
|
||||
If there is no such note, returns an invalid MPENote. The notes are sorted
|
||||
such that the most recently added note is the last element.
|
||||
*/
|
||||
MPENote getNote (int index) const noexcept;
|
||||
|
||||
/** Returns the note currently playing on the given midiChannel with the
|
||||
specified initial MIDI note number, if there is such a note.
|
||||
Otherwise, this returns an invalid MPENote
|
||||
(check with note.isValid() before use!)
|
||||
specified initial MIDI note number, if there is such a note. Otherwise,
|
||||
this returns an invalid MPENote (check with note.isValid() before use!)
|
||||
*/
|
||||
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
|
||||
|
||||
/** Returns the most recent note that is playing on the given midiChannel
|
||||
(this will be the note which has received the most recent note-on without
|
||||
a corresponding note-off), if there is such a note.
|
||||
Otherwise, this returns an invalid MPENote
|
||||
(check with note.isValid() before use!)
|
||||
a corresponding note-off), if there is such a note. Otherwise, this returns an
|
||||
invalid MPENote (check with note.isValid() before use!)
|
||||
*/
|
||||
MPENote getMostRecentNote (int midiChannel) const noexcept;
|
||||
|
||||
/** Returns the most recent note that is not the note passed in.
|
||||
If there is no such note, this returns an invalid MPENote
|
||||
(check with note.isValid() before use!)
|
||||
/** Returns the most recent note that is not the note passed in. If there is no
|
||||
such note, this returns an invalid MPENote (check with note.isValid() before use!).
|
||||
|
||||
This helper method might be useful for some custom voice handling algorithms.
|
||||
*/
|
||||
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
|
||||
|
|
@ -233,32 +241,34 @@ public:
|
|||
/** Destructor. */
|
||||
virtual ~Listener() {}
|
||||
|
||||
/** Implement this callback to be informed whenever a new expressive
|
||||
MIDI note is triggered.
|
||||
/** Implement this callback to be informed whenever a new expressive MIDI
|
||||
note is triggered.
|
||||
*/
|
||||
virtual void noteAdded (MPENote newNote) = 0;
|
||||
|
||||
/** Implement this callback to be informed whenever a currently
|
||||
playing MPE note's pressure value changes.
|
||||
/** Implement this callback to be informed whenever a currently playing
|
||||
MPE note's pressure value changes.
|
||||
*/
|
||||
virtual void notePressureChanged (MPENote changedNote) = 0;
|
||||
|
||||
/** Implement this callback to be informed whenever a currently
|
||||
playing MPE note's pitchbend value changes.
|
||||
/** Implement this callback to be informed whenever a currently playing
|
||||
MPE note's pitchbend value changes.
|
||||
|
||||
Note: This can happen if the note itself is bent, if there is a
|
||||
master channel pitchbend event, or if both occur simultaneously.
|
||||
Call MPENote::getFrequencyInHertz to get the effective note frequency.
|
||||
*/
|
||||
virtual void notePitchbendChanged (MPENote changedNote) = 0;
|
||||
|
||||
/** Implement this callback to be informed whenever a currently
|
||||
playing MPE note's timbre value changes.
|
||||
/** Implement this callback to be informed whenever a currently playing
|
||||
MPE note's timbre value changes.
|
||||
*/
|
||||
virtual void noteTimbreChanged (MPENote changedNote) = 0;
|
||||
|
||||
/** Implement this callback to be informed whether a currently playing
|
||||
MPE note's key state (whether the key is down and/or the note is
|
||||
sustained) has changed.
|
||||
|
||||
Note: if the key state changes to MPENote::off, noteReleased is
|
||||
called instead.
|
||||
*/
|
||||
|
|
@ -329,7 +339,7 @@ private:
|
|||
|
||||
uint8 lastPressureLowerBitReceivedOnChannel[16];
|
||||
uint8 lastTimbreLowerBitReceivedOnChannel[16];
|
||||
bool isNoteChannelSustained[16];
|
||||
bool isMemberChannelSustained[16];
|
||||
|
||||
struct LegacyMode
|
||||
{
|
||||
|
|
@ -351,7 +361,7 @@ private:
|
|||
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
|
||||
|
||||
void updateDimension (int midiChannel, MPEDimension&, MPEValue);
|
||||
void updateDimensionMaster (const MPEZone&, MPEDimension&, MPEValue);
|
||||
void updateDimensionMaster (bool, MPEDimension&, MPEValue);
|
||||
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
|
||||
void callListenersDimensionChanged (const MPENote&, const MPEDimension&);
|
||||
MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
|
||||
|
|
@ -361,7 +371,7 @@ private:
|
|||
void processMidiPitchWheelMessage (const MidiMessage&);
|
||||
void processMidiChannelPressureMessage (const MidiMessage&);
|
||||
void processMidiControllerMessage (const MidiMessage&);
|
||||
void processMidiAllNotesOffMessage (const MidiMessage&);
|
||||
void processMidiResetAllControllersMessage (const MidiMessage&);
|
||||
void handlePressureMSB (int midiChannel, int value) noexcept;
|
||||
void handlePressureLSB (int midiChannel, int value) noexcept;
|
||||
void handleTimbreMSB (int midiChannel, int value) noexcept;
|
||||
|
|
|
|||
|
|
@ -23,46 +23,85 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
MidiBuffer MPEMessages::addZone (MPEZone zone)
|
||||
MidiBuffer MPEMessages::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
|
||||
{
|
||||
auto buffer = MidiRPNGenerator::generate (zone.getFirstNoteChannel(),
|
||||
zoneLayoutMessagesRpnNumber,
|
||||
zone.getNumNoteChannels(),
|
||||
false, false);
|
||||
auto buffer = MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
|
||||
|
||||
buffer.addEvents (perNotePitchbendRange (zone), 0, -1, 0);
|
||||
buffer.addEvents (masterPitchbendRange (zone), 0, -1, 0);
|
||||
buffer.addEvents (setLowerZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
|
||||
buffer.addEvents (setLowerZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::perNotePitchbendRange (MPEZone zone)
|
||||
MidiBuffer MPEMessages::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (zone.getFirstNoteChannel(), 0,
|
||||
zone.getPerNotePitchbendRange(),
|
||||
false, false);
|
||||
auto buffer = MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
|
||||
|
||||
buffer.addEvents (setUpperZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
|
||||
buffer.addEvents (setUpperZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::masterPitchbendRange (MPEZone zone)
|
||||
MidiBuffer MPEMessages::setLowerZonePerNotePitchbendRange (int perNotePitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (zone.getMasterChannel(), 0,
|
||||
zone.getMasterPitchbendRange(),
|
||||
false, false);
|
||||
return MidiRPNGenerator::generate (2, 0, perNotePitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setUpperZonePerNotePitchbendRange (int perNotePitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (15, 0, perNotePitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setLowerZoneMasterPitchbendRange (int masterPitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (1, 0, masterPitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setUpperZoneMasterPitchbendRange (int masterPitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (16, 0, masterPitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearLowerZone()
|
||||
{
|
||||
return MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, 0, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearUpperZone()
|
||||
{
|
||||
return MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, 0, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearAllZones()
|
||||
{
|
||||
return MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, 16, false, false);
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer.addEvents (clearLowerZone(), 0, -1, 0);
|
||||
buffer.addEvents (clearUpperZone(), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setZoneLayout (const MPEZoneLayout& layout)
|
||||
MidiBuffer MPEMessages::setZoneLayout (MPEZoneLayout layout)
|
||||
{
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer.addEvents (clearAllZones(), 0, -1, 0);
|
||||
|
||||
for (int i = 0; i < layout.getNumZones(); ++i)
|
||||
buffer.addEvents (addZone (*layout.getZoneByIndex (i)), 0, -1, 0);
|
||||
auto lowerZone = layout.getLowerZone();
|
||||
if (lowerZone.isActive())
|
||||
buffer.addEvents (setLowerZone (lowerZone.numMemberChannels,
|
||||
lowerZone.perNotePitchbendRange,
|
||||
lowerZone.masterPitchbendRange),
|
||||
0, -1, 0);
|
||||
|
||||
auto upperZone = layout.getUpperZone();
|
||||
if (upperZone.isActive())
|
||||
buffer.addEvents (setUpperZone (upperZone.numMemberChannels,
|
||||
upperZone.perNotePitchbendRange,
|
||||
upperZone.masterPitchbendRange),
|
||||
0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
|
@ -81,11 +120,11 @@ public:
|
|||
beginTest ("add zone");
|
||||
{
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::addZone (MPEZone (1, 7));
|
||||
MidiBuffer buffer = MPEMessages::setLowerZone (7);
|
||||
|
||||
const uint8 expectedBytes[] =
|
||||
{
|
||||
0xb1, 0x64, 0x06, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x07, // set up zone
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone
|
||||
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x30, // per-note pbrange (default = 48)
|
||||
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x02 // master pbrange (default = 2)
|
||||
};
|
||||
|
|
@ -93,13 +132,13 @@ public:
|
|||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::addZone (MPEZone (11, 5, 96, 0));
|
||||
MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0);
|
||||
|
||||
const uint8 expectedBytes[] =
|
||||
{
|
||||
0xbb, 0x64, 0x06, 0xbb, 0x65, 0x00, 0xbb, 0x06, 0x05, // set up zone
|
||||
0xbb, 0x64, 0x00, 0xbb, 0x65, 0x00, 0xbb, 0x06, 0x60, // per-note pbrange (custom)
|
||||
0xba, 0x64, 0x00, 0xba, 0x65, 0x00, 0xba, 0x06, 0x00 // master pbrange (custom)
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone
|
||||
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x60, // per-note pbrange (custom)
|
||||
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // master pbrange (custom)
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
|
|
@ -108,10 +147,9 @@ public:
|
|||
|
||||
beginTest ("set per-note pitchbend range");
|
||||
{
|
||||
MPEZone zone (3, 7, 96);
|
||||
MidiBuffer buffer = MPEMessages::perNotePitchbendRange (zone);
|
||||
MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96);
|
||||
|
||||
const uint8 expectedBytes[] = { 0xb3, 0x64, 0x00, 0xb3, 0x65, 0x00, 0xb3, 0x06, 0x60 };
|
||||
const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 };
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
|
@ -119,10 +157,9 @@ public:
|
|||
|
||||
beginTest ("set master pitchbend range");
|
||||
{
|
||||
MPEZone zone (3, 7, 48, 60);
|
||||
MidiBuffer buffer = MPEMessages::masterPitchbendRange (zone);
|
||||
MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60);
|
||||
|
||||
const uint8 expectedBytes[] = { 0xb2, 0x64, 0x00, 0xb2, 0x65, 0x00, 0xb2, 0x06, 0x3c };
|
||||
const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c };
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
|
@ -131,7 +168,9 @@ public:
|
|||
{
|
||||
MidiBuffer buffer = MPEMessages::clearAllZones();
|
||||
|
||||
const uint8 expectedBytes[] = { 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x10 };
|
||||
const uint8 expectedBytes[] = { 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // clear upper zone
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
|
@ -139,22 +178,21 @@ public:
|
|||
beginTest ("set complete state");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
layout.addZone (MPEZone (1, 7, 96, 0));
|
||||
layout.addZone (MPEZone (9, 7));
|
||||
layout.addZone (MPEZone (5, 3));
|
||||
layout.addZone (MPEZone (5, 4));
|
||||
layout.addZone (MPEZone (6, 4));
|
||||
|
||||
layout.setLowerZone (7, 96, 0);
|
||||
layout.setUpperZone (7);
|
||||
|
||||
MidiBuffer buffer = MPEMessages::setZoneLayout (layout);
|
||||
|
||||
const uint8 expectedBytes[] = {
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x10, // clear all zones
|
||||
0xb1, 0x64, 0x06, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x03, // set zone 1 (1, 3)
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00, // clear upper zone
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set lower zone
|
||||
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60, // per-note pbrange (custom)
|
||||
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // master pbrange (custom)
|
||||
0xb6, 0x64, 0x06, 0xb6, 0x65, 0x00, 0xb6, 0x06, 0x04, // set zone 2 (6, 4)
|
||||
0xb6, 0x64, 0x00, 0xb6, 0x65, 0x00, 0xb6, 0x06, 0x30, // per-note pbrange (default = 48)
|
||||
0xb5, 0x64, 0x00, 0xb5, 0x65, 0x00, 0xb5, 0x06, 0x02 // master pbrange (default = 2)
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x07, // set upper zone
|
||||
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x30, // per-note pbrange (default = 48)
|
||||
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x02 // master pbrange (default = 2)
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
|
|
|
|||
|
|
@ -26,10 +26,9 @@ namespace juce
|
|||
//==============================================================================
|
||||
/**
|
||||
This helper class contains the necessary helper functions to generate
|
||||
MIDI messages that are exclusive to MPE, such as defining
|
||||
MIDI messages that are exclusive to MPE, such as defining the upper and lower
|
||||
MPE zones and setting per-note and master pitchbend ranges.
|
||||
You can then send them to your MPE device using
|
||||
MidiOutput::sendBlockOfMessagesNow.
|
||||
You can then send them to your MPE device using MidiOutput::sendBlockOfMessagesNow.
|
||||
|
||||
All other MPE messages like per-note pitchbend, pressure, and third
|
||||
dimension, are ordinary MIDI messages that should be created using the MidiMessage
|
||||
|
|
@ -42,46 +41,70 @@ namespace juce
|
|||
MPEZoneLayout class itself. You should also make sure that the Expressive
|
||||
MIDI zone layout of your C++ code and of the MPE device are kept in sync.
|
||||
|
||||
@see MidiMessage, MPEZoneLayout, MPEZone
|
||||
@see MidiMessage, MPEZoneLayout
|
||||
*/
|
||||
class JUCE_API MPEMessages
|
||||
{
|
||||
public:
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will define a new MPE zone.
|
||||
MIDI device, will set the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer addZone (MPEZone zone);
|
||||
static MidiBuffer setLowerZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will change the per-note pitchbend range of an
|
||||
existing MPE zone.
|
||||
MIDI device, will set the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer perNotePitchbendRange (MPEZone zone);
|
||||
static MidiBuffer setUpperZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will change the master pitchbend range of an
|
||||
existing MPE zone.
|
||||
MIDI device, will set the per-note pitchbend range of the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer masterPitchbendRange (MPEZone zone);
|
||||
static MidiBuffer setLowerZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will erase all currently defined MPE zones.
|
||||
MIDI device, will set the per-note pitchbend range of the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer setUpperZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the master pitchbend range of the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer setLowerZoneMasterPitchbendRange (int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the master pitchbend range of the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer setUpperZoneMasterPitchbendRange (int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the lower zone.
|
||||
*/
|
||||
static MidiBuffer clearLowerZone();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the upper zone.
|
||||
*/
|
||||
static MidiBuffer clearUpperZone();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the lower and upper zones.
|
||||
*/
|
||||
static MidiBuffer clearAllZones();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will reset the whole MPE zone layout of the
|
||||
device to the laoyut passed in. This will first clear all currently
|
||||
defined MPE zones, then add all zones contained in the
|
||||
passed-in zone layout, and set their per-note and master pitchbend
|
||||
ranges to their current values.
|
||||
device to the laoyut passed in. This will first clear the current lower and upper
|
||||
zones, then then set the zones contained in the passed-in zone layout, and set their
|
||||
per-note and master pitchbend ranges to their current values.
|
||||
*/
|
||||
static MidiBuffer setZoneLayout (const MPEZoneLayout& layout);
|
||||
static MidiBuffer setZoneLayout (MPEZoneLayout layout);
|
||||
|
||||
/** The RPN number used for MPE zone layout messages.
|
||||
|
||||
Note: This number can change in later versions of MPE.
|
||||
|
||||
Pitchbend range messages (both per-note and master) are instead sent
|
||||
on RPN 0 as in standard MIDI 1.0.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ MPENote::MPENote (int midiChannel_,
|
|||
noteOnVelocity (noteOnVelocity_),
|
||||
pitchbend (pitchbend_),
|
||||
pressure (pressure_),
|
||||
initialTimbre (timbre_),
|
||||
timbre (timbre_),
|
||||
keyState (keyState_)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ namespace juce
|
|||
struct JUCE_API MPENote
|
||||
{
|
||||
//==============================================================================
|
||||
/** Possible values for the note key state. */
|
||||
enum KeyState
|
||||
{
|
||||
off = 0, /**< The key is up (off). */
|
||||
|
|
@ -48,8 +49,8 @@ struct JUCE_API MPENote
|
|||
//==============================================================================
|
||||
/** Constructor.
|
||||
|
||||
@param midiChannel The MIDI channel of the note, between 2 and 16.
|
||||
(Channel 1 can never be a note channel in MPE).
|
||||
@param midiChannel The MIDI channel of the note, between 2 and 15.
|
||||
(Channel 1 and channel 16 can never be note channels in MPE).
|
||||
|
||||
@param initialNote The MIDI note number, between 0 and 127.
|
||||
|
||||
|
|
@ -129,8 +130,13 @@ struct JUCE_API MPENote
|
|||
*/
|
||||
MPEValue pressure { MPEValue::centreValue() };
|
||||
|
||||
/** Current value of the note's third expressive dimension, tyically
|
||||
encoding some kind of timbre parameter.
|
||||
/** Inital value of timbre when the note was triggered.
|
||||
This should never change during the lifetime of an MPENote object.
|
||||
*/
|
||||
MPEValue initialTimbre { MPEValue::centreValue() };
|
||||
|
||||
/** Current value of the note's third expressive dimension, typically
|
||||
encoding some kind of timbre parameter.
|
||||
This dimension can be modulated while the note sounds.
|
||||
*/
|
||||
MPEValue timbre { MPEValue::centreValue() };
|
||||
|
|
@ -139,7 +145,7 @@ struct JUCE_API MPENote
|
|||
received.
|
||||
This dimension will only have a meaningful value after a note-off has
|
||||
been received for the note (and keyState is set to MPENote::off or
|
||||
MPENOte::sustained). Initially, the value is undefined.
|
||||
MPENote::sustained). Initially, the value is undefined.
|
||||
*/
|
||||
MPEValue noteOffVelocity { MPEValue::minValue() };
|
||||
|
||||
|
|
@ -161,7 +167,7 @@ struct JUCE_API MPENote
|
|||
KeyState keyState { MPENote::off };
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the current frequency of the note in Hertz. This is the a sum of
|
||||
/** Returns the current frequency of the note in Hertz. This is the sum of
|
||||
the initialNote and the totalPitchbendInSemitones, converted to Hertz.
|
||||
*/
|
||||
double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace juce
|
|||
MPESynthesiser::MPESynthesiser()
|
||||
{
|
||||
MPEZoneLayout zoneLayout;
|
||||
zoneLayout.addZone ({ 1, 15 });
|
||||
zoneLayout.setLowerZone (15);
|
||||
setZoneLayout (zoneLayout);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ void MPESynthesiser::noteReleased (MPENote finishedNote)
|
|||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (int i = voices.size(); --i >= 0;)
|
||||
for (auto i = voices.size(); --i >= 0;)
|
||||
{
|
||||
auto* voice = voices.getUnchecked (i);
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate)
|
|||
|
||||
turnOffAllVoices (false);
|
||||
|
||||
for (int i = voices.size(); --i >= 0;)
|
||||
for (auto i = voices.size(); --i >= 0;)
|
||||
voices.getUnchecked (i)->setCurrentSampleRate (newRate);
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ void MPESynthesiser::reduceNumVoices (const int newNumVoices)
|
|||
|
||||
while (voices.size() > newNumVoices)
|
||||
{
|
||||
if (MPESynthesiserVoice* voice = findFreeVoice ({}, true))
|
||||
if (auto* voice = findFreeVoice ({}, true))
|
||||
voices.removeObject (voice);
|
||||
else
|
||||
voices.remove (0); // if there's no voice to steal, kill the oldest voice
|
||||
|
|
|
|||
424
modules/juce_audio_basics/mpe/juce_MPEUtils.cpp
Normal file
424
modules/juce_audio_basics/mpe/juce_MPEUtils.cpp
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
MPEChannelAssigner::MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse)
|
||||
: zone (zoneToUse),
|
||||
channelIncrement (zone.isLowerZone() ? 1 : -1),
|
||||
numChannels (zone.numMemberChannels),
|
||||
firstChannel (zone.getFirstMemberChannel()),
|
||||
lastChannel (zone.getLastMemberChannel()),
|
||||
midiChannelLastAssigned (firstChannel - channelIncrement)
|
||||
{
|
||||
// must be an active MPE zone!
|
||||
jassert (numChannels > 0);
|
||||
}
|
||||
|
||||
int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
|
||||
{
|
||||
if (numChannels == 1)
|
||||
return firstChannel;
|
||||
|
||||
for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
|
||||
{
|
||||
if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
|
||||
{
|
||||
midiChannelLastAssigned = ch;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
|
||||
{
|
||||
if (ch == lastChannel + channelIncrement) // loop wrap-around
|
||||
ch = firstChannel;
|
||||
|
||||
if (midiChannels[ch].isFree())
|
||||
{
|
||||
midiChannelLastAssigned = ch;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
|
||||
if (ch == midiChannelLastAssigned)
|
||||
break; // no free channels!
|
||||
}
|
||||
|
||||
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
|
||||
midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
|
||||
|
||||
return midiChannelLastAssigned;
|
||||
}
|
||||
|
||||
void MPEChannelAssigner::noteOff (int noteNumber)
|
||||
{
|
||||
for (auto& ch : midiChannels)
|
||||
{
|
||||
if (ch.notes.removeAllInstancesOf (noteNumber) > 0)
|
||||
{
|
||||
ch.lastNotePlayed = noteNumber;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPEChannelAssigner::allNotesOff()
|
||||
{
|
||||
for (auto& ch : midiChannels)
|
||||
{
|
||||
if (ch.notes.size() > 0)
|
||||
ch.lastNotePlayed = ch.notes.getLast();
|
||||
|
||||
ch.notes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
|
||||
{
|
||||
auto channelWithClosestNote = firstChannel;
|
||||
int closestNoteDistance = 127;
|
||||
|
||||
for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
|
||||
{
|
||||
for (auto note : midiChannels[ch].notes)
|
||||
{
|
||||
auto noteDistance = std::abs (note - noteNumber);
|
||||
|
||||
if (noteDistance > 0 && noteDistance < closestNoteDistance)
|
||||
{
|
||||
closestNoteDistance = noteDistance;
|
||||
channelWithClosestNote = ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channelWithClosestNote;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEChannelRemapper::MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap)
|
||||
: zone (zoneToRemap),
|
||||
channelIncrement (zone.isLowerZone() ? 1 : -1),
|
||||
firstChannel (zone.getFirstMemberChannel()),
|
||||
lastChannel (zone.getLastMemberChannel())
|
||||
{
|
||||
// must be an active MPE zone!
|
||||
jassert (zone.numMemberChannels > 0);
|
||||
zeroArrays();
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
|
||||
{
|
||||
auto channel = message.getChannel();
|
||||
|
||||
if (! zone.isUsingChannelAsMemberChannel (channel))
|
||||
return;
|
||||
|
||||
if (channel == zone.getMasterChannel() && message.isResetAllControllers())
|
||||
{
|
||||
clearSource (mpeSourceID);
|
||||
return;
|
||||
}
|
||||
|
||||
auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
|
||||
|
||||
if (messageIsNoteData (message))
|
||||
{
|
||||
++counter;
|
||||
|
||||
// fast path - no remap
|
||||
if (applyRemapIfExisting (channel, sourceAndChannelID, message))
|
||||
return;
|
||||
|
||||
// find existing remap
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
if (applyRemapIfExisting (chan, sourceAndChannelID, message))
|
||||
return;
|
||||
|
||||
// no remap necessary
|
||||
if (sourceAndChannel[channel] == notMPE)
|
||||
{
|
||||
lastUsed[channel] = counter;
|
||||
sourceAndChannel[channel] = sourceAndChannelID;
|
||||
return;
|
||||
}
|
||||
|
||||
// remap source & channel to new channel
|
||||
auto chan = getBestChanToReuse();
|
||||
|
||||
sourceAndChannel[chan] = sourceAndChannelID;
|
||||
lastUsed[chan] = counter;
|
||||
message.setChannel (chan);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::reset() noexcept
|
||||
{
|
||||
for (auto& s : sourceAndChannel)
|
||||
s = notMPE;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::clearChannel (int channel) noexcept
|
||||
{
|
||||
sourceAndChannel[channel] = notMPE;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
|
||||
{
|
||||
for (auto& s : sourceAndChannel)
|
||||
{
|
||||
if (uint32 (s >> 5) == mpeSourceID)
|
||||
{
|
||||
s = notMPE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
|
||||
{
|
||||
if (sourceAndChannel[channel] == sourceAndChannelID)
|
||||
{
|
||||
if (m.isNoteOff())
|
||||
sourceAndChannel[channel] = notMPE;
|
||||
else
|
||||
lastUsed[channel] = counter;
|
||||
|
||||
m.setChannel (channel);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int MPEChannelRemapper::getBestChanToReuse() const noexcept
|
||||
{
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
if (sourceAndChannel[chan] == notMPE)
|
||||
return chan;
|
||||
|
||||
auto bestChan = firstChannel;
|
||||
auto bestLastUse = counter;
|
||||
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
{
|
||||
if (lastUsed[chan] < bestLastUse)
|
||||
{
|
||||
bestLastUse = lastUsed[chan];
|
||||
bestChan = chan;
|
||||
}
|
||||
}
|
||||
|
||||
return bestChan;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::zeroArrays()
|
||||
{
|
||||
for (int i = 0; i < 17; ++i)
|
||||
{
|
||||
sourceAndChannel[i] = 0;
|
||||
lastUsed[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
struct MPEUtilsUnitTests : public UnitTest
|
||||
{
|
||||
MPEUtilsUnitTests()
|
||||
: UnitTest ("MPE Utilities", "MIDI/MPE")
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("MPEChannelAssigner");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
{
|
||||
layout.setLowerZone (15);
|
||||
|
||||
// lower zone
|
||||
MPEChannelAssigner channelAssigner (layout.getLowerZone());
|
||||
|
||||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
for (int ch = 2; ch <= 16; ++ch)
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
|
||||
|
||||
// check that note-offs are processed
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
||||
// last note played
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
|
||||
|
||||
// normal assignment
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
|
||||
}
|
||||
|
||||
{
|
||||
layout.setUpperZone (15);
|
||||
|
||||
// upper zone
|
||||
MPEChannelAssigner channelAssigner (layout.getUpperZone());
|
||||
|
||||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
for (int ch = 15; ch >= 1; --ch)
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
|
||||
|
||||
// check that note-offs are processed
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
||||
// last note played
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
|
||||
|
||||
// normal assignment
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("MPEChannelRemapper");
|
||||
{
|
||||
// 3 different MPE 'sources', constant IDs
|
||||
const int sourceID1 = 0;
|
||||
const int sourceID2 = 1;
|
||||
const int sourceID3 = 2;
|
||||
|
||||
MPEZoneLayout layout;
|
||||
|
||||
{
|
||||
layout.setLowerZone (15);
|
||||
|
||||
// lower zone
|
||||
MPEChannelRemapper channelRemapper (layout.getLowerZone());
|
||||
|
||||
// first source, shouldn't remap
|
||||
for (int ch = 2; ch <= 16; ++ch)
|
||||
{
|
||||
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
|
||||
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
|
||||
expectEquals (noteOn.getChannel(), ch);
|
||||
}
|
||||
|
||||
auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
|
||||
expectEquals (noteOn.getChannel(), 2);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
|
||||
expectEquals (noteOn.getChannel(), 3);
|
||||
|
||||
// remap to correct channel for source ID
|
||||
auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
|
||||
expectEquals (noteOff.getChannel(), 3);
|
||||
}
|
||||
|
||||
{
|
||||
layout.setUpperZone (15);
|
||||
|
||||
// upper zone
|
||||
MPEChannelRemapper channelRemapper (layout.getUpperZone());
|
||||
|
||||
// first source, shouldn't remap
|
||||
for (int ch = 15; ch >= 1; --ch)
|
||||
{
|
||||
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
|
||||
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
|
||||
expectEquals (noteOn.getChannel(), ch);
|
||||
}
|
||||
|
||||
auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
|
||||
expectEquals (noteOn.getChannel(), 15);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
|
||||
expectEquals (noteOn.getChannel(), 14);
|
||||
|
||||
// remap to correct channel for source ID
|
||||
auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
|
||||
expectEquals (noteOff.getChannel(), 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static MPEUtilsUnitTests MPEUtilsUnitTests;
|
||||
|
||||
#endif
|
||||
} // namespace juce
|
||||
136
modules/juce_audio_basics/mpe/juce_MPEUtils.h
Normal file
136
modules/juce_audio_basics/mpe/juce_MPEUtils.h
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This class handles the assignment of new MIDI notes to member channels of an active
|
||||
MPE zone.
|
||||
|
||||
To use it, create an instance passing in the MPE zone that it should operate on
|
||||
and then call use the findMidiChannelForNewNote() method for all note-on messages
|
||||
and the noteOff() method for all note-off messages.
|
||||
*/
|
||||
class MPEChannelAssigner
|
||||
{
|
||||
public:
|
||||
/** Constructor */
|
||||
MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse);
|
||||
|
||||
/** This method will use a set of rules recommended in the MPE specification to
|
||||
determine which member channel the specified MIDI note should be assigned to
|
||||
and will return this channel number.
|
||||
|
||||
The rules have the following precedence:
|
||||
- find a free channel on which the last note played was the same as the one specified
|
||||
- find the next free channel in round-robin assignment
|
||||
- find the channel number that is currently playing the closest note (but not the same)
|
||||
|
||||
@param noteNumber the MIDI note number to be assigned to a channel
|
||||
@returns the zone's member channel that this note should be assigned to
|
||||
*/
|
||||
int findMidiChannelForNewNote (int noteNumber) noexcept;
|
||||
|
||||
/** You must call this method for all note-offs that you receive so that this class
|
||||
can keep track of the currently playing notes internally.
|
||||
*/
|
||||
void noteOff (int noteNumber);
|
||||
|
||||
/** Call this to clear all currently playing notes. */
|
||||
void allNotesOff();
|
||||
|
||||
private:
|
||||
MPEZoneLayout::Zone zone;
|
||||
int channelIncrement, numChannels, firstChannel, lastChannel, midiChannelLastAssigned;
|
||||
|
||||
//==============================================================================
|
||||
struct MidiChannel
|
||||
{
|
||||
Array<int> notes;
|
||||
int lastNotePlayed = -1;
|
||||
bool isFree() const noexcept { return notes.isEmpty(); }
|
||||
};
|
||||
MidiChannel midiChannels[17];
|
||||
|
||||
//==============================================================================
|
||||
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This class handles the logic for remapping MIDI note messages from multiple MPE
|
||||
sources onto a specified MPE zone.
|
||||
*/
|
||||
class MPEChannelRemapper
|
||||
{
|
||||
public:
|
||||
/** Used to indicate that a particular source & channel combination is not currently using MPE. */
|
||||
static const uint32 notMPE = 0;
|
||||
|
||||
/** Constructor */
|
||||
MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap);
|
||||
|
||||
//==============================================================================
|
||||
/** Remaps the MIDI channel of the specified MIDI message (if necessary).
|
||||
|
||||
Note that the MidiMessage object passed in will have it's channel changed if it
|
||||
needs to be remapped.
|
||||
|
||||
@param message the message to be remapped
|
||||
@param mpeSourceID the ID of the MPE source of the message. This is up to the
|
||||
user to define and keep constant
|
||||
*/
|
||||
void remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Resets all the source & channel combinations. */
|
||||
void reset() noexcept;
|
||||
|
||||
/** Clears a specified channel of this MPE zone. */
|
||||
void clearChannel (int channel) noexcept;
|
||||
|
||||
/** Clears all channels in use by a specified source. */
|
||||
void clearSource (uint32 mpeSourceID);
|
||||
|
||||
private:
|
||||
MPEZoneLayout::Zone zone;
|
||||
|
||||
int channelIncrement;
|
||||
int firstChannel, lastChannel;
|
||||
|
||||
uint32 sourceAndChannel[17];
|
||||
uint32 lastUsed[17];
|
||||
uint32 counter = 0;
|
||||
|
||||
//==============================================================================
|
||||
bool applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept;
|
||||
int getBestChanToReuse() const noexcept;
|
||||
|
||||
void zeroArrays();
|
||||
|
||||
//==============================================================================
|
||||
bool messageIsNoteData (const MidiMessage& m) { return (*m.getRawData() & 0xf0) != 0xf0; }
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
|
@ -35,8 +35,9 @@ class JUCE_API MPEValue
|
|||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Default constructor. Constructs an MPEValue corresponding
|
||||
to the centre value.
|
||||
/** Default constructor.
|
||||
|
||||
Constructs an MPEValue corresponding to the centre value.
|
||||
*/
|
||||
MPEValue() noexcept;
|
||||
|
||||
|
|
@ -60,12 +61,14 @@ public:
|
|||
static MPEValue maxValue() noexcept;
|
||||
|
||||
/** Retrieves the current value as an integer between 0 and 127.
|
||||
|
||||
Information will be lost if the value was initialised with a precision
|
||||
higher than 7-bit.
|
||||
*/
|
||||
int as7BitInt() const noexcept;
|
||||
|
||||
/** Retrieves the current value as an integer between 0 and 16383.
|
||||
|
||||
Resolution will be lost if the value was initialised with a precision
|
||||
higher than 14-bit.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,319 +0,0 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
void checkAndLimitZoneParameters (int minValue,
|
||||
int maxValue,
|
||||
int& valueToCheckAndLimit) noexcept
|
||||
{
|
||||
if (valueToCheckAndLimit < minValue || valueToCheckAndLimit > maxValue)
|
||||
{
|
||||
// if you hit this, one of the parameters you supplied for MPEZone
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEZone::MPEZone (int masterChannel_,
|
||||
int numNoteChannels_,
|
||||
int perNotePitchbendRange_,
|
||||
int masterPitchbendRange_) noexcept
|
||||
: masterChannel (masterChannel_),
|
||||
numNoteChannels (numNoteChannels_),
|
||||
perNotePitchbendRange (perNotePitchbendRange_),
|
||||
masterPitchbendRange (masterPitchbendRange_)
|
||||
{
|
||||
checkAndLimitZoneParameters (1, 15, masterChannel);
|
||||
checkAndLimitZoneParameters (1, 16 - masterChannel, numNoteChannels);
|
||||
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
|
||||
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int MPEZone::getMasterChannel() const noexcept
|
||||
{
|
||||
return masterChannel;
|
||||
}
|
||||
|
||||
int MPEZone::getNumNoteChannels() const noexcept
|
||||
{
|
||||
return numNoteChannels;
|
||||
}
|
||||
|
||||
int MPEZone::getFirstNoteChannel() const noexcept
|
||||
{
|
||||
return masterChannel + 1;
|
||||
}
|
||||
|
||||
int MPEZone::getLastNoteChannel() const noexcept
|
||||
{
|
||||
return masterChannel + numNoteChannels;
|
||||
}
|
||||
|
||||
Range<int> MPEZone::getNoteChannelRange() const noexcept
|
||||
{
|
||||
return Range<int>::withStartAndLength (getFirstNoteChannel(), getNumNoteChannels());
|
||||
}
|
||||
|
||||
bool MPEZone::isUsingChannel (int channel) const noexcept
|
||||
{
|
||||
jassert (channel > 0 && channel <= 16);
|
||||
return channel >= masterChannel && channel <= masterChannel + numNoteChannels;
|
||||
}
|
||||
|
||||
bool MPEZone::isUsingChannelAsNoteChannel (int channel) const noexcept
|
||||
{
|
||||
jassert (channel > 0 && channel <= 16);
|
||||
return channel > masterChannel && channel <= masterChannel + numNoteChannels;
|
||||
}
|
||||
|
||||
int MPEZone::getPerNotePitchbendRange() const noexcept
|
||||
{
|
||||
return perNotePitchbendRange;
|
||||
}
|
||||
|
||||
int MPEZone::getMasterPitchbendRange() const noexcept
|
||||
{
|
||||
return masterPitchbendRange;
|
||||
}
|
||||
|
||||
void MPEZone::setPerNotePitchbendRange (int rangeInSemitones) noexcept
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, rangeInSemitones);
|
||||
perNotePitchbendRange = rangeInSemitones;
|
||||
}
|
||||
|
||||
void MPEZone::setMasterPitchbendRange (int rangeInSemitones) noexcept
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, rangeInSemitones);
|
||||
masterPitchbendRange = rangeInSemitones;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEZone::overlapsWith (MPEZone other) const noexcept
|
||||
{
|
||||
if (masterChannel == other.masterChannel)
|
||||
return true;
|
||||
|
||||
if (masterChannel > other.masterChannel)
|
||||
return other.overlapsWith (*this);
|
||||
|
||||
return masterChannel + numNoteChannels >= other.masterChannel;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEZone::truncateToFit (MPEZone other) noexcept
|
||||
{
|
||||
auto masterChannelDiff = other.masterChannel - masterChannel;
|
||||
|
||||
// we need at least 2 channels to be left after truncation:
|
||||
// 1 master channel and 1 note channel. otherwise we can't truncate.
|
||||
if (masterChannelDiff < 2)
|
||||
return false;
|
||||
|
||||
numNoteChannels = jmin (numNoteChannels, masterChannelDiff - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEZone::operator== (const MPEZone& other) const noexcept
|
||||
{
|
||||
return masterChannel == other.masterChannel
|
||||
&& numNoteChannels == other.numNoteChannels
|
||||
&& perNotePitchbendRange == other.perNotePitchbendRange
|
||||
&& masterPitchbendRange == other.masterPitchbendRange;
|
||||
}
|
||||
|
||||
bool MPEZone::operator!= (const MPEZone& other) const noexcept
|
||||
{
|
||||
return ! operator== (other);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class MPEZoneTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
MPEZoneTests() : UnitTest ("MPEZone class", "MIDI/MPE") {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("initialisation");
|
||||
{
|
||||
{
|
||||
MPEZone zone (1, 10);
|
||||
|
||||
expectEquals (zone.getMasterChannel(), 1);
|
||||
expectEquals (zone.getNumNoteChannels(), 10);
|
||||
expectEquals (zone.getFirstNoteChannel(), 2);
|
||||
expectEquals (zone.getLastNoteChannel(), 11);
|
||||
expectEquals (zone.getPerNotePitchbendRange(), 48);
|
||||
expectEquals (zone.getMasterPitchbendRange(), 2);
|
||||
|
||||
expect (zone.isUsingChannel (1));
|
||||
expect (zone.isUsingChannel (2));
|
||||
expect (zone.isUsingChannel (10));
|
||||
expect (zone.isUsingChannel (11));
|
||||
expect (! zone.isUsingChannel (12));
|
||||
expect (! zone.isUsingChannel (16));
|
||||
|
||||
expect (! zone.isUsingChannelAsNoteChannel (1));
|
||||
expect (zone.isUsingChannelAsNoteChannel (2));
|
||||
expect (zone.isUsingChannelAsNoteChannel (10));
|
||||
expect (zone.isUsingChannelAsNoteChannel (11));
|
||||
expect (! zone.isUsingChannelAsNoteChannel (12));
|
||||
expect (! zone.isUsingChannelAsNoteChannel (16));
|
||||
}
|
||||
{
|
||||
MPEZone zone (5, 4);
|
||||
|
||||
expectEquals (zone.getMasterChannel(), 5);
|
||||
expectEquals (zone.getNumNoteChannels(), 4);
|
||||
expectEquals (zone.getFirstNoteChannel(), 6);
|
||||
expectEquals (zone.getLastNoteChannel(), 9);
|
||||
expectEquals (zone.getPerNotePitchbendRange(), 48);
|
||||
expectEquals (zone.getMasterPitchbendRange(), 2);
|
||||
|
||||
expect (! zone.isUsingChannel (1));
|
||||
expect (! zone.isUsingChannel (4));
|
||||
expect (zone.isUsingChannel (5));
|
||||
expect (zone.isUsingChannel (6));
|
||||
expect (zone.isUsingChannel (8));
|
||||
expect (zone.isUsingChannel (9));
|
||||
expect (! zone.isUsingChannel (10));
|
||||
expect (! zone.isUsingChannel (16));
|
||||
|
||||
expect (! zone.isUsingChannelAsNoteChannel (5));
|
||||
expect (zone.isUsingChannelAsNoteChannel (6));
|
||||
expect (zone.isUsingChannelAsNoteChannel (8));
|
||||
expect (zone.isUsingChannelAsNoteChannel (9));
|
||||
expect (! zone.isUsingChannelAsNoteChannel (10));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
beginTest ("getNoteChannelRange");
|
||||
{
|
||||
MPEZone zone (2, 10);
|
||||
|
||||
Range<int> noteChannelRange = zone.getNoteChannelRange();
|
||||
expectEquals (noteChannelRange.getStart(), 3);
|
||||
expectEquals (noteChannelRange.getEnd(), 13);
|
||||
}
|
||||
|
||||
beginTest ("setting master pitchbend range");
|
||||
{
|
||||
MPEZone zone (1, 10);
|
||||
|
||||
zone.setMasterPitchbendRange (96);
|
||||
expectEquals (zone.getMasterPitchbendRange(), 96);
|
||||
zone.setMasterPitchbendRange (0);
|
||||
expectEquals (zone.getMasterPitchbendRange(), 0);
|
||||
|
||||
expectEquals (zone.getPerNotePitchbendRange(), 48);
|
||||
}
|
||||
|
||||
beginTest ("setting per-note pitchbend range");
|
||||
{
|
||||
MPEZone zone (1, 10);
|
||||
|
||||
zone.setPerNotePitchbendRange (96);
|
||||
expectEquals (zone.getPerNotePitchbendRange(), 96);
|
||||
zone.setPerNotePitchbendRange (0);
|
||||
expectEquals (zone.getPerNotePitchbendRange(), 0);
|
||||
|
||||
expectEquals (zone.getMasterPitchbendRange(), 2);
|
||||
}
|
||||
|
||||
beginTest ("checking overlap");
|
||||
{
|
||||
testOverlapsWith (1, 10, 1, 10, true);
|
||||
testOverlapsWith (1, 4, 6, 3, false);
|
||||
testOverlapsWith (1, 4, 8, 3, false);
|
||||
testOverlapsWith (2, 10, 2, 8, true);
|
||||
testOverlapsWith (1, 10, 3, 2, true);
|
||||
testOverlapsWith (3, 10, 5, 9, true);
|
||||
}
|
||||
|
||||
beginTest ("truncating");
|
||||
{
|
||||
testTruncateToFit (1, 10, 3, 10, true, 1, 1);
|
||||
testTruncateToFit (3, 10, 1, 10, false, 3, 10);
|
||||
testTruncateToFit (1, 10, 5, 8, true, 1, 3);
|
||||
testTruncateToFit (5, 8, 1, 10, false, 5, 8);
|
||||
testTruncateToFit (1, 10, 4, 3, true, 1, 2);
|
||||
testTruncateToFit (4, 3, 1, 10, false, 4, 3);
|
||||
testTruncateToFit (1, 3, 5, 3, true, 1, 3);
|
||||
testTruncateToFit (5, 3, 1, 3, false, 5, 3);
|
||||
testTruncateToFit (1, 3, 7, 3, true, 1, 3);
|
||||
testTruncateToFit (7, 3, 1, 3, false, 7, 3);
|
||||
testTruncateToFit (1, 10, 2, 10, false, 1, 10);
|
||||
testTruncateToFit (2, 10, 1, 10, false, 2, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void testOverlapsWith (int masterChannelFirst, int numNoteChannelsFirst,
|
||||
int masterChannelSecond, int numNoteChannelsSecond,
|
||||
bool expectedRetVal)
|
||||
{
|
||||
MPEZone first (masterChannelFirst, numNoteChannelsFirst);
|
||||
MPEZone second (masterChannelSecond, numNoteChannelsSecond);
|
||||
|
||||
expect (first.overlapsWith (second) == expectedRetVal);
|
||||
expect (second.overlapsWith (first) == expectedRetVal);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void testTruncateToFit (int masterChannelFirst, int numNoteChannelsFirst,
|
||||
int masterChannelSecond, int numNoteChannelsSecond,
|
||||
bool expectedRetVal,
|
||||
int masterChannelFirstAfter, int numNoteChannelsFirstAfter)
|
||||
{
|
||||
MPEZone first (masterChannelFirst, numNoteChannelsFirst);
|
||||
MPEZone second (masterChannelSecond, numNoteChannelsSecond);
|
||||
|
||||
expect (first.truncateToFit (second) == expectedRetVal);
|
||||
expectEquals (first.getMasterChannel(), masterChannelFirstAfter);
|
||||
expectEquals (first.getNumNoteChannels(), numNoteChannelsFirstAfter);
|
||||
}
|
||||
};
|
||||
|
||||
static MPEZoneTests MPEZoneUnitTests;
|
||||
|
||||
#endif // JUCE_UNIT_TESTS
|
||||
|
||||
} // namespace juce
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This struct represents an MPE Zone.
|
||||
|
||||
An MPE Zone occupies one master MIDI channel and an arbitrary
|
||||
number of note channels that immediately follow the master channel.
|
||||
It also defines a pitchbend range (in semitones) to be applied for per-note
|
||||
pitchbends and master pitchbends, respectively.
|
||||
|
||||
@see MPEZoneLayout
|
||||
*/
|
||||
struct JUCE_API MPEZone
|
||||
{
|
||||
/** Constructor.
|
||||
Creates an MPE zone with the given master channel and
|
||||
number of note channels.
|
||||
|
||||
@param masterChannel The master MIDI channel of the new zone.
|
||||
All master (not per-note) messages should be send to this channel.
|
||||
Must be between 1 and 15. Otherwise, the behaviour
|
||||
is undefined.
|
||||
|
||||
@param numNoteChannels The number of note channels that the new zone
|
||||
should use. The first note channel will be one higher
|
||||
than the master channel. The number of note channels
|
||||
must be at least 1 and no greater than 16 - masterChannel.
|
||||
Otherwise, the behaviour is undefined.
|
||||
|
||||
@param perNotePitchbendRange The per-note pitchbend range in semitones of the new zone.
|
||||
Must be between 0 and 96. Otherwise the behaviour is undefined.
|
||||
If unspecified, the default setting of +/- 48 semitones
|
||||
will be used.
|
||||
|
||||
@param masterPitchbendRange The master pitchbend range in semitones of the new zone.
|
||||
Must be between 0 and 96. Otherwise the behaviour is undefined.
|
||||
If unspecified, the default setting of +/- 2 semitones
|
||||
will be used.
|
||||
*/
|
||||
MPEZone (int masterChannel,
|
||||
int numNoteChannels,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2) noexcept;
|
||||
|
||||
/* Returns the MIDI master channel number (in the range 1-16) of this zone. */
|
||||
int getMasterChannel() const noexcept;
|
||||
|
||||
/** Returns the number of note channels occupied by this zone. */
|
||||
int getNumNoteChannels() const noexcept;
|
||||
|
||||
/* Returns the MIDI channel number (in the range 1-16) of the
|
||||
lowest-numbered note channel of this zone.
|
||||
*/
|
||||
int getFirstNoteChannel() const noexcept;
|
||||
|
||||
/* Returns the MIDI channel number (in the range 1-16) of the
|
||||
highest-numbered note channel of this zone.
|
||||
*/
|
||||
int getLastNoteChannel() const noexcept;
|
||||
|
||||
/** Returns the MIDI channel numbers (in the range 1-16) of the
|
||||
note channels of this zone as a Range.
|
||||
*/
|
||||
Range<int> getNoteChannelRange() const noexcept;
|
||||
|
||||
/** Returns true if the MIDI channel (in the range 1-16) is used by this zone
|
||||
either as a note channel or as the master channel; false otherwise.
|
||||
*/
|
||||
bool isUsingChannel (int channel) const noexcept;
|
||||
|
||||
/** Returns true if the MIDI channel (in the range 1-16) is used by this zone
|
||||
as a note channel; false otherwise.
|
||||
*/
|
||||
bool isUsingChannelAsNoteChannel (int channel) const noexcept;
|
||||
|
||||
/** Returns the per-note pitchbend range in semitones set for this zone. */
|
||||
int getPerNotePitchbendRange() const noexcept;
|
||||
|
||||
/** Returns the master pitchbend range in semitones set for this zone. */
|
||||
int getMasterPitchbendRange() const noexcept;
|
||||
|
||||
/** Sets the per-note pitchbend range in semitones for this zone. */
|
||||
void setPerNotePitchbendRange (int rangeInSemitones) noexcept;
|
||||
|
||||
/** Sets the master pitchbend range in semitones for this zone. */
|
||||
void setMasterPitchbendRange (int rangeInSemitones) noexcept;
|
||||
|
||||
/** Returns true if the MIDI channels occupied by this zone
|
||||
overlap with those occupied by the other zone.
|
||||
*/
|
||||
bool overlapsWith (MPEZone other) const noexcept;
|
||||
|
||||
/** Tries to truncate this zone in such a way that the range of MIDI channels
|
||||
it occupies do not overlap with the other zone, by reducing this zone's
|
||||
number of note channels.
|
||||
|
||||
@returns true if the truncation succeeded or if no truncation is necessary
|
||||
because the zones do not overlap. False if the zone cannot be truncated
|
||||
in a way that would remove the overlap (in this case you need to delete
|
||||
the zone to remove the overlap).
|
||||
*/
|
||||
bool truncateToFit (MPEZone zoneToAvoid) noexcept;
|
||||
|
||||
/** @returns true if this zone is equal to the one passed in. */
|
||||
bool operator== (const MPEZone& other) const noexcept;
|
||||
|
||||
/** @returns true if this zone is not equal to the one passed in. */
|
||||
bool operator!= (const MPEZone& other) const noexcept;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
int masterChannel;
|
||||
int numNoteChannels;
|
||||
int perNotePitchbendRange;
|
||||
int masterPitchbendRange;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
|
@ -26,14 +26,18 @@ namespace juce
|
|||
MPEZoneLayout::MPEZoneLayout() noexcept {}
|
||||
|
||||
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
|
||||
: zones (other.zones)
|
||||
: lowerZone (other.lowerZone),
|
||||
upperZone (other.upperZone)
|
||||
{
|
||||
}
|
||||
|
||||
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other)
|
||||
{
|
||||
zones = other.zones;
|
||||
lowerZone = other.lowerZone;
|
||||
upperZone = other.upperZone;
|
||||
|
||||
sendLayoutChangeMessage();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
|
@ -43,51 +47,48 @@ void MPEZoneLayout::sendLayoutChangeMessage()
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEZoneLayout::addZone (MPEZone newZone)
|
||||
void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
bool noOtherZonesModified = true;
|
||||
checkAndLimitZoneParameters (0, 15, numMemberChannels);
|
||||
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
|
||||
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
|
||||
|
||||
for (int i = zones.size(); --i >= 0;)
|
||||
if (isLower)
|
||||
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
else
|
||||
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
|
||||
if (numMemberChannels > 0)
|
||||
{
|
||||
auto& zone = zones.getReference (i);
|
||||
auto totalChannels = lowerZone.numMemberChannels + upperZone.numMemberChannels;
|
||||
|
||||
if (zone.overlapsWith (newZone))
|
||||
if (totalChannels >= 15)
|
||||
{
|
||||
if (! zone.truncateToFit (newZone))
|
||||
zones.removeRange (i, 1);
|
||||
// can't use zones.remove (i) because that requires a default c'tor :-(
|
||||
|
||||
noOtherZonesModified = false;
|
||||
if (isLower)
|
||||
upperZone.numMemberChannels = 14 - numMemberChannels;
|
||||
else
|
||||
lowerZone.numMemberChannels = 14 - numMemberChannels;
|
||||
}
|
||||
}
|
||||
|
||||
zones.add (newZone);
|
||||
sendLayoutChangeMessage();
|
||||
return noOtherZonesModified;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int MPEZoneLayout::getNumZones() const noexcept
|
||||
void MPEZoneLayout::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
return zones.size();
|
||||
setZone (true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
|
||||
}
|
||||
|
||||
const MPEZone* MPEZoneLayout::getZoneByIndex (int index) const noexcept
|
||||
void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
if (zones.size() < index)
|
||||
return nullptr;
|
||||
|
||||
return &(zones.getReference (index));
|
||||
}
|
||||
|
||||
MPEZone* MPEZoneLayout::getZoneByIndex (int index) noexcept
|
||||
{
|
||||
return const_cast<MPEZone*> (static_cast<const MPEZoneLayout&> (*this).getZoneByIndex (index));
|
||||
setZone (false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
|
||||
}
|
||||
|
||||
void MPEZoneLayout::clearAllZones()
|
||||
{
|
||||
zones.clear();
|
||||
lowerZone = { true, 0 };
|
||||
upperZone = { false, 0 };
|
||||
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
|
||||
|
|
@ -119,36 +120,53 @@ void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn)
|
|||
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
|
||||
{
|
||||
if (rpn.value < 16)
|
||||
addZone (MPEZone (rpn.channel - 1, rpn.value));
|
||||
else
|
||||
clearAllZones();
|
||||
{
|
||||
if (rpn.channel == 1)
|
||||
setLowerZone (rpn.value);
|
||||
else if (rpn.channel == 16)
|
||||
setUpperZone (rpn.value);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
|
||||
{
|
||||
if (zone.masterPitchbendRange != value)
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange);
|
||||
zone.masterPitchbendRange = value;
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value)
|
||||
{
|
||||
if (zone.perNotePitchbendRange != value)
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange);
|
||||
zone.perNotePitchbendRange = value;
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn)
|
||||
{
|
||||
if (auto* zone = getZoneByFirstNoteChannel (rpn.channel))
|
||||
if (rpn.channel == 1)
|
||||
{
|
||||
if (zone->getPerNotePitchbendRange() != rpn.value)
|
||||
{
|
||||
zone->setPerNotePitchbendRange (rpn.value);
|
||||
sendLayoutChangeMessage();
|
||||
return;
|
||||
}
|
||||
updateMasterPitchbend (lowerZone, rpn.value);
|
||||
}
|
||||
|
||||
if (auto* zone = getZoneByMasterChannel (rpn.channel))
|
||||
else if (rpn.channel == 16)
|
||||
{
|
||||
if (zone->getMasterPitchbendRange() != rpn.value)
|
||||
{
|
||||
zone->setMasterPitchbendRange (rpn.value);
|
||||
sendLayoutChangeMessage();
|
||||
return;
|
||||
}
|
||||
updateMasterPitchbend (upperZone, rpn.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lowerZone.isUsingChannelAsMemberChannel (rpn.channel))
|
||||
updatePerNotePitchbendRange (lowerZone, rpn.value);
|
||||
else if (upperZone.isUsingChannelAsMemberChannel (rpn.channel))
|
||||
updatePerNotePitchbendRange (upperZone, rpn.value);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
|
||||
{
|
||||
MidiBuffer::Iterator iter (buffer);
|
||||
|
|
@ -159,63 +177,6 @@ void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
|
|||
processNextMidiEvent (message);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const MPEZone* MPEZoneLayout::getZoneByChannel (int channel) const noexcept
|
||||
{
|
||||
for (auto& zone : zones)
|
||||
if (zone.isUsingChannel (channel))
|
||||
return &zone;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MPEZone* MPEZoneLayout::getZoneByChannel (int channel) noexcept
|
||||
{
|
||||
return const_cast<MPEZone*> (static_cast<const MPEZoneLayout&> (*this).getZoneByChannel (channel));
|
||||
}
|
||||
|
||||
const MPEZone* MPEZoneLayout::getZoneByMasterChannel (int channel) const noexcept
|
||||
{
|
||||
for (auto& zone : zones)
|
||||
if (zone.getMasterChannel() == channel)
|
||||
return &zone;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MPEZone* MPEZoneLayout::getZoneByMasterChannel (int channel) noexcept
|
||||
{
|
||||
return const_cast<MPEZone*> (static_cast<const MPEZoneLayout&> (*this).getZoneByMasterChannel (channel));
|
||||
}
|
||||
|
||||
const MPEZone* MPEZoneLayout::getZoneByFirstNoteChannel (int channel) const noexcept
|
||||
{
|
||||
for (auto& zone : zones)
|
||||
if (zone.getFirstNoteChannel() == channel)
|
||||
return &zone;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MPEZone* MPEZoneLayout::getZoneByFirstNoteChannel (int channel) noexcept
|
||||
{
|
||||
return const_cast<MPEZone*> (static_cast<const MPEZoneLayout&> (*this).getZoneByFirstNoteChannel (channel));
|
||||
}
|
||||
|
||||
const MPEZone* MPEZoneLayout::getZoneByNoteChannel (int channel) const noexcept
|
||||
{
|
||||
for (auto& zone : zones)
|
||||
if (zone.isUsingChannelAsNoteChannel (channel))
|
||||
return &zone;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MPEZone* MPEZoneLayout::getZoneByNoteChannel (int channel) noexcept
|
||||
{
|
||||
return const_cast<MPEZone*> (static_cast<const MPEZoneLayout&> (*this).getZoneByNoteChannel (channel));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept
|
||||
{
|
||||
|
|
@ -227,11 +188,27 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
|
||||
class MPEZoneLayoutTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
|
|
@ -242,107 +219,73 @@ public:
|
|||
beginTest ("initialisation");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
expectEquals (layout.getNumZones(), 0);
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("adding zones");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
expect (layout.addZone (MPEZone (1, 7)));
|
||||
layout.setLowerZone (7);
|
||||
|
||||
expectEquals (layout.getNumZones(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 7);
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
|
||||
expect (layout.addZone (MPEZone (9, 7)));
|
||||
layout.setUpperZone (7);
|
||||
|
||||
expectEquals (layout.getNumZones(), 2);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 7);
|
||||
expectEquals (layout.getZoneByIndex (1)->getMasterChannel(), 9);
|
||||
expectEquals (layout.getZoneByIndex (1)->getNumNoteChannels(), 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);
|
||||
|
||||
expect (! layout.addZone (MPEZone (5, 3)));
|
||||
layout.setLowerZone (3);
|
||||
|
||||
expectEquals (layout.getNumZones(), 3);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 3);
|
||||
expectEquals (layout.getZoneByIndex (1)->getMasterChannel(), 9);
|
||||
expectEquals (layout.getZoneByIndex (1)->getNumNoteChannels(), 7);
|
||||
expectEquals (layout.getZoneByIndex (2)->getMasterChannel(), 5);
|
||||
expectEquals (layout.getZoneByIndex (2)->getNumNoteChannels(), 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);
|
||||
|
||||
expect (! layout.addZone (MPEZone (5, 4)));
|
||||
layout.setUpperZone (3);
|
||||
|
||||
expectEquals (layout.getNumZones(), 2);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 3);
|
||||
expectEquals (layout.getZoneByIndex (1)->getMasterChannel(), 5);
|
||||
expectEquals (layout.getZoneByIndex (1)->getNumNoteChannels(), 4);
|
||||
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);
|
||||
|
||||
expect (! layout.addZone (MPEZone (6, 4)));
|
||||
layout.setLowerZone (15);
|
||||
|
||||
expectEquals (layout.getNumZones(), 2);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 3);
|
||||
expectEquals (layout.getZoneByIndex (1)->getMasterChannel(), 6);
|
||||
expectEquals (layout.getZoneByIndex (1)->getNumNoteChannels(), 4);
|
||||
}
|
||||
|
||||
beginTest ("querying zones");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
layout.addZone (MPEZone (2, 5));
|
||||
layout.addZone (MPEZone (9, 4));
|
||||
|
||||
expect (layout.getZoneByMasterChannel (1) == nullptr);
|
||||
expect (layout.getZoneByMasterChannel (2) != nullptr);
|
||||
expect (layout.getZoneByMasterChannel (3) == nullptr);
|
||||
expect (layout.getZoneByMasterChannel (8) == nullptr);
|
||||
expect (layout.getZoneByMasterChannel (9) != nullptr);
|
||||
expect (layout.getZoneByMasterChannel (10) == nullptr);
|
||||
|
||||
expectEquals (layout.getZoneByMasterChannel (2)->getNumNoteChannels(), 5);
|
||||
expectEquals (layout.getZoneByMasterChannel (9)->getNumNoteChannels(), 4);
|
||||
|
||||
expect (layout.getZoneByFirstNoteChannel (2) == nullptr);
|
||||
expect (layout.getZoneByFirstNoteChannel (3) != nullptr);
|
||||
expect (layout.getZoneByFirstNoteChannel (4) == nullptr);
|
||||
expect (layout.getZoneByFirstNoteChannel (9) == nullptr);
|
||||
expect (layout.getZoneByFirstNoteChannel (10) != nullptr);
|
||||
expect (layout.getZoneByFirstNoteChannel (11) == nullptr);
|
||||
|
||||
expectEquals (layout.getZoneByFirstNoteChannel (3)->getNumNoteChannels(), 5);
|
||||
expectEquals (layout.getZoneByFirstNoteChannel (10)->getNumNoteChannels(), 4);
|
||||
|
||||
expect (layout.getZoneByNoteChannel (2) == nullptr);
|
||||
expect (layout.getZoneByNoteChannel (3) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (4) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (6) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (7) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (8) == nullptr);
|
||||
expect (layout.getZoneByNoteChannel (9) == nullptr);
|
||||
expect (layout.getZoneByNoteChannel (10) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (11) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (12) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (13) != nullptr);
|
||||
expect (layout.getZoneByNoteChannel (14) == nullptr);
|
||||
|
||||
expectEquals (layout.getZoneByNoteChannel (5)->getNumNoteChannels(), 5);
|
||||
expectEquals (layout.getZoneByNoteChannel (13)->getNumNoteChannels(), 4);
|
||||
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.addZone (MPEZone (1, 7)));
|
||||
expect (layout.addZone (MPEZone (10, 2)));
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
|
||||
layout.setLowerZone (7);
|
||||
layout.setUpperZone (2);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
|
||||
layout.clearAllZones();
|
||||
|
||||
expectEquals (layout.getNumZones(), 0);
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("process MIDI buffers");
|
||||
|
|
@ -350,57 +293,88 @@ public:
|
|||
MPEZoneLayout layout;
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer = MPEMessages::addZone (MPEZone (1, 7));
|
||||
buffer = MPEMessages::setLowerZone (7);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expectEquals (layout.getNumZones(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 7);
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
|
||||
buffer = MPEMessages::addZone (MPEZone (9, 7));
|
||||
buffer = MPEMessages::setUpperZone (7);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expectEquals (layout.getNumZones(), 2);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 7);
|
||||
expectEquals (layout.getZoneByIndex (1)->getMasterChannel(), 9);
|
||||
expectEquals (layout.getZoneByIndex (1)->getNumNoteChannels(), 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);
|
||||
|
||||
MPEZone zone (1, 10);
|
||||
{
|
||||
buffer = MPEMessages::setLowerZone (10);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
buffer = MPEMessages::addZone (zone);
|
||||
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);
|
||||
|
||||
expectEquals (layout.getNumZones(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 10);
|
||||
|
||||
zone.setPerNotePitchbendRange (33);
|
||||
zone.setMasterPitchbendRange (44);
|
||||
|
||||
buffer = MPEMessages::masterPitchbendRange (zone);
|
||||
buffer.addEvents (MPEMessages::perNotePitchbendRange (zone), 0, -1, 0);
|
||||
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expectEquals (layout.getZoneByIndex (0)->getPerNotePitchbendRange(), 33);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterPitchbendRange(), 44);
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("process individual MIDI messages");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
layout.processNextMidiEvent (MidiMessage (0x80, 0x59, 0xd0)); // unrelated note-off msg
|
||||
layout.processNextMidiEvent (MidiMessage (0xb1, 0x64, 0x06)); // RPN part 1
|
||||
layout.processNextMidiEvent (MidiMessage (0xb1, 0x65, 0x00)); // RPN part 2
|
||||
layout.processNextMidiEvent (MidiMessage (0xb8, 0x0b, 0x66)); // unrelated CC msg
|
||||
layout.processNextMidiEvent (MidiMessage (0xb1, 0x06, 0x03)); // RPN part 3
|
||||
layout.processNextMidiEvent (MidiMessage (0x90, 0x60, 0x00)); // unrelated note-on msg
|
||||
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
|
||||
|
||||
expectEquals (layout.getNumZones(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1);
|
||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 3);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,22 +25,31 @@ namespace juce
|
|||
|
||||
//==============================================================================
|
||||
/**
|
||||
This class represents the current MPE zone layout of a device
|
||||
capable of handling MPE.
|
||||
This class represents the current MPE zone layout of a device capable of handling MPE.
|
||||
|
||||
An MPE device can have up to two zones: a lower zone with master channel 1 and
|
||||
allocated MIDI channels increasing from channel 2, and an upper zone with master
|
||||
channel 16 and allocated MIDI channels decreasing from channel 15. MPE mode is
|
||||
enabled on a device when one of these zones is active and disabled when both
|
||||
are inactive.
|
||||
|
||||
Use the MPEMessages helper class to convert the zone layout represented
|
||||
by this object to MIDI message sequences that you can send to an Expressive
|
||||
MIDI device to set its zone layout, add zones etc.
|
||||
|
||||
@see MPEZone, MPEInstrument
|
||||
@see MPEInstrument
|
||||
*/
|
||||
class JUCE_API MPEZoneLayout
|
||||
{
|
||||
public:
|
||||
/** Default constructor.
|
||||
|
||||
This will create a layout with no MPE zones.
|
||||
You can add an MPE zone using the method addZone.
|
||||
This will create a layout with inactive lower and upper zones, representing
|
||||
a device with MPE mode disabled.
|
||||
|
||||
You can set the lower or upper MPE zones using the setZone() method.
|
||||
|
||||
@see setZone
|
||||
*/
|
||||
MPEZoneLayout() noexcept;
|
||||
|
||||
|
|
@ -54,25 +63,89 @@ public:
|
|||
*/
|
||||
MPEZoneLayout& operator= (const MPEZoneLayout& other);
|
||||
|
||||
/** Adds a new MPE zone to the layout.
|
||||
//==============================================================================
|
||||
/**
|
||||
This struct represents an MPE zone.
|
||||
|
||||
@param newZone The zone to add.
|
||||
It can either be a lower or an upper zone, where:
|
||||
- A lower zone encompasses master channel 1 and an arbitrary number of ascending
|
||||
MIDI channels, increasing from channel 2.
|
||||
- An upper zone encompasses master channel 16 and an arbitrary number of descending
|
||||
MIDI channels, decreasing from channel 15.
|
||||
|
||||
@return true if the zone was added without modifying any other zones
|
||||
added previously to the same zone layout object (if any);
|
||||
false if any existing MPE zones had to be truncated
|
||||
or deleted entirely in order to to add this new zone.
|
||||
(Note: the zone itself will always be added with the channel bounds
|
||||
that were specified; this will not fail.)
|
||||
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
|
||||
master pitchbends, respectively.
|
||||
*/
|
||||
bool addZone (MPEZone newZone);
|
||||
struct Zone
|
||||
{
|
||||
Zone (const Zone& other) noexcept
|
||||
: numMemberChannels (other.numMemberChannels),
|
||||
perNotePitchbendRange (other.perNotePitchbendRange),
|
||||
masterPitchbendRange (other.masterPitchbendRange),
|
||||
lowerZone (other.lowerZone)
|
||||
{
|
||||
}
|
||||
|
||||
/** Removes all currently present MPE zones. */
|
||||
bool isLowerZone() const noexcept { return lowerZone; }
|
||||
bool isUpperZone() const noexcept { return ! lowerZone; }
|
||||
|
||||
bool isActive() const noexcept { return numMemberChannels > 0; }
|
||||
|
||||
int getMasterChannel() const noexcept { return lowerZone ? 1 : 16; }
|
||||
int getFirstMemberChannel() const noexcept { return lowerZone ? 2 : 15; }
|
||||
int getLastMemberChannel() const noexcept { return lowerZone ? (1 + numMemberChannels)
|
||||
: (16 - numMemberChannels); }
|
||||
|
||||
bool isUsingChannelAsMemberChannel (int channel) const noexcept
|
||||
{
|
||||
return lowerZone ? (channel > 1 && channel <= 1 + numMemberChannels)
|
||||
: (channel < 16 && channel >= 16 - numMemberChannels);
|
||||
}
|
||||
|
||||
int numMemberChannels;
|
||||
int perNotePitchbendRange;
|
||||
int masterPitchbendRange;
|
||||
|
||||
private:
|
||||
friend class MPEZoneLayout;
|
||||
|
||||
Zone (bool lower, int memberChans = 0, int perNotePb = 48, int masterPb = 2) noexcept
|
||||
: numMemberChannels (memberChans),
|
||||
perNotePitchbendRange (perNotePb),
|
||||
masterPitchbendRange (masterPb),
|
||||
lowerZone (lower)
|
||||
{
|
||||
}
|
||||
|
||||
bool lowerZone;
|
||||
};
|
||||
|
||||
/** Sets the lower zone of this layout. */
|
||||
void setLowerZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2) noexcept;
|
||||
|
||||
/** Sets the upper zone of this layout. */
|
||||
void setUpperZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2) noexcept;
|
||||
|
||||
/** Returns a struct representing the lower MPE zone. */
|
||||
const Zone getLowerZone() const noexcept { return lowerZone; }
|
||||
|
||||
/** Returns a struct representing the upper MPE zone. */
|
||||
const Zone getUpperZone() const noexcept { return upperZone; }
|
||||
|
||||
/** Clears the lower and upper zones of this layout, making them both inactive
|
||||
and disabling MPE mode.
|
||||
*/
|
||||
void clearAllZones();
|
||||
|
||||
//==============================================================================
|
||||
/** Pass incoming MIDI messages to an object of this class if you want the
|
||||
zone layout to properly react to MPE RPN messages like an
|
||||
MPE device.
|
||||
|
||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
|
||||
set the per-note or master pitchbend ranges.
|
||||
|
||||
|
|
@ -85,6 +158,7 @@ public:
|
|||
/** Pass incoming MIDI buffers to an object of this class if you want the
|
||||
zone layout to properly react to MPE RPN messages like an
|
||||
MPE device.
|
||||
|
||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
|
||||
set the per-note or master pitchbend ranges.
|
||||
|
||||
|
|
@ -94,40 +168,6 @@ public:
|
|||
*/
|
||||
void processNextMidiBuffer (const MidiBuffer& buffer);
|
||||
|
||||
/** Returns the current number of MPE zones. */
|
||||
int getNumZones() const noexcept;
|
||||
|
||||
/** Returns a pointer to the MPE zone at the given index, or nullptr if there
|
||||
is no such zone. Zones are sorted by insertion order (most recently added
|
||||
zone last).
|
||||
*/
|
||||
MPEZone* getZoneByIndex (int index) noexcept;
|
||||
const MPEZone* getZoneByIndex (int index) const noexcept;
|
||||
|
||||
/** Returns a pointer to the zone which uses the specified channel (1-16),
|
||||
or nullptr if there is no such zone.
|
||||
*/
|
||||
MPEZone* getZoneByChannel (int midiChannel) noexcept;
|
||||
const MPEZone* getZoneByChannel (int midiChannel) const noexcept;
|
||||
|
||||
/** Returns a pointer to the zone which has the specified channel (1-16)
|
||||
as its master channel, or nullptr if there is no such zone.
|
||||
*/
|
||||
MPEZone* getZoneByMasterChannel (int midiChannel) noexcept;
|
||||
const MPEZone* getZoneByMasterChannel (int midiChannel) const noexcept;
|
||||
|
||||
/** Returns a pointer to the zone which has the specified channel (1-16)
|
||||
as its first note channel, or nullptr if there is no such zone.
|
||||
*/
|
||||
MPEZone* getZoneByFirstNoteChannel (int midiChannel) noexcept;
|
||||
const MPEZone* getZoneByFirstNoteChannel (int midiChannel) const noexcept;
|
||||
|
||||
/** Returns a pointer to the zone which has the specified channel (1-16)
|
||||
as one of its note channels, or nullptr if there is no such zone.
|
||||
*/
|
||||
MPEZone* getZoneByNoteChannel (int midiChannel) noexcept;
|
||||
const MPEZone* getZoneByNoteChannel (int midiChannel) const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Listener class. Derive from this class to allow your class to be
|
||||
notified about changes to the zone layout.
|
||||
|
|
@ -154,14 +194,24 @@ public:
|
|||
|
||||
private:
|
||||
//==============================================================================
|
||||
Array<MPEZone> zones;
|
||||
Zone lowerZone { true, 0 };
|
||||
Zone upperZone { false, 0 };
|
||||
|
||||
MidiRPNDetector rpnDetector;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
//==============================================================================
|
||||
void setZone (bool, int, int, int) noexcept;
|
||||
|
||||
void processRpnMessage (MidiRPNMessage);
|
||||
void processZoneLayoutRpnMessage (MidiRPNMessage);
|
||||
void processPitchbendRangeRpnMessage (MidiRPNMessage);
|
||||
|
||||
void updateMasterPitchbend (Zone&, int);
|
||||
void updatePerNotePitchbendRange (Zone&, int);
|
||||
|
||||
void sendLayoutChangeMessage();
|
||||
void checkAndLimitZoneParameters (int, int, int&) noexcept;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue