Browse Source

Updated JUCE's MPE classes to comply with the new MMA-adopted specification

tags/2021-05-28
ed 7 years ago
parent
commit
6ca97fc897
17 changed files with 1240 additions and 956 deletions
  1. +50
    -0
      BREAKING-CHANGES.txt
  2. +1
    -1
      modules/juce_audio_basics/juce_audio_basics.cpp
  3. +1
    -1
      modules/juce_audio_basics/juce_audio_basics.h
  4. +141
    -111
      modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp
  5. +53
    -43
      modules/juce_audio_basics/mpe/juce_MPEInstrument.h
  6. +80
    -42
      modules/juce_audio_basics/mpe/juce_MPEMessages.cpp
  7. +43
    -20
      modules/juce_audio_basics/mpe/juce_MPEMessages.h
  8. +1
    -0
      modules/juce_audio_basics/mpe/juce_MPENote.cpp
  9. +12
    -6
      modules/juce_audio_basics/mpe/juce_MPENote.h
  10. +4
    -4
      modules/juce_audio_basics/mpe/juce_MPESynthesiser.cpp
  11. +424
    -0
      modules/juce_audio_basics/mpe/juce_MPEUtils.cpp
  12. +136
    -0
      modules/juce_audio_basics/mpe/juce_MPEUtils.h
  13. +5
    -2
      modules/juce_audio_basics/mpe/juce_MPEValue.h
  14. +0
    -319
      modules/juce_audio_basics/mpe/juce_MPEZone.cpp
  15. +0
    -142
      modules/juce_audio_basics/mpe/juce_MPEZone.h
  16. +189
    -215
      modules/juce_audio_basics/mpe/juce_MPEZoneLayout.cpp
  17. +100
    -50
      modules/juce_audio_basics/mpe/juce_MPEZoneLayout.h

+ 50
- 0
BREAKING-CHANGES.txt View File

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


+ 1
- 1
modules/juce_audio_basics/juce_audio_basics.cpp View File

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


+ 1
- 1
modules/juce_audio_basics/juce_audio_basics.h View File

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


+ 141
- 111
modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp View File

@@ -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);
}


+ 53
- 43
modules/juce_audio_basics/mpe/juce_MPEInstrument.h View File

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


+ 80
- 42
modules/juce_audio_basics/mpe/juce_MPEMessages.cpp View File

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


+ 43
- 20
modules/juce_audio_basics/mpe/juce_MPEMessages.h View File

@@ -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.
*/


+ 1
- 0
modules/juce_audio_basics/mpe/juce_MPENote.cpp View File

@@ -48,6 +48,7 @@ MPENote::MPENote (int midiChannel_,
noteOnVelocity (noteOnVelocity_),
pitchbend (pitchbend_),
pressure (pressure_),
initialTimbre (timbre_),
timbre (timbre_),
keyState (keyState_)
{


+ 12
- 6
modules/juce_audio_basics/mpe/juce_MPENote.h View File

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


+ 4
- 4
modules/juce_audio_basics/mpe/juce_MPESynthesiser.cpp View File

@@ -26,7 +26,7 @@ namespace juce
MPESynthesiser::MPESynthesiser()
{
MPEZoneLayout zoneLayout;
zoneLayout.addZone ({ 1, 15 });
zoneLayout.setLowerZone (15);
setZoneLayout (zoneLayout);
}
@@ -122,7 +122,7 @@ void MPESynthesiser::noteReleased (MPENote finishedNote)
{
const ScopedLock sl (voicesLock);
for (int i = voices.size(); --i >= 0;)
for (auto i = voices.size(); --i >= 0;)
{
auto* voice = voices.getUnchecked (i);
@@ -139,7 +139,7 @@ void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate)
turnOffAllVoices (false);
for (int i = voices.size(); --i >= 0;)
for (auto i = voices.size(); --i >= 0;)
voices.getUnchecked (i)->setCurrentSampleRate (newRate);
}
@@ -287,7 +287,7 @@ void MPESynthesiser::reduceNumVoices (const int newNumVoices)
while (voices.size() > newNumVoices)
{
if (MPESynthesiserVoice* voice = findFreeVoice ({}, true))
if (auto* voice = findFreeVoice ({}, true))
voices.removeObject (voice);
else
voices.remove (0); // if there's no voice to steal, kill the oldest voice


+ 424
- 0
modules/juce_audio_basics/mpe/juce_MPEUtils.cpp View File

@@ -0,0 +1,424 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPEChannelAssigner::MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse)
: zone (zoneToUse),
channelIncrement (zone.isLowerZone() ? 1 : -1),
numChannels (zone.numMemberChannels),
firstChannel (zone.getFirstMemberChannel()),
lastChannel (zone.getLastMemberChannel()),
midiChannelLastAssigned (firstChannel - channelIncrement)
{
// must be an active MPE zone!
jassert (numChannels > 0);
}
int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
{
if (numChannels == 1)
return firstChannel;
for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{
if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
{
midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber);
return ch;
}
}
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
{
if (ch == lastChannel + channelIncrement) // loop wrap-around
ch = firstChannel;
if (midiChannels[ch].isFree())
{
midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber);
return ch;
}
if (ch == midiChannelLastAssigned)
break; // no free channels!
}
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
return midiChannelLastAssigned;
}
void MPEChannelAssigner::noteOff (int noteNumber)
{
for (auto& ch : midiChannels)
{
if (ch.notes.removeAllInstancesOf (noteNumber) > 0)
{
ch.lastNotePlayed = noteNumber;
return;
}
}
}
void MPEChannelAssigner::allNotesOff()
{
for (auto& ch : midiChannels)
{
if (ch.notes.size() > 0)
ch.lastNotePlayed = ch.notes.getLast();
ch.notes.clear();
}
}
int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
{
auto channelWithClosestNote = firstChannel;
int closestNoteDistance = 127;
for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{
for (auto note : midiChannels[ch].notes)
{
auto noteDistance = std::abs (note - noteNumber);
if (noteDistance > 0 && noteDistance < closestNoteDistance)
{
closestNoteDistance = noteDistance;
channelWithClosestNote = ch;
}
}
}
return channelWithClosestNote;
}
//==============================================================================
MPEChannelRemapper::MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap)
: zone (zoneToRemap),
channelIncrement (zone.isLowerZone() ? 1 : -1),
firstChannel (zone.getFirstMemberChannel()),
lastChannel (zone.getLastMemberChannel())
{
// must be an active MPE zone!
jassert (zone.numMemberChannels > 0);
zeroArrays();
}
void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
{
auto channel = message.getChannel();
if (! zone.isUsingChannelAsMemberChannel (channel))
return;
if (channel == zone.getMasterChannel() && message.isResetAllControllers())
{
clearSource (mpeSourceID);
return;
}
auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
if (messageIsNoteData (message))
{
++counter;
// fast path - no remap
if (applyRemapIfExisting (channel, sourceAndChannelID, message))
return;
// find existing remap
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
if (applyRemapIfExisting (chan, sourceAndChannelID, message))
return;
// no remap necessary
if (sourceAndChannel[channel] == notMPE)
{
lastUsed[channel] = counter;
sourceAndChannel[channel] = sourceAndChannelID;
return;
}
// remap source & channel to new channel
auto chan = getBestChanToReuse();
sourceAndChannel[chan] = sourceAndChannelID;
lastUsed[chan] = counter;
message.setChannel (chan);
}
}
void MPEChannelRemapper::reset() noexcept
{
for (auto& s : sourceAndChannel)
s = notMPE;
}
void MPEChannelRemapper::clearChannel (int channel) noexcept
{
sourceAndChannel[channel] = notMPE;
}
void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
{
for (auto& s : sourceAndChannel)
{
if (uint32 (s >> 5) == mpeSourceID)
{
s = notMPE;
return;
}
}
}
bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
{
if (sourceAndChannel[channel] == sourceAndChannelID)
{
if (m.isNoteOff())
sourceAndChannel[channel] = notMPE;
else
lastUsed[channel] = counter;
m.setChannel (channel);
return true;
}
return false;
}
int MPEChannelRemapper::getBestChanToReuse() const noexcept
{
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
if (sourceAndChannel[chan] == notMPE)
return chan;
auto bestChan = firstChannel;
auto bestLastUse = counter;
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
{
if (lastUsed[chan] < bestLastUse)
{
bestLastUse = lastUsed[chan];
bestChan = chan;
}
}
return bestChan;
}
void MPEChannelRemapper::zeroArrays()
{
for (int i = 0; i < 17; ++i)
{
sourceAndChannel[i] = 0;
lastUsed[i] = 0;
}
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MPEUtilsUnitTests : public UnitTest
{
MPEUtilsUnitTests()
: UnitTest ("MPE Utilities", "MIDI/MPE")
{}
void runTest() override
{
beginTest ("MPEChannelAssigner");
{
MPEZoneLayout layout;
{
layout.setLowerZone (15);
// lower zone
MPEChannelAssigner channelAssigner (layout.getLowerZone());
// check that channels are assigned in correct order
int noteNum = 60;
for (int ch = 2; ch <= 16; ++ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
// check that note-offs are processed
channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
// check that assigned channel was last to play note
channelAssigner.noteOff (65);
channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
// find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
// all notes off
channelAssigner.allNotesOff();
// last note played
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
// normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
}
{
layout.setUpperZone (15);
// upper zone
MPEChannelAssigner channelAssigner (layout.getUpperZone());
// check that channels are assigned in correct order
int noteNum = 60;
for (int ch = 15; ch >= 1; --ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
// check that note-offs are processed
channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
// check that assigned channel was last to play note
channelAssigner.noteOff (65);
channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
// find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
// all notes off
channelAssigner.allNotesOff();
// last note played
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
// normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
}
}
beginTest ("MPEChannelRemapper");
{
// 3 different MPE 'sources', constant IDs
const int sourceID1 = 0;
const int sourceID2 = 1;
const int sourceID3 = 2;
MPEZoneLayout layout;
{
layout.setLowerZone (15);
// lower zone
MPEChannelRemapper channelRemapper (layout.getLowerZone());
// first source, shouldn't remap
for (int ch = 2; ch <= 16; ++ch)
{
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
expectEquals (noteOn.getChannel(), ch);
}
auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
// remap onto oldest last-used channel
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
expectEquals (noteOn.getChannel(), 2);
// remap onto oldest last-used channel
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
expectEquals (noteOn.getChannel(), 3);
// remap to correct channel for source ID
auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
expectEquals (noteOff.getChannel(), 3);
}
{
layout.setUpperZone (15);
// upper zone
MPEChannelRemapper channelRemapper (layout.getUpperZone());
// first source, shouldn't remap
for (int ch = 15; ch >= 1; --ch)
{
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
expectEquals (noteOn.getChannel(), ch);
}
auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
// remap onto oldest last-used channel
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
expectEquals (noteOn.getChannel(), 15);
// remap onto oldest last-used channel
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
expectEquals (noteOn.getChannel(), 14);
// remap to correct channel for source ID
auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
expectEquals (noteOff.getChannel(), 14);
}
}
}
};
static MPEUtilsUnitTests MPEUtilsUnitTests;
#endif
} // namespace juce

+ 136
- 0
modules/juce_audio_basics/mpe/juce_MPEUtils.h View File

@@ -0,0 +1,136 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class handles the assignment of new MIDI notes to member channels of an active
MPE zone.
To use it, create an instance passing in the MPE zone that it should operate on
and then call use the findMidiChannelForNewNote() method for all note-on messages
and the noteOff() method for all note-off messages.
*/
class MPEChannelAssigner
{
public:
/** Constructor */
MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse);
/** This method will use a set of rules recommended in the MPE specification to
determine which member channel the specified MIDI note should be assigned to
and will return this channel number.
The rules have the following precedence:
- find a free channel on which the last note played was the same as the one specified
- find the next free channel in round-robin assignment
- find the channel number that is currently playing the closest note (but not the same)
@param noteNumber the MIDI note number to be assigned to a channel
@returns the zone's member channel that this note should be assigned to
*/
int findMidiChannelForNewNote (int noteNumber) noexcept;
/** You must call this method for all note-offs that you receive so that this class
can keep track of the currently playing notes internally.
*/
void noteOff (int noteNumber);
/** Call this to clear all currently playing notes. */
void allNotesOff();
private:
MPEZoneLayout::Zone zone;
int channelIncrement, numChannels, firstChannel, lastChannel, midiChannelLastAssigned;
//==============================================================================
struct MidiChannel
{
Array<int> notes;
int lastNotePlayed = -1;
bool isFree() const noexcept { return notes.isEmpty(); }
};
MidiChannel midiChannels[17];
//==============================================================================
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
};
//==============================================================================
/**
This class handles the logic for remapping MIDI note messages from multiple MPE
sources onto a specified MPE zone.
*/
class MPEChannelRemapper
{
public:
/** Used to indicate that a particular source & channel combination is not currently using MPE. */
static const uint32 notMPE = 0;
/** Constructor */
MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap);
//==============================================================================
/** Remaps the MIDI channel of the specified MIDI message (if necessary).
Note that the MidiMessage object passed in will have it's channel changed if it
needs to be remapped.
@param message the message to be remapped
@param mpeSourceID the ID of the MPE source of the message. This is up to the
user to define and keep constant
*/
void remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept;
//==============================================================================
/** Resets all the source & channel combinations. */
void reset() noexcept;
/** Clears a specified channel of this MPE zone. */
void clearChannel (int channel) noexcept;
/** Clears all channels in use by a specified source. */
void clearSource (uint32 mpeSourceID);
private:
MPEZoneLayout::Zone zone;
int channelIncrement;
int firstChannel, lastChannel;
uint32 sourceAndChannel[17];
uint32 lastUsed[17];
uint32 counter = 0;
//==============================================================================
bool applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept;
int getBestChanToReuse() const noexcept;
void zeroArrays();
//==============================================================================
bool messageIsNoteData (const MidiMessage& m) { return (*m.getRawData() & 0xf0) != 0xf0; }
};
} // namespace juce

+ 5
- 2
modules/juce_audio_basics/mpe/juce_MPEValue.h View File

@@ -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.
*/


+ 0
- 319
modules/juce_audio_basics/mpe/juce_MPEZone.cpp View File

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

+ 0
- 142
modules/juce_audio_basics/mpe/juce_MPEZone.h View File

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

+ 189
- 215
modules/juce_audio_basics/mpe/juce_MPEZoneLayout.cpp View File

@@ -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);
}
}
};


+ 100
- 50
modules/juce_audio_basics/mpe/juce_MPEZoneLayout.h View File

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

Loading…
Cancel
Save