495 lines
17 KiB
C++
495 lines
17 KiB
C++
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE library.
|
||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||
|
|
||
|
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 <= 16)
|
||
|
{
|
||
|
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
|