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