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