1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

MPE: replaced omniMode (all MIDI channels) by legacyMode which allows to define a custom MIDI channel range. Fixed a few minor MPE bugs.

This commit is contained in:
Timur Doumler 2015-12-16 17:40:59 +00:00
parent 1fe45f3d41
commit e0bd51f26c
4 changed files with 236 additions and 75 deletions

View file

@ -39,8 +39,9 @@ MPEInstrument::MPEInstrument() noexcept
pressureDimension.value = &MPENote::pressure;
timbreDimension.value = &MPENote::timbre;
omniMode.isEnabled = false;
omniMode.pitchbendRange = 2;
legacyMode.isEnabled = false;
legacyMode.pitchbendRange = 2;
legacyMode.channelRange = Range<int> (1, 17);
}
MPEInstrument::~MPEInstrument()
@ -58,24 +59,53 @@ void MPEInstrument::setZoneLayout (MPEZoneLayout newLayout)
releaseAllNotes();
const ScopedLock sl (lock);
omniMode.isEnabled = false;
legacyMode.isEnabled = false;
zoneLayout = newLayout;
}
//==============================================================================
void MPEInstrument::enableOmniMode (int pitchbendRange)
void MPEInstrument::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
releaseAllNotes();
const ScopedLock sl (lock);
omniMode.isEnabled = true;
omniMode.pitchbendRange = pitchbendRange;
legacyMode.isEnabled = true;
legacyMode.pitchbendRange = pitchbendRange;
legacyMode.channelRange = channelRange;
zoneLayout.clearAllZones();
}
bool MPEInstrument::isOmniModeEnabled() const noexcept
bool MPEInstrument::isLegacyModeEnabled() const noexcept
{
return omniMode.isEnabled;
return legacyMode.isEnabled;
}
Range<int> MPEInstrument::getLegacyModeChannelRange() const noexcept
{
return legacyMode.channelRange;
}
void MPEInstrument::setLegacyModeChannelRange (Range<int> channelRange)
{
jassert (Range<int>(1, 17).contains (channelRange));
releaseAllNotes();
const ScopedLock sl (lock);
legacyMode.channelRange = channelRange;
}
int MPEInstrument::getLegacyModePitchbendRange() const noexcept
{
return legacyMode.pitchbendRange;
}
void MPEInstrument::setLegacyModePitchbendRange (int pitchbendRange)
{
jassert (pitchbendRange >= 0 && pitchbendRange <= 96);
releaseAllNotes();
const ScopedLock sl (lock);
legacyMode.pitchbendRange = pitchbendRange;
}
//==============================================================================
@ -187,7 +217,10 @@ void MPEInstrument::processMidiControllerMessage (const MidiMessage& message)
//==============================================================================
void MPEInstrument::processMidiAllNotesOffMessage (const MidiMessage& message)
{
if (omniMode.isEnabled)
// 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).
if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel()))
{
for (int i = notes.size(); --i >= 0;)
{
@ -267,7 +300,7 @@ void MPEInstrument::noteOn (int midiChannel,
int midiNoteNumber,
MPEValue midiNoteOnVelocity)
{
if (! isNoteChannel (midiChannel) && ! omniMode.isEnabled)
if (! isNoteChannel (midiChannel))
return;
MPENote newNote (midiChannel,
@ -299,7 +332,7 @@ void MPEInstrument::noteOff (int midiChannel,
int midiNoteNumber,
MPEValue midiNoteOffVelocity)
{
if (notes.empty() || (! isNoteChannel (midiChannel) && ! omniMode.isEnabled))
if (notes.empty() || ! isNoteChannel (midiChannel))
return;
const ScopedLock sl (lock);
@ -357,7 +390,7 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
{
updateDimensionMaster (*zone, dimension, value);
}
else if (isNoteChannel (midiChannel) || omniMode.isEnabled)
else if (isNoteChannel (midiChannel))
{
if (dimension.trackingMode == allNotesOnChannel)
{
@ -429,9 +462,9 @@ void MPEInstrument::callListenersDimensionChanged (MPENote& note, MPEDimension&
//==============================================================================
void MPEInstrument::updateNoteTotalPitchbend (MPENote& note)
{
if (omniMode.isEnabled)
if (legacyMode.isEnabled)
{
note.totalPitchbendInSemitones = note.pitchbend.asSignedFloat() * omniMode.pitchbendRange;
note.totalPitchbendInSemitones = note.pitchbend.asSignedFloat() * legacyMode.pitchbendRange;
}
else
{
@ -465,18 +498,19 @@ void MPEInstrument::sostenutoPedal (int midiChannel, bool isDown)
//==============================================================================
void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto)
{
// 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).
MPEZone* affectedZone = zoneLayout.getZoneByMasterChannel (midiChannel);
if (affectedZone == nullptr && ! omniMode.isEnabled)
if (legacyMode.isEnabled ? (! legacyMode.channelRange.contains (midiChannel)) : (affectedZone == nullptr))
return;
for (int i = notes.size(); --i >= 0;)
{
MPENote& note = notes.getReference (i);
if ((omniMode.isEnabled
|| note.midiChannel == midiChannel)
|| affectedZone->isUsingChannel (note.midiChannel))
if (legacyMode.isEnabled ? (note.midiChannel == midiChannel) : affectedZone->isUsingChannel (note.midiChannel))
{
if (note.keyState == MPENote::keyDown && isDown)
note.keyState = MPENote::keyDownAndSustained;
@ -499,7 +533,7 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
if (! isSostenuto)
{
if (omniMode.isEnabled)
if (legacyMode.isEnabled)
isNoteChannelSustained[midiChannel - 1] = isDown;
else
for (int i = affectedZone->getFirstNoteChannel(); i <= affectedZone->getLastNoteChannel(); ++i)
@ -510,11 +544,17 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
//==============================================================================
bool MPEInstrument::isNoteChannel (int midiChannel) const noexcept
{
if (legacyMode.isEnabled)
return legacyMode.channelRange.contains (midiChannel);
return zoneLayout.getZoneByNoteChannel (midiChannel) != nullptr;
}
bool MPEInstrument::isMasterChannel (int midiChannel) const noexcept
{
if (legacyMode.isEnabled)
return false;
return zoneLayout.getZoneByMasterChannel (midiChannel) != nullptr;
}
@ -1672,10 +1712,10 @@ public:
expectEquals (test.getNumPlayingNotes(), 0);
}
beginTest ("MIDI all notes off (omni mode)");
beginTest ("MIDI all notes off (legacy mode)");
{
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.noteOn (3, 60, MPEValue::from7BitInt (100));
test.noteOn (4, 61, MPEValue::from7BitInt (100));
test.noteOn (15, 62, MPEValue::from7BitInt (100));
@ -1719,27 +1759,48 @@ public:
expectNote (test.getMostRecentNote (3), 100, 33, 4444, 55, MPENote::keyDown);
}
beginTest ("Omni mode");
beginTest ("Legacy mode");
{
{
// basic check
MPEInstrument test;
expect (! test.isOmniModeEnabled());
expect (! test.isLegacyModeEnabled());
test.setZoneLayout (testLayout);
expect (! test.isOmniModeEnabled());
expect (! test.isLegacyModeEnabled());
test.enableOmniMode();
expect (test.isOmniModeEnabled());
test.enableLegacyMode();
expect (test.isLegacyModeEnabled());
test.setZoneLayout (testLayout);
expect (! test.isOmniModeEnabled());
expect (! test.isLegacyModeEnabled());
}
{
// constructor w/o default arguments
MPEInstrument test;
test.enableLegacyMode (0, Range<int> (1, 11));
expectEquals (test.getLegacyModePitchbendRange(), 0);
expect (test.getLegacyModeChannelRange() == Range<int> (1, 11));
}
{
// getters and setters
MPEInstrument test;
test.enableLegacyMode();
expectEquals (test.getLegacyModePitchbendRange(), 2);
expect (test.getLegacyModeChannelRange() == Range<int> (1, 17));
test.setLegacyModePitchbendRange (96);
expectEquals (test.getLegacyModePitchbendRange(), 96);
test.setLegacyModeChannelRange (Range<int> (10, 12));
expect (test.getLegacyModeChannelRange() == Range<int> (10, 12));
}
{
// note on should trigger notes on all 16 channels
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.noteOn (1, 60, MPEValue::from7BitInt (100));
test.noteOn (2, 60, MPEValue::from7BitInt (100));
@ -1758,7 +1819,7 @@ public:
expectNote (test.getNote (15, 60), 100, 100, 8192, 77, MPENote::keyDown);
expectNote (test.getNote (16, 60), 100, 100, 8192, 64, MPENote::keyDown);
// note off should work in omni mode
// note off should work in legacy mode
test.noteOff (15, 60, MPEValue::from7BitInt (0));
test.noteOff (1, 60, MPEValue::from7BitInt (0));
@ -1767,10 +1828,31 @@ public:
expectEquals (test.getNumPlayingNotes(), 0);
}
{
// tracking mode in omni mode
// legacy mode w/ custom channel range: note on should trigger notes only within range
UnitTestInstrument test;
test.enableLegacyMode (2, Range<int> (3, 8)); // channels 3-7
test.noteOn (1, 60, MPEValue::from7BitInt (100));
test.noteOn (2, 60, MPEValue::from7BitInt (100));
test.noteOn (3, 60, MPEValue::from7BitInt (100)); // should trigger
test.noteOn (4, 60, MPEValue::from7BitInt (100)); // should trigger
test.noteOn (6, 60, MPEValue::from7BitInt (100)); // should trigger
test.noteOn (7, 60, MPEValue::from7BitInt (100)); // should trigger
test.noteOn (8, 60, MPEValue::from7BitInt (100));
test.noteOn (16, 60, MPEValue::from7BitInt (100));
expectEquals (test.getNumPlayingNotes(), 4);
expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown);
expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown);
expectNote (test.getNote (6, 60), 100, 100, 8192, 64, MPENote::keyDown);
expectNote (test.getNote (7, 60), 100, 100, 8192, 64, MPENote::keyDown);
}
{
// tracking mode in legacy mode
{
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.setPitchbendTrackingMode (MPEInstrument::lastNotePlayedOnChannel);
test.noteOn (1, 60, MPEValue::from7BitInt (100));
@ -1783,7 +1865,7 @@ public:
}
{
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.setPitchbendTrackingMode (MPEInstrument::lowestNoteOnChannel);
test.noteOn (1, 60, MPEValue::from7BitInt (100));
@ -1796,7 +1878,7 @@ public:
}
{
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.setPitchbendTrackingMode (MPEInstrument::highestNoteOnChannel);
test.noteOn (1, 60, MPEValue::from7BitInt (100));
@ -1809,7 +1891,7 @@ public:
}
{
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.setPitchbendTrackingMode (MPEInstrument::allNotesOnChannel);
test.noteOn (1, 60, MPEValue::from7BitInt (100));
@ -1822,18 +1904,18 @@ public:
}
}
{
// custom pitchbend range in omni mode.
// custom pitchbend range in legacy mode.
UnitTestInstrument test;
test.enableOmniMode (11);
test.enableLegacyMode (11);
test.pitchbend (1, MPEValue::from14BitInt (4096));
test.noteOn (1, 60, MPEValue::from7BitInt (100));
expectDoubleWithinRelativeError (test.getMostRecentNote (1).totalPitchbendInSemitones, -5.5, 0.01);
}
{
// sustain pedal should be per channel in omni mode.
// sustain pedal should be per channel in legacy mode.
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.sustainPedal (1, true);
test.noteOn (2, 61, MPEValue::from7BitInt (100));
@ -1846,11 +1928,17 @@ public:
test.sustainPedal (1, false);
expectEquals (test.getNumPlayingNotes(), 0);
test.noteOn (2, 61, MPEValue::from7BitInt (100));
test.sustainPedal (1, true);
test.noteOff (2, 61, MPEValue::from7BitInt (100));
expectEquals (test.getNumPlayingNotes(), 0);
}
{
// sostenuto pedal should be per channel in omni mode.
// sostenuto pedal should be per channel in legacy mode.
UnitTestInstrument test;
test.enableOmniMode();
test.enableLegacyMode();
test.noteOn (1, 60, MPEValue::from7BitInt (100));
test.sostenutoPedal (1, true);
@ -1863,6 +1951,11 @@ public:
test.sostenutoPedal (1, false);
expectEquals (test.getNumPlayingNotes(), 0);
test.noteOn (2, 61, MPEValue::from7BitInt (100));
test.sostenutoPedal (1, true);
test.noteOff (2, 61, MPEValue::from7BitInt (100));
expectEquals (test.getNumPlayingNotes(), 0);
}
{
// all notes released when switching layout
@ -1871,7 +1964,7 @@ public:
test.noteOn (3, 60, MPEValue::from7BitInt (100));
expectEquals (test.getNumPlayingNotes(), 1);
test.enableOmniMode();
test.enableLegacyMode();
expectEquals (test.getNumPlayingNotes(), 0);
test.noteOn (3, 60, MPEValue::from7BitInt (100));
expectEquals (test.getNumPlayingNotes(), 1);

View file

@ -61,7 +61,7 @@ public:
This will construct an MPE instrument with initially no MPE zones.
In order to process incoming MIDI, call setZoneLayout, define the layout
via MIDI RPN messages, or set the instrument to omni mode.
via MIDI RPN messages, or set the instrument to legacy mode.
*/
MPEInstrument() noexcept;
@ -72,7 +72,7 @@ public:
/** Returns the current zone layout of the instrument.
This happens by value, to enforce thread-safety and class invariants.
Note: If the instrument is in Omni mode, the return value of this
Note: If the instrument is in legacy mode, the return value of this
method is unspecified.
*/
MPEZoneLayout getZoneLayout() const noexcept;
@ -81,38 +81,23 @@ public:
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This will also disable Omni Mode in case it was enabled previously.
This will also disable legacy mode in case it was enabled previously.
*/
void setZoneLayout (MPEZoneLayout newLayout);
/** 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;
/** 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.
*/
bool isMasterChannel (int midiChannel) const noexcept;
/** Sets the instrument to Omni Mode.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This special zone layout mode is for backwards compatibility with
non-MPE MIDI devices. In this mode, the instrument will ignore the
current zone layout. It will instead treat all 16 MIDI channels as note
channels, with no master channel.
@param pitchbendRange The pitchbend range in semitones that should be
used while the instrument is in Omni mode. Must
be between 0 and 96, otherwise behaviour is undefined.
*/
void enableOmniMode (int pitchbendRange = 2);
/** Returns true if the instrument is in Omni mode, false otherwise. */
bool isOmniModeEnabled() const noexcept;
//==========================================================================
/** The MPE note tracking mode. In case there is more than one note playing
simultaneously on the same MIDI channel, this determines which of these
@ -300,6 +285,44 @@ public:
/** Removes a listener. */
void removeListener (Listener* const listenerToRemove) noexcept;
//==========================================================================
/** Puts the instrument into legacy mode.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This special zone layout mode is for backwards compatibility with
non-MPE MIDI devices. In this mode, the instrument will ignore the
current MPE zone layout. It will instead take a range of MIDI channels
(default: all channels 1-16) and treat them as note channels, with no
master channel. MIDI channels outside of this range will be ignored.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
protected:
//==========================================================================
/** This method defines what initial pitchbend value should be used for newly
@ -341,9 +364,10 @@ private:
uint8 lastTimbreLowerBitReceivedOnChannel[16];
bool isNoteChannelSustained[16];
struct OmniMode
struct LegacyMode
{
bool isEnabled;
Range<int> channelRange;
int pitchbendRange;
};
@ -356,7 +380,7 @@ private:
MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
};
OmniMode omniMode;
LegacyMode legacyMode;
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
void updateDimension (int midiChannel, MPEDimension&, MPEValue);

View file

@ -50,14 +50,35 @@ void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
instrument->setZoneLayout (newLayout);
}
void MPESynthesiserBase::enableOmniMode (int pitchbendRange)
//==============================================================================
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
instrument->enableOmniMode (pitchbendRange);
instrument->enableLegacyMode (pitchbendRange, channelRange);
}
bool MPESynthesiserBase::isOmniModeEnabled() const noexcept
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
{
return instrument->isOmniModeEnabled();
return instrument->isLegacyModeEnabled();
}
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
{
return instrument->getLegacyModeChannelRange();
}
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
{
instrument->setLegacyModeChannelRange (channelRange);
}
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
{
return instrument->getLegacyModePitchbendRange();
}
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
{
instrument->setLegacyModePitchbendRange (pitchbendRange);
}
//==============================================================================

View file

@ -69,16 +69,10 @@ public:
/** Re-sets the synthesiser's internal MPE zone layout to the one passed in.
As a side effect, this will discard all currently playing notes,
call noteReleased for all of them, and disable Omni mode (if previously enabled).
call noteReleased for all of them, and disable legacy mode (if previously enabled).
*/
void setZoneLayout (MPEZoneLayout newLayout);
/** Sets the synthesiser to Omni mode. */
void enableOmniMode (int pitchbendRange = 2);
/** Returns true if the synthesiser is currently in Omni mode. */
bool isOmniModeEnabled() const noexcept;
//==========================================================================
/** Tells the synthesiser what the sample rate is for the audio it's being
used to render.
@ -136,6 +130,35 @@ public:
*/
void setMinimumRenderingSubdivisionSize (int numSamples) noexcept;
//==========================================================================
/** Puts the synthesiser into legacy mode.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
protected:
//==========================================================================
/** Implement this method to render your audio inside.