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