@@ -4,6 +4,34 @@ JUCE breaking changes | |||||
Develop | 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 | 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. | 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 | Change | ||||
------ | ------ | ||||
On Windows, release builds will now link to the dynamic C++ runtime by default | On Windows, release builds will now link to the dynamic C++ runtime by default | ||||
@@ -67,13 +67,13 @@ | |||||
#include "midi/juce_MidiRPN.cpp" | #include "midi/juce_MidiRPN.cpp" | ||||
#include "mpe/juce_MPEValue.cpp" | #include "mpe/juce_MPEValue.cpp" | ||||
#include "mpe/juce_MPENote.cpp" | #include "mpe/juce_MPENote.cpp" | ||||
#include "mpe/juce_MPEZone.cpp" | |||||
#include "mpe/juce_MPEZoneLayout.cpp" | #include "mpe/juce_MPEZoneLayout.cpp" | ||||
#include "mpe/juce_MPEInstrument.cpp" | #include "mpe/juce_MPEInstrument.cpp" | ||||
#include "mpe/juce_MPEMessages.cpp" | #include "mpe/juce_MPEMessages.cpp" | ||||
#include "mpe/juce_MPESynthesiserBase.cpp" | #include "mpe/juce_MPESynthesiserBase.cpp" | ||||
#include "mpe/juce_MPESynthesiserVoice.cpp" | #include "mpe/juce_MPESynthesiserVoice.cpp" | ||||
#include "mpe/juce_MPESynthesiser.cpp" | #include "mpe/juce_MPESynthesiser.cpp" | ||||
#include "mpe/juce_MPEUtils.cpp" | |||||
#include "sources/juce_BufferingAudioSource.cpp" | #include "sources/juce_BufferingAudioSource.cpp" | ||||
#include "sources/juce_ChannelRemappingAudioSource.cpp" | #include "sources/juce_ChannelRemappingAudioSource.cpp" | ||||
#include "sources/juce_IIRFilterAudioSource.cpp" | #include "sources/juce_IIRFilterAudioSource.cpp" | ||||
@@ -98,13 +98,13 @@ | |||||
#include "midi/juce_MidiRPN.h" | #include "midi/juce_MidiRPN.h" | ||||
#include "mpe/juce_MPEValue.h" | #include "mpe/juce_MPEValue.h" | ||||
#include "mpe/juce_MPENote.h" | #include "mpe/juce_MPENote.h" | ||||
#include "mpe/juce_MPEZone.h" | |||||
#include "mpe/juce_MPEZoneLayout.h" | #include "mpe/juce_MPEZoneLayout.h" | ||||
#include "mpe/juce_MPEInstrument.h" | #include "mpe/juce_MPEInstrument.h" | ||||
#include "mpe/juce_MPEMessages.h" | #include "mpe/juce_MPEMessages.h" | ||||
#include "mpe/juce_MPESynthesiserBase.h" | #include "mpe/juce_MPESynthesiserBase.h" | ||||
#include "mpe/juce_MPESynthesiserVoice.h" | #include "mpe/juce_MPESynthesiserVoice.h" | ||||
#include "mpe/juce_MPESynthesiser.h" | #include "mpe/juce_MPESynthesiser.h" | ||||
#include "mpe/juce_MPEUtils.h" | |||||
#include "sources/juce_AudioSource.h" | #include "sources/juce_AudioSource.h" | ||||
#include "sources/juce_PositionableAudioSource.h" | #include "sources/juce_PositionableAudioSource.h" | ||||
#include "sources/juce_BufferingAudioSource.h" | #include "sources/juce_BufferingAudioSource.h" | ||||
@@ -34,7 +34,7 @@ MPEInstrument::MPEInstrument() noexcept | |||||
{ | { | ||||
std::fill_n (lastPressureLowerBitReceivedOnChannel, 16, noLSBValueReceived); | std::fill_n (lastPressureLowerBitReceivedOnChannel, 16, noLSBValueReceived); | ||||
std::fill_n (lastTimbreLowerBitReceivedOnChannel, 16, noLSBValueReceived); | std::fill_n (lastTimbreLowerBitReceivedOnChannel, 16, noLSBValueReceived); | ||||
std::fill_n (isNoteChannelSustained, 16, false); | |||||
std::fill_n (isMemberChannelSustained, 16, false); | |||||
pitchbendDimension.value = &MPENote::pitchbend; | pitchbendDimension.value = &MPENote::pitchbend; | ||||
pressureDimension.value = &MPENote::pressure; | pressureDimension.value = &MPENote::pressure; | ||||
@@ -144,12 +144,12 @@ void MPEInstrument::processNextMidiEvent (const MidiMessage& message) | |||||
{ | { | ||||
zoneLayout.processNextMidiEvent (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())) | 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;) | for (auto i = notes.size(); --i >= 0;) | ||||
{ | { | ||||
auto& note = notes.getReference (i); | auto& note = notes.getReference (i); | ||||
if (zone->isUsingChannelAsNoteChannel (note.midiChannel)) | |||||
if (zone.isUsingChannelAsMemberChannel (note.midiChannel)) | |||||
{ | { | ||||
note.keyState = MPENote::off; | note.keyState = MPENote::off; | ||||
note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number | note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number | ||||
@@ -280,7 +283,7 @@ void MPEInstrument::noteOn (int midiChannel, | |||||
int midiNoteNumber, | int midiNoteNumber, | ||||
MPEValue midiNoteOnVelocity) | MPEValue midiNoteOnVelocity) | ||||
{ | { | ||||
if (! isNoteChannel (midiChannel)) | |||||
if (! isMemberChannel (midiChannel)) | |||||
return; | return; | ||||
MPENote newNote (midiChannel, | MPENote newNote (midiChannel, | ||||
@@ -289,7 +292,7 @@ void MPEInstrument::noteOn (int midiChannel, | |||||
getInitialValueForNewNote (midiChannel, pitchbendDimension), | getInitialValueForNewNote (midiChannel, pitchbendDimension), | ||||
getInitialValueForNewNote (midiChannel, pressureDimension), | getInitialValueForNewNote (midiChannel, pressureDimension), | ||||
getInitialValueForNewNote (midiChannel, timbreDimension), | getInitialValueForNewNote (midiChannel, timbreDimension), | ||||
isNoteChannelSustained[midiChannel - 1] ? MPENote::keyDownAndSustained : MPENote::keyDown); | |||||
isMemberChannelSustained[midiChannel - 1] ? MPENote::keyDownAndSustained : MPENote::keyDown); | |||||
const ScopedLock sl (lock); | const ScopedLock sl (lock); | ||||
updateNoteTotalPitchbend (newNote); | updateNoteTotalPitchbend (newNote); | ||||
@@ -312,7 +315,7 @@ void MPEInstrument::noteOff (int midiChannel, | |||||
int midiNoteNumber, | int midiNoteNumber, | ||||
MPEValue midiNoteOffVelocity) | MPEValue midiNoteOffVelocity) | ||||
{ | { | ||||
if (notes.isEmpty() || ! isNoteChannel (midiChannel)) | |||||
if (notes.isEmpty() || ! isMemberChannel (midiChannel)) | |||||
return; | return; | ||||
const ScopedLock sl (lock); | const ScopedLock sl (lock); | ||||
@@ -375,11 +378,11 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M | |||||
if (notes.isEmpty()) | if (notes.isEmpty()) | ||||
return; | 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) | 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;) | for (auto i = notes.size(); --i >= 0;) | ||||
{ | { | ||||
auto& note = notes.getReference (i); | auto& note = notes.getReference (i); | ||||
if (! channels.contains (note.midiChannel)) | |||||
if (! zone.isUsingChannelAsMemberChannel (note.midiChannel)) | |||||
continue; | continue; | ||||
if (&dimension == &pitchbendDimension) | if (&dimension == &pitchbendDimension) | ||||
@@ -457,17 +464,29 @@ void MPEInstrument::updateNoteTotalPitchbend (MPENote& note) | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
if (auto* zone = zoneLayout.getZoneByNoteChannel (note.midiChannel)) | |||||
{ | |||||
auto notePitchbendInSemitones = note.pitchbend.asSignedFloat() * zone->getPerNotePitchbendRange(); | |||||
auto masterPitchbendInSemitones = pitchbendDimension.lastValueReceivedOnChannel[zone->getMasterChannel() - 1].asSignedFloat() * zone->getMasterPitchbendRange(); | |||||
note.totalPitchbendInSemitones = notePitchbendInSemitones + masterPitchbendInSemitones; | |||||
} | |||||
else | |||||
auto zone = zoneLayout.getLowerZone(); | |||||
if (! zone.isUsingChannelAsMemberChannel (note.midiChannel)) | |||||
{ | { | ||||
// 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 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). | // 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; | return; | ||||
auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone() | |||||
: zoneLayout.getUpperZone()); | |||||
for (auto i = notes.size(); --i >= 0;) | for (auto i = notes.size(); --i >= 0;) | ||||
{ | { | ||||
auto& note = notes.getReference (i); | 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) | if (note.keyState == MPENote::keyDown && isDown) | ||||
note.keyState = MPENote::keyDownAndSustained; | note.keyState = MPENote::keyDownAndSustained; | ||||
@@ -523,20 +543,29 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool | |||||
if (! isSostenuto) | if (! isSostenuto) | ||||
{ | { | ||||
if (legacyMode.isEnabled) | if (legacyMode.isEnabled) | ||||
isNoteChannelSustained[midiChannel - 1] = isDown; | |||||
{ | |||||
isMemberChannelSustained[midiChannel - 1] = isDown; | |||||
} | |||||
else | 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) | if (legacyMode.isEnabled) | ||||
return legacyMode.channelRange.contains (midiChannel); | 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 | bool MPEInstrument::isMasterChannel (int midiChannel) const noexcept | ||||
@@ -544,9 +573,8 @@ bool MPEInstrument::isMasterChannel (int midiChannel) const noexcept | |||||
if (legacyMode.isEnabled) | if (legacyMode.isEnabled) | ||||
return false; | return false; | ||||
return zoneLayout.getZoneByMasterChannel (midiChannel) != nullptr; | |||||
return (midiChannel == 1 || midiChannel == 16); | |||||
} | } | ||||
//============================================================================== | //============================================================================== | ||||
int MPEInstrument::getNumPlayingNotes() const noexcept | int MPEInstrument::getNumPlayingNotes() const noexcept | ||||
{ | { | ||||
@@ -725,13 +753,13 @@ public: | |||||
MPEInstrumentTests() | MPEInstrumentTests() | ||||
: UnitTest ("MPEInstrument class", "MIDI/MPE") | : 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 | // 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 | void runTest() override | ||||
@@ -739,7 +767,8 @@ public: | |||||
beginTest ("initial zone layout"); | beginTest ("initial zone layout"); | ||||
{ | { | ||||
MPEInstrument test; | MPEInstrument test; | ||||
expectEquals (test.getZoneLayout().getNumZones(), 0); | |||||
expect (! test.getZoneLayout().getLowerZone().isActive()); | |||||
expect (! test.getZoneLayout().getUpperZone().isActive()); | |||||
} | } | ||||
beginTest ("get/setZoneLayout"); | beginTest ("get/setZoneLayout"); | ||||
@@ -747,12 +776,14 @@ public: | |||||
MPEInstrument test; | MPEInstrument test; | ||||
test.setZoneLayout (testLayout); | 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"); | beginTest ("noteOn / noteOff"); | ||||
@@ -767,16 +798,16 @@ public: | |||||
test.setZoneLayout (testLayout); | test.setZoneLayout (testLayout); | ||||
// note-on on master channel - ignore | // note-on on master channel - ignore | ||||
test.noteOn (9, 60, MPEValue::from7BitInt (100)); | |||||
test.noteOn (1, 60, MPEValue::from7BitInt (100)); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
expectEquals (test.noteAddedCallCounter, 0); | expectEquals (test.noteAddedCallCounter, 0); | ||||
// note-on on any other channel - ignore | // note-on on any other channel - ignore | ||||
test.noteOn (1, 60, MPEValue::from7BitInt (100)); | |||||
test.noteOn (7, 60, MPEValue::from7BitInt (100)); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
expectEquals (test.noteAddedCallCounter, 0); | expectEquals (test.noteAddedCallCounter, 0); | ||||
// note-on on note channel - create new note | |||||
// note-on on member channel - create new note | |||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | test.noteOn (3, 60, MPEValue::from7BitInt (100)); | ||||
expectEquals (test.getNumPlayingNotes(), 1); | expectEquals (test.getNumPlayingNotes(), 1); | ||||
expectEquals (test.noteAddedCallCounter, 1); | expectEquals (test.noteAddedCallCounter, 1); | ||||
@@ -861,38 +892,37 @@ public: | |||||
{ | { | ||||
UnitTestInstrument test; | UnitTestInstrument test; | ||||
test.setZoneLayout (testLayout); | 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. | // sustain pedal on per-note channel shouldn't do anything. | ||||
test.sustainPedal (3, true); | 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 (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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 0); | expectEquals (test.noteKeyStateChangedCallCounter, 0); | ||||
// sustain pedal on non-zone channel shouldn't do anything either. | // 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 (3, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 0); | 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 (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained); | ||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 1); | expectEquals (test.noteKeyStateChangedCallCounter, 1); | ||||
// release | // release | ||||
test.sustainPedal (2, false); | |||||
test.sustainPedal (1, false); | |||||
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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 2); | expectEquals (test.noteKeyStateChangedCallCounter, 2); | ||||
// should also sustain new notes added after the press | // should also sustain new notes added after the press | ||||
test.sustainPedal (2, true); | |||||
test.sustainPedal (1, true); | |||||
expectEquals (test.noteKeyStateChangedCallCounter, 3); | expectEquals (test.noteKeyStateChangedCallCounter, 3); | ||||
test.noteOn (4, 51, MPEValue::from7BitInt (100)); | test.noteOn (4, 51, MPEValue::from7BitInt (100)); | ||||
expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::keyDownAndSustained); | 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); | expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::sustained); | ||||
// notes should be turned off when pedal is released | // notes should be turned off when pedal is released | ||||
test.sustainPedal (2, false); | |||||
test.sustainPedal (1, false); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
expectEquals (test.noteReleasedCallCounter, 4); | expectEquals (test.noteReleasedCallCounter, 4); | ||||
} | } | ||||
@@ -925,8 +955,8 @@ public: | |||||
{ | { | ||||
UnitTestInstrument test; | UnitTestInstrument test; | ||||
test.setZoneLayout (testLayout); | 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. | // sostenuto pedal on per-note channel shouldn't do anything. | ||||
test.sostenutoPedal (3, true); | test.sostenutoPedal (3, true); | ||||
@@ -935,25 +965,25 @@ public: | |||||
expectEquals (test.noteKeyStateChangedCallCounter, 0); | expectEquals (test.noteKeyStateChangedCallCounter, 0); | ||||
// sostenuto pedal on non-zone channel shouldn't do anything either. | // 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 (3, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 0); | expectEquals (test.noteKeyStateChangedCallCounter, 0); | ||||
// sostenuto pedal on master channel should sustain notes on *that* zone. | // 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 (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained); | ||||
expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 1); | expectEquals (test.noteKeyStateChangedCallCounter, 1); | ||||
// release | // release | ||||
test.sostenutoPedal (2, false); | |||||
test.sostenutoPedal (1, false); | |||||
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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteKeyStateChangedCallCounter, 2); | expectEquals (test.noteKeyStateChangedCallCounter, 2); | ||||
// should only sustain notes turned on *before* the press (difference to sustain pedal) | // 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); | expectEquals (test.noteKeyStateChangedCallCounter, 3); | ||||
test.noteOn (4, 51, MPEValue::from7BitInt (100)); | test.noteOn (4, 51, MPEValue::from7BitInt (100)); | ||||
expectEquals (test.getNumPlayingNotes(), 3); | expectEquals (test.getNumPlayingNotes(), 3); | ||||
@@ -973,7 +1003,7 @@ public: | |||||
expectEquals (test.noteKeyStateChangedCallCounter, 4); | expectEquals (test.noteKeyStateChangedCallCounter, 4); | ||||
// notes should be turned off when pedal is released | // notes should be turned off when pedal is released | ||||
test.sustainPedal (2, false); | |||||
test.sustainPedal (1, false); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
expectEquals (test.noteReleasedCallCounter, 3); | expectEquals (test.noteReleasedCallCounter, 3); | ||||
} | } | ||||
@@ -987,31 +1017,31 @@ public: | |||||
test.noteOn (3, 61, MPEValue::from7BitInt (100)); | test.noteOn (3, 61, MPEValue::from7BitInt (100)); | ||||
{ | { | ||||
MPENote note = test.getMostRecentNote (2); | |||||
auto note = test.getMostRecentNote (2); | |||||
expect (! note.isValid()); | expect (! note.isValid()); | ||||
} | } | ||||
{ | { | ||||
MPENote note = test.getMostRecentNote (3); | |||||
auto note = test.getMostRecentNote (3); | |||||
expect (note.isValid()); | expect (note.isValid()); | ||||
expectEquals (int (note.midiChannel), 3); | expectEquals (int (note.midiChannel), 3); | ||||
expectEquals (int (note.initialNote), 61); | expectEquals (int (note.initialNote), 61); | ||||
} | } | ||||
test.sustainPedal (2, true); | |||||
test.sustainPedal (1, true); | |||||
test.noteOff (3, 61, MPEValue::from7BitInt (100)); | test.noteOff (3, 61, MPEValue::from7BitInt (100)); | ||||
{ | { | ||||
MPENote note = test.getMostRecentNote (3); | |||||
auto note = test.getMostRecentNote (3); | |||||
expect (note.isValid()); | expect (note.isValid()); | ||||
expectEquals (int (note.midiChannel), 3); | expectEquals (int (note.midiChannel), 3); | ||||
expectEquals (int (note.initialNote), 60); | expectEquals (int (note.initialNote), 60); | ||||
} | } | ||||
test.sustainPedal (2, false); | |||||
test.sustainPedal (1, false); | |||||
test.noteOff (3, 60, MPEValue::from7BitInt (100)); | test.noteOff (3, 60, MPEValue::from7BitInt (100)); | ||||
{ | { | ||||
MPENote note = test.getMostRecentNote (3); | |||||
auto note = test.getMostRecentNote (3); | |||||
expect (! note.isValid()); | expect (! note.isValid()); | ||||
} | } | ||||
} | } | ||||
@@ -1074,14 +1104,14 @@ public: | |||||
expectEquals (test.notePressureChangedCallCounter, 1); | expectEquals (test.notePressureChangedCallCounter, 1); | ||||
// applying pressure on a master channel should modulate all notes in this zone | // 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 (3, 60), 100, 44, 8192, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.notePressureChangedCallCounter, 3); | expectEquals (test.notePressureChangedCallCounter, 3); | ||||
// applying pressure on an unrelated channel should be ignored | // 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 (3, 60), 100, 44, 8192, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 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); | 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. | // value of per-note pitchbend. Tests covering master pitchbend below. | ||||
// Note: noteChanged will be called anyway for notes in that zone | // Note: noteChanged will be called anyway for notes in that zone | ||||
// because the total pitchbend for those notes has changed | // 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 (3, 60), 100, 0, 1111, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.notePitchbendChangedCallCounter, 3); | expectEquals (test.notePitchbendChangedCallCounter, 3); | ||||
// applying pitchbend on an unrelated channel should do nothing. | // 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 (3, 60), 100, 0, 1111, 64, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 60), 100, 0, 8192, 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); | 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. | // - the first note should not be bent, only the second one. | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | test.noteOn (3, 60, MPEValue::from7BitInt (100)); | ||||
test.sustainPedal (2, true); | |||||
test.sustainPedal (1, true); | |||||
test.noteOff (3, 60, MPEValue::from7BitInt (64)); | test.noteOff (3, 60, MPEValue::from7BitInt (64)); | ||||
expectEquals (test.getNumPlayingNotes(), 1); | expectEquals (test.getNumPlayingNotes(), 1); | ||||
expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); | expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); | ||||
@@ -1275,19 +1305,19 @@ public: | |||||
test.pitchbend (3, MPEValue::from14BitInt (4096)); | test.pitchbend (3, MPEValue::from14BitInt (4096)); | ||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -24.0, 0.01); | expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -24.0, 0.01); | ||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (96); | |||||
layout.setLowerZone (5, 96); | |||||
test.setZoneLayout (layout); | test.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | test.noteOn (3, 60, MPEValue::from7BitInt (100)); | ||||
test.pitchbend (3, MPEValue::from14BitInt (0)); // -max | test.pitchbend (3, MPEValue::from14BitInt (0)); // -max | ||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); | expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); | ||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (1); | |||||
layout.setLowerZone (5, 1); | |||||
test.setZoneLayout (layout); | test.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | test.noteOn (3, 60, MPEValue::from7BitInt (100)); | ||||
test.pitchbend (3, MPEValue::from14BitInt (16383)); // +max | test.pitchbend (3, MPEValue::from14BitInt (16383)); // +max | ||||
expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01); | 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.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | test.noteOn (3, 60, MPEValue::from7BitInt (100)); | ||||
test.pitchbend (3, MPEValue::from14BitInt (12345)); | test.pitchbend (3, MPEValue::from14BitInt (12345)); | ||||
@@ -1301,25 +1331,25 @@ public: | |||||
MPEZoneLayout layout = testLayout; | MPEZoneLayout layout = testLayout; | ||||
test.setZoneLayout (layout); // default should be +/- 2 semitones | test.setZoneLayout (layout); // default should be +/- 2 semitones | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | 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); | expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -1.0, 0.01); | ||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (96); | |||||
layout.setLowerZone (5, 48, 96); | |||||
test.setZoneLayout (layout); | test.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | 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); | expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); | ||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (1); | |||||
layout.setLowerZone (5, 48, 1); | |||||
test.setZoneLayout (layout); | test.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | 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); | 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.setZoneLayout (layout); | ||||
test.noteOn (3, 60, MPEValue::from7BitInt (100)); | 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); | expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01); | ||||
} | } | ||||
{ | { | ||||
@@ -1329,11 +1359,10 @@ public: | |||||
UnitTestInstrument test; | UnitTestInstrument test; | ||||
MPEZoneLayout layout = testLayout; | MPEZoneLayout layout = testLayout; | ||||
layout.getZoneByIndex (0)->setPerNotePitchbendRange (12); | |||||
layout.getZoneByIndex (0)->setMasterPitchbendRange (1); | |||||
layout.setLowerZone (5, 12, 1); | |||||
test.setZoneLayout (layout); | 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 | test.pitchbend (3, MPEValue::from14BitInt (0)); // per-note pitchbend 12 semitones down | ||||
// additionally, note should react to both pitchbend messages | // additionally, note should react to both pitchbend messages | ||||
// correctly even if they arrived before the note-on. | // correctly even if they arrived before the note-on. | ||||
@@ -1360,14 +1389,14 @@ public: | |||||
expectEquals (test.noteTimbreChangedCallCounter, 1); | expectEquals (test.noteTimbreChangedCallCounter, 1); | ||||
// modulating timbre on a master channel should modulate all notes in this zone | // 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 (3, 60), 100, 0, 8192, 44, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
expectEquals (test.noteTimbreChangedCallCounter, 3); | expectEquals (test.noteTimbreChangedCallCounter, 3); | ||||
// modulating timbre on an unrelated channel should be ignored | // 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 (3, 60), 100, 0, 8192, 44, MPENote::keyDown); | ||||
expectNote (test.getNote (4, 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); | expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); | ||||
@@ -1727,8 +1756,8 @@ public: | |||||
MPEInstrument test; | MPEInstrument test; | ||||
MidiBuffer buffer; | 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); | MidiBuffer::Iterator iter (buffer); | ||||
MidiMessage message; | MidiMessage message; | ||||
@@ -1737,11 +1766,12 @@ public: | |||||
while (iter.getNextEvent (message, samplePosition)) | while (iter.getNextEvent (message, samplePosition)) | ||||
test.processNextMidiEvent (message); | 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"); | beginTest ("MIDI all notes off"); | ||||
@@ -1755,17 +1785,17 @@ public: | |||||
expectEquals (test.getNumPlayingNotes(), 4); | expectEquals (test.getNumPlayingNotes(), 4); | ||||
// on note channel: ignore. | // on note channel: ignore. | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (3)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (3)); | |||||
expectEquals (test.getNumPlayingNotes(), 4); | expectEquals (test.getNumPlayingNotes(), 4); | ||||
// on unused channel: ignore. | // on unused channel: ignore. | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (1)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (9)); | |||||
expectEquals (test.getNumPlayingNotes(), 4); | expectEquals (test.getNumPlayingNotes(), 4); | ||||
// on master channel: release notes in that zone only. | // on master channel: release notes in that zone only. | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (2)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (1)); | |||||
expectEquals (test.getNumPlayingNotes(), 2); | expectEquals (test.getNumPlayingNotes(), 2); | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (9)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (16)); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
} | } | ||||
@@ -1779,13 +1809,13 @@ public: | |||||
test.noteOn (15, 63, MPEValue::from7BitInt (100)); | test.noteOn (15, 63, MPEValue::from7BitInt (100)); | ||||
expectEquals (test.getNumPlayingNotes(), 4); | expectEquals (test.getNumPlayingNotes(), 4); | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (3)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (3)); | |||||
expectEquals (test.getNumPlayingNotes(), 3); | expectEquals (test.getNumPlayingNotes(), 3); | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (15)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (15)); | |||||
expectEquals (test.getNumPlayingNotes(), 1); | expectEquals (test.getNumPlayingNotes(), 1); | ||||
test.processNextMidiEvent (MidiMessage::allNotesOff (4)); | |||||
test.processNextMidiEvent (MidiMessage::allControllersOff (4)); | |||||
expectEquals (test.getNumPlayingNotes(), 0); | expectEquals (test.getNumPlayingNotes(), 0); | ||||
} | } | ||||
@@ -31,8 +31,8 @@ namespace juce | |||||
active (playing) notes and the values of their dimensions of expression. | active (playing) notes and the values of their dimensions of expression. | ||||
You can trigger and modulate notes: | 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 | 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 | MPE. If you pass it a message, it will know what notes on what | ||||
@@ -53,9 +53,9 @@ namespace juce | |||||
class JUCE_API MPEInstrument | class JUCE_API MPEInstrument | ||||
{ | { | ||||
public: | public: | ||||
/** Constructor. | /** 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 | In order to process incoming MIDI, call setZoneLayout, define the layout | ||||
via MIDI RPN messages, or set the instrument to legacy mode. | 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 | /** Returns true if the given MIDI channel (1-16) is a note channel in any | ||||
of the MPEInstrument's MPE zones; false otherwise. | of the MPEInstrument's MPE zones; false otherwise. | ||||
When in legacy mode, this will return true if the given channel is | When in legacy mode, this will return true if the given channel is | ||||
contained in the current legacy mode channel range; false otherwise. | 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; | bool isMasterChannel (int midiChannel) const noexcept; | ||||
@@ -132,19 +134,23 @@ public: | |||||
//============================================================================== | //============================================================================== | ||||
/** Request a note-on on the given channel, with the given initial note | /** Request a note-on on the given channel, with the given initial note | ||||
number and velocity. | number and velocity. | ||||
If the message arrives on a valid note channel, this will create a | If the message arrives on a valid note channel, this will create a | ||||
new MPENote and call the noteAdded callback. | new MPENote and call the noteAdded callback. | ||||
*/ | */ | ||||
virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity); | 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); | virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity); | ||||
/** Request a pitchbend on the given channel with the given value (in units | /** Request a pitchbend on the given channel with the given value (in units | ||||
of MIDI pitchwheel position). | of MIDI pitchwheel position). | ||||
Internally, this will determine whether the pitchwheel move is a | Internally, this will determine whether the pitchwheel move is a | ||||
per-note pitchbend or a master pitchbend (depending on midiChannel), | per-note pitchbend or a master pitchbend (depending on midiChannel), | ||||
take the correct per-note or master pitchbend range of the affected MPE | 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); | virtual void pitchbend (int midiChannel, MPEValue pitchbend); | ||||
/** Request a pressure change on the given channel with the given value. | /** Request a pressure change on the given channel with the given value. | ||||
This will modify the pressure dimension of the note currently held down | 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, | 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. | 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 | /** Request a third dimension (timbre) change on the given channel with the | ||||
given value. | given value. | ||||
This will modify the timbre dimension of the note currently held down | 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, | 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. | the timbre change will be broadcast to all notes in this zone. | ||||
*/ | */ | ||||
virtual void timbre (int midiChannel, MPEValue value); | 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); | 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); | virtual void sostenutoPedal (int midiChannel, bool isDown); | ||||
/** Discard all currently playing notes. | /** Discard all currently playing notes. | ||||
This will also call the noteReleased listener callback for all of them. | This will also call the noteReleased listener callback for all of them. | ||||
*/ | */ | ||||
void releaseAllNotes(); | 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; | 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; | MPENote getNote (int index) const noexcept; | ||||
/** Returns the note currently playing on the given midiChannel with the | /** 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; | MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept; | ||||
/** Returns the most recent note that is playing on the given midiChannel | /** 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 | (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; | 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. | This helper method might be useful for some custom voice handling algorithms. | ||||
*/ | */ | ||||
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept; | MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept; | ||||
@@ -233,32 +241,34 @@ public: | |||||
/** Destructor. */ | /** Destructor. */ | ||||
virtual ~Listener() {} | 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; | 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; | 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 | Note: This can happen if the note itself is bent, if there is a | ||||
master channel pitchbend event, or if both occur simultaneously. | master channel pitchbend event, or if both occur simultaneously. | ||||
Call MPENote::getFrequencyInHertz to get the effective note frequency. | Call MPENote::getFrequencyInHertz to get the effective note frequency. | ||||
*/ | */ | ||||
virtual void notePitchbendChanged (MPENote changedNote) = 0; | 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; | virtual void noteTimbreChanged (MPENote changedNote) = 0; | ||||
/** Implement this callback to be informed whether a currently playing | /** 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 | MPE note's key state (whether the key is down and/or the note is | ||||
sustained) has changed. | sustained) has changed. | ||||
Note: if the key state changes to MPENote::off, noteReleased is | Note: if the key state changes to MPENote::off, noteReleased is | ||||
called instead. | called instead. | ||||
*/ | */ | ||||
@@ -329,7 +339,7 @@ private: | |||||
uint8 lastPressureLowerBitReceivedOnChannel[16]; | uint8 lastPressureLowerBitReceivedOnChannel[16]; | ||||
uint8 lastTimbreLowerBitReceivedOnChannel[16]; | uint8 lastTimbreLowerBitReceivedOnChannel[16]; | ||||
bool isNoteChannelSustained[16]; | |||||
bool isMemberChannelSustained[16]; | |||||
struct LegacyMode | struct LegacyMode | ||||
{ | { | ||||
@@ -351,7 +361,7 @@ private: | |||||
MPEDimension pitchbendDimension, pressureDimension, timbreDimension; | MPEDimension pitchbendDimension, pressureDimension, timbreDimension; | ||||
void updateDimension (int midiChannel, MPEDimension&, MPEValue); | void updateDimension (int midiChannel, MPEDimension&, MPEValue); | ||||
void updateDimensionMaster (const MPEZone&, MPEDimension&, MPEValue); | |||||
void updateDimensionMaster (bool, MPEDimension&, MPEValue); | |||||
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue); | void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue); | ||||
void callListenersDimensionChanged (const MPENote&, const MPEDimension&); | void callListenersDimensionChanged (const MPENote&, const MPEDimension&); | ||||
MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const; | MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const; | ||||
@@ -361,7 +371,7 @@ private: | |||||
void processMidiPitchWheelMessage (const MidiMessage&); | void processMidiPitchWheelMessage (const MidiMessage&); | ||||
void processMidiChannelPressureMessage (const MidiMessage&); | void processMidiChannelPressureMessage (const MidiMessage&); | ||||
void processMidiControllerMessage (const MidiMessage&); | void processMidiControllerMessage (const MidiMessage&); | ||||
void processMidiAllNotesOffMessage (const MidiMessage&); | |||||
void processMidiResetAllControllersMessage (const MidiMessage&); | |||||
void handlePressureMSB (int midiChannel, int value) noexcept; | void handlePressureMSB (int midiChannel, int value) noexcept; | ||||
void handlePressureLSB (int midiChannel, int value) noexcept; | void handlePressureLSB (int midiChannel, int value) noexcept; | ||||
void handleTimbreMSB (int midiChannel, int value) noexcept; | void handleTimbreMSB (int midiChannel, int value) noexcept; | ||||
@@ -23,46 +23,85 @@ | |||||
namespace juce | 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; | 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::setLowerZonePerNotePitchbendRange (int perNotePitchbendRange) | |||||
{ | |||||
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::masterPitchbendRange (MPEZone zone) | |||||
MidiBuffer MPEMessages::setUpperZoneMasterPitchbendRange (int masterPitchbendRange) | |||||
{ | { | ||||
return MidiRPNGenerator::generate (zone.getMasterChannel(), 0, | |||||
zone.getMasterPitchbendRange(), | |||||
false, false); | |||||
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() | 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; | MidiBuffer buffer; | ||||
buffer.addEvents (clearAllZones(), 0, -1, 0); | 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; | return buffer; | ||||
} | } | ||||
@@ -81,11 +120,11 @@ public: | |||||
beginTest ("add zone"); | beginTest ("add zone"); | ||||
{ | { | ||||
{ | { | ||||
MidiBuffer buffer = MPEMessages::addZone (MPEZone (1, 7)); | |||||
MidiBuffer buffer = MPEMessages::setLowerZone (7); | |||||
const uint8 expectedBytes[] = | 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) | 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) | 0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x02 // master pbrange (default = 2) | ||||
}; | }; | ||||
@@ -93,13 +132,13 @@ public: | |||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
} | } | ||||
{ | { | ||||
MidiBuffer buffer = MPEMessages::addZone (MPEZone (11, 5, 96, 0)); | |||||
MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0); | |||||
const uint8 expectedBytes[] = | 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)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
@@ -108,10 +147,9 @@ public: | |||||
beginTest ("set per-note pitchbend range"); | 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)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
} | } | ||||
@@ -119,10 +157,9 @@ public: | |||||
beginTest ("set master pitchbend range"); | 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)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
} | } | ||||
@@ -131,7 +168,9 @@ public: | |||||
{ | { | ||||
MidiBuffer buffer = MPEMessages::clearAllZones(); | 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)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
} | } | ||||
@@ -139,22 +178,21 @@ public: | |||||
beginTest ("set complete state"); | beginTest ("set complete state"); | ||||
{ | { | ||||
MPEZoneLayout layout; | 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); | MidiBuffer buffer = MPEMessages::setZoneLayout (layout); | ||||
const uint8 expectedBytes[] = { | 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) | 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) | 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)); | testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); | ||||
@@ -26,10 +26,9 @@ namespace juce | |||||
//============================================================================== | //============================================================================== | ||||
/** | /** | ||||
This helper class contains the necessary helper functions to generate | 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. | 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 | All other MPE messages like per-note pitchbend, pressure, and third | ||||
dimension, are ordinary MIDI messages that should be created using the MidiMessage | 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 | 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. | 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 | class JUCE_API MPEMessages | ||||
{ | { | ||||
public: | public: | ||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive | /** 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 | /** 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 | /** 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 | /** 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(); | static MidiBuffer clearAllZones(); | ||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive | /** Returns the sequence of MIDI messages that, if sent to an Expressive | ||||
MIDI device, will reset the whole MPE zone layout of the | 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. | /** 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 | Pitchbend range messages (both per-note and master) are instead sent | ||||
on RPN 0 as in standard MIDI 1.0. | on RPN 0 as in standard MIDI 1.0. | ||||
*/ | */ | ||||
@@ -48,6 +48,7 @@ MPENote::MPENote (int midiChannel_, | |||||
noteOnVelocity (noteOnVelocity_), | noteOnVelocity (noteOnVelocity_), | ||||
pitchbend (pitchbend_), | pitchbend (pitchbend_), | ||||
pressure (pressure_), | pressure (pressure_), | ||||
initialTimbre (timbre_), | |||||
timbre (timbre_), | timbre (timbre_), | ||||
keyState (keyState_) | keyState (keyState_) | ||||
{ | { | ||||
@@ -37,6 +37,7 @@ namespace juce | |||||
struct JUCE_API MPENote | struct JUCE_API MPENote | ||||
{ | { | ||||
//============================================================================== | //============================================================================== | ||||
/** Possible values for the note key state. */ | |||||
enum KeyState | enum KeyState | ||||
{ | { | ||||
off = 0, /**< The key is up (off). */ | off = 0, /**< The key is up (off). */ | ||||
@@ -48,8 +49,8 @@ struct JUCE_API MPENote | |||||
//============================================================================== | //============================================================================== | ||||
/** Constructor. | /** 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. | @param initialNote The MIDI note number, between 0 and 127. | ||||
@@ -129,8 +130,13 @@ struct JUCE_API MPENote | |||||
*/ | */ | ||||
MPEValue pressure { MPEValue::centreValue() }; | 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. | This dimension can be modulated while the note sounds. | ||||
*/ | */ | ||||
MPEValue timbre { MPEValue::centreValue() }; | MPEValue timbre { MPEValue::centreValue() }; | ||||
@@ -139,7 +145,7 @@ struct JUCE_API MPENote | |||||
received. | received. | ||||
This dimension will only have a meaningful value after a note-off has | 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 | 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() }; | MPEValue noteOffVelocity { MPEValue::minValue() }; | ||||
@@ -161,7 +167,7 @@ struct JUCE_API MPENote | |||||
KeyState keyState { MPENote::off }; | 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. | the initialNote and the totalPitchbendInSemitones, converted to Hertz. | ||||
*/ | */ | ||||
double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept; | double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept; | ||||
@@ -26,7 +26,7 @@ namespace juce | |||||
MPESynthesiser::MPESynthesiser() | MPESynthesiser::MPESynthesiser() | ||||
{ | { | ||||
MPEZoneLayout zoneLayout; | MPEZoneLayout zoneLayout; | ||||
zoneLayout.addZone ({ 1, 15 }); | |||||
zoneLayout.setLowerZone (15); | |||||
setZoneLayout (zoneLayout); | setZoneLayout (zoneLayout); | ||||
} | } | ||||
@@ -122,7 +122,7 @@ void MPESynthesiser::noteReleased (MPENote finishedNote) | |||||
{ | { | ||||
const ScopedLock sl (voicesLock); | const ScopedLock sl (voicesLock); | ||||
for (int i = voices.size(); --i >= 0;) | |||||
for (auto i = voices.size(); --i >= 0;) | |||||
{ | { | ||||
auto* voice = voices.getUnchecked (i); | auto* voice = voices.getUnchecked (i); | ||||
@@ -139,7 +139,7 @@ void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate) | |||||
turnOffAllVoices (false); | turnOffAllVoices (false); | ||||
for (int i = voices.size(); --i >= 0;) | |||||
for (auto i = voices.size(); --i >= 0;) | |||||
voices.getUnchecked (i)->setCurrentSampleRate (newRate); | voices.getUnchecked (i)->setCurrentSampleRate (newRate); | ||||
} | } | ||||
@@ -287,7 +287,7 @@ void MPESynthesiser::reduceNumVoices (const int newNumVoices) | |||||
while (voices.size() > newNumVoices) | while (voices.size() > newNumVoices) | ||||
{ | { | ||||
if (MPESynthesiserVoice* voice = findFreeVoice ({}, true)) | |||||
if (auto* voice = findFreeVoice ({}, true)) | |||||
voices.removeObject (voice); | voices.removeObject (voice); | ||||
else | else | ||||
voices.remove (0); // if there's no voice to steal, kill the oldest voice | voices.remove (0); // if there's no voice to steal, kill the oldest voice | ||||
@@ -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 |
@@ -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: | public: | ||||
//============================================================================== | //============================================================================== | ||||
/** Default constructor. Constructs an MPEValue corresponding | |||||
to the centre value. | |||||
/** Default constructor. | |||||
Constructs an MPEValue corresponding to the centre value. | |||||
*/ | */ | ||||
MPEValue() noexcept; | MPEValue() noexcept; | ||||
@@ -60,12 +61,14 @@ public: | |||||
static MPEValue maxValue() noexcept; | static MPEValue maxValue() noexcept; | ||||
/** Retrieves the current value as an integer between 0 and 127. | /** Retrieves the current value as an integer between 0 and 127. | ||||
Information will be lost if the value was initialised with a precision | Information will be lost if the value was initialised with a precision | ||||
higher than 7-bit. | higher than 7-bit. | ||||
*/ | */ | ||||
int as7BitInt() const noexcept; | int as7BitInt() const noexcept; | ||||
/** Retrieves the current value as an integer between 0 and 16383. | /** Retrieves the current value as an integer between 0 and 16383. | ||||
Resolution will be lost if the value was initialised with a precision | Resolution will be lost if the value was initialised with a precision | ||||
higher than 14-bit. | 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() noexcept {} | ||||
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other) | MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other) | ||||
: zones (other.zones) | |||||
: lowerZone (other.lowerZone), | |||||
upperZone (other.upperZone) | |||||
{ | { | ||||
} | } | ||||
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other) | MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other) | ||||
{ | { | ||||
zones = other.zones; | |||||
lowerZone = other.lowerZone; | |||||
upperZone = other.upperZone; | |||||
sendLayoutChangeMessage(); | sendLayoutChangeMessage(); | ||||
return *this; | 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); | |||||
if (isLower) | |||||
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange }; | |||||
else | |||||
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange }; | |||||
for (int i = zones.size(); --i >= 0;) | |||||
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(); | 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() | void MPEZoneLayout::clearAllZones() | ||||
{ | { | ||||
zones.clear(); | |||||
lowerZone = { true, 0 }; | |||||
upperZone = { false, 0 }; | |||||
sendLayoutChangeMessage(); | sendLayoutChangeMessage(); | ||||
} | } | ||||
@@ -119,36 +120,53 @@ void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn) | |||||
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn) | void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn) | ||||
{ | { | ||||
if (rpn.value < 16) | 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::processPitchbendRangeRpnMessage (MidiRPNMessage rpn) | |||||
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value) | |||||
{ | { | ||||
if (auto* zone = getZoneByFirstNoteChannel (rpn.channel)) | |||||
if (zone.masterPitchbendRange != value) | |||||
{ | { | ||||
if (zone->getPerNotePitchbendRange() != rpn.value) | |||||
{ | |||||
zone->setPerNotePitchbendRange (rpn.value); | |||||
sendLayoutChangeMessage(); | |||||
return; | |||||
} | |||||
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange); | |||||
zone.masterPitchbendRange = value; | |||||
sendLayoutChangeMessage(); | |||||
} | } | ||||
} | |||||
if (auto* zone = getZoneByMasterChannel (rpn.channel)) | |||||
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value) | |||||
{ | |||||
if (zone.perNotePitchbendRange != value) | |||||
{ | { | ||||
if (zone->getMasterPitchbendRange() != rpn.value) | |||||
{ | |||||
zone->setMasterPitchbendRange (rpn.value); | |||||
sendLayoutChangeMessage(); | |||||
return; | |||||
} | |||||
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange); | |||||
zone.perNotePitchbendRange = value; | |||||
sendLayoutChangeMessage(); | |||||
} | |||||
} | |||||
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn) | |||||
{ | |||||
if (rpn.channel == 1) | |||||
{ | |||||
updateMasterPitchbend (lowerZone, rpn.value); | |||||
} | |||||
else if (rpn.channel == 16) | |||||
{ | |||||
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) | void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer) | ||||
{ | { | ||||
MidiBuffer::Iterator iter (buffer); | MidiBuffer::Iterator iter (buffer); | ||||
@@ -159,63 +177,6 @@ void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer) | |||||
processNextMidiEvent (message); | 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 | void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept | ||||
{ | { | ||||
@@ -227,11 +188,27 @@ void MPEZoneLayout::removeListener (Listener* const listenerToRemove) noexcept | |||||
listeners.remove (listenerToRemove); | 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 | #if JUCE_UNIT_TESTS | ||||
class MPEZoneLayoutTests : public UnitTest | class MPEZoneLayoutTests : public UnitTest | ||||
{ | { | ||||
public: | public: | ||||
@@ -242,107 +219,73 @@ public: | |||||
beginTest ("initialisation"); | beginTest ("initialisation"); | ||||
{ | { | ||||
MPEZoneLayout layout; | MPEZoneLayout layout; | ||||
expectEquals (layout.getNumZones(), 0); | |||||
expect (! layout.getLowerZone().isActive()); | |||||
expect (! layout.getUpperZone().isActive()); | |||||
} | } | ||||
beginTest ("adding zones"); | beginTest ("adding zones"); | ||||
{ | { | ||||
MPEZoneLayout layout; | 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); | |||||
expect (layout.getLowerZone().isActive()); | |||||
expect (! layout.getUpperZone().isActive()); | |||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1); | |||||
expectEquals (layout.getLowerZone().numMemberChannels, 15); | |||||
} | } | ||||
beginTest ("querying zones"); | |||||
beginTest ("clear all zones"); | |||||
{ | { | ||||
MPEZoneLayout layout; | 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()); | |||||
beginTest ("clear all zones"); | |||||
{ | |||||
MPEZoneLayout layout; | |||||
layout.setLowerZone (7); | |||||
layout.setUpperZone (2); | |||||
expect (layout.getLowerZone().isActive()); | |||||
expect (layout.getUpperZone().isActive()); | |||||
expect (layout.addZone (MPEZone (1, 7))); | |||||
expect (layout.addZone (MPEZone (10, 2))); | |||||
layout.clearAllZones(); | layout.clearAllZones(); | ||||
expectEquals (layout.getNumZones(), 0); | |||||
expect (! layout.getLowerZone().isActive()); | |||||
expect (! layout.getUpperZone().isActive()); | |||||
} | } | ||||
beginTest ("process MIDI buffers"); | beginTest ("process MIDI buffers"); | ||||
@@ -350,57 +293,88 @@ public: | |||||
MPEZoneLayout layout; | MPEZoneLayout layout; | ||||
MidiBuffer buffer; | MidiBuffer buffer; | ||||
buffer = MPEMessages::addZone (MPEZone (1, 7)); | |||||
buffer = MPEMessages::setLowerZone (7); | |||||
layout.processNextMidiBuffer (buffer); | 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); | 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); | |||||
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::addZone (zone); | |||||
layout.processNextMidiBuffer (buffer); | |||||
expectEquals (layout.getNumZones(), 1); | |||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1); | |||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 10); | |||||
buffer = MPEMessages::setLowerZone (10, 33, 44); | |||||
layout.processNextMidiBuffer (buffer); | |||||
zone.setPerNotePitchbendRange (33); | |||||
zone.setMasterPitchbendRange (44); | |||||
expectEquals (layout.getLowerZone().numMemberChannels, 10); | |||||
expectEquals (layout.getLowerZone().perNotePitchbendRange, 33); | |||||
expectEquals (layout.getLowerZone().masterPitchbendRange, 44); | |||||
} | |||||
buffer = MPEMessages::masterPitchbendRange (zone); | |||||
buffer.addEvents (MPEMessages::perNotePitchbendRange (zone), 0, -1, 0); | |||||
{ | |||||
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); | 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"); | beginTest ("process individual MIDI messages"); | ||||
{ | { | ||||
MPEZoneLayout layout; | 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 | |||||
expectEquals (layout.getNumZones(), 1); | |||||
expectEquals (layout.getZoneByIndex (0)->getMasterChannel(), 1); | |||||
expectEquals (layout.getZoneByIndex (0)->getNumNoteChannels(), 3); | |||||
layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg | |||||
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1 | |||||
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2 | |||||
layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg | |||||
layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3 | |||||
layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg | |||||
expect (layout.getLowerZone().isActive()); | |||||
expect (! layout.getUpperZone().isActive()); | |||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1); | |||||
expectEquals (layout.getLowerZone().numMemberChannels, 3); | |||||
expectEquals (layout.getLowerZone().perNotePitchbendRange, 48); | |||||
expectEquals (layout.getLowerZone().masterPitchbendRange, 2); | |||||
} | } | ||||
} | } | ||||
}; | }; | ||||
@@ -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 | 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 | by this object to MIDI message sequences that you can send to an Expressive | ||||
MIDI device to set its zone layout, add zones etc. | MIDI device to set its zone layout, add zones etc. | ||||
@see MPEZone, MPEInstrument | |||||
@see MPEInstrument | |||||
*/ | */ | ||||
class JUCE_API MPEZoneLayout | class JUCE_API MPEZoneLayout | ||||
{ | { | ||||
public: | public: | ||||
/** Default constructor. | /** 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; | MPEZoneLayout() noexcept; | ||||
@@ -54,25 +63,89 @@ public: | |||||
*/ | */ | ||||
MPEZoneLayout& operator= (const MPEZoneLayout& other); | 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) | |||||
{ | |||||
} | |||||
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; | |||||
/** Removes all currently present MPE zones. */ | |||||
/** 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(); | void clearAllZones(); | ||||
//============================================================================== | |||||
/** Pass incoming MIDI messages to an object of this class if you want the | /** 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 | zone layout to properly react to MPE RPN messages like an | ||||
MPE device. | MPE device. | ||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will | MPEMessages::rpnNumber will add or remove zones; RPN 0 will | ||||
set the per-note or master pitchbend ranges. | 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 | /** 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 | zone layout to properly react to MPE RPN messages like an | ||||
MPE device. | MPE device. | ||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will | MPEMessages::rpnNumber will add or remove zones; RPN 0 will | ||||
set the per-note or master pitchbend ranges. | set the per-note or master pitchbend ranges. | ||||
@@ -94,40 +168,6 @@ public: | |||||
*/ | */ | ||||
void processNextMidiBuffer (const MidiBuffer& buffer); | 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 | /** Listener class. Derive from this class to allow your class to be | ||||
notified about changes to the zone layout. | notified about changes to the zone layout. | ||||
@@ -154,14 +194,24 @@ public: | |||||
private: | private: | ||||
//============================================================================== | //============================================================================== | ||||
Array<MPEZone> zones; | |||||
Zone lowerZone { true, 0 }; | |||||
Zone upperZone { false, 0 }; | |||||
MidiRPNDetector rpnDetector; | MidiRPNDetector rpnDetector; | ||||
ListenerList<Listener> listeners; | ListenerList<Listener> listeners; | ||||
//============================================================================== | |||||
void setZone (bool, int, int, int) noexcept; | |||||
void processRpnMessage (MidiRPNMessage); | void processRpnMessage (MidiRPNMessage); | ||||
void processZoneLayoutRpnMessage (MidiRPNMessage); | void processZoneLayoutRpnMessage (MidiRPNMessage); | ||||
void processPitchbendRangeRpnMessage (MidiRPNMessage); | void processPitchbendRangeRpnMessage (MidiRPNMessage); | ||||
void updateMasterPitchbend (Zone&, int); | |||||
void updatePerNotePitchbendRange (Zone&, int); | |||||
void sendLayoutChangeMessage(); | void sendLayoutChangeMessage(); | ||||
void checkAndLimitZoneParameters (int, int, int&) noexcept; | |||||
}; | }; | ||||
} // namespace juce | } // namespace juce |