|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- /*
- ==============================================================================
-
- This file is part of the JUCE library.
- Copyright (c) 2022 - Raw Material Software Limited
-
- JUCE is an open source library subject to commercial or open-source
- licensing.
-
- The code included in this file is provided under the terms of the ISC license
- http://www.isc.org/downloads/software-support-policy/isc-license. Permission
- To use, copy, modify, and/or distribute this software for any purpose with or
- without fee is hereby granted provided that the above copyright notice and
- this permission notice appear in all copies.
-
- JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
- EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
- DISCLAIMED.
-
- ==============================================================================
- */
-
- namespace juce
- {
-
- //==============================================================================
- /**
- This class represents an instrument handling MPE.
-
- It has an MPE zone layout and maintains a state of currently
- active (playing) notes and the values of their dimensions of expression.
-
- You can trigger and modulate notes:
- - by passing MIDI messages with the method processNextMidiEvent;
- - by directly calling the methods noteOn, noteOff etc.
-
- The class implements the channel and note management logic specified in
- MPE. If you pass it a message, it will know what notes on what
- channels (if any) should be affected by that message.
-
- The class has a Listener class that can be used to react to note and
- state changes and trigger some functionality for your application.
- For example, you can use this class to write an MPE visualiser.
-
- If you want to write a real-time audio synth with MPE functionality,
- you should instead use the classes MPESynthesiserBase, which adds
- the ability to render audio and to manage voices.
-
- @see MPENote, MPEZoneLayout, MPESynthesiser
-
- @tags{Audio}
- */
- class JUCE_API MPEInstrument
- {
- public:
- /** Constructor.
-
- This will construct an MPE instrument with inactive lower and upper zones.
-
- In order to process incoming MIDI messages call setZoneLayout, use the MPEZoneLayout
- constructor, define the layout via MIDI RPN messages, or set the instrument to legacy mode.
- */
- MPEInstrument() noexcept;
-
- /** Constructs an MPE instrument with the specified zone layout. */
- MPEInstrument (MPEZoneLayout layout);
-
- /** Destructor. */
- virtual ~MPEInstrument();
-
- //==============================================================================
- /** Returns the current zone layout of the instrument.
- This happens by value, to enforce thread-safety and class invariants.
-
- Note: If the instrument is in legacy mode, the return value of this
- method is unspecified.
- */
- MPEZoneLayout getZoneLayout() const noexcept;
-
- /** Re-sets the zone layout of the instrument to the one passed in.
- As a side effect, this will discard all currently playing notes,
- and call noteReleased for all of them.
-
- This will also disable legacy mode in case it was enabled previously.
- */
- void setZoneLayout (MPEZoneLayout newLayout);
-
- /** Returns true if the given MIDI channel (1-16) is a note channel in any
- of the MPEInstrument's MPE zones; false otherwise.
-
- When in legacy mode, this will return true if the given channel is
- contained in the current legacy mode channel range; false otherwise.
- */
- bool isMemberChannel (int midiChannel) const noexcept;
-
- /** Returns true if the given MIDI channel (1-16) is a master channel (channel
- 1 or 16).
-
- In legacy mode, this will always return false.
- */
- bool isMasterChannel (int midiChannel) const noexcept;
-
- /** Returns true if the given MIDI channel (1-16) is used by any of the
- MPEInstrument's MPE zones; false otherwise.
-
- When in legacy mode, this will return true if the given channel is
- contained in the current legacy mode channel range; false otherwise.
- */
- bool isUsingChannel (int midiChannel) const noexcept;
-
- //==============================================================================
- /** The MPE note tracking mode. In case there is more than one note playing
- simultaneously on the same MIDI channel, this determines which of these
- notes will be modulated by an incoming MPE message on that channel
- (pressure, pitchbend, or timbre).
-
- The default is lastNotePlayedOnChannel.
- */
- enum TrackingMode
- {
- lastNotePlayedOnChannel, /**< The most recent note on the channel that is still played (key down and/or sustained). */
- lowestNoteOnChannel, /**< The lowest note (by initialNote) on the channel with the note key still down. */
- highestNoteOnChannel, /**< The highest note (by initialNote) on the channel with the note key still down. */
- allNotesOnChannel /**< All notes on the channel (key down and/or sustained). */
- };
-
- /** Set the MPE tracking mode for the pressure dimension. */
- void setPressureTrackingMode (TrackingMode modeToUse);
-
- /** Set the MPE tracking mode for the pitchbend dimension. */
- void setPitchbendTrackingMode (TrackingMode modeToUse);
-
- /** Set the MPE tracking mode for the timbre dimension. */
- void setTimbreTrackingMode (TrackingMode modeToUse);
-
- //==============================================================================
- /** Process a MIDI message and trigger the appropriate method calls
- (noteOn, noteOff etc.)
-
- You can override this method if you need some special MIDI message
- treatment on top of the standard MPE logic implemented here.
- */
- virtual void processNextMidiEvent (const MidiMessage& message);
-
- //==============================================================================
- /** Request a note-on on the given channel, with the given initial note
- number and velocity.
-
- If the message arrives on a valid note channel, this will create a
- new MPENote and call the noteAdded callback.
- */
- virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
-
- /** Request a note-off.
-
- If there is a matching playing note, this will release the note
- (except if it is sustained by a sustain or sostenuto pedal) and call
- the noteReleased callback.
- */
- virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
-
- /** Request a pitchbend on the given channel with the given value (in units
- of MIDI pitchwheel position).
-
- Internally, this will determine whether the pitchwheel move is a
- per-note pitchbend or a master pitchbend (depending on midiChannel),
- take the correct per-note or master pitchbend range of the affected MPE
- zone, and apply the resulting pitchbend to the affected note(s) (if any).
- */
- virtual void pitchbend (int midiChannel, MPEValue pitchbend);
-
- /** Request a pressure change on the given channel with the given value.
-
- This will modify the pressure dimension of the note currently held down
- on this channel (if any). If the channel is a zone master channel,
- the pressure change will be broadcast to all notes in this zone.
- */
- virtual void pressure (int midiChannel, MPEValue value);
-
- /** Request a third dimension (timbre) change on the given channel with the
- given value.
-
- This will modify the timbre dimension of the note currently held down
- on this channel (if any). If the channel is a zone master channel,
- the timbre change will be broadcast to all notes in this zone.
- */
- virtual void timbre (int midiChannel, MPEValue value);
-
- /** Request a poly-aftertouch change for a given note number.
-
- The change will be broadcast to all notes sharing the channel and note
- number of the change message.
- */
- virtual void polyAftertouch (int midiChannel, int midiNoteNumber, MPEValue value);
-
- /** Request a sustain pedal press or release.
-
- If midiChannel is a zone's master channel, this will act on all notes in
- that zone; otherwise, nothing will happen.
- */
- virtual void sustainPedal (int midiChannel, bool isDown);
-
- /** Request a sostenuto pedal press or release.
-
- If midiChannel is a zone's master channel, this will act on all notes in
- that zone; otherwise, nothing will happen.
- */
- virtual void sostenutoPedal (int midiChannel, bool isDown);
-
- /** Discard all currently playing notes.
-
- This will also call the noteReleased listener callback for all of them.
- */
- void releaseAllNotes();
-
- //==============================================================================
- /** Returns the number of MPE notes currently played by the instrument. */
- int getNumPlayingNotes() const noexcept;
-
- /** Returns the note at the given index.
-
- If there is no such note, returns an invalid MPENote. The notes are sorted
- such that the most recently added note is the last element.
- */
- MPENote getNote (int index) const noexcept;
-
- /** Returns the note currently playing on the given midiChannel with the
- specified initial MIDI note number, if there is such a note. Otherwise,
- this returns an invalid MPENote (check with note.isValid() before use!)
- */
- MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
-
- /** Returns the note with a given ID. */
- MPENote getNoteWithID (uint16 noteID) const noexcept;
-
- /** Returns the most recent note that is playing on the given midiChannel
- (this will be the note which has received the most recent note-on without
- a corresponding note-off), if there is such a note. Otherwise, this returns an
- invalid MPENote (check with note.isValid() before use!)
- */
- MPENote getMostRecentNote (int midiChannel) const noexcept;
-
- /** Returns the most recent note that is not the note passed in. If there is no
- such note, this returns an invalid MPENote (check with note.isValid() before use!).
-
- This helper method might be useful for some custom voice handling algorithms.
- */
- MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
-
- //==============================================================================
- /** Derive from this class to be informed about any changes in the MPE notes played
- by this instrument, and any changes to its zone layout.
-
- Note: This listener type receives its callbacks immediately, and not
- via the message thread (so you might be for example in the MIDI thread).
- Therefore you should never do heavy work such as graphics rendering etc.
- inside those callbacks.
- */
- class JUCE_API Listener
- {
- public:
- /** Destructor. */
- virtual ~Listener() = default;
-
- /** Implement this callback to be informed whenever a new expressive MIDI
- note is triggered.
- */
- virtual void noteAdded (MPENote newNote) { ignoreUnused (newNote); }
-
- /** Implement this callback to be informed whenever a currently playing
- MPE note's pressure value changes.
- */
- virtual void notePressureChanged (MPENote changedNote) { ignoreUnused (changedNote); }
-
- /** Implement this callback to be informed whenever a currently playing
- MPE note's pitchbend value changes.
-
- Note: This can happen if the note itself is bent, if there is a
- master channel pitchbend event, or if both occur simultaneously.
- Call MPENote::getFrequencyInHertz to get the effective note frequency.
- */
- virtual void notePitchbendChanged (MPENote changedNote) { ignoreUnused (changedNote); }
-
- /** Implement this callback to be informed whenever a currently playing
- MPE note's timbre value changes.
- */
- virtual void noteTimbreChanged (MPENote changedNote) { ignoreUnused (changedNote); }
-
- /** Implement this callback to be informed whether a currently playing
- MPE note's key state (whether the key is down and/or the note is
- sustained) has changed.
-
- Note: If the key state changes to MPENote::off, noteReleased is
- called instead.
- */
- virtual void noteKeyStateChanged (MPENote changedNote) { ignoreUnused (changedNote); }
-
- /** Implement this callback to be informed whenever an MPE note
- is released (either by a note-off message, or by a sustain/sostenuto
- pedal release for a note that already received a note-off),
- and should therefore stop playing.
- */
- virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
-
- /** Implement this callback to be informed whenever the MPE zone layout
- or legacy mode settings of this instrument have been changed.
- */
- virtual void zoneLayoutChanged() {}
- };
-
- //==============================================================================
- /** Adds a listener. */
- void addListener (Listener* listenerToAdd);
-
- /** Removes a listener. */
- void removeListener (Listener* listenerToRemove);
-
- //==============================================================================
- /** Puts the instrument into legacy mode. If legacy mode is already enabled this method
- does nothing.
-
- As a side effect, this will discard all currently playing notes,
- and call noteReleased for all of them.
-
- This special zone layout mode is for backwards compatibility with
- non-MPE MIDI devices. In this mode, the instrument will ignore the
- current MPE zone layout. It will instead take a range of MIDI channels
- (default: all channels 1-16) and treat them as note channels, with no
- master channel. MIDI channels outside of this range will be ignored.
-
- @param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
- Must be between 0 and 96, otherwise behaviour is undefined.
- The default pitchbend range in legacy mode is +/- 2 semitones.
-
- @param channelRange The range of MIDI channels to use for notes when in legacy mode.
- The default is to use all MIDI channels (1-16).
-
- To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
- */
- void enableLegacyMode (int pitchbendRange = 2,
- Range<int> channelRange = Range<int> (1, 17));
-
- /** Returns true if the instrument is in legacy mode, false otherwise. */
- bool isLegacyModeEnabled() const noexcept;
-
- /** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
- Range<int> getLegacyModeChannelRange() const noexcept;
-
- /** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
- void setLegacyModeChannelRange (Range<int> channelRange);
-
- /** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
- int getLegacyModePitchbendRange() const noexcept;
-
- /** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
- void setLegacyModePitchbendRange (int pitchbendRange);
-
- protected:
- //==============================================================================
- CriticalSection lock;
-
- private:
- //==============================================================================
- Array<MPENote> notes;
- MPEZoneLayout zoneLayout;
- ListenerList<Listener> listeners;
-
- uint8 lastPressureLowerBitReceivedOnChannel[16];
- uint8 lastTimbreLowerBitReceivedOnChannel[16];
- bool isMemberChannelSustained[16];
-
- struct LegacyMode
- {
- bool isEnabled = false;
- Range<int> channelRange;
- int pitchbendRange = 2;
- };
-
- struct MPEDimension
- {
- TrackingMode trackingMode = lastNotePlayedOnChannel;
- MPEValue lastValueReceivedOnChannel[16];
- MPEValue MPENote::* value;
- MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
- };
-
- LegacyMode legacyMode;
- MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
-
- void resetLastReceivedValues();
-
- void updateDimension (int midiChannel, MPEDimension&, MPEValue);
- void updateDimensionMaster (bool, MPEDimension&, MPEValue);
- void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
- void callListenersDimensionChanged (const MPENote&, const MPEDimension&);
- MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
-
- void processMidiNoteOnMessage (const MidiMessage&);
- void processMidiNoteOffMessage (const MidiMessage&);
- void processMidiPitchWheelMessage (const MidiMessage&);
- void processMidiChannelPressureMessage (const MidiMessage&);
- void processMidiControllerMessage (const MidiMessage&);
- void processMidiResetAllControllersMessage (const MidiMessage&);
- void processMidiAfterTouchMessage (const MidiMessage&);
- void handlePressureMSB (int midiChannel, int value) noexcept;
- void handlePressureLSB (int midiChannel, int value) noexcept;
- void handleTimbreMSB (int midiChannel, int value) noexcept;
- void handleTimbreLSB (int midiChannel, int value) noexcept;
- void handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto);
-
- const MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
- MPENote* getNotePtr (int midiChannel, int midiNoteNumber) noexcept;
- const MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
- MPENote* getNotePtr (int midiChannel, TrackingMode) noexcept;
- const MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
- MPENote* getLastNotePlayedPtr (int midiChannel) noexcept;
- const MPENote* getHighestNotePtr (int midiChannel) const noexcept;
- MPENote* getHighestNotePtr (int midiChannel) noexcept;
- const MPENote* getLowestNotePtr (int midiChannel) const noexcept;
- MPENote* getLowestNotePtr (int midiChannel) noexcept;
- void updateNoteTotalPitchbend (MPENote&);
-
- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
- };
-
- } // namespace juce
|