OpenShot Library | OpenShotAudio  0.2.1
juce_MPEUtils.cpp
1 /*
2  ==============================================================================
3 
4  This file is part of the JUCE library.
5  Copyright (c) 2017 - ROLI Ltd.
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 (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
56  {
57  if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
58  {
59  midiChannelLastAssigned = ch;
60  midiChannels[ch].notes.add (noteNumber);
61  return ch;
62  }
63  }
64 
65  for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
66  {
67  if (ch == lastChannel + channelIncrement) // loop wrap-around
68  ch = firstChannel;
69 
70  if (midiChannels[ch].isFree())
71  {
72  midiChannelLastAssigned = ch;
73  midiChannels[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[midiChannelLastAssigned].notes.add (noteNumber);
83 
84  return midiChannelLastAssigned;
85 }
86 
87 void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
88 {
89  const auto removeNote = [] (MidiChannel& ch, int noteNum)
90  {
91  if (ch.notes.removeAllInstancesOf (noteNum) > 0)
92  {
93  ch.lastNotePlayed = noteNum;
94  return true;
95  }
96 
97  return false;
98  };
99 
100  if (midiChannel >= 0 && midiChannel < 17)
101  {
102  removeNote (midiChannels[midiChannel], noteNumber);
103  return;
104  }
105 
106  for (auto& ch : midiChannels)
107  {
108  if (removeNote (ch, noteNumber))
109  return;
110  }
111 }
112 
114 {
115  for (auto& ch : midiChannels)
116  {
117  if (ch.notes.size() > 0)
118  ch.lastNotePlayed = ch.notes.getLast();
119 
120  ch.notes.clear();
121  }
122 }
123 
124 int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
125 {
126  auto channelWithClosestNote = firstChannel;
127  int closestNoteDistance = 127;
128 
129  for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
130  {
131  for (auto note : midiChannels[ch].notes)
132  {
133  auto noteDistance = std::abs (note - noteNumber);
134 
135  if (noteDistance > 0 && noteDistance < closestNoteDistance)
136  {
137  closestNoteDistance = noteDistance;
138  channelWithClosestNote = ch;
139  }
140  }
141  }
142 
143  return channelWithClosestNote;
144 }
145 
146 //==============================================================================
148  : zone (zoneToRemap),
149  channelIncrement (zone.isLowerZone() ? 1 : -1),
150  firstChannel (zone.getFirstMemberChannel()),
151  lastChannel (zone.getLastMemberChannel())
152 {
153  // must be an active MPE zone!
154  jassert (zone.numMemberChannels > 0);
155  zeroArrays();
156 }
157 
158 void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
159 {
160  auto channel = message.getChannel();
161 
162  if (! zone.isUsingChannelAsMemberChannel (channel))
163  return;
164 
165  if (channel == zone.getMasterChannel() && (message.isResetAllControllers() || message.isAllNotesOff()))
166  {
167  clearSource (mpeSourceID);
168  return;
169  }
170 
171  auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
172 
173  if (messageIsNoteData (message))
174  {
175  ++counter;
176 
177  // fast path - no remap
178  if (applyRemapIfExisting (channel, sourceAndChannelID, message))
179  return;
180 
181  // find existing remap
182  for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
183  if (applyRemapIfExisting (chan, sourceAndChannelID, message))
184  return;
185 
186  // no remap necessary
187  if (sourceAndChannel[channel] == notMPE)
188  {
189  lastUsed[channel] = counter;
190  sourceAndChannel[channel] = sourceAndChannelID;
191  return;
192  }
193 
194  // remap source & channel to new channel
195  auto chan = getBestChanToReuse();
196 
197  sourceAndChannel[chan] = sourceAndChannelID;
198  lastUsed[chan] = counter;
199  message.setChannel (chan);
200  }
201 }
202 
204 {
205  for (auto& s : sourceAndChannel)
206  s = notMPE;
207 }
208 
209 void MPEChannelRemapper::clearChannel (int channel) noexcept
210 {
211  sourceAndChannel[channel] = notMPE;
212 }
213 
214 void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
215 {
216  for (auto& s : sourceAndChannel)
217  {
218  if (uint32 (s >> 5) == mpeSourceID)
219  {
220  s = notMPE;
221  return;
222  }
223  }
224 }
225 
226 bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
227 {
228  if (sourceAndChannel[channel] == sourceAndChannelID)
229  {
230  if (m.isNoteOff())
231  sourceAndChannel[channel] = notMPE;
232  else
233  lastUsed[channel] = counter;
234 
235  m.setChannel (channel);
236  return true;
237  }
238 
239  return false;
240 }
241 
242 int MPEChannelRemapper::getBestChanToReuse() const noexcept
243 {
244  for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
245  if (sourceAndChannel[chan] == notMPE)
246  return chan;
247 
248  auto bestChan = firstChannel;
249  auto bestLastUse = counter;
250 
251  for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
252  {
253  if (lastUsed[chan] < bestLastUse)
254  {
255  bestLastUse = lastUsed[chan];
256  bestChan = chan;
257  }
258  }
259 
260  return bestChan;
261 }
262 
263 void MPEChannelRemapper::zeroArrays()
264 {
265  for (int i = 0; i < 17; ++i)
266  {
267  sourceAndChannel[i] = 0;
268  lastUsed[i] = 0;
269  }
270 }
271 
272 
273 //==============================================================================
274 //==============================================================================
275 #if JUCE_UNIT_TESTS
276 
277 struct MPEUtilsUnitTests : public UnitTest
278 {
279  MPEUtilsUnitTests()
280  : UnitTest ("MPE Utilities", UnitTestCategories::midi)
281  {}
282 
283  void runTest() override
284  {
285  beginTest ("MPEChannelAssigner");
286  {
287  MPEZoneLayout layout;
288 
289  // lower
290  {
291  layout.setLowerZone (15);
292 
293  // lower zone
294  MPEChannelAssigner channelAssigner (layout.getLowerZone());
295 
296  // check that channels are assigned in correct order
297  int noteNum = 60;
298  for (int ch = 2; ch <= 16; ++ch)
299  expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
300 
301  // check that note-offs are processed
302  channelAssigner.noteOff (60);
303  expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
304 
305  channelAssigner.noteOff (61);
306  expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
307 
308  // check that assigned channel was last to play note
309  channelAssigner.noteOff (65);
310  channelAssigner.noteOff (66);
311  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
312  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
313 
314  // find closest channel playing nonequal note
315  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
316  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
317 
318  // all notes off
319  channelAssigner.allNotesOff();
320 
321  // last note played
322  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
323  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
324  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
325  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
326 
327  // normal assignment
328  expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
329  expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
330  }
331 
332  // upper
333  {
334  layout.setUpperZone (15);
335 
336  // upper zone
337  MPEChannelAssigner channelAssigner (layout.getUpperZone());
338 
339  // check that channels are assigned in correct order
340  int noteNum = 60;
341  for (int ch = 15; ch >= 1; --ch)
342  expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
343 
344  // check that note-offs are processed
345  channelAssigner.noteOff (60);
346  expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
347 
348  channelAssigner.noteOff (61);
349  expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
350 
351  // check that assigned channel was last to play note
352  channelAssigner.noteOff (65);
353  channelAssigner.noteOff (66);
354  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
355  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
356 
357  // find closest channel playing nonequal note
358  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
359  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
360 
361  // all notes off
362  channelAssigner.allNotesOff();
363 
364  // last note played
365  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
366  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
367  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
368  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
369 
370  // normal assignment
371  expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
372  expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
373  }
374 
375  // legacy
376  {
377  MPEChannelAssigner channelAssigner;
378 
379  // check that channels are assigned in correct order
380  int noteNum = 60;
381  for (int ch = 1; ch <= 16; ++ch)
382  expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
383 
384  // check that note-offs are processed
385  channelAssigner.noteOff (60);
386  expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
387 
388  channelAssigner.noteOff (61);
389  expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
390 
391  // check that assigned channel was last to play note
392  channelAssigner.noteOff (65);
393  channelAssigner.noteOff (66);
394  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
395  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
396 
397  // find closest channel playing nonequal note
398  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
399  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
400 
401  // all notes off
402  channelAssigner.allNotesOff();
403 
404  // last note played
405  expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
406  expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
407  expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
408  expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
409 
410  // normal assignment
411  expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
412  expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
413  }
414  }
415 
416  beginTest ("MPEChannelRemapper");
417  {
418  // 3 different MPE 'sources', constant IDs
419  const int sourceID1 = 0;
420  const int sourceID2 = 1;
421  const int sourceID3 = 2;
422 
423  MPEZoneLayout layout;
424 
425  {
426  layout.setLowerZone (15);
427 
428  // lower zone
429  MPEChannelRemapper channelRemapper (layout.getLowerZone());
430 
431  // first source, shouldn't remap
432  for (int ch = 2; ch <= 16; ++ch)
433  {
434  auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
435 
436  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
437  expectEquals (noteOn.getChannel(), ch);
438  }
439 
440  auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
441 
442  // remap onto oldest last-used channel
443  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
444  expectEquals (noteOn.getChannel(), 2);
445 
446  // remap onto oldest last-used channel
447  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
448  expectEquals (noteOn.getChannel(), 3);
449 
450  // remap to correct channel for source ID
451  auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
452  channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
453  expectEquals (noteOff.getChannel(), 3);
454  }
455 
456  {
457  layout.setUpperZone (15);
458 
459  // upper zone
460  MPEChannelRemapper channelRemapper (layout.getUpperZone());
461 
462  // first source, shouldn't remap
463  for (int ch = 15; ch >= 1; --ch)
464  {
465  auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
466 
467  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
468  expectEquals (noteOn.getChannel(), ch);
469  }
470 
471  auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
472 
473  // remap onto oldest last-used channel
474  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
475  expectEquals (noteOn.getChannel(), 15);
476 
477  // remap onto oldest last-used channel
478  channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
479  expectEquals (noteOn.getChannel(), 14);
480 
481  // remap to correct channel for source ID
482  auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
483  channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
484  expectEquals (noteOff.getChannel(), 14);
485  }
486  }
487  }
488 };
489 
490 static MPEUtilsUnitTests MPEUtilsUnitTests;
491 
492 #endif
493 
494 } // namespace juce
void reset() noexcept
Resets all the source & channel combinations.
This class represents the current MPE zone layout of a device capable of handling MPE...
void remapMidiChannelIfNeeded(MidiMessage &message, uint32 mpeSourceID) noexcept
Remaps the MIDI channel of the specified MIDI message (if necessary).
Encapsulates a MIDI message.
void noteOff(int noteNumber, int midiChannel=-1)
You must call this method for all note-offs that you receive so that this class can keep track of the...
MPEChannelAssigner(MPEZoneLayout::Zone zoneToUse)
Constructor.
This struct represents an MPE zone.
This class handles the logic for remapping MIDI note messages from multiple MPE sources onto a specif...
MPEChannelRemapper(MPEZoneLayout::Zone zoneToRemap)
Constructor.
void setUpperZone(int numMemberChannels=0, int perNotePitchbendRange=48, int masterPitchbendRange=2) noexcept
Sets the upper zone of this layout.
void clearChannel(int channel) noexcept
Clears a specified channel of this MPE zone.
static const uint32 notMPE
Used to indicate that a particular source & channel combination is not currently using MPE...
const Zone getUpperZone() const noexcept
Returns a struct representing the upper MPE zone.
This is a base class for classes that perform a unit test.
Definition: juce_UnitTest.h:73
static MidiMessage noteOff(int channel, int noteNumber, float velocity) noexcept
Creates a key-up message.
void setChannel(int newChannelNumber) noexcept
Changes the message&#39;s midi channel.
const Zone getLowerZone() const noexcept
Returns a struct representing the lower MPE zone.
void clearSource(uint32 mpeSourceID)
Clears all channels in use by a specified source.
This class handles the assignment of new MIDI notes to member channels of an active MPE zone...
Definition: juce_MPEUtils.h:41
bool isNoteOff(bool returnTrueForNoteOnVelocity0=true) const noexcept
Returns true if this message is a &#39;key-up&#39; event.
JUCE_CONSTEXPR bool isEmpty() const noexcept
Returns true if the range has a length of zero.
Definition: juce_Range.h:93
void allNotesOff()
Call this to clear all currently playing notes.
int findMidiChannelForNewNote(int noteNumber) noexcept
This method will use a set of rules recommended in the MPE specification to determine which member ch...
static MidiMessage noteOn(int channel, int noteNumber, float velocity) noexcept
Creates a key-down message (using a floating-point velocity).
void setLowerZone(int numMemberChannels=0, int perNotePitchbendRange=48, int masterPitchbendRange=2) noexcept
Sets the lower zone of this layout.