Audio plugin host https://kx.studio/carla
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.

414 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - Raw Material Software Limited
  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. //==============================================================================
  20. /**
  21. This class represents an instrument handling MPE.
  22. It has an MPE zone layout and maintains a state of currently
  23. active (playing) notes and the values of their dimensions of expression.
  24. You can trigger and modulate notes:
  25. - by passing MIDI messages with the method processNextMidiEvent;
  26. - by directly calling the methods noteOn, noteOff etc.
  27. The class implements the channel and note management logic specified in
  28. MPE. If you pass it a message, it will know what notes on what
  29. channels (if any) should be affected by that message.
  30. The class has a Listener class with the three callbacks MPENoteAdded,
  31. MPENoteChanged, and MPENoteFinished. Implement such a
  32. Listener class to react to note changes and trigger some functionality for
  33. your application that depends on the MPE note state.
  34. For example, you can use this class to write an MPE visualiser.
  35. If you want to write a real-time audio synth with MPE functionality,
  36. you should instead use the classes MPESynthesiserBase, which adds
  37. the ability to render audio and to manage voices.
  38. @see MPENote, MPEZoneLayout, MPESynthesiser
  39. @tags{Audio}
  40. */
  41. class JUCE_API MPEInstrument
  42. {
  43. public:
  44. /** Constructor.
  45. This will construct an MPE instrument with inactive lower and upper zones.
  46. In order to process incoming MIDI, call setZoneLayout, define the layout
  47. via MIDI RPN messages, or set the instrument to legacy mode.
  48. */
  49. MPEInstrument() noexcept;
  50. /** Destructor. */
  51. virtual ~MPEInstrument();
  52. //==============================================================================
  53. /** Returns the current zone layout of the instrument.
  54. This happens by value, to enforce thread-safety and class invariants.
  55. Note: If the instrument is in legacy mode, the return value of this
  56. method is unspecified.
  57. */
  58. MPEZoneLayout getZoneLayout() const noexcept;
  59. /** Re-sets the zone layout of the instrument to the one passed in.
  60. As a side effect, this will discard all currently playing notes,
  61. and call noteReleased for all of them.
  62. This will also disable legacy mode in case it was enabled previously.
  63. */
  64. void setZoneLayout (MPEZoneLayout newLayout);
  65. /** Returns true if the given MIDI channel (1-16) is a note channel in any
  66. of the MPEInstrument's MPE zones; false otherwise.
  67. When in legacy mode, this will return true if the given channel is
  68. contained in the current legacy mode channel range; false otherwise.
  69. */
  70. bool isMemberChannel (int midiChannel) const noexcept;
  71. /** Returns true if the given MIDI channel (1-16) is a master channel (channel
  72. 1 or 16).
  73. In legacy mode, this will always return false.
  74. */
  75. bool isMasterChannel (int midiChannel) const noexcept;
  76. /** Returns true if the given MIDI channel (1-16) is used by any of the
  77. MPEInstrument's MPE zones; false otherwise.
  78. When in legacy mode, this will return true if the given channel is
  79. contained in the current legacy mode channel range; false otherwise.
  80. */
  81. bool isUsingChannel (int midiChannel) const noexcept;
  82. //==============================================================================
  83. /** The MPE note tracking mode. In case there is more than one note playing
  84. simultaneously on the same MIDI channel, this determines which of these
  85. notes will be modulated by an incoming MPE message on that channel
  86. (pressure, pitchbend, or timbre).
  87. The default is lastNotePlayedOnChannel.
  88. */
  89. enum TrackingMode
  90. {
  91. lastNotePlayedOnChannel, /**< The most recent note on the channel that is still played (key down and/or sustained). */
  92. lowestNoteOnChannel, /**< The lowest note (by initialNote) on the channel with the note key still down. */
  93. highestNoteOnChannel, /**< The highest note (by initialNote) on the channel with the note key still down. */
  94. allNotesOnChannel /**< All notes on the channel (key down and/or sustained). */
  95. };
  96. /** Set the MPE tracking mode for the pressure dimension. */
  97. void setPressureTrackingMode (TrackingMode modeToUse);
  98. /** Set the MPE tracking mode for the pitchbend dimension. */
  99. void setPitchbendTrackingMode (TrackingMode modeToUse);
  100. /** Set the MPE tracking mode for the timbre dimension. */
  101. void setTimbreTrackingMode (TrackingMode modeToUse);
  102. //==============================================================================
  103. /** Process a MIDI message and trigger the appropriate method calls
  104. (noteOn, noteOff etc.)
  105. You can override this method if you need some special MIDI message
  106. treatment on top of the standard MPE logic implemented here.
  107. */
  108. virtual void processNextMidiEvent (const MidiMessage& message);
  109. //==============================================================================
  110. /** Request a note-on on the given channel, with the given initial note
  111. number and velocity.
  112. If the message arrives on a valid note channel, this will create a
  113. new MPENote and call the noteAdded callback.
  114. */
  115. virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
  116. /** Request a note-off.
  117. If there is a matching playing note, this will release the note
  118. (except if it is sustained by a sustain or sostenuto pedal) and call
  119. the noteReleased callback.
  120. */
  121. virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
  122. /** Request a pitchbend on the given channel with the given value (in units
  123. of MIDI pitchwheel position).
  124. Internally, this will determine whether the pitchwheel move is a
  125. per-note pitchbend or a master pitchbend (depending on midiChannel),
  126. take the correct per-note or master pitchbend range of the affected MPE
  127. zone, and apply the resulting pitchbend to the affected note(s) (if any).
  128. */
  129. virtual void pitchbend (int midiChannel, MPEValue pitchbend);
  130. /** Request a pressure change on the given channel with the given value.
  131. This will modify the pressure dimension of the note currently held down
  132. on this channel (if any). If the channel is a zone master channel,
  133. the pressure change will be broadcast to all notes in this zone.
  134. */
  135. virtual void pressure (int midiChannel, MPEValue value);
  136. /** Request a third dimension (timbre) change on the given channel with the
  137. given value.
  138. This will modify the timbre dimension of the note currently held down
  139. on this channel (if any). If the channel is a zone master channel,
  140. the timbre change will be broadcast to all notes in this zone.
  141. */
  142. virtual void timbre (int midiChannel, MPEValue value);
  143. /** Request a poly-aftertouch change for a given note number.
  144. The change will be broadcast to all notes sharing the channel and note
  145. number of the change message.
  146. */
  147. virtual void polyAftertouch (int midiChannel, int midiNoteNumber, MPEValue value);
  148. /** Request a sustain pedal press or release.
  149. If midiChannel is a zone's master channel, this will act on all notes in
  150. that zone; otherwise, nothing will happen.
  151. */
  152. virtual void sustainPedal (int midiChannel, bool isDown);
  153. /** Request a sostenuto pedal press or release.
  154. If midiChannel is a zone's master channel, this will act on all notes in
  155. that zone; otherwise, nothing will happen.
  156. */
  157. virtual void sostenutoPedal (int midiChannel, bool isDown);
  158. /** Discard all currently playing notes.
  159. This will also call the noteReleased listener callback for all of them.
  160. */
  161. void releaseAllNotes();
  162. //==============================================================================
  163. /** Returns the number of MPE notes currently played by the instrument. */
  164. int getNumPlayingNotes() const noexcept;
  165. /** Returns the note at the given index.
  166. If there is no such note, returns an invalid MPENote. The notes are sorted
  167. such that the most recently added note is the last element.
  168. */
  169. MPENote getNote (int index) const noexcept;
  170. /** Returns the note currently playing on the given midiChannel with the
  171. specified initial MIDI note number, if there is such a note. Otherwise,
  172. this returns an invalid MPENote (check with note.isValid() before use!)
  173. */
  174. MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
  175. /** Returns the most recent note that is playing on the given midiChannel
  176. (this will be the note which has received the most recent note-on without
  177. a corresponding note-off), if there is such a note. Otherwise, this returns an
  178. invalid MPENote (check with note.isValid() before use!)
  179. */
  180. MPENote getMostRecentNote (int midiChannel) const noexcept;
  181. /** Returns the most recent note that is not the note passed in. If there is no
  182. such note, this returns an invalid MPENote (check with note.isValid() before use!).
  183. This helper method might be useful for some custom voice handling algorithms.
  184. */
  185. MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
  186. //==============================================================================
  187. /** Derive from this class to be informed about any changes in the expressive
  188. MIDI notes played by this instrument.
  189. Note: This listener type receives its callbacks immediately, and not
  190. via the message thread (so you might be for example in the MIDI thread).
  191. Therefore you should never do heavy work such as graphics rendering etc.
  192. inside those callbacks.
  193. */
  194. class JUCE_API Listener
  195. {
  196. public:
  197. /** Destructor. */
  198. virtual ~Listener() = default;
  199. /** Implement this callback to be informed whenever a new expressive MIDI
  200. note is triggered.
  201. */
  202. virtual void noteAdded (MPENote newNote) { ignoreUnused (newNote); }
  203. /** Implement this callback to be informed whenever a currently playing
  204. MPE note's pressure value changes.
  205. */
  206. virtual void notePressureChanged (MPENote changedNote) { ignoreUnused (changedNote); }
  207. /** Implement this callback to be informed whenever a currently playing
  208. MPE note's pitchbend value changes.
  209. Note: This can happen if the note itself is bent, if there is a
  210. master channel pitchbend event, or if both occur simultaneously.
  211. Call MPENote::getFrequencyInHertz to get the effective note frequency.
  212. */
  213. virtual void notePitchbendChanged (MPENote changedNote) { ignoreUnused (changedNote); }
  214. /** Implement this callback to be informed whenever a currently playing
  215. MPE note's timbre value changes.
  216. */
  217. virtual void noteTimbreChanged (MPENote changedNote) { ignoreUnused (changedNote); }
  218. /** Implement this callback to be informed whether a currently playing
  219. MPE note's key state (whether the key is down and/or the note is
  220. sustained) has changed.
  221. Note: If the key state changes to MPENote::off, noteReleased is
  222. called instead.
  223. */
  224. virtual void noteKeyStateChanged (MPENote changedNote) { ignoreUnused (changedNote); }
  225. /** Implement this callback to be informed whenever an MPE note
  226. is released (either by a note-off message, or by a sustain/sostenuto
  227. pedal release for a note that already received a note-off),
  228. and should therefore stop playing.
  229. */
  230. virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
  231. };
  232. //==============================================================================
  233. /** Adds a listener. */
  234. void addListener (Listener* listenerToAdd);
  235. /** Removes a listener. */
  236. void removeListener (Listener* listenerToRemove);
  237. //==============================================================================
  238. /** Puts the instrument into legacy mode.
  239. As a side effect, this will discard all currently playing notes,
  240. and call noteReleased for all of them.
  241. This special zone layout mode is for backwards compatibility with
  242. non-MPE MIDI devices. In this mode, the instrument will ignore the
  243. current MPE zone layout. It will instead take a range of MIDI channels
  244. (default: all channels 1-16) and treat them as note channels, with no
  245. master channel. MIDI channels outside of this range will be ignored.
  246. @param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
  247. Must be between 0 and 96, otherwise behaviour is undefined.
  248. The default pitchbend range in legacy mode is +/- 2 semitones.
  249. @param channelRange The range of MIDI channels to use for notes when in legacy mode.
  250. The default is to use all MIDI channels (1-16).
  251. To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
  252. */
  253. void enableLegacyMode (int pitchbendRange = 2,
  254. Range<int> channelRange = Range<int> (1, 17));
  255. /** Returns true if the instrument is in legacy mode, false otherwise. */
  256. bool isLegacyModeEnabled() const noexcept;
  257. /** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
  258. Range<int> getLegacyModeChannelRange() const noexcept;
  259. /** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
  260. void setLegacyModeChannelRange (Range<int> channelRange);
  261. /** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
  262. int getLegacyModePitchbendRange() const noexcept;
  263. /** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
  264. void setLegacyModePitchbendRange (int pitchbendRange);
  265. protected:
  266. //==============================================================================
  267. CriticalSection lock;
  268. private:
  269. //==============================================================================
  270. Array<MPENote> notes;
  271. MPEZoneLayout zoneLayout;
  272. ListenerList<Listener> listeners;
  273. uint8 lastPressureLowerBitReceivedOnChannel[16];
  274. uint8 lastTimbreLowerBitReceivedOnChannel[16];
  275. bool isMemberChannelSustained[16];
  276. struct LegacyMode
  277. {
  278. bool isEnabled;
  279. Range<int> channelRange;
  280. int pitchbendRange;
  281. };
  282. struct MPEDimension
  283. {
  284. TrackingMode trackingMode = lastNotePlayedOnChannel;
  285. MPEValue lastValueReceivedOnChannel[16];
  286. MPEValue MPENote::* value;
  287. MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
  288. };
  289. LegacyMode legacyMode;
  290. MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
  291. void updateDimension (int midiChannel, MPEDimension&, MPEValue);
  292. void updateDimensionMaster (bool, MPEDimension&, MPEValue);
  293. void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
  294. void callListenersDimensionChanged (const MPENote&, const MPEDimension&);
  295. MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
  296. void processMidiNoteOnMessage (const MidiMessage&);
  297. void processMidiNoteOffMessage (const MidiMessage&);
  298. void processMidiPitchWheelMessage (const MidiMessage&);
  299. void processMidiChannelPressureMessage (const MidiMessage&);
  300. void processMidiControllerMessage (const MidiMessage&);
  301. void processMidiResetAllControllersMessage (const MidiMessage&);
  302. void processMidiAfterTouchMessage (const MidiMessage&);
  303. void handlePressureMSB (int midiChannel, int value) noexcept;
  304. void handlePressureLSB (int midiChannel, int value) noexcept;
  305. void handleTimbreMSB (int midiChannel, int value) noexcept;
  306. void handleTimbreLSB (int midiChannel, int value) noexcept;
  307. void handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto);
  308. const MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
  309. MPENote* getNotePtr (int midiChannel, int midiNoteNumber) noexcept;
  310. const MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
  311. MPENote* getNotePtr (int midiChannel, TrackingMode) noexcept;
  312. const MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
  313. MPENote* getLastNotePlayedPtr (int midiChannel) noexcept;
  314. const MPENote* getHighestNotePtr (int midiChannel) const noexcept;
  315. MPENote* getHighestNotePtr (int midiChannel) noexcept;
  316. const MPENote* getLowestNotePtr (int midiChannel) const noexcept;
  317. MPENote* getLowestNotePtr (int midiChannel) noexcept;
  318. void updateNoteTotalPitchbend (MPENote&);
  319. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
  320. };
  321. } // namespace juce