/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ namespace { const uint8 noLSBValueReceived = 0xff; const Range allChannels = Range (1, 17); } //============================================================================== MPEInstrument::MPEInstrument() noexcept { std::fill_n (lastPressureLowerBitReceivedOnChannel, 16, noLSBValueReceived); std::fill_n (lastTimbreLowerBitReceivedOnChannel, 16, noLSBValueReceived); std::fill_n (isNoteChannelSustained, 16, false); pitchbendDimension.value = &MPENote::pitchbend; pressureDimension.value = &MPENote::pressure; timbreDimension.value = &MPENote::timbre; legacyMode.isEnabled = false; legacyMode.pitchbendRange = 2; legacyMode.channelRange = Range (1, 17); } MPEInstrument::~MPEInstrument() { } //============================================================================== MPEZoneLayout MPEInstrument::getZoneLayout() const noexcept { return zoneLayout; } void MPEInstrument::setZoneLayout (MPEZoneLayout newLayout) { releaseAllNotes(); const ScopedLock sl (lock); legacyMode.isEnabled = false; zoneLayout = newLayout; } //============================================================================== void MPEInstrument::enableLegacyMode (int pitchbendRange, Range channelRange) { releaseAllNotes(); const ScopedLock sl (lock); legacyMode.isEnabled = true; legacyMode.pitchbendRange = pitchbendRange; legacyMode.channelRange = channelRange; zoneLayout.clearAllZones(); } bool MPEInstrument::isLegacyModeEnabled() const noexcept { return legacyMode.isEnabled; } Range MPEInstrument::getLegacyModeChannelRange() const noexcept { return legacyMode.channelRange; } void MPEInstrument::setLegacyModeChannelRange (Range channelRange) { jassert (Range(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; } //============================================================================== void MPEInstrument::setPressureTrackingMode (TrackingMode modeToUse) { pressureDimension.trackingMode = modeToUse; } void MPEInstrument::setPitchbendTrackingMode (TrackingMode modeToUse) { pitchbendDimension.trackingMode = modeToUse; } void MPEInstrument::setTimbreTrackingMode (TrackingMode modeToUse) { timbreDimension.trackingMode = modeToUse; } //============================================================================== void MPEInstrument::addListener (Listener* const listenerToAdd) noexcept { listeners.add (listenerToAdd); } void MPEInstrument::removeListener (Listener* const listenerToRemove) noexcept { listeners.remove (listenerToRemove); } //============================================================================== 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); } //============================================================================== void MPEInstrument::processMidiNoteOnMessage (const MidiMessage& message) { // Note: if a note-on with velocity = 0 is used to convey a note-off, // then the actual note-off velocity is not known. In this case, // the MPE convention is to use note-off velocity = 64. if (message.getVelocity() == 0) { noteOff (message.getChannel(), message.getNoteNumber(), MPEValue::from7BitInt (64)); } else { noteOn (message.getChannel(), message.getNoteNumber(), MPEValue::from7BitInt (message.getVelocity())); } } //============================================================================== void MPEInstrument::processMidiNoteOffMessage (const MidiMessage& message) { noteOff (message.getChannel(), message.getNoteNumber(), MPEValue::from7BitInt (message.getVelocity())); } //============================================================================== void MPEInstrument::processMidiPitchWheelMessage (const MidiMessage& message) { pitchbend (message.getChannel(), MPEValue::from14BitInt (message.getPitchWheelValue())); } //============================================================================== void MPEInstrument::processMidiChannelPressureMessage (const MidiMessage& message) { pressure (message.getChannel(), MPEValue::from7BitInt (message.getChannelPressureValue())); } //============================================================================== void MPEInstrument::processMidiControllerMessage (const MidiMessage& message) { switch (message.getControllerNumber()) { case 64: sustainPedal (message.getChannel(), message.isSustainPedalOn()); break; case 66: sostenutoPedal (message.getChannel(), message.isSostenutoPedalOn()); break; case 70: handlePressureMSB (message.getChannel(), message.getControllerValue()); break; case 74: handleTimbreMSB (message.getChannel(), message.getControllerValue()); break; case 102: handlePressureLSB (message.getChannel(), message.getControllerValue()); break; case 106: handleTimbreLSB (message.getChannel(), message.getControllerValue()); break; default: break; } } //============================================================================== void MPEInstrument::processMidiAllNotesOffMessage (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). if (legacyMode.isEnabled && legacyMode.channelRange.contains (message.getChannel())) { for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (note.midiChannel == message.getChannel()) { note.keyState = MPENote::off; note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number listeners.call (&MPEInstrument::Listener::noteReleased, note); notes.remove (i); } } } else if (MPEZone* zone = zoneLayout.getZoneByMasterChannel (message.getChannel())) { for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (zone->isUsingChannelAsNoteChannel (note.midiChannel)) { note.keyState = MPENote::off; note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number listeners.call (&MPEInstrument::Listener::noteReleased, note); notes.remove (i); } } } } //============================================================================== void MPEInstrument::handlePressureMSB (int midiChannel, int value) noexcept { const uint8 lsb = lastPressureLowerBitReceivedOnChannel[midiChannel - 1]; pressure (midiChannel, lsb == noLSBValueReceived ? MPEValue::from7BitInt (value) : MPEValue::from14BitInt (lsb + (value << 7))); } void MPEInstrument::handlePressureLSB (int midiChannel, int value) noexcept { lastPressureLowerBitReceivedOnChannel[midiChannel - 1] = uint8 (value); } void MPEInstrument::handleTimbreMSB (int midiChannel, int value) noexcept { const uint8 lsb = lastTimbreLowerBitReceivedOnChannel[midiChannel - 1]; timbre (midiChannel, lsb == noLSBValueReceived ? MPEValue::from7BitInt (value) : MPEValue::from14BitInt (lsb + (value << 7))); } void MPEInstrument::handleTimbreLSB (int midiChannel, int value) noexcept { lastTimbreLowerBitReceivedOnChannel[midiChannel - 1] = uint8 (value); } //============================================================================== MPEValue MPEInstrument::getInitialPitchbendForNoteOn (int midiChannel, int /*midiNoteNumber*/, MPEValue /*midiNoteOnVelocity*/) const { return pitchbendDimension.lastValueReceivedOnChannel[midiChannel - 1]; } MPEValue MPEInstrument::getInitialPressureForNoteOn (int /*midiChannel*/, int /*midiNoteNumber*/, MPEValue midiNoteOnVelocity) const { return midiNoteOnVelocity; } MPEValue MPEInstrument::getInitialTimbreForNoteOn (int midiChannel, int /*midiNoteNumber*/, MPEValue /*midiNoteOnVelocity*/) const { return timbreDimension.lastValueReceivedOnChannel[midiChannel - 1]; } //============================================================================== void MPEInstrument::noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity) { if (! isNoteChannel (midiChannel)) return; MPENote newNote (midiChannel, midiNoteNumber, midiNoteOnVelocity, getInitialPitchbendForNoteOn (midiChannel, midiNoteNumber, midiNoteOnVelocity), getInitialPressureForNoteOn (midiChannel, midiNoteNumber, midiNoteOnVelocity), getInitialTimbreForNoteOn (midiChannel, midiNoteNumber, midiNoteOnVelocity), isNoteChannelSustained[midiChannel - 1] ? MPENote::keyDownAndSustained : MPENote::keyDown); const ScopedLock sl (lock); updateNoteTotalPitchbend (newNote); if (MPENote* alreadyPlayingNote = getNotePtr (midiChannel, midiNoteNumber)) { // pathological case: second note-on received for same note -> retrigger it alreadyPlayingNote->keyState = MPENote::off; alreadyPlayingNote->noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number listeners.call (&MPEInstrument::Listener::noteReleased, *alreadyPlayingNote); notes.remove (alreadyPlayingNote); } notes.add (newNote); listeners.call (&MPEInstrument::Listener::noteAdded, newNote); } //============================================================================== void MPEInstrument::noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity) { if (notes.empty() || ! isNoteChannel (midiChannel)) return; const ScopedLock sl (lock); if (MPENote* note = getNotePtr (midiChannel, midiNoteNumber)) { note->keyState = (note->keyState == MPENote::keyDownAndSustained) ? MPENote::sustained : MPENote::off; note->noteOffVelocity = midiNoteOffVelocity; // last pitchbend and timbre values received for this note should not be re-used for // any new notes, so reset them: pitchbendDimension.lastValueReceivedOnChannel[midiChannel - 1] = MPEValue(); timbreDimension.lastValueReceivedOnChannel[midiChannel - 1] = MPEValue(); if (note->keyState == MPENote::off) { listeners.call (&MPEInstrument::Listener::noteReleased, *note); notes.remove (note); } else { listeners.call (&MPEInstrument::Listener::noteKeyStateChanged, *note); } } } //============================================================================== void MPEInstrument::pitchbend (int midiChannel, MPEValue value) { const ScopedLock sl (lock); updateDimension (midiChannel, pitchbendDimension, value); } void MPEInstrument::pressure (int midiChannel, MPEValue value) { const ScopedLock sl (lock); updateDimension (midiChannel, pressureDimension, value); } void MPEInstrument::timbre (int midiChannel, MPEValue value) { const ScopedLock sl (lock); updateDimension (midiChannel, timbreDimension, value); } //============================================================================== void MPEInstrument::updateDimension (int midiChannel, MPEDimension& dimension, MPEValue value) { dimension.lastValueReceivedOnChannel[midiChannel - 1] = value; if (notes.empty()) return; if (MPEZone* zone = zoneLayout.getZoneByMasterChannel (midiChannel)) { updateDimensionMaster (*zone, dimension, value); } else if (isNoteChannel (midiChannel)) { if (dimension.trackingMode == allNotesOnChannel) { for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (note.midiChannel == midiChannel) updateDimensionForNote (note, dimension, value); } } else { if (MPENote* note = getNotePtr (midiChannel, dimension.trackingMode)) updateDimensionForNote (*note, dimension, value); } } } //============================================================================== void MPEInstrument::updateDimensionMaster (MPEZone& zone, MPEDimension& dimension, MPEValue value) { const Range channels (zone.getNoteChannelRange()); for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (! channels.contains (note.midiChannel)) continue; if (&dimension == &pitchbendDimension) { // master pitchbend is a special case: we don't change the note's own pitchbend, // instead we have to update its total (master + note) pitchbend. updateNoteTotalPitchbend (note); listeners.call (&MPEInstrument::Listener::notePitchbendChanged, note); } else if (dimension.getValue (note) != value) { dimension.getValue (note) = value; callListenersDimensionChanged (note, dimension); } } } //============================================================================== void MPEInstrument::updateDimensionForNote (MPENote& note, MPEDimension& dimension, MPEValue value) { if (dimension.getValue (note) != value) { dimension.getValue (note) = value; if (&dimension == &pitchbendDimension) updateNoteTotalPitchbend (note); callListenersDimensionChanged (note, dimension); } } //============================================================================== void MPEInstrument::callListenersDimensionChanged (MPENote& note, MPEDimension& dimension) { if (&dimension == &pressureDimension) { listeners.call (&MPEInstrument::Listener::notePressureChanged, note); return; } if (&dimension == &timbreDimension) { listeners.call (&MPEInstrument::Listener::noteTimbreChanged, note); return; } if (&dimension == &pitchbendDimension) { listeners.call (&MPEInstrument::Listener::notePitchbendChanged, note); return; } } //============================================================================== void MPEInstrument::updateNoteTotalPitchbend (MPENote& note) { if (legacyMode.isEnabled) { note.totalPitchbendInSemitones = note.pitchbend.asSignedFloat() * legacyMode.pitchbendRange; } else { if (MPEZone* zone = zoneLayout.getZoneByNoteChannel (note.midiChannel)) { double notePitchbendInSemitones = note.pitchbend.asSignedFloat() * zone->getPerNotePitchbendRange(); double masterPitchbendInSemitones = pitchbendDimension.lastValueReceivedOnChannel[zone->getMasterChannel() - 1].asSignedFloat() * zone->getMasterPitchbendRange(); note.totalPitchbendInSemitones = notePitchbendInSemitones + masterPitchbendInSemitones; } else { // oops - this note seems to not belong to any zone! jassertfalse; } } } //============================================================================== void MPEInstrument::sustainPedal (int midiChannel, bool isDown) { const ScopedLock sl (lock); handleSustainOrSostenuto (midiChannel, isDown, false); } void MPEInstrument::sostenutoPedal (int midiChannel, bool isDown) { const ScopedLock sl (lock); handleSustainOrSostenuto (midiChannel, isDown, true); } //============================================================================== 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 (legacyMode.isEnabled ? (! legacyMode.channelRange.contains (midiChannel)) : (affectedZone == nullptr)) return; for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (legacyMode.isEnabled ? (note.midiChannel == midiChannel) : affectedZone->isUsingChannel (note.midiChannel)) { if (note.keyState == MPENote::keyDown && isDown) note.keyState = MPENote::keyDownAndSustained; else if (note.keyState == MPENote::sustained && ! isDown) note.keyState = MPENote::off; else if (note.keyState == MPENote::keyDownAndSustained && ! isDown) note.keyState = MPENote::keyDown; if (note.keyState == MPENote::off) { listeners.call (&MPEInstrument::Listener::noteReleased, note); notes.remove (i); } else { listeners.call (&MPEInstrument::Listener::noteKeyStateChanged, note); } } } if (! isSostenuto) { if (legacyMode.isEnabled) isNoteChannelSustained[midiChannel - 1] = isDown; else for (int i = affectedZone->getFirstNoteChannel(); i <= affectedZone->getLastNoteChannel(); ++i) isNoteChannelSustained[i - 1] = isDown; } } //============================================================================== 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; } //============================================================================== int MPEInstrument::getNumPlayingNotes() const noexcept { return notes.size(); } MPENote MPEInstrument::getNote (int midiChannel, int midiNoteNumber) const noexcept { if (MPENote* note = getNotePtr (midiChannel, midiNoteNumber)) return *note; return MPENote(); } MPENote MPEInstrument::getNote (int index) const noexcept { return notes[index]; } //============================================================================== MPENote MPEInstrument::getMostRecentNote (int midiChannel) const noexcept { if (MPENote* note = getLastNotePlayedPtr (midiChannel)) return *note; return MPENote(); } MPENote MPEInstrument::getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept { for (int i = notes.size(); --i >= 0;) { const MPENote& note = notes.getReference (i); if (note != otherThanThisNote) return note; } return MPENote(); } //============================================================================== MPENote* MPEInstrument::getNotePtr (int midiChannel, int midiNoteNumber) const noexcept { for (int i = 0; i < notes.size(); ++i) { MPENote& note = notes.getReference (i); if (note.midiChannel == midiChannel && note.initialNote == midiNoteNumber) return ¬e; } return nullptr; } //============================================================================== MPENote* MPEInstrument::getNotePtr (int midiChannel, TrackingMode mode) const noexcept { // for the "all notes" tracking mode, this method can never possibly // work because it returns 0 or 1 note but there might be more than one! jassert (mode != allNotesOnChannel); if (mode == lastNotePlayedOnChannel) return getLastNotePlayedPtr (midiChannel); if (mode == lowestNoteOnChannel) return getLowestNotePtr (midiChannel); if (mode == highestNoteOnChannel) return getHighestNotePtr (midiChannel); return nullptr; } //============================================================================== MPENote* MPEInstrument::getLastNotePlayedPtr (int midiChannel) const noexcept { for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (note.midiChannel == midiChannel && (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained)) return ¬e; } return nullptr; } //============================================================================== MPENote* MPEInstrument::getHighestNotePtr (int midiChannel) const noexcept { int initialNoteMax = -1; MPENote* result = nullptr; for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (note.midiChannel == midiChannel && (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained) && note.initialNote > initialNoteMax) { result = ¬e; initialNoteMax = note.initialNote; } } return result; } MPENote* MPEInstrument::getLowestNotePtr (int midiChannel) const noexcept { int initialNoteMin = 128; MPENote* result = nullptr; for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); if (note.midiChannel == midiChannel && (note.keyState == MPENote::keyDown || note.keyState == MPENote::keyDownAndSustained) && note.initialNote < initialNoteMin) { result = ¬e; initialNoteMin = note.initialNote; } } return result; } //============================================================================== void MPEInstrument::releaseAllNotes() { const ScopedLock sl (lock); for (int i = notes.size(); --i >= 0;) { MPENote& note = notes.getReference (i); note.keyState = MPENote::off; note.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number listeners.call (&MPEInstrument::Listener::noteReleased, note); } notes.clear(); } //============================================================================== //============================================================================== #if JUCE_UNIT_TESTS class MPEInstrumentTests : public UnitTest { public: MPEInstrumentTests() : UnitTest ("MPEInstrument class") { // using two 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)); } void runTest() override { beginTest ("initial zone layout"); { MPEInstrument test; expectEquals (test.getZoneLayout().getNumZones(), 0); } beginTest ("get/setZoneLayout"); { 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); } beginTest ("noteOn / noteOff"); { { MPEInstrument test; test.setZoneLayout (testLayout); expectEquals (test.getNumPlayingNotes(), 0); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // note-on on master channel - ignore test.noteOn (9, 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)); expectEquals (test.getNumPlayingNotes(), 0); expectEquals (test.noteAddedCallCounter, 0); // note-on on note channel - create new note test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 1); expectEquals (test.noteAddedCallCounter, 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); // note-off test.noteOff (3, 60, MPEValue::from7BitInt (33)); expectEquals (test.getNumPlayingNotes(), 0); expectEquals (test.noteReleasedCallCounter, 1); expectHasFinishedNote (test, 3, 60, 33); } { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note off with non-matching note number shouldn't do anything test.noteOff (3, 61, MPEValue::from7BitInt (33)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteReleasedCallCounter, 0); // note off with non-matching midi channel shouldn't do anything test.noteOff (2, 60, MPEValue::from7BitInt (33)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteReleasedCallCounter, 0); } { // can have multiple notes on the same channel UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 0, MPEValue::from7BitInt (100)); test.noteOn (3, 1, MPEValue::from7BitInt (100)); test.noteOn (3, 2, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 3); expectNote (test.getNote (3, 0), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 1), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 2), 100, 100, 8192, 64, MPENote::keyDown); } { // pathological case: second note-on for same note should retrigger it. UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 0, MPEValue::from7BitInt (100)); test.noteOn (3, 0, MPEValue::from7BitInt (60)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 0), 60, 60, 8192, 64, MPENote::keyDown); } } beginTest ("noteReleased after setZoneLayout"); { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.noteOn (4, 61, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 3); expectEquals (test.noteReleasedCallCounter, 0); test.setZoneLayout (testLayout); expectEquals (test.getNumPlayingNotes(), 0); expectEquals (test.noteReleasedCallCounter, 3); } beginTest ("releaseAllNotes"); { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 61, MPEValue::from7BitInt (100)); test.noteOn (15, 62, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 3); test.releaseAllNotes(); expectEquals (test.getNumPlayingNotes(), 0); } beginTest ("sustainPedal"); { 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 // sustain pedal on per-note channel shouldn't do anything. test.sustainPedal (3, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 0); // sustain pedal on non-zone channel shouldn't do anything either. test.sustainPedal (1, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 0); // sustain pedal on master channel should sustain notes on *that* zone. test.sustainPedal (2, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDownAndSustained); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 1); // release test.sustainPedal (2, false); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 2); // should also sustain new notes added after the press test.sustainPedal (2, true); expectEquals (test.noteKeyStateChangedCallCounter, 3); test.noteOn (4, 51, MPEValue::from7BitInt (100)); expectNote (test.getNote (4, 51), 100, 100, 8192, 64, MPENote::keyDownAndSustained); expectEquals (test.noteKeyStateChangedCallCounter, 3); // ...but only if that sustain came on the master channel of that zone! test.sustainPedal (11, true); test.noteOn (11, 52, MPEValue::from7BitInt (100)); expectNote (test.getNote (11, 52), 100, 100, 8192, 64, MPENote::keyDown); test.noteOff (11, 52, MPEValue::from7BitInt (100)); expectEquals (test.noteReleasedCallCounter, 1); // note-off should not turn off sustained notes inside the same zone test.noteOff (3, 60, MPEValue::from7BitInt (100)); test.noteOff (4, 51, MPEValue::from7BitInt (100)); test.noteOff (10, 60, MPEValue::from7BitInt (100)); // not affected! expectEquals (test.getNumPlayingNotes(), 2); expectEquals (test.noteReleasedCallCounter, 2); expectEquals (test.noteKeyStateChangedCallCounter, 5); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::sustained); expectNote (test.getNote (4, 51), 100, 100, 8192, 64, MPENote::sustained); // notes should be turned off when pedal is released test.sustainPedal (2, false); expectEquals (test.getNumPlayingNotes(), 0); expectEquals (test.noteReleasedCallCounter, 4); } beginTest ("sostenutoPedal"); { 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 // sostenuto pedal on per-note channel shouldn't do anything. test.sostenutoPedal (3, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 0); // sostenuto pedal on non-zone channel shouldn't do anything either. test.sostenutoPedal (1, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 0); // sostenuto pedal on master channel should sustain notes on *that* zone. test.sostenutoPedal (2, true); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDownAndSustained); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 1); // release test.sostenutoPedal (2, false); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 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); expectEquals (test.noteKeyStateChangedCallCounter, 3); test.noteOn (4, 51, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 3); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDownAndSustained); expectNote (test.getNote (4, 51), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteKeyStateChangedCallCounter, 3); // note-off should not turn off sustained notes inside the same zone, // but only if they were turned on *before* the sostenuto pedal (difference to sustain pedal) test.noteOff (3, 60, MPEValue::from7BitInt (100)); test.noteOff (4, 51, MPEValue::from7BitInt (100)); test.noteOff (10, 60, MPEValue::from7BitInt (100)); // not affected! expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::sustained); expectEquals (test.noteReleasedCallCounter, 2); expectEquals (test.noteKeyStateChangedCallCounter, 4); // notes should be turned off when pedal is released test.sustainPedal (2, false); expectEquals (test.getNumPlayingNotes(), 0); expectEquals (test.noteReleasedCallCounter, 3); } beginTest ("getMostRecentNote"); { MPEInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); { MPENote note = test.getMostRecentNote (2); expect (! note.isValid()); } { MPENote note = test.getMostRecentNote (3); expect (note.isValid()); expectEquals (int (note.midiChannel), 3); expectEquals (int (note.initialNote), 61); } test.sustainPedal (2, true); test.noteOff (3, 61, MPEValue::from7BitInt (100)); { MPENote note = test.getMostRecentNote (3); expect (note.isValid()); expectEquals (int (note.midiChannel), 3); expectEquals (int (note.initialNote), 60); } test.sustainPedal (2, false); test.noteOff (3, 60, MPEValue::from7BitInt (100)); { MPENote note = test.getMostRecentNote (3); expect (! note.isValid()); } } beginTest ("getMostRecentNoteOtherThan"); { MPENote testNote (3, 60, MPEValue::centreValue(), MPEValue::centreValue(), MPEValue::centreValue(), MPEValue::centreValue()); { // case 1: the note to exclude is not the most recent one. MPEInstrument test; test.setZoneLayout (testLayout); expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); test.noteOn (4, 61, MPEValue::from7BitInt (100)); expect (test.getMostRecentNoteOtherThan (testNote).isValid()); expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); } { // case 2: the note to exclude is the most recent one. MPEInstrument test; test.setZoneLayout (testLayout); expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); test.noteOn (4, 61, MPEValue::from7BitInt (100)); expect (test.getMostRecentNoteOtherThan (testNote).isValid()); expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expect (test.getMostRecentNoteOtherThan (testNote).isValid()); expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); } } beginTest ("pressure"); { { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 60, MPEValue::from7BitInt (100)); test.noteOn (10, 60, MPEValue::from7BitInt (100)); // applying pressure on a per-note channel should modulate one note test.pressure (3, MPEValue::from7BitInt (33)); expectNote (test.getNote (3, 60), 100, 33, 8192, 64, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); // applying pressure on a master channel should modulate all notes in this zone test.pressure (2, 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, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 3); // applying pressure on an unrelated channel should be ignored test.pressure (1, 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, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 3); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // two notes on same channel - only last added should be modulated test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (66)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 66, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // edge case: two notes on same channel, one gets released, // then the other should be modulated test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.noteOff (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (77)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 77, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); } } beginTest ("pitchbend"); { { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 60, MPEValue::from7BitInt (100)); test.noteOn (10, 60, MPEValue::from7BitInt (100)); // applying pitchbend on a per-note channel should modulate one note test.pitchbend (3, MPEValue::from14BitInt (1111)); expectNote (test.getNote (3, 60), 100, 100, 1111, 64, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); // applying pitchbend on a master channel should be ignored for the // 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)); expectNote (test.getNote (3, 60), 100, 100, 1111, 64, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 3); // applying pitchbend on an unrelated channel should do nothing. test.pitchbend (1, MPEValue::from14BitInt (3333)); expectNote (test.getNote (3, 60), 100, 100, 1111, 64, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 3); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // two notes on same channel - only last added should be bent test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (4444)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 4444, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // edge case: two notes on same channel, one gets released, // then the other should be bent test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.noteOff (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (5555)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 5555, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // Richard's edge case: // - press one note // - press sustain (careful: must be sent on master channel) // - release first note (is still sustained!) // - press another note (happens to be on the same MIDI channel!) // - pitchbend that other note // - the first note should not be bent, only the second one. test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.sustainPedal (2, true); test.noteOff (3, 60, MPEValue::from7BitInt (64)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::sustained); expectEquals (test.noteKeyStateChangedCallCounter, 2); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (6666)); expectEquals (test.getNumPlayingNotes(), 2); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::sustained); expectNote (test.getNote (3, 61), 100, 100, 6666, 64, MPENote::keyDownAndSustained); expectEquals (test.notePitchbendChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // Zsolt's edge case: // - press one note // - modulate pitchbend or timbre // - release the note // - press same note again without sending a pitchbend or timbre message before the note-on // - the note should be turned on with a default value for pitchbend/timbre, // and *not* the last value received on channel. test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (5555)); expectNote (test.getNote (3, 60), 100, 100, 5555, 64, MPENote::keyDown); test.noteOff (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); } { // applying per-note pitchbend should set the note's totalPitchbendInSemitones // correctly depending on the per-note pitchbend range of the zone. UnitTestInstrument test; MPEZoneLayout layout = testLayout; test.setZoneLayout (layout); // default should be +/- 48 semitones test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (4096)); expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -24.0, 0.01); layout.getZoneByIndex (0)->setPerNotePitchbendRange (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); 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 test.setZoneLayout (layout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (12345)); expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01); } { // applying master pitchbend should set the note's totalPitchbendInSemitones // correctly depending on the master pitchbend range of the zone. UnitTestInstrument test; 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 expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -1.0, 0.01); layout.getZoneByIndex (0)->setMasterPitchbendRange (96); test.setZoneLayout (layout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (2, MPEValue::from14BitInt (0)); // -max expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); layout.getZoneByIndex (0)->setMasterPitchbendRange (1); test.setZoneLayout (layout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (2, MPEValue::from14BitInt (16383)); // +max expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01); layout.getZoneByIndex (0)->setMasterPitchbendRange (0); // pitchbendrange = 0 --> no pitchbend at all test.setZoneLayout (layout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.pitchbend (2, MPEValue::from14BitInt (12345)); expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01); } { // applying both per-note and master pitchbend simultaneously should set // the note's totalPitchbendInSemitones to the sum of both, correctly // weighted with the per-note and master pitchbend range, respectively. UnitTestInstrument test; MPEZoneLayout layout = testLayout; layout.getZoneByIndex (0)->setPerNotePitchbendRange (12); layout.getZoneByIndex (0)->setMasterPitchbendRange (1); test.setZoneLayout (layout); test.pitchbend (2, 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. test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -12.5, 0.01); } } beginTest ("timbre"); { { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 60, MPEValue::from7BitInt (100)); test.noteOn (10, 60, MPEValue::from7BitInt (100)); // modulating timbre on a per-note channel should modulate one note test.timbre (3, MPEValue::from7BitInt (33)); expectNote (test.getNote (3, 60), 100, 100, 8192, 33, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); // modulating timbre on a master channel should modulate all notes in this zone test.timbre (2, MPEValue::from7BitInt (44)); expectNote (test.getNote (3, 60), 100, 100, 8192, 44, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 44, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 3); // modulating timbre on an unrelated channel should be ignored test.timbre (1, MPEValue::from7BitInt (55)); expectNote (test.getNote (3, 60), 100, 100, 8192, 44, MPENote::keyDown); expectNote (test.getNote (4, 60), 100, 100, 8192, 44, MPENote::keyDown); expectNote (test.getNote (10, 60), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 3); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // two notes on same channel - only last added should be modulated test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (66)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 66, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // edge case: two notes on same channel, one gets released, // then the other should be modulated test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.noteOff (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (77)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (3, 60), 100, 100, 8192, 77, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); } { UnitTestInstrument test; test.setZoneLayout (testLayout); // Zsolt's edge case for timbre test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (42)); expectNote (test.getNote (3, 60), 100, 100, 8192, 42, MPENote::keyDown); test.noteOff (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); } } beginTest ("setPressureTrackingMode"); { { // last note played (= default) UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPressureTrackingMode (MPEInstrument::lastNotePlayedOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 99, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); } { // lowest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPressureTrackingMode (MPEInstrument::lowestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 99, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); } { // highest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPressureTrackingMode (MPEInstrument::highestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 99, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 1); } { // all notes UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPressureTrackingMode (MPEInstrument::allNotesOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pressure (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 99, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 99, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 99, 8192, 64, MPENote::keyDown); expectEquals (test.notePressureChangedCallCounter, 3); } } beginTest ("setPitchbendTrackingMode"); { { // last note played (= default) UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPitchbendTrackingMode (MPEInstrument::lastNotePlayedOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (9999)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 9999, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); } { // lowest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPitchbendTrackingMode (MPEInstrument::lowestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (9999)); expectNote (test.getNote (3, 60), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); } { // highest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPitchbendTrackingMode (MPEInstrument::highestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (9999)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 1); } { // all notes UnitTestInstrument test; test.setZoneLayout (testLayout); test.setPitchbendTrackingMode (MPEInstrument::allNotesOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.pitchbend (3, MPEValue::from14BitInt (9999)); expectNote (test.getNote (3, 60), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 9999, 64, MPENote::keyDown); expectEquals (test.notePitchbendChangedCallCounter, 3); } } beginTest ("setTimbreTrackingMode"); { { // last note played (= default) UnitTestInstrument test; test.setZoneLayout (testLayout); test.setTimbreTrackingMode (MPEInstrument::lastNotePlayedOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 99, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); } { // lowest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setTimbreTrackingMode (MPEInstrument::lowestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 99, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); } { // highest note UnitTestInstrument test; test.setZoneLayout (testLayout); test.setTimbreTrackingMode (MPEInstrument::highestNoteOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 99, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 64, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 1); } { // all notes UnitTestInstrument test; test.setZoneLayout (testLayout); test.setTimbreTrackingMode (MPEInstrument::allNotesOnChannel); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (3, 62, MPEValue::from7BitInt (100)); test.noteOn (3, 61, MPEValue::from7BitInt (100)); test.timbre (3, MPEValue::from7BitInt (99)); expectNote (test.getNote (3, 60), 100, 100, 8192, 99, MPENote::keyDown); expectNote (test.getNote (3, 62), 100, 100, 8192, 99, MPENote::keyDown); expectNote (test.getNote (3, 61), 100, 100, 8192, 99, MPENote::keyDown); expectEquals (test.noteTimbreChangedCallCounter, 3); } } beginTest ("processNextMidiEvent"); { UnitTestInstrument test; // note on should trigger noteOn method call test.processNextMidiEvent (MidiMessage::noteOn (3, 42, uint8 (92))); expectEquals (test.noteOnCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 3); expectEquals (test.lastMidiNoteNumberReceived, 42); expectEquals (test.lastMPEValueReceived.as7BitInt(), 92); // note off should trigger noteOff method call test.processNextMidiEvent (MidiMessage::noteOff (4, 12, uint8 (33))); expectEquals (test.noteOffCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 4); expectEquals (test.lastMidiNoteNumberReceived, 12); expectEquals (test.lastMPEValueReceived.as7BitInt(), 33); // note on with velocity = 0 should trigger noteOff method call // with a note off velocity of 64 (centre value) test.processNextMidiEvent (MidiMessage::noteOn (5, 11, uint8 (0))); expectEquals (test.noteOffCallCounter, 2); expectEquals (test.lastMidiChannelReceived, 5); expectEquals (test.lastMidiNoteNumberReceived, 11); expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); // pitchwheel message should trigger pitchbend method call test.processNextMidiEvent (MidiMessage::pitchWheel (1, 3333)); expectEquals (test.pitchbendCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 1); expectEquals (test.lastMPEValueReceived.as14BitInt(), 3333); // pressure using channel pressure message (7-bit value) should // trigger pressure method call test.processNextMidiEvent (MidiMessage::channelPressureChange (10, 35)); expectEquals (test.pressureCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 10); expectEquals (test.lastMPEValueReceived.as7BitInt(), 35); // pressure using 14-bit value over CC70 and CC102 should trigger // pressure method call after the MSB is sent // a) sending only the MSB test.processNextMidiEvent (MidiMessage::controllerEvent (3, 70, 120)); expectEquals (test.pressureCallCounter, 2); expectEquals (test.lastMidiChannelReceived, 3); expectEquals (test.lastMPEValueReceived.as7BitInt(), 120); // b) sending LSB and MSB (only the MSB should trigger the call) - per MIDI channel! test.processNextMidiEvent (MidiMessage::controllerEvent (4, 102, 121)); expectEquals (test.pressureCallCounter, 2); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 102, 122)); expectEquals (test.pressureCallCounter, 2); test.processNextMidiEvent (MidiMessage::controllerEvent (4, 70, 123)); expectEquals (test.pressureCallCounter, 3); expectEquals (test.lastMidiChannelReceived, 4); expectEquals (test.lastMPEValueReceived.as14BitInt(), 121 + (123 << 7)); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 70, 124)); expectEquals (test.pressureCallCounter, 4); expectEquals (test.lastMidiChannelReceived, 5); expectEquals (test.lastMPEValueReceived.as14BitInt(), 122 + (124 << 7)); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 70, 64)); expectEquals (test.pressureCallCounter, 5); expectEquals (test.lastMidiChannelReceived, 5); expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); // same for timbre 14-bit value over CC74 and CC106 test.processNextMidiEvent (MidiMessage::controllerEvent (3, 74, 120)); expectEquals (test.timbreCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 3); expectEquals (test.lastMPEValueReceived.as7BitInt(), 120); test.processNextMidiEvent (MidiMessage::controllerEvent (4, 106, 121)); expectEquals (test.timbreCallCounter, 1); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 106, 122)); expectEquals (test.timbreCallCounter, 1); test.processNextMidiEvent (MidiMessage::controllerEvent (4, 74, 123)); expectEquals (test.timbreCallCounter, 2); expectEquals (test.lastMidiChannelReceived, 4); expectEquals (test.lastMPEValueReceived.as14BitInt(), 121 + (123 << 7)); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 74, 124)); expectEquals (test.timbreCallCounter, 3); expectEquals (test.lastMidiChannelReceived, 5); expectEquals (test.lastMPEValueReceived.as14BitInt(), 122 + (124 << 7)); test.processNextMidiEvent (MidiMessage::controllerEvent (5, 74, 64)); expectEquals (test.timbreCallCounter, 4); expectEquals (test.lastMidiChannelReceived, 5); expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); // sustain pedal message (CC64) should trigger sustainPedal method call test.processNextMidiEvent (MidiMessage::controllerEvent (1, 64, 127)); expectEquals (test.sustainPedalCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 1); expect (test.lastSustainPedalValueReceived); test.processNextMidiEvent (MidiMessage::controllerEvent (16, 64, 0)); expectEquals (test.sustainPedalCallCounter, 2); expectEquals (test.lastMidiChannelReceived, 16); expect (! test.lastSustainPedalValueReceived); // sostenuto pedal message (CC66) should trigger sostenutoPedal method call test.processNextMidiEvent (MidiMessage::controllerEvent (1, 66, 127)); expectEquals (test.sostenutoPedalCallCounter, 1); expectEquals (test.lastMidiChannelReceived, 1); expect (test.lastSostenutoPedalValueReceived); test.processNextMidiEvent (MidiMessage::controllerEvent (16, 66, 0)); expectEquals (test.sostenutoPedalCallCounter, 2); expectEquals (test.lastMidiChannelReceived, 16); expect (! test.lastSostenutoPedalValueReceived); } { // MIDI messages modifying the zone layout should be correctly // forwarded to the internal zone layout and modify it. // (testing the actual logic of the zone layout is done in the // MPEZoneLayout unit tests) MPEInstrument test; MidiBuffer buffer; buffer.addEvents (MPEMessages::addZone (MPEZone (2, 5)), 0, -1, 0); buffer.addEvents (MPEMessages::addZone (MPEZone (9, 6)), 0, -1, 0); MidiBuffer::Iterator iter (buffer); MidiMessage message; int samplePosition; // not actually used, so no need to initialise. 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); } beginTest ("MIDI all notes off"); { UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 61, MPEValue::from7BitInt (100)); test.noteOn (15, 62, MPEValue::from7BitInt (100)); test.noteOn (15, 63, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 4); // on note channel: ignore. test.processNextMidiEvent (MidiMessage::allNotesOff (3)); expectEquals (test.getNumPlayingNotes(), 4); // on unused channel: ignore. test.processNextMidiEvent (MidiMessage::allNotesOff (1)); expectEquals (test.getNumPlayingNotes(), 4); // on master channel: release notes in that zone only. test.processNextMidiEvent (MidiMessage::allNotesOff (2)); expectEquals (test.getNumPlayingNotes(), 2); test.processNextMidiEvent (MidiMessage::allNotesOff (9)); expectEquals (test.getNumPlayingNotes(), 0); } beginTest ("MIDI all notes off (legacy mode)"); { UnitTestInstrument test; test.enableLegacyMode(); test.noteOn (3, 60, MPEValue::from7BitInt (100)); test.noteOn (4, 61, MPEValue::from7BitInt (100)); test.noteOn (15, 62, MPEValue::from7BitInt (100)); test.noteOn (15, 63, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 4); test.processNextMidiEvent (MidiMessage::allNotesOff (3)); expectEquals (test.getNumPlayingNotes(), 3); test.processNextMidiEvent (MidiMessage::allNotesOff (15)); expectEquals (test.getNumPlayingNotes(), 1); test.processNextMidiEvent (MidiMessage::allNotesOff (4)); expectEquals (test.getNumPlayingNotes(), 0); } beginTest ("default getInitial...ForNoteOn"); { MPEInstrument test; test.setZoneLayout (testLayout); test.pitchbend (3, MPEValue::from14BitInt (3333)); // use for next note-on on ch. 3 test.pitchbend (2, MPEValue::from14BitInt (4444)); // ignore test.pitchbend (2, MPEValue::from14BitInt (5555)); // ignore test.timbre (3, MPEValue::from7BitInt (66)); // use for next note-on on ch. 3 test.timbre (2, MPEValue::from7BitInt (77)); // ignore test.timbre (2, MPEValue::from7BitInt (88)); // ignore test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectNote (test.getMostRecentNote (3), 100, 100, 3333, 66, MPENote::keyDown); } beginTest ("overriding getInitial...ForNoteOn"); { CustomInitialValuesTest<33, 4444, 55> test; test.setZoneLayout (testLayout); test.noteOn (3, 61, MPEValue::from7BitInt (100)); expectNote (test.getMostRecentNote (3), 100, 33, 4444, 55, MPENote::keyDown); } beginTest ("Legacy mode"); { { // basic check MPEInstrument test; expect (! test.isLegacyModeEnabled()); test.setZoneLayout (testLayout); expect (! test.isLegacyModeEnabled()); test.enableLegacyMode(); expect (test.isLegacyModeEnabled()); test.setZoneLayout (testLayout); expect (! test.isLegacyModeEnabled()); } { // constructor w/o default arguments MPEInstrument test; test.enableLegacyMode (0, Range (1, 11)); expectEquals (test.getLegacyModePitchbendRange(), 0); expect (test.getLegacyModeChannelRange() == Range (1, 11)); } { // getters and setters MPEInstrument test; test.enableLegacyMode(); expectEquals (test.getLegacyModePitchbendRange(), 2); expect (test.getLegacyModeChannelRange() == Range (1, 17)); test.setLegacyModePitchbendRange (96); expectEquals (test.getLegacyModePitchbendRange(), 96); test.setLegacyModeChannelRange (Range (10, 12)); expect (test.getLegacyModeChannelRange() == Range (10, 12)); } { // note on should trigger notes on all 16 channels UnitTestInstrument test; test.enableLegacyMode(); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOn (2, 60, MPEValue::from7BitInt (100)); test.noteOn (15, 60, MPEValue::from7BitInt (100)); test.noteOn (16, 60, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 4); // polyphonic modulation should work across all 16 channels test.pitchbend (1, MPEValue::from14BitInt (9999)); test.pressure (2, MPEValue::from7BitInt (88)); test.timbre (15, MPEValue::from7BitInt (77)); expectNote (test.getNote (1, 60), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (2, 60), 100, 88, 8192, 64, MPENote::keyDown); 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 legacy mode test.noteOff (15, 60, MPEValue::from7BitInt (0)); test.noteOff (1, 60, MPEValue::from7BitInt (0)); test.noteOff (2, 60, MPEValue::from7BitInt (0)); test.noteOff (16, 60, MPEValue::from7BitInt (0)); expectEquals (test.getNumPlayingNotes(), 0); } { // legacy mode w/ custom channel range: note on should trigger notes only within range UnitTestInstrument test; test.enableLegacyMode (2, Range (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.enableLegacyMode(); test.setPitchbendTrackingMode (MPEInstrument::lastNotePlayedOnChannel); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOn (1, 62, MPEValue::from7BitInt (100)); test.noteOn (1, 61, MPEValue::from7BitInt (100)); test.pitchbend (1, MPEValue::from14BitInt (9999)); expectNote (test.getNote (1, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (1, 61), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (1, 62), 100, 100, 8192, 64, MPENote::keyDown); } { UnitTestInstrument test; test.enableLegacyMode(); test.setPitchbendTrackingMode (MPEInstrument::lowestNoteOnChannel); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOn (1, 62, MPEValue::from7BitInt (100)); test.noteOn (1, 61, MPEValue::from7BitInt (100)); test.pitchbend (1, MPEValue::from14BitInt (9999)); expectNote (test.getNote (1, 60), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (1, 61), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (1, 62), 100, 100, 8192, 64, MPENote::keyDown); } { UnitTestInstrument test; test.enableLegacyMode(); test.setPitchbendTrackingMode (MPEInstrument::highestNoteOnChannel); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOn (1, 62, MPEValue::from7BitInt (100)); test.noteOn (1, 61, MPEValue::from7BitInt (100)); test.pitchbend (1, MPEValue::from14BitInt (9999)); expectNote (test.getNote (1, 60), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (1, 61), 100, 100, 8192, 64, MPENote::keyDown); expectNote (test.getNote (1, 62), 100, 100, 9999, 64, MPENote::keyDown); } { UnitTestInstrument test; test.enableLegacyMode(); test.setPitchbendTrackingMode (MPEInstrument::allNotesOnChannel); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOn (1, 62, MPEValue::from7BitInt (100)); test.noteOn (1, 61, MPEValue::from7BitInt (100)); test.pitchbend (1, MPEValue::from14BitInt (9999)); expectNote (test.getNote (1, 60), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (1, 61), 100, 100, 9999, 64, MPENote::keyDown); expectNote (test.getNote (1, 62), 100, 100, 9999, 64, MPENote::keyDown); } } { // custom pitchbend range in legacy mode. UnitTestInstrument test; 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 legacy mode. UnitTestInstrument test; test.enableLegacyMode(); test.sustainPedal (1, true); test.noteOn (2, 61, MPEValue::from7BitInt (100)); test.noteOff (2, 61, MPEValue::from7BitInt (100)); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.noteOff (1, 60, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (1, 60), 100, 100, 8192, 64, MPENote::sustained); 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 legacy mode. UnitTestInstrument test; test.enableLegacyMode(); test.noteOn (1, 60, MPEValue::from7BitInt (100)); test.sostenutoPedal (1, true); test.noteOff (1, 60, MPEValue::from7BitInt (100)); test.noteOn (2, 61, MPEValue::from7BitInt (100)); test.noteOff (2, 61, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 1); expectNote (test.getNote (1, 60), 100, 100, 8192, 64, MPENote::sustained); 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 UnitTestInstrument test; test.setZoneLayout (testLayout); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 1); test.enableLegacyMode(); expectEquals (test.getNumPlayingNotes(), 0); test.noteOn (3, 60, MPEValue::from7BitInt (100)); expectEquals (test.getNumPlayingNotes(), 1); test.setZoneLayout (testLayout); expectEquals (test.getNumPlayingNotes(), 0); } } } private: //============================================================================== /* This mock class is used for unit testing whether the methods of MPEInstrument are called correctly. */ class UnitTestInstrument : public MPEInstrument, private MPEInstrument::Listener { typedef MPEInstrument Base; public: UnitTestInstrument() : noteOnCallCounter (0), noteOffCallCounter (0), pitchbendCallCounter (0), pressureCallCounter (0), timbreCallCounter (0), sustainPedalCallCounter (0), sostenutoPedalCallCounter (0), noteAddedCallCounter (0), notePressureChangedCallCounter (0), notePitchbendChangedCallCounter (0), noteTimbreChangedCallCounter (0), noteKeyStateChangedCallCounter (0), noteReleasedCallCounter (0), lastMidiChannelReceived (-1), lastMidiNoteNumberReceived (-1), lastSustainPedalValueReceived (false), lastSostenutoPedalValueReceived (false) { addListener (this); } void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity) override { Base::noteOn (midiChannel, midiNoteNumber, midiNoteOnVelocity); noteOnCallCounter++; lastMidiChannelReceived = midiChannel; lastMidiNoteNumberReceived = midiNoteNumber; lastMPEValueReceived = midiNoteOnVelocity; } void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity) override { Base::noteOff (midiChannel, midiNoteNumber, midiNoteOffVelocity); noteOffCallCounter++; lastMidiChannelReceived = midiChannel; lastMidiNoteNumberReceived = midiNoteNumber; lastMPEValueReceived = midiNoteOffVelocity; } void pitchbend (int midiChannel, MPEValue value) override { Base::pitchbend (midiChannel, value); pitchbendCallCounter++; lastMidiChannelReceived = midiChannel; lastMPEValueReceived = value; } void pressure (int midiChannel, MPEValue value) override { Base::pressure (midiChannel, value); pressureCallCounter++; lastMidiChannelReceived = midiChannel; lastMPEValueReceived = value; } void timbre (int midiChannel, MPEValue value) override { Base::timbre (midiChannel, value); timbreCallCounter++; lastMidiChannelReceived = midiChannel; lastMPEValueReceived = value; } void sustainPedal (int midiChannel, bool value) override { Base::sustainPedal (midiChannel, value); sustainPedalCallCounter++; lastMidiChannelReceived = midiChannel; lastSustainPedalValueReceived = value; } void sostenutoPedal (int midiChannel, bool value) override { Base::sostenutoPedal (midiChannel, value); sostenutoPedalCallCounter++; lastMidiChannelReceived = midiChannel; lastSostenutoPedalValueReceived = value; } int noteOnCallCounter, noteOffCallCounter, pitchbendCallCounter, pressureCallCounter, timbreCallCounter, sustainPedalCallCounter, sostenutoPedalCallCounter, noteAddedCallCounter, notePressureChangedCallCounter, notePitchbendChangedCallCounter, noteTimbreChangedCallCounter, noteKeyStateChangedCallCounter, noteReleasedCallCounter, lastMidiChannelReceived, lastMidiNoteNumberReceived; bool lastSustainPedalValueReceived, lastSostenutoPedalValueReceived; MPEValue lastMPEValueReceived; ScopedPointer lastNoteFinished; private: //============================================================================== void noteAdded (MPENote) override { noteAddedCallCounter++; } void notePressureChanged (MPENote) override { notePressureChangedCallCounter++; } void notePitchbendChanged (MPENote) override { notePitchbendChangedCallCounter++; } void noteTimbreChanged (MPENote) override { noteTimbreChangedCallCounter++; } void noteKeyStateChanged (MPENote) override { noteKeyStateChangedCallCounter++; } void noteReleased (MPENote finishedNote) override { noteReleasedCallCounter++; lastNoteFinished = new MPENote (finishedNote); } }; //============================================================================== template class CustomInitialValuesTest : public MPEInstrument { MPEValue getInitialPitchbendForNoteOn (int, int, MPEValue) const override { return MPEValue::from14BitInt (initial14BitPitchbend); } MPEValue getInitialPressureForNoteOn (int, int, MPEValue) const override { return MPEValue::from7BitInt (initial7BitPressure); } MPEValue getInitialTimbreForNoteOn (int, int, MPEValue) const override { return MPEValue::from7BitInt (initial7BitTimbre); } }; //============================================================================== void expectNote (MPENote noteToTest, int noteOnVelocity7Bit, int pressure7Bit, int pitchbend14Bit, int timbre7Bit, MPENote::KeyState keyState) { expect (noteToTest.isValid()); expectEquals (noteToTest.noteOnVelocity.as7BitInt(), noteOnVelocity7Bit); expectEquals (noteToTest.pressure.as7BitInt(), pressure7Bit); expectEquals (noteToTest.pitchbend.as14BitInt(), pitchbend14Bit); expectEquals (noteToTest.timbre.as7BitInt(),timbre7Bit); expect (noteToTest.keyState == keyState); } void expectHasFinishedNote (const UnitTestInstrument& test, int channel, int noteNumber, int noteOffVelocity7Bit) { expect (test.lastNoteFinished != nullptr); expectEquals (int (test.lastNoteFinished->midiChannel), channel); expectEquals (int (test.lastNoteFinished->initialNote), noteNumber); expectEquals (test.lastNoteFinished->noteOffVelocity.as7BitInt(), noteOffVelocity7Bit); expect (test.lastNoteFinished->keyState == MPENote::off); } void expectDoubleWithinRelativeError (double actual, double expected, double maxRelativeError) { const double maxAbsoluteError = jmax (1.0, std::fabs (expected)) * maxRelativeError; expect (std::fabs (expected - actual) < maxAbsoluteError); } //============================================================================== MPEZoneLayout testLayout; }; static MPEInstrumentTests MPEInstrumentUnitTests; #endif // JUCE_UNIT_TESTS