The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
14KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. The code included in this file is provided under the terms of the ISC license
  8. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  9. To use, copy, modify, and/or distribute this software for any purpose with or
  10. without fee is hereby granted provided that the above copyright notice and
  11. this permission notice appear in all copies.
  12. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  13. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  14. DISCLAIMED.
  15. ==============================================================================
  16. */
  17. namespace juce
  18. {
  19. MPEChannelAssigner::MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse)
  20. : zone (zoneToUse),
  21. channelIncrement (zone.isLowerZone() ? 1 : -1),
  22. numChannels (zone.numMemberChannels),
  23. firstChannel (zone.getFirstMemberChannel()),
  24. lastChannel (zone.getLastMemberChannel()),
  25. midiChannelLastAssigned (firstChannel - channelIncrement)
  26. {
  27. // must be an active MPE zone!
  28. jassert (numChannels > 0);
  29. }
  30. int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
  31. {
  32. if (numChannels == 1)
  33. return firstChannel;
  34. for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
  35. {
  36. if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
  37. {
  38. midiChannelLastAssigned = ch;
  39. midiChannels[ch].notes.add (noteNumber);
  40. return ch;
  41. }
  42. }
  43. for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
  44. {
  45. if (ch == lastChannel + channelIncrement) // loop wrap-around
  46. ch = firstChannel;
  47. if (midiChannels[ch].isFree())
  48. {
  49. midiChannelLastAssigned = ch;
  50. midiChannels[ch].notes.add (noteNumber);
  51. return ch;
  52. }
  53. if (ch == midiChannelLastAssigned)
  54. break; // no free channels!
  55. }
  56. midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
  57. midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
  58. return midiChannelLastAssigned;
  59. }
  60. void MPEChannelAssigner::noteOff (int noteNumber)
  61. {
  62. for (auto& ch : midiChannels)
  63. {
  64. if (ch.notes.removeAllInstancesOf (noteNumber) > 0)
  65. {
  66. ch.lastNotePlayed = noteNumber;
  67. return;
  68. }
  69. }
  70. }
  71. void MPEChannelAssigner::allNotesOff()
  72. {
  73. for (auto& ch : midiChannels)
  74. {
  75. if (ch.notes.size() > 0)
  76. ch.lastNotePlayed = ch.notes.getLast();
  77. ch.notes.clear();
  78. }
  79. }
  80. int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
  81. {
  82. auto channelWithClosestNote = firstChannel;
  83. int closestNoteDistance = 127;
  84. for (auto ch = firstChannel; (zone.isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
  85. {
  86. for (auto note : midiChannels[ch].notes)
  87. {
  88. auto noteDistance = std::abs (note - noteNumber);
  89. if (noteDistance > 0 && noteDistance < closestNoteDistance)
  90. {
  91. closestNoteDistance = noteDistance;
  92. channelWithClosestNote = ch;
  93. }
  94. }
  95. }
  96. return channelWithClosestNote;
  97. }
  98. //==============================================================================
  99. MPEChannelRemapper::MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap)
  100. : zone (zoneToRemap),
  101. channelIncrement (zone.isLowerZone() ? 1 : -1),
  102. firstChannel (zone.getFirstMemberChannel()),
  103. lastChannel (zone.getLastMemberChannel())
  104. {
  105. // must be an active MPE zone!
  106. jassert (zone.numMemberChannels > 0);
  107. zeroArrays();
  108. }
  109. void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
  110. {
  111. auto channel = message.getChannel();
  112. if (! zone.isUsingChannelAsMemberChannel (channel))
  113. return;
  114. if (channel == zone.getMasterChannel() && message.isResetAllControllers())
  115. {
  116. clearSource (mpeSourceID);
  117. return;
  118. }
  119. auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
  120. if (messageIsNoteData (message))
  121. {
  122. ++counter;
  123. // fast path - no remap
  124. if (applyRemapIfExisting (channel, sourceAndChannelID, message))
  125. return;
  126. // find existing remap
  127. for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
  128. if (applyRemapIfExisting (chan, sourceAndChannelID, message))
  129. return;
  130. // no remap necessary
  131. if (sourceAndChannel[channel] == notMPE)
  132. {
  133. lastUsed[channel] = counter;
  134. sourceAndChannel[channel] = sourceAndChannelID;
  135. return;
  136. }
  137. // remap source & channel to new channel
  138. auto chan = getBestChanToReuse();
  139. sourceAndChannel[chan] = sourceAndChannelID;
  140. lastUsed[chan] = counter;
  141. message.setChannel (chan);
  142. }
  143. }
  144. void MPEChannelRemapper::reset() noexcept
  145. {
  146. for (auto& s : sourceAndChannel)
  147. s = notMPE;
  148. }
  149. void MPEChannelRemapper::clearChannel (int channel) noexcept
  150. {
  151. sourceAndChannel[channel] = notMPE;
  152. }
  153. void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
  154. {
  155. for (auto& s : sourceAndChannel)
  156. {
  157. if (uint32 (s >> 5) == mpeSourceID)
  158. {
  159. s = notMPE;
  160. return;
  161. }
  162. }
  163. }
  164. bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
  165. {
  166. if (sourceAndChannel[channel] == sourceAndChannelID)
  167. {
  168. if (m.isNoteOff())
  169. sourceAndChannel[channel] = notMPE;
  170. else
  171. lastUsed[channel] = counter;
  172. m.setChannel (channel);
  173. return true;
  174. }
  175. return false;
  176. }
  177. int MPEChannelRemapper::getBestChanToReuse() const noexcept
  178. {
  179. for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
  180. if (sourceAndChannel[chan] == notMPE)
  181. return chan;
  182. auto bestChan = firstChannel;
  183. auto bestLastUse = counter;
  184. for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
  185. {
  186. if (lastUsed[chan] < bestLastUse)
  187. {
  188. bestLastUse = lastUsed[chan];
  189. bestChan = chan;
  190. }
  191. }
  192. return bestChan;
  193. }
  194. void MPEChannelRemapper::zeroArrays()
  195. {
  196. for (int i = 0; i < 17; ++i)
  197. {
  198. sourceAndChannel[i] = 0;
  199. lastUsed[i] = 0;
  200. }
  201. }
  202. //==============================================================================
  203. //==============================================================================
  204. #if JUCE_UNIT_TESTS
  205. struct MPEUtilsUnitTests : public UnitTest
  206. {
  207. MPEUtilsUnitTests()
  208. : UnitTest ("MPE Utilities", "MIDI/MPE")
  209. {}
  210. void runTest() override
  211. {
  212. beginTest ("MPEChannelAssigner");
  213. {
  214. MPEZoneLayout layout;
  215. {
  216. layout.setLowerZone (15);
  217. // lower zone
  218. MPEChannelAssigner channelAssigner (layout.getLowerZone());
  219. // check that channels are assigned in correct order
  220. int noteNum = 60;
  221. for (int ch = 2; ch <= 16; ++ch)
  222. expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
  223. // check that note-offs are processed
  224. channelAssigner.noteOff (60);
  225. expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
  226. channelAssigner.noteOff (61);
  227. expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
  228. // check that assigned channel was last to play note
  229. channelAssigner.noteOff (65);
  230. channelAssigner.noteOff (66);
  231. expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
  232. expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
  233. // find closest channel playing nonequal note
  234. expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
  235. expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
  236. // all notes off
  237. channelAssigner.allNotesOff();
  238. // last note played
  239. expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
  240. expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
  241. expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
  242. expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
  243. // normal assignment
  244. expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
  245. expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
  246. }
  247. {
  248. layout.setUpperZone (15);
  249. // upper zone
  250. MPEChannelAssigner channelAssigner (layout.getUpperZone());
  251. // check that channels are assigned in correct order
  252. int noteNum = 60;
  253. for (int ch = 15; ch >= 1; --ch)
  254. expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
  255. // check that note-offs are processed
  256. channelAssigner.noteOff (60);
  257. expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
  258. channelAssigner.noteOff (61);
  259. expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
  260. // check that assigned channel was last to play note
  261. channelAssigner.noteOff (65);
  262. channelAssigner.noteOff (66);
  263. expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
  264. expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
  265. // find closest channel playing nonequal note
  266. expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
  267. expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
  268. // all notes off
  269. channelAssigner.allNotesOff();
  270. // last note played
  271. expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
  272. expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
  273. expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
  274. expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
  275. // normal assignment
  276. expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
  277. expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
  278. }
  279. }
  280. beginTest ("MPEChannelRemapper");
  281. {
  282. // 3 different MPE 'sources', constant IDs
  283. const int sourceID1 = 0;
  284. const int sourceID2 = 1;
  285. const int sourceID3 = 2;
  286. MPEZoneLayout layout;
  287. {
  288. layout.setLowerZone (15);
  289. // lower zone
  290. MPEChannelRemapper channelRemapper (layout.getLowerZone());
  291. // first source, shouldn't remap
  292. for (int ch = 2; ch <= 16; ++ch)
  293. {
  294. auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
  295. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
  296. expectEquals (noteOn.getChannel(), ch);
  297. }
  298. auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
  299. // remap onto oldest last-used channel
  300. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
  301. expectEquals (noteOn.getChannel(), 2);
  302. // remap onto oldest last-used channel
  303. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
  304. expectEquals (noteOn.getChannel(), 3);
  305. // remap to correct channel for source ID
  306. auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
  307. channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
  308. expectEquals (noteOff.getChannel(), 3);
  309. }
  310. {
  311. layout.setUpperZone (15);
  312. // upper zone
  313. MPEChannelRemapper channelRemapper (layout.getUpperZone());
  314. // first source, shouldn't remap
  315. for (int ch = 15; ch >= 1; --ch)
  316. {
  317. auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
  318. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
  319. expectEquals (noteOn.getChannel(), ch);
  320. }
  321. auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
  322. // remap onto oldest last-used channel
  323. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
  324. expectEquals (noteOn.getChannel(), 15);
  325. // remap onto oldest last-used channel
  326. channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
  327. expectEquals (noteOn.getChannel(), 14);
  328. // remap to correct channel for source ID
  329. auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
  330. channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
  331. expectEquals (noteOff.getChannel(), 14);
  332. }
  333. }
  334. }
  335. };
  336. static MPEUtilsUnitTests MPEUtilsUnitTests;
  337. #endif
  338. } // namespace juce