OpenShot Audio Library | OpenShotAudio  0.6.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 
23 namespace 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 
50 int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
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 
97 void 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 
134 int 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 
168 void 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 
219 void MPEChannelRemapper::clearChannel (int channel) noexcept
220 {
221  sourceAndChannel[channel] = notMPE;
222 }
223 
224 void 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 
236 bool 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 
252 int 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 
273 void 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 
287 struct 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 
551 static 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