OpenShot Audio Library | OpenShotAudio 0.4.0
juce_MPEUtils.cpp
1/*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 The code included in this file is provided under the terms of the ISC license
11 http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12 To use, copy, modify, and/or distribute this software for any purpose with or
13 without fee is hereby granted provided that the above copyright notice and
14 this permission notice appear in all copies.
15
16 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
18 DISCLAIMED.
19
20 ==============================================================================
21*/
22
23namespace juce
24{
25
27 : zone (new MPEZoneLayout::Zone (zoneToUse)),
28 channelIncrement (zone->isLowerZone() ? 1 : -1),
29 numChannels (zone->numMemberChannels),
30 firstChannel (zone->getFirstMemberChannel()),
31 lastChannel (zone->getLastMemberChannel()),
32 midiChannelLastAssigned (firstChannel - channelIncrement)
33{
34 // must be an active MPE zone!
35 jassert (numChannels > 0);
36}
37
39 : isLegacy (true),
40 channelIncrement (1),
41 numChannels (channelRange.getLength()),
42 firstChannel (channelRange.getStart()),
43 lastChannel (channelRange.getEnd() - 1),
44 midiChannelLastAssigned (firstChannel - channelIncrement)
45{
46 // must have at least one channel!
47 jassert (! channelRange.isEmpty());
48}
49
51{
52 if (numChannels <= 1)
53 return firstChannel;
54
55 for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
56 {
57 if (midiChannels[(size_t) ch].isFree() && midiChannels[(size_t) ch].lastNotePlayed == noteNumber)
58 {
59 midiChannelLastAssigned = ch;
60 midiChannels[(size_t) ch].notes.add (noteNumber);
61 return ch;
62 }
63 }
64
65 for (int ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
66 {
67 if (ch == lastChannel + channelIncrement) // loop wrap-around
68 ch = firstChannel;
69
70 if (midiChannels[(size_t) ch].isFree())
71 {
72 midiChannelLastAssigned = ch;
73 midiChannels[(size_t) ch].notes.add (noteNumber);
74 return ch;
75 }
76
77 if (ch == midiChannelLastAssigned)
78 break; // no free channels!
79 }
80
81 midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
82 midiChannels[(size_t) midiChannelLastAssigned].notes.add (noteNumber);
83
84 return midiChannelLastAssigned;
85}
86
88{
89 const auto iter = std::find_if (midiChannels.cbegin(), midiChannels.cend(), [&] (auto& ch)
90 {
91 return std::find (ch.notes.begin(), ch.notes.end(), noteNumber) != ch.notes.end();
92 });
93
94 return iter != midiChannels.cend() ? (int) std::distance (midiChannels.cbegin(), iter) : -1;
95}
96
97void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
98{
99 const auto removeNote = [] (MidiChannel& ch, int noteNum)
100 {
101 if (ch.notes.removeAllInstancesOf (noteNum) > 0)
102 {
103 ch.lastNotePlayed = noteNum;
104 return true;
105 }
106
107 return false;
108 };
109
110 if (midiChannel >= 0 && midiChannel <= 16)
111 {
112 removeNote (midiChannels[(size_t) midiChannel], noteNumber);
113 return;
114 }
115
116 for (auto& ch : midiChannels)
117 {
118 if (removeNote (ch, noteNumber))
119 return;
120 }
121}
122
124{
125 for (auto& ch : midiChannels)
126 {
127 if (ch.notes.size() > 0)
128 ch.lastNotePlayed = ch.notes.getLast();
129
130 ch.notes.clear();
131 }
132}
133
134int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
135{
136 auto channelWithClosestNote = firstChannel;
137 int closestNoteDistance = 127;
138
139 for (int ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
140 {
141 for (auto note : midiChannels[(size_t) ch].notes)
142 {
143 auto noteDistance = std::abs (note - noteNumber);
144
145 if (noteDistance > 0 && noteDistance < closestNoteDistance)
146 {
147 closestNoteDistance = noteDistance;
148 channelWithClosestNote = ch;
149 }
150 }
151 }
152
153 return channelWithClosestNote;
154}
155
156//==============================================================================
158 : zone (zoneToRemap),
159 channelIncrement (zone.isLowerZone() ? 1 : -1),
160 firstChannel (zone.getFirstMemberChannel()),
161 lastChannel (zone.getLastMemberChannel())
162{
163 // must be an active MPE zone!
164 jassert (zone.numMemberChannels > 0);
165 zeroArrays();
166}
167
168void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
169{
170 auto channel = message.getChannel();
171
172 if (! zone.isUsingChannelAsMemberChannel (channel))
173 return;
174
175 if (channel == zone.getMasterChannel() && (message.isResetAllControllers() || message.isAllNotesOff()))
176 {
177 clearSource (mpeSourceID);
178 return;
179 }
180
181 auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
182
183 if (messageIsNoteData (message))
184 {
185 ++counter;
186
187 // fast path - no remap
188 if (applyRemapIfExisting (channel, sourceAndChannelID, message))
189 return;
190
191 // find existing remap
192 for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
193 if (applyRemapIfExisting (chan, sourceAndChannelID, message))
194 return;
195
196 // no remap necessary
197 if (sourceAndChannel[channel] == notMPE)
198 {
199 lastUsed[channel] = counter;
200 sourceAndChannel[channel] = sourceAndChannelID;
201 return;
202 }
203
204 // remap source & channel to new channel
205 auto chan = getBestChanToReuse();
206
207 sourceAndChannel[chan] = sourceAndChannelID;
208 lastUsed[chan] = counter;
209 message.setChannel (chan);
210 }
211}
212
214{
215 for (auto& s : sourceAndChannel)
216 s = notMPE;
217}
218
219void MPEChannelRemapper::clearChannel (int channel) noexcept
220{
221 sourceAndChannel[channel] = notMPE;
222}
223
224void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
225{
226 for (auto& s : sourceAndChannel)
227 {
228 if (uint32 (s >> 5) == mpeSourceID)
229 {
230 s = notMPE;
231 return;
232 }
233 }
234}
235
236bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
237{
238 if (sourceAndChannel[channel] == sourceAndChannelID)
239 {
240 if (m.isNoteOff())
241 sourceAndChannel[channel] = notMPE;
242 else
243 lastUsed[channel] = counter;
244
245 m.setChannel (channel);
246 return true;
247 }
248
249 return false;
250}
251
252int MPEChannelRemapper::getBestChanToReuse() const noexcept
253{
254 for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
255 if (sourceAndChannel[chan] == notMPE)
256 return chan;
257
258 auto bestChan = firstChannel;
259 auto bestLastUse = counter;
260
261 for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
262 {
263 if (lastUsed[chan] < bestLastUse)
264 {
265 bestLastUse = lastUsed[chan];
266 bestChan = chan;
267 }
268 }
269
270 return bestChan;
271}
272
273void MPEChannelRemapper::zeroArrays()
274{
275 for (int i = 0; i < 17; ++i)
276 {
277 sourceAndChannel[i] = 0;
278 lastUsed[i] = 0;
279 }
280}
281
282
283//==============================================================================
284//==============================================================================
285#if JUCE_UNIT_TESTS
286
287struct MPEUtilsUnitTests final : public UnitTest
288{
289 MPEUtilsUnitTests()
290 : UnitTest ("MPE Utilities", UnitTestCategories::midi)
291 {}
292
293 void runTest() override
294 {
295 beginTest ("MPEChannelAssigner");
296 {
297 MPEZoneLayout layout;
298
299 // lower
300 {
301 layout.setLowerZone (15);
302
303 // lower zone
304 MPEChannelAssigner channelAssigner (layout.getLowerZone());
305
306 // check that channels are assigned in correct order
307 int noteNum = 60;
308 for (int ch = 2; ch <= 16; ++ch)
309 {
310 expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
311 expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
312
313 ++noteNum;
314 }
315
316 // check that note-offs are processed
317 channelAssigner.noteOff (60);
318 expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
319 expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 2);
320
321 channelAssigner.noteOff (61);
322 expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
323 expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 3);
324
325 // check that assigned channel was last to play note
326 channelAssigner.noteOff (65);
327 channelAssigner.noteOff (66);
328 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
329 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
330 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
331 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
332
333 // find closest channel playing nonequal note
334 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
335 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
336 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
337 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
338
339 // all notes off
340 channelAssigner.allNotesOff();
341
342 // last note played
343 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
344 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
345 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
346 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
347 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8);
348 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7);
349 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
350 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2);
351
352 // normal assignment
353 expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
354 expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
355 expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 3);
356 expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 4);
357 }
358
359 // upper
360 {
361 layout.setUpperZone (15);
362
363 // upper zone
364 MPEChannelAssigner channelAssigner (layout.getUpperZone());
365
366 // check that channels are assigned in correct order
367 int noteNum = 60;
368 for (int ch = 15; ch >= 1; --ch)
369 {
370 expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
371 expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
372
373 ++noteNum;
374 }
375
376 // check that note-offs are processed
377 channelAssigner.noteOff (60);
378 expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
379 expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 15);
380
381 channelAssigner.noteOff (61);
382 expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
383 expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 14);
384
385 // check that assigned channel was last to play note
386 channelAssigner.noteOff (65);
387 channelAssigner.noteOff (66);
388 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
389 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
390 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
391 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
392
393 // find closest channel playing nonequal note
394 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
395 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
396 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
397 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
398
399 // all notes off
400 channelAssigner.allNotesOff();
401
402 // last note played
403 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
404 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
405 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
406 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
407 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9);
408 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10);
409 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1);
410 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15);
411
412 // normal assignment
413 expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
414 expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
415 expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 14);
416 expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 13);
417 }
418
419 // legacy
420 {
421 MPEChannelAssigner channelAssigner;
422
423 // check that channels are assigned in correct order
424 int noteNum = 60;
425 for (int ch = 1; ch <= 16; ++ch)
426 {
427 expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch);
428 expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch);
429
430 ++noteNum;
431 }
432
433 // check that note-offs are processed
434 channelAssigner.noteOff (60);
435 expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
436 expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 1);
437
438 channelAssigner.noteOff (61);
439 expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
440 expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 2);
441
442 // check that assigned channel was last to play note
443 channelAssigner.noteOff (65);
444 channelAssigner.noteOff (66);
445 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
446 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
447 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
448 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
449
450 // find closest channel playing nonequal note
451 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
452 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
453 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
454 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
455
456 // all notes off
457 channelAssigner.allNotesOff();
458
459 // last note played
460 expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
461 expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
462 expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
463 expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
464 expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7);
465 expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6);
466 expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16);
467 expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1);
468
469 // normal assignment
470 expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
471 expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
472 expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 2);
473 expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 3);
474 }
475 }
476
477 beginTest ("MPEChannelRemapper");
478 {
479 // 3 different MPE 'sources', constant IDs
480 const int sourceID1 = 0;
481 const int sourceID2 = 1;
482 const int sourceID3 = 2;
483
484 MPEZoneLayout layout;
485
486 {
487 layout.setLowerZone (15);
488
489 // lower zone
490 MPEChannelRemapper channelRemapper (layout.getLowerZone());
491
492 // first source, shouldn't remap
493 for (int ch = 2; ch <= 16; ++ch)
494 {
495 auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
496
497 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
498 expectEquals (noteOn.getChannel(), ch);
499 }
500
501 auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
502
503 // remap onto oldest last-used channel
504 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
505 expectEquals (noteOn.getChannel(), 2);
506
507 // remap onto oldest last-used channel
508 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
509 expectEquals (noteOn.getChannel(), 3);
510
511 // remap to correct channel for source ID
512 auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
513 channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
514 expectEquals (noteOff.getChannel(), 3);
515 }
516
517 {
518 layout.setUpperZone (15);
519
520 // upper zone
521 MPEChannelRemapper channelRemapper (layout.getUpperZone());
522
523 // first source, shouldn't remap
524 for (int ch = 15; ch >= 1; --ch)
525 {
526 auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
527
528 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
529 expectEquals (noteOn.getChannel(), ch);
530 }
531
532 auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
533
534 // remap onto oldest last-used channel
535 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
536 expectEquals (noteOn.getChannel(), 15);
537
538 // remap onto oldest last-used channel
539 channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
540 expectEquals (noteOn.getChannel(), 14);
541
542 // remap to correct channel for source ID
543 auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
544 channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
545 expectEquals (noteOff.getChannel(), 14);
546 }
547 }
548 }
549};
550
551static MPEUtilsUnitTests MPEUtilsUnitTests;
552
553#endif
554
555} // namespace juce
int findMidiChannelForExistingNote(int initialNoteOnNumber) noexcept
MPEChannelAssigner(MPEZoneLayout::Zone zoneToUse)
int findMidiChannelForNewNote(int noteNumber) noexcept
void noteOff(int noteNumber, int midiChannel=-1)
void remapMidiChannelIfNeeded(MidiMessage &message, uint32 mpeSourceID) noexcept
static const uint32 notMPE
void clearChannel(int channel) noexcept
void clearSource(uint32 mpeSourceID)
MPEChannelRemapper(MPEZoneLayout::Zone zoneToRemap)
static MidiMessage noteOn(int channel, int noteNumber, float velocity) noexcept
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
constexpr bool isEmpty() const noexcept
Definition: juce_Range.h:89