| @@ -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 | |||