OpenShot Library | libopenshot-audio 0.2.0
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
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 (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
87void 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
110int 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
144void 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
195void MPEChannelRemapper::clearChannel (int channel) noexcept
196{
197 sourceAndChannel[channel] = notMPE;
198}
199
200void 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
212bool 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
228int 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
249void 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
262struct 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
475static MPEUtilsUnitTests MPEUtilsUnitTests;
476
477#endif
478} // namespace juce
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...
MPEChannelAssigner(MPEZoneLayout::Zone zoneToUse)
Constructor.
int findMidiChannelForNewNote(int noteNumber) noexcept
This method will use a set of rules recommended in the MPE specification to determine which member ch...
void allNotesOff()
Call this to clear all currently playing notes.
void reset() noexcept
Resets all the source & channel combinations.
void remapMidiChannelIfNeeded(MidiMessage &message, uint32 mpeSourceID) noexcept
Remaps the MIDI channel of the specified MIDI message (if necessary).
static const uint32 notMPE
Used to indicate that a particular source & channel combination is not currently using MPE.
void clearChannel(int channel) noexcept
Clears a specified channel of this MPE zone.
void clearSource(uint32 mpeSourceID)
Clears all channels in use by a specified source.
MPEChannelRemapper(MPEZoneLayout::Zone zoneToRemap)
Constructor.
This class represents the current MPE zone layout of a device capable of handling MPE.
Encapsulates a MIDI message.
A general-purpose range object, that simply represents any linear range with a start and end point.
Definition juce_Range.h:44
JUCE_CONSTEXPR bool isEmpty() const noexcept
Returns true if the range has a length of zero.
Definition juce_Range.h:93
This struct represents an MPE zone.