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

Add KeyboardComponentBase class for custom MIDI keyboard components and MPEKeyboardComponent class

This commit is contained in:
ed 2021-12-17 15:25:29 +00:00 committed by Tom Poole
parent 461192b355
commit e0e8e85d6b
28 changed files with 2219 additions and 1502 deletions

View file

@ -43,16 +43,20 @@ MPEInstrument::MPEInstrument() noexcept
mpeInstrumentFill (isMemberChannelSustained, false);
pitchbendDimension.value = &MPENote::pitchbend;
pressureDimension.value = &MPENote::pressure;
timbreDimension.value = &MPENote::timbre;
pressureDimension.value = &MPENote::pressure;
timbreDimension.value = &MPENote::timbre;
resetLastReceivedValues();
legacyMode.isEnabled = false;
legacyMode.pitchbendRange = 2;
legacyMode.channelRange = allChannels;
}
MPEInstrument::MPEInstrument (MPEZoneLayout layout)
: MPEInstrument()
{
setZoneLayout (layout);
}
MPEInstrument::~MPEInstrument() = default;
//==============================================================================
@ -84,21 +88,30 @@ void MPEInstrument::setZoneLayout (MPEZoneLayout newLayout)
const ScopedLock sl (lock);
legacyMode.isEnabled = false;
zoneLayout = newLayout;
resetLastReceivedValues();
if (zoneLayout != newLayout)
{
zoneLayout = newLayout;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
}
//==============================================================================
void MPEInstrument::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
if (legacyMode.isEnabled)
return;
releaseAllNotes();
const ScopedLock sl (lock);
legacyMode.isEnabled = true;
legacyMode.pitchbendRange = pitchbendRange;
legacyMode.channelRange = channelRange;
zoneLayout.clearAllZones();
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
bool MPEInstrument::isLegacyModeEnabled() const noexcept
@ -117,7 +130,12 @@ void MPEInstrument::setLegacyModeChannelRange (Range<int> channelRange)
releaseAllNotes();
const ScopedLock sl (lock);
legacyMode.channelRange = channelRange;
if (legacyMode.channelRange != channelRange)
{
legacyMode.channelRange = channelRange;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
}
int MPEInstrument::getLegacyModePitchbendRange() const noexcept
@ -131,7 +149,12 @@ void MPEInstrument::setLegacyModePitchbendRange (int pitchbendRange)
releaseAllNotes();
const ScopedLock sl (lock);
legacyMode.pitchbendRange = pitchbendRange;
if (legacyMode.pitchbendRange != pitchbendRange)
{
legacyMode.pitchbendRange = pitchbendRange;
listeners.call ([=] (Listener& l) { l.zoneLayoutChanged(); });
}
}
//==============================================================================
@ -242,7 +265,7 @@ void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& me
if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel()))
{
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -260,7 +283,7 @@ void MPEInstrument::processMidiResetAllControllersMessage (const MidiMessage& me
auto zone = (message.getChannel() == 1 ? zoneLayout.getLowerZone()
: zoneLayout.getUpperZone());
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -348,11 +371,11 @@ void MPEInstrument::noteOff (int midiChannel,
int midiNoteNumber,
MPEValue midiNoteOffVelocity)
{
const ScopedLock sl (lock);
if (notes.isEmpty() || ! isUsingChannel (midiChannel))
return;
const ScopedLock sl (lock);
if (auto* note = getNotePtr (midiChannel, midiNoteNumber))
{
note->keyState = (note->keyState == MPENote::keyDownAndSustained) ? MPENote::sustained : MPENote::off;
@ -401,7 +424,7 @@ void MPEInstrument::polyAftertouch (int midiChannel, int midiNoteNumber, MPEValu
{
const ScopedLock sl (lock);
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -435,7 +458,7 @@ void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, M
{
if (dimension.trackingMode == allNotesOnChannel)
{
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -464,7 +487,7 @@ void MPEInstrument::updateDimensionMaster (bool isLowerZone, MPEDimension& dimen
if (! zone.isActive())
return;
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -573,7 +596,7 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
auto zone = (midiChannel == 1 ? zoneLayout.getLowerZone()
: zoneLayout.getUpperZone());
for (auto i = notes.size(); --i >= 0;)
for (int i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);
@ -605,11 +628,15 @@ void MPEInstrument::handleSustainOrSostenuto (int midiChannel, bool isDown, bool
if (! legacyMode.isEnabled)
{
if (zone.isLowerZone())
for (auto i = zone.getFirstMemberChannel(); i <= zone.getLastMemberChannel(); ++i)
{
for (int i = zone.getFirstMemberChannel(); i <= zone.getLastMemberChannel(); ++i)
isMemberChannelSustained[i - 1] = isDown;
}
else
for (auto i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
{
for (int i = zone.getFirstMemberChannel(); i >= zone.getLastMemberChannel(); --i)
isMemberChannelSustained[i - 1] = isDown;
}
}
}
}
@ -664,6 +691,17 @@ MPENote MPEInstrument::getNote (int index) const noexcept
return notes[index];
}
MPENote MPEInstrument::getNoteWithID (uint16 noteID) const noexcept
{
const ScopedLock sl (lock);
for (auto& note : notes)
if (note.noteID == noteID)
return note;
return {};
}
//==============================================================================
MPENote MPEInstrument::getMostRecentNote (int midiChannel) const noexcept
{
@ -727,6 +765,8 @@ MPENote* MPEInstrument::getNotePtr (int midiChannel, TrackingMode mode) noexcept
//==============================================================================
const MPENote* MPEInstrument::getLastNotePlayedPtr (int midiChannel) const noexcept
{
const ScopedLock sl (lock);
for (auto i = notes.size(); --i >= 0;)
{
auto& note = notes.getReference (i);

View file

@ -38,10 +38,8 @@ namespace juce
MPE. If you pass it a message, it will know what notes on what
channels (if any) should be affected by that message.
The class has a Listener class with the three callbacks MPENoteAdded,
MPENoteChanged, and MPENoteFinished. Implement such a
Listener class to react to note changes and trigger some functionality for
your application that depends on the MPE note state.
The class has a Listener class that can be used to react to note and
state changes and trigger some functionality for your application.
For example, you can use this class to write an MPE visualiser.
If you want to write a real-time audio synth with MPE functionality,
@ -59,11 +57,14 @@ public:
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.
In order to process incoming MIDI messages call setZoneLayout, use the MPEZoneLayout
constructor, define the layout via MIDI RPN messages, or set the instrument to legacy mode.
*/
MPEInstrument() noexcept;
/** Constructs an MPE instrument with the specified zone layout. */
MPEInstrument (MPEZoneLayout layout);
/** Destructor. */
virtual ~MPEInstrument();
@ -229,6 +230,9 @@ public:
*/
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns the note with a given ID. */
MPENote getNoteWithID (uint16 noteID) 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
@ -244,8 +248,8 @@ public:
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
//==============================================================================
/** Derive from this class to be informed about any changes in the expressive
MIDI notes played by this instrument.
/** Derive from this class to be informed about any changes in the MPE notes played
by this instrument, and any changes to its zone layout.
Note: This listener type receives its callbacks immediately, and not
via the message thread (so you might be for example in the MIDI thread).
@ -297,6 +301,11 @@ public:
and should therefore stop playing.
*/
virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
/** Implement this callback to be informed whenever the MPE zone layout
or legacy mode settings of this instrument have been changed.
*/
virtual void zoneLayoutChanged() {}
};
//==============================================================================
@ -307,7 +316,9 @@ public:
void removeListener (Listener* listenerToRemove);
//==============================================================================
/** Puts the instrument into legacy mode.
/** Puts the instrument into legacy mode. If legacy mode is already enabled this method
does nothing.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
@ -360,9 +371,9 @@ private:
struct LegacyMode
{
bool isEnabled;
bool isEnabled = false;
Range<int> channelRange;
int pitchbendRange;
int pitchbendRange = 2;
};
struct MPEDimension

View file

@ -115,7 +115,7 @@ struct JUCE_API MPENote
*/
MPEValue noteOnVelocity { MPEValue::minValue() };
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel
position). This dimension can be modulated while the note sounds.
Note: This value is not aware of the currently used pitchbend range,

View file

@ -25,12 +25,10 @@ namespace juce
MPESynthesiser::MPESynthesiser()
{
MPEZoneLayout zoneLayout;
zoneLayout.setLowerZone (15);
setZoneLayout (zoneLayout);
}
MPESynthesiser::MPESynthesiser (MPEInstrument* mpeInstrument) : MPESynthesiserBase (mpeInstrument)
MPESynthesiser::MPESynthesiser (MPEInstrument& mpeInstrument)
: MPESynthesiserBase (mpeInstrument)
{
}
@ -314,7 +312,7 @@ void MPESynthesiser::turnOffAllVoices (bool allowTailOff)
}
// finally make sure the MPE Instrument also doesn't have any notes anymore.
instrument->releaseAllNotes();
instrument.releaseAllNotes();
}
//==============================================================================

View file

@ -65,11 +65,10 @@ public:
/** Constructor to pass to the synthesiser a custom MPEInstrument object
to handle the MPE note state, MIDI channel assignment etc.
(in case you need custom logic for this that goes beyond MIDI and MPE).
The synthesiser will take ownership of this object.
@see MPESynthesiserBase, MPEInstrument
*/
MPESynthesiser (MPEInstrument* instrumentToUse);
MPESynthesiser (MPEInstrument& instrumentToUse);
/** Destructor. */
~MPESynthesiser() override;
@ -303,7 +302,7 @@ protected:
private:
//==============================================================================
bool shouldStealVoices = false;
std::atomic<bool> shouldStealVoices { false };
uint32 lastNoteOnCounter = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)

View file

@ -24,80 +24,79 @@ namespace juce
{
MPESynthesiserBase::MPESynthesiserBase()
: instrument (new MPEInstrument)
: instrument (defaultInstrument)
{
instrument->addListener (this);
instrument.addListener (this);
}
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument* inst)
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument& inst)
: instrument (inst)
{
jassert (instrument != nullptr);
instrument->addListener (this);
instrument.addListener (this);
}
//==============================================================================
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
{
return instrument->getZoneLayout();
return instrument.getZoneLayout();
}
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
{
instrument->setZoneLayout (newLayout);
instrument.setZoneLayout (newLayout);
}
//==============================================================================
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
instrument->enableLegacyMode (pitchbendRange, channelRange);
instrument.enableLegacyMode (pitchbendRange, channelRange);
}
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
{
return instrument->isLegacyModeEnabled();
return instrument.isLegacyModeEnabled();
}
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
{
return instrument->getLegacyModeChannelRange();
return instrument.getLegacyModeChannelRange();
}
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
{
instrument->setLegacyModeChannelRange (channelRange);
instrument.setLegacyModeChannelRange (channelRange);
}
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
{
return instrument->getLegacyModePitchbendRange();
return instrument.getLegacyModePitchbendRange();
}
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
{
instrument->setLegacyModePitchbendRange (pitchbendRange);
instrument.setLegacyModePitchbendRange (pitchbendRange);
}
//==============================================================================
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
{
instrument->setPressureTrackingMode (modeToUse);
instrument.setPressureTrackingMode (modeToUse);
}
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
{
instrument->setPitchbendTrackingMode (modeToUse);
instrument.setPitchbendTrackingMode (modeToUse);
}
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
{
instrument->setTimbreTrackingMode (modeToUse);
instrument.setTimbreTrackingMode (modeToUse);
}
//==============================================================================
void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m)
{
instrument->processNextMidiEvent (m);
instrument.processNextMidiEvent (m);
}
//==============================================================================
@ -148,7 +147,7 @@ void MPESynthesiserBase::setCurrentPlaybackSampleRate (const double newRate)
if (sampleRate != newRate)
{
const ScopedLock sl (noteStateLock);
instrument->releaseAllNotes();
instrument.releaseAllNotes();
sampleRate = newRate;
}
}

View file

@ -52,13 +52,12 @@ public:
/** Constructor.
If you use this constructor, the synthesiser will take ownership of the
provided instrument object, and will use it internally to handle the
MPE note state logic.
If you use this constructor, the synthesiser will use the provided instrument
object to handle the MPE note state logic.
This is useful if you want to use an instance of your own class derived
from MPEInstrument for the MPE logic.
*/
MPESynthesiserBase (MPEInstrument* instrument);
MPESynthesiserBase (MPEInstrument& instrument);
//==============================================================================
/** Returns the synthesiser's internal MPE zone layout.
@ -200,10 +199,12 @@ protected:
protected:
//==============================================================================
/** @internal */
std::unique_ptr<MPEInstrument> instrument;
MPEInstrument& instrument;
private:
//==============================================================================
MPEInstrument defaultInstrument { MPEZone (MPEZone::Type::lower, 15) };
CriticalSection noteStateLock;
double sampleRate = 0.0;
int minimumSubBlockSize = 32;

View file

@ -52,25 +52,25 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
if (numChannels <= 1)
return firstChannel;
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{
if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
if (midiChannels[(size_t) ch].isFree() && midiChannels[(size_t) ch].lastNotePlayed == noteNumber)
{
midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber);
midiChannels[(size_t) ch].notes.add (noteNumber);
return ch;
}
}
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
for (int ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
{
if (ch == lastChannel + channelIncrement) // loop wrap-around
ch = firstChannel;
if (midiChannels[ch].isFree())
if (midiChannels[(size_t) ch].isFree())
{
midiChannelLastAssigned = ch;
midiChannels[ch].notes.add (noteNumber);
midiChannels[(size_t) ch].notes.add (noteNumber);
return ch;
}
@ -79,11 +79,21 @@ int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
}
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
midiChannels[(size_t) midiChannelLastAssigned].notes.add (noteNumber);
return midiChannelLastAssigned;
}
int MPEChannelAssigner::findMidiChannelForExistingNote (int noteNumber) noexcept
{
const auto iter = std::find_if (midiChannels.cbegin(), midiChannels.cend(), [&] (auto& ch)
{
return std::find (ch.notes.begin(), ch.notes.end(), noteNumber) != ch.notes.end();
});
return iter != midiChannels.cend() ? (int) std::distance (midiChannels.cbegin(), iter) : -1;
}
void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
{
const auto removeNote = [] (MidiChannel& ch, int noteNum)
@ -99,7 +109,7 @@ void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
if (midiChannel >= 0 && midiChannel <= 16)
{
removeNote (midiChannels[midiChannel], noteNumber);
removeNote (midiChannels[(size_t) midiChannel], noteNumber);
return;
}
@ -126,9 +136,9 @@ int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumbe
auto channelWithClosestNote = firstChannel;
int closestNoteDistance = 127;
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
{
for (auto note : midiChannels[ch].notes)
for (auto note : midiChannels[(size_t) ch].notes)
{
auto noteDistance = std::abs (note - noteNumber);
@ -296,24 +306,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order
int noteNum = 60;
for (int ch = 2; ch <= 16; ++ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
{
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed
channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 2);
channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (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);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
// find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
// all notes off
channelAssigner.allNotesOff();
@ -323,10 +344,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
// normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 4);
}
// upper
@ -339,24 +366,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order
int noteNum = 60;
for (int ch = 15; ch >= 1; --ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
{
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed
channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 15);
channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
expectEquals (channelAssigner.findMidiChannelForExistingNote (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);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
// find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
// all notes off
channelAssigner.allNotesOff();
@ -366,10 +404,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
// normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 14);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 13);
}
// legacy
@ -379,24 +423,35 @@ struct MPEUtilsUnitTests : public UnitTest
// check that channels are assigned in correct order
int noteNum = 60;
for (int ch = 1; ch <= 16; ++ch)
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
{
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
++noteNum;
}
// check that note-offs are processed
channelAssigner.noteOff (60);
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 1);
channelAssigner.noteOff (61);
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 2);
// check that assigned channel was last to play note
channelAssigner.noteOff (65);
channelAssigner.noteOff (66);
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
// find closest channel playing nonequal note
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
// all notes off
channelAssigner.allNotesOff();
@ -406,10 +461,16 @@ struct MPEUtilsUnitTests : public UnitTest
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
// normal assignment
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 2);
expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 3);
}
}

View file

@ -63,6 +63,11 @@ public:
*/
int findMidiChannelForNewNote (int noteNumber) noexcept;
/** If a note has been added using findMidiChannelForNewNote() this will return the channel
to which it was assigned, otherwise it will return -1.
*/
int findMidiChannelForExistingNote (int initialNoteOnNumber) 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.
@ -86,7 +91,7 @@ private:
int lastNotePlayed = -1;
bool isFree() const noexcept { return notes.isEmpty(); }
};
MidiChannel midiChannels[17];
std::array<MidiChannel, 17> midiChannels;
//==============================================================================
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;

View file

@ -43,6 +43,18 @@ MPEValue MPEValue::from14BitInt (int value) noexcept
return { value };
}
MPEValue MPEValue::fromUnsignedFloat (float value) noexcept
{
jassert (0.0f <= value && value <= 1.0f);
return { roundToInt (value * 16383.0f) };
}
MPEValue MPEValue::fromSignedFloat (float value) noexcept
{
jassert (-1.0f <= value && value <= 1.0f);
return { roundToInt (((value + 1.0f) * 16383.0f) / 2.0f) };
}
//==============================================================================
MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); }
MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); }
@ -121,26 +133,34 @@ public:
beginTest ("zero/minimum value");
{
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f);
}
beginTest ("maximum value");
{
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
}
beginTest ("centre value");
{
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.5f), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromSignedFloat (0.0f), 64, 8192, 0.0f, 0.5f);
}
beginTest ("value halfway between min and centre");
{
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f);
}
}

View file

@ -53,6 +53,12 @@ public:
*/
static MPEValue from14BitInt (int value) noexcept;
/** Constructs an MPEValue from a float between 0.0f and 1.0f. */
static MPEValue fromUnsignedFloat (float value) noexcept;
/** Constructs an MPEValue from a float between -1.0f and 1.0f. */
static MPEValue fromSignedFloat (float value) noexcept;
/** Constructs an MPEValue corresponding to the centre value. */
static MPEValue centreValue() noexcept;

View file

@ -23,7 +23,17 @@
namespace juce
{
MPEZoneLayout::MPEZoneLayout() noexcept {}
MPEZoneLayout::MPEZoneLayout (MPEZone lower, MPEZone upper)
: lowerZone (lower), upperZone (upper)
{
}
MPEZoneLayout::MPEZoneLayout (MPEZone zone)
: lowerZone (zone.isLowerZone() ? zone : MPEZone()),
upperZone (! zone.isLowerZone() ? zone : MPEZone())
{
}
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
: lowerZone (other.lowerZone),
@ -54,9 +64,9 @@ void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePit
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
if (isLower)
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
lowerZone = { MPEZone::Type::lower, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
else
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
upperZone = { MPEZone::Type::upper, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
if (numMemberChannels > 0)
{
@ -86,8 +96,8 @@ void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRan
void MPEZoneLayout::clearAllZones()
{
lowerZone = { true, 0 };
upperZone = { false, 0 };
lowerZone = { MPEZone::Type::lower, 0 };
upperZone = { MPEZone::Type::upper, 0 };
sendLayoutChangeMessage();
}
@ -128,7 +138,7 @@ void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
}
}
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
void MPEZoneLayout::updateMasterPitchbend (MPEZone& zone, int value)
{
if (zone.masterPitchbendRange != value)
{
@ -138,7 +148,7 @@ void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
}
}
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value)
void MPEZoneLayout::updatePerNotePitchbendRange (MPEZone& zone, int value)
{
if (zone.perNotePitchbendRange != value)
{

View file

@ -23,6 +23,83 @@
namespace juce
{
//==============================================================================
/**
This struct represents an MPE zone.
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.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct MPEZone
{
enum class Type { lower, upper };
MPEZone() = default;
MPEZone (const MPEZone& other) = default;
MPEZone (Type type, int memberChannels = 0, int perNotePitchbend = 48, int masterPitchbend = 2)
: zoneType (type),
numMemberChannels (memberChannels),
perNotePitchbendRange (perNotePitchbend),
masterPitchbendRange (masterPitchbend)
{}
bool isLowerZone() const noexcept { return zoneType == Type::lower; }
bool isUpperZone() const noexcept { return zoneType == Type::upper; }
bool isActive() const noexcept { return numMemberChannels > 0; }
int getMasterChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel : upperZoneMasterChannel; }
int getFirstMemberChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel + 1 : upperZoneMasterChannel - 1; }
int getLastMemberChannel() const noexcept { return isLowerZone() ? (lowerZoneMasterChannel + numMemberChannels)
: (upperZoneMasterChannel - numMemberChannels); }
bool isUsingChannelAsMemberChannel (int channel) const noexcept
{
return isLowerZone() ? (lowerZoneMasterChannel < channel && channel <= getLastMemberChannel())
: (channel < upperZoneMasterChannel && getLastMemberChannel() <= channel);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
static auto tie (const MPEZone& z)
{
return std::tie (z.zoneType,
z.numMemberChannels,
z.perNotePitchbendRange,
z.masterPitchbendRange);
}
bool operator== (const MPEZone& other) const
{
return tie (*this) == tie (other);
}
bool operator!= (const MPEZone& other) const
{
return tie (*this) != tie (other);
}
//==============================================================================
static constexpr int lowerZoneMasterChannel = 1,
upperZoneMasterChannel = 16;
Type zoneType = Type::lower;
int numMemberChannels = 0;
int perNotePitchbendRange = 48;
int masterPitchbendRange = 2;
};
//==============================================================================
/**
This class represents the current MPE zone layout of a device capable of handling MPE.
@ -44,89 +121,28 @@ namespace juce
class JUCE_API MPEZoneLayout
{
public:
/** Default constructor.
//==============================================================================
/** Creates a layout with inactive upper and lower zones. */
MPEZoneLayout() = default;
This will create a layout with inactive lower and upper zones, representing
a device with MPE mode disabled.
/** Creates a layout with the given upper and lower zones. */
MPEZoneLayout (MPEZone lower, MPEZone upper);
You can set the lower or upper MPE zones using the setZone() method.
/** Creates a layout with a single upper or lower zone, leaving the other zone uninitialised. */
MPEZoneLayout (MPEZone singleZone);
@see setZone
*/
MPEZoneLayout() noexcept;
/** Copy constuctor.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout (const MPEZoneLayout& other);
/** Copy assignment operator.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout& operator= (const MPEZoneLayout& other);
bool operator== (const MPEZoneLayout& other) const { return lowerZone == other.lowerZone && upperZone == other.upperZone; }
bool operator!= (const MPEZoneLayout& other) const { return ! operator== (other); }
//==============================================================================
/**
This struct represents an MPE zone.
/** Returns a struct representing the lower MPE zone. */
MPEZone getLowerZone() const noexcept { return lowerZone; }
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.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct Zone
{
Zone (const Zone& other) = default;
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);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
bool operator== (const Zone& other) const noexcept { return lowerZone == other.lowerZone
&& numMemberChannels == other.numMemberChannels
&& perNotePitchbendRange == other.perNotePitchbendRange
&& masterPitchbendRange == other.masterPitchbendRange; }
bool operator!= (const Zone& other) const noexcept { return ! operator== (other); }
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;
};
/** Returns a struct representing the upper MPE zone. */
MPEZone getUpperZone() const noexcept { return upperZone; }
/** Sets the lower zone of this layout. */
void setLowerZone (int numMemberChannels = 0,
@ -138,17 +154,14 @@ public:
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept;
/** 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();
/** Returns true if either of the zones are active. */
bool isActive() const { return lowerZone.isActive() || upperZone.isActive(); }
//==============================================================================
/** 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
@ -200,10 +213,14 @@ public:
/** Removes a listener. */
void removeListener (Listener* const listenerToRemove) noexcept;
#ifndef DOXYGEN
using Zone = MPEZone;
#endif
private:
//==============================================================================
Zone lowerZone { true, 0 };
Zone upperZone { false, 0 };
MPEZone lowerZone { MPEZone::Type::lower, 0 };
MPEZone upperZone { MPEZone::Type::upper, 0 };
MidiRPNDetector rpnDetector;
ListenerList<Listener> listeners;
@ -215,8 +232,8 @@ private:
void processZoneLayoutRpnMessage (MidiRPNMessage);
void processPitchbendRangeRpnMessage (MidiRPNMessage);
void updateMasterPitchbend (Zone&, int);
void updatePerNotePitchbendRange (Zone&, int);
void updateMasterPitchbend (MPEZone&, int);
void updatePerNotePitchbendRange (MPEZone&, int);
void sendLayoutChangeMessage();
void checkAndLimitZoneParameters (int, int, int&) noexcept;