git subrepo clone --branch=sono6good https://github.com/essej/JUCE.git deps/juce
subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
This commit is contained in:
2321
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp
vendored
Normal file
2321
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2167
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp.orig
vendored
Normal file
2167
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.cpp.orig
vendored
Normal file
File diff suppressed because it is too large
Load Diff
415
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.h
vendored
Normal file
415
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.h
vendored
Normal file
@ -0,0 +1,415 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 with the three callbacks MPENoteAdded,
|
||||
MPENoteChanged, and MPENoteFinished. Implement such a
|
||||
Listener class to react to note changes and trigger some functionality for
|
||||
your application that depends on the MPE note state.
|
||||
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, call setZoneLayout, define the layout
|
||||
via MIDI RPN messages, or set the instrument to legacy mode.
|
||||
*/
|
||||
MPEInstrument() noexcept;
|
||||
|
||||
/** 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 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 expressive
|
||||
MIDI notes played by this instrument.
|
||||
|
||||
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); }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a listener. */
|
||||
void addListener (Listener* listenerToAdd);
|
||||
|
||||
/** Removes a listener. */
|
||||
void removeListener (Listener* listenerToRemove);
|
||||
|
||||
//==============================================================================
|
||||
/** Puts the instrument into legacy mode.
|
||||
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;
|
||||
Range<int> channelRange;
|
||||
int pitchbendRange;
|
||||
};
|
||||
|
||||
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
|
376
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.h.orig
vendored
Normal file
376
deps/juce/modules/juce_audio_basics/mpe/juce_MPEInstrument.h.orig
vendored
Normal file
@ -0,0 +1,376 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/*
|
||||
This class represents an instrument handling MPE.
|
||||
|
||||
It has an MPE zone layout and maintans 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 with the three callbacks MPENoteAdded,
|
||||
MPENoteChanged, and MPENoteFinished. Implement such a
|
||||
Listener class to react to note changes and trigger some functionality for
|
||||
your application that depends on the MPE note state.
|
||||
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
|
||||
*/
|
||||
class JUCE_API MPEInstrument
|
||||
{
|
||||
public:
|
||||
|
||||
/** Constructor.
|
||||
This will construct an MPE instrument with initially no MPE zones.
|
||||
|
||||
In order to process incoming MIDI, call setZoneLayout, define the layout
|
||||
via MIDI RPN messages, or set the instrument to legacy mode.
|
||||
*/
|
||||
MPEInstrument() noexcept;
|
||||
|
||||
/** 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 isNoteChannel (int midiChannel) const noexcept;
|
||||
|
||||
/** Returns true if the given MIDI channel (1-16) is a master channel in any
|
||||
of the MPEInstrument's MPE zones; false otherwise.
|
||||
When in legacy mode, this will always return false.
|
||||
*/
|
||||
bool isMasterChannel (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 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 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 expressive
|
||||
MIDI notes played by this instrument.
|
||||
|
||||
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() {}
|
||||
|
||||
/** Implement this callback to be informed whenever a new expressive
|
||||
MIDI note is triggered.
|
||||
*/
|
||||
virtual void noteAdded (MPENote newNote) = 0;
|
||||
|
||||
/** Implement this callback to be informed whenever a currently
|
||||
playing MPE note's pressure value changes.
|
||||
*/
|
||||
virtual void notePressureChanged (MPENote changedNote) = 0;
|
||||
|
||||
/** 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) = 0;
|
||||
|
||||
/** Implement this callback to be informed whenever a currently
|
||||
playing MPE note's timbre value changes.
|
||||
*/
|
||||
virtual void noteTimbreChanged (MPENote changedNote) = 0;
|
||||
|
||||
/** 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) = 0;
|
||||
|
||||
/** 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) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a listener. */
|
||||
void addListener (Listener* listenerToAdd) noexcept;
|
||||
|
||||
/** Removes a listener. */
|
||||
void removeListener (Listener* listenerToRemove) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Puts the instrument into legacy mode.
|
||||
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);
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
CriticalSection lock;
|
||||
Array<MPENote> notes;
|
||||
MPEZoneLayout zoneLayout;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
uint8 lastPressureLowerBitReceivedOnChannel[16];
|
||||
uint8 lastTimbreLowerBitReceivedOnChannel[16];
|
||||
bool isNoteChannelSustained[16];
|
||||
|
||||
struct LegacyMode
|
||||
{
|
||||
bool isEnabled;
|
||||
Range<int> channelRange;
|
||||
int pitchbendRange;
|
||||
};
|
||||
|
||||
struct MPEDimension
|
||||
{
|
||||
MPEDimension() noexcept : trackingMode (lastNotePlayedOnChannel) {}
|
||||
TrackingMode trackingMode;
|
||||
MPEValue lastValueReceivedOnChannel[16];
|
||||
MPEValue MPENote::* value;
|
||||
MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
|
||||
};
|
||||
|
||||
LegacyMode legacyMode;
|
||||
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
|
||||
|
||||
void updateDimension (int midiChannel, MPEDimension&, MPEValue);
|
||||
void updateDimensionMaster (MPEZone&, MPEDimension&, MPEValue);
|
||||
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
|
||||
void callListenersDimensionChanged (MPENote&, 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 processMidiAllNotesOffMessage (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);
|
||||
|
||||
MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
|
||||
MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
|
||||
MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
|
||||
MPENote* getHighestNotePtr (int midiChannel) const noexcept;
|
||||
MPENote* getLowestNotePtr (int midiChannel) const noexcept;
|
||||
void updateNoteTotalPitchbend (MPENote&);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
|
||||
};
|
238
deps/juce/modules/juce_audio_basics/mpe/juce_MPEMessages.cpp
vendored
Normal file
238
deps/juce/modules/juce_audio_basics/mpe/juce_MPEMessages.cpp
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MidiBuffer MPEMessages::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
|
||||
{
|
||||
auto buffer = MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
|
||||
|
||||
buffer.addEvents (setLowerZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
|
||||
buffer.addEvents (setLowerZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
|
||||
{
|
||||
auto buffer = MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
|
||||
|
||||
buffer.addEvents (setUpperZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
|
||||
buffer.addEvents (setUpperZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setLowerZonePerNotePitchbendRange (int perNotePitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (2, 0, perNotePitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setUpperZonePerNotePitchbendRange (int perNotePitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (15, 0, perNotePitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setLowerZoneMasterPitchbendRange (int masterPitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (1, 0, masterPitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setUpperZoneMasterPitchbendRange (int masterPitchbendRange)
|
||||
{
|
||||
return MidiRPNGenerator::generate (16, 0, masterPitchbendRange, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearLowerZone()
|
||||
{
|
||||
return MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, 0, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearUpperZone()
|
||||
{
|
||||
return MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, 0, false, false);
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::clearAllZones()
|
||||
{
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer.addEvents (clearLowerZone(), 0, -1, 0);
|
||||
buffer.addEvents (clearUpperZone(), 0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MidiBuffer MPEMessages::setZoneLayout (MPEZoneLayout layout)
|
||||
{
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer.addEvents (clearAllZones(), 0, -1, 0);
|
||||
|
||||
auto lowerZone = layout.getLowerZone();
|
||||
if (lowerZone.isActive())
|
||||
buffer.addEvents (setLowerZone (lowerZone.numMemberChannels,
|
||||
lowerZone.perNotePitchbendRange,
|
||||
lowerZone.masterPitchbendRange),
|
||||
0, -1, 0);
|
||||
|
||||
auto upperZone = layout.getUpperZone();
|
||||
if (upperZone.isActive())
|
||||
buffer.addEvents (setUpperZone (upperZone.numMemberChannels,
|
||||
upperZone.perNotePitchbendRange,
|
||||
upperZone.masterPitchbendRange),
|
||||
0, -1, 0);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class MPEMessagesTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
MPEMessagesTests()
|
||||
: UnitTest ("MPEMessages class", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("add zone");
|
||||
{
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::setLowerZone (7);
|
||||
|
||||
const uint8 expectedBytes[] =
|
||||
{
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone
|
||||
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x30, // per-note pbrange (default = 48)
|
||||
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x02 // master pbrange (default = 2)
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0);
|
||||
|
||||
const uint8 expectedBytes[] =
|
||||
{
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone
|
||||
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x60, // per-note pbrange (custom)
|
||||
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // master pbrange (custom)
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("set per-note pitchbend range");
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96);
|
||||
|
||||
const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 };
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
||||
|
||||
beginTest ("set master pitchbend range");
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60);
|
||||
|
||||
const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c };
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
||||
beginTest ("clear all zones");
|
||||
{
|
||||
MidiBuffer buffer = MPEMessages::clearAllZones();
|
||||
|
||||
const uint8 expectedBytes[] = { 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // clear upper zone
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
|
||||
beginTest ("set complete state");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
layout.setLowerZone (7, 96, 0);
|
||||
layout.setUpperZone (7);
|
||||
|
||||
MidiBuffer buffer = MPEMessages::setZoneLayout (layout);
|
||||
|
||||
const uint8 expectedBytes[] = {
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00, // clear upper zone
|
||||
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set lower zone
|
||||
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60, // per-note pbrange (custom)
|
||||
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // master pbrange (custom)
|
||||
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x07, // set upper zone
|
||||
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x30, // per-note pbrange (default = 48)
|
||||
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x02 // master pbrange (default = 2)
|
||||
};
|
||||
|
||||
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void testMidiBuffer (MidiBuffer& buffer, const uint8* expectedBytes, int expectedBytesSize)
|
||||
{
|
||||
uint8 actualBytes[128] = { 0 };
|
||||
extractRawBinaryData (buffer, actualBytes, sizeof (actualBytes));
|
||||
|
||||
expectEquals (std::memcmp (actualBytes, expectedBytes, (std::size_t) expectedBytesSize), 0);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void extractRawBinaryData (const MidiBuffer& midiBuffer, const uint8* bufferToCopyTo, std::size_t maxBytes)
|
||||
{
|
||||
std::size_t pos = 0;
|
||||
|
||||
for (const auto metadata : midiBuffer)
|
||||
{
|
||||
const uint8* data = metadata.data;
|
||||
std::size_t dataSize = (std::size_t) metadata.numBytes;
|
||||
|
||||
if (pos + dataSize > maxBytes)
|
||||
return;
|
||||
|
||||
std::memcpy ((void*) (bufferToCopyTo + pos), data, dataSize);
|
||||
pos += dataSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static MPEMessagesTests MPEMessagesUnitTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
116
deps/juce/modules/juce_audio_basics/mpe/juce_MPEMessages.h
vendored
Normal file
116
deps/juce/modules/juce_audio_basics/mpe/juce_MPEMessages.h
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 helper class contains the necessary helper functions to generate
|
||||
MIDI messages that are exclusive to MPE, such as defining the upper and lower
|
||||
MPE zones and setting per-note and master pitchbend ranges.
|
||||
You can then send them to your MPE device using MidiOutput::sendBlockOfMessagesNow.
|
||||
|
||||
All other MPE messages like per-note pitchbend, pressure, and third
|
||||
dimension, are ordinary MIDI messages that should be created using the MidiMessage
|
||||
class instead. You just need to take care to send them to the appropriate
|
||||
per-note MIDI channel.
|
||||
|
||||
Note: If you are working with an MPEZoneLayout object inside your app,
|
||||
you should not use the message sequences provided here. Instead, you should
|
||||
change the zone layout programmatically with the member functions provided in the
|
||||
MPEZoneLayout class itself. You should also make sure that the Expressive
|
||||
MIDI zone layout of your C++ code and of the MPE device are kept in sync.
|
||||
|
||||
@see MidiMessage, MPEZoneLayout
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MPEMessages
|
||||
{
|
||||
public:
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer setLowerZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer setUpperZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the per-note pitchbend range of the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer setLowerZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the per-note pitchbend range of the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer setUpperZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the master pitchbend range of the lower MPE zone.
|
||||
*/
|
||||
static MidiBuffer setLowerZoneMasterPitchbendRange (int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will set the master pitchbend range of the upper MPE zone.
|
||||
*/
|
||||
static MidiBuffer setUpperZoneMasterPitchbendRange (int masterPitchbendRange = 2);
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the lower zone.
|
||||
*/
|
||||
static MidiBuffer clearLowerZone();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the upper zone.
|
||||
*/
|
||||
static MidiBuffer clearUpperZone();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will clear the lower and upper zones.
|
||||
*/
|
||||
static MidiBuffer clearAllZones();
|
||||
|
||||
/** Returns the sequence of MIDI messages that, if sent to an Expressive
|
||||
MIDI device, will reset the whole MPE zone layout of the
|
||||
device to the layout passed in. This will first clear the current lower and upper
|
||||
zones, then then set the zones contained in the passed-in zone layout, and set their
|
||||
per-note and master pitchbend ranges to their current values.
|
||||
*/
|
||||
static MidiBuffer setZoneLayout (MPEZoneLayout layout);
|
||||
|
||||
/** The RPN number used for MPE zone layout messages.
|
||||
|
||||
Pitchbend range messages (both per-note and master) are instead sent
|
||||
on RPN 0 as in standard MIDI 1.0.
|
||||
*/
|
||||
static const int zoneLayoutMessagesRpnNumber = 6;
|
||||
};
|
||||
|
||||
} // namespace juce
|
127
deps/juce/modules/juce_audio_basics/mpe/juce_MPENote.cpp
vendored
Normal file
127
deps/juce/modules/juce_audio_basics/mpe/juce_MPENote.cpp
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
uint16 generateNoteID (int midiChannel, int midiNoteNumber) noexcept
|
||||
{
|
||||
jassert (midiChannel > 0 && midiChannel <= 16);
|
||||
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
|
||||
|
||||
return uint16 ((midiChannel << 7) + midiNoteNumber);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPENote::MPENote (int midiChannel_,
|
||||
int initialNote_,
|
||||
MPEValue noteOnVelocity_,
|
||||
MPEValue pitchbend_,
|
||||
MPEValue pressure_,
|
||||
MPEValue timbre_,
|
||||
KeyState keyState_) noexcept
|
||||
: noteID (generateNoteID (midiChannel_, initialNote_)),
|
||||
midiChannel (uint8 (midiChannel_)),
|
||||
initialNote (uint8 (initialNote_)),
|
||||
noteOnVelocity (noteOnVelocity_),
|
||||
pitchbend (pitchbend_),
|
||||
pressure (pressure_),
|
||||
initialTimbre (timbre_),
|
||||
timbre (timbre_),
|
||||
keyState (keyState_)
|
||||
{
|
||||
jassert (keyState != MPENote::off);
|
||||
jassert (isValid());
|
||||
}
|
||||
|
||||
MPENote::MPENote() noexcept {}
|
||||
|
||||
//==============================================================================
|
||||
bool MPENote::isValid() const noexcept
|
||||
{
|
||||
return midiChannel > 0 && midiChannel <= 16 && initialNote < 128;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
double MPENote::getFrequencyInHertz (double frequencyOfA) const noexcept
|
||||
{
|
||||
auto pitchInSemitones = double (initialNote) + totalPitchbendInSemitones;
|
||||
return frequencyOfA * std::pow (2.0, (pitchInSemitones - 69.0) / 12.0);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPENote::operator== (const MPENote& other) const noexcept
|
||||
{
|
||||
jassert (isValid() && other.isValid());
|
||||
return noteID == other.noteID;
|
||||
}
|
||||
|
||||
bool MPENote::operator!= (const MPENote& other) const noexcept
|
||||
{
|
||||
jassert (isValid() && other.isValid());
|
||||
return noteID != other.noteID;
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class MPENoteTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
MPENoteTests()
|
||||
: UnitTest ("MPENote class", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
//==============================================================================
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("getFrequencyInHertz");
|
||||
{
|
||||
MPENote note;
|
||||
note.initialNote = 60;
|
||||
note.totalPitchbendInSemitones = -0.5;
|
||||
expectEqualsWithinOneCent (note.getFrequencyInHertz(), 254.178);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void expectEqualsWithinOneCent (double frequencyInHertzActual,
|
||||
double frequencyInHertzExpected)
|
||||
{
|
||||
double ratio = frequencyInHertzActual / frequencyInHertzExpected;
|
||||
double oneCent = 1.0005946;
|
||||
expect (ratio < oneCent);
|
||||
expect (ratio > 1.0 / oneCent);
|
||||
}
|
||||
};
|
||||
|
||||
static MPENoteTests MPENoteUnitTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
184
deps/juce/modules/juce_audio_basics/mpe/juce_MPENote.h
vendored
Normal file
184
deps/juce/modules/juce_audio_basics/mpe/juce_MPENote.h
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 struct represents a playing MPE note.
|
||||
|
||||
A note is identified by a unique ID, or alternatively, by a MIDI channel
|
||||
and an initial note. It is characterised by five dimensions of continuous
|
||||
expressive control. Their current values are represented as
|
||||
MPEValue objects.
|
||||
|
||||
@see MPEValue
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct JUCE_API MPENote
|
||||
{
|
||||
//==============================================================================
|
||||
/** Possible values for the note key state. */
|
||||
enum KeyState
|
||||
{
|
||||
off = 0, /**< The key is up (off). */
|
||||
keyDown = 1, /**< The note key is currently down (pressed). */
|
||||
sustained = 2, /**< The note is sustained (by a sustain or sostenuto pedal). */
|
||||
keyDownAndSustained = 3 /**< The note key is down and sustained (by a sustain or sostenuto pedal). */
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Constructor.
|
||||
|
||||
@param midiChannel The MIDI channel of the note, between 2 and 15.
|
||||
(Channel 1 and channel 16 can never be note channels in MPE).
|
||||
|
||||
@param initialNote The MIDI note number, between 0 and 127.
|
||||
|
||||
@param velocity The note-on velocity of the note.
|
||||
|
||||
@param pitchbend The initial per-note pitchbend of the note.
|
||||
|
||||
@param pressure The initial pressure of the note.
|
||||
|
||||
@param timbre The timbre value of the note.
|
||||
|
||||
@param keyState The key state of the note (whether the key is down
|
||||
and/or the note is sustained). This value must not
|
||||
be MPENote::off, since you are triggering a new note.
|
||||
(If not specified, the default value will be MPENote::keyDown.)
|
||||
*/
|
||||
MPENote (int midiChannel,
|
||||
int initialNote,
|
||||
MPEValue velocity,
|
||||
MPEValue pitchbend,
|
||||
MPEValue pressure,
|
||||
MPEValue timbre,
|
||||
KeyState keyState = MPENote::keyDown) noexcept;
|
||||
|
||||
/** Default constructor.
|
||||
|
||||
Constructs an invalid MPE note (a note with the key state MPENote::off
|
||||
and an invalid MIDI channel. The only allowed use for such a note is to
|
||||
call isValid() on it; everything else is undefined behaviour.
|
||||
*/
|
||||
MPENote() noexcept;
|
||||
|
||||
/** Checks whether the MPE note is valid. */
|
||||
bool isValid() const noexcept;
|
||||
|
||||
//==============================================================================
|
||||
// Invariants that define the note.
|
||||
|
||||
/** A unique ID. Useful to distinguish the note from other simultaneously
|
||||
sounding notes that may use the same note number or MIDI channel.
|
||||
This should never change during the lifetime of a note object.
|
||||
*/
|
||||
uint16 noteID = 0;
|
||||
|
||||
/** The MIDI channel which this note uses.
|
||||
This should never change during the lifetime of an MPENote object.
|
||||
*/
|
||||
uint8 midiChannel = 0;
|
||||
|
||||
/** The MIDI note number that was sent when the note was triggered.
|
||||
This should never change during the lifetime of an MPENote object.
|
||||
*/
|
||||
uint8 initialNote = 0;
|
||||
|
||||
//==============================================================================
|
||||
// The five dimensions of continuous expressive control
|
||||
|
||||
/** The velocity ("strike") of the note-on.
|
||||
This dimension will stay constant after the note has been turned on.
|
||||
*/
|
||||
MPEValue noteOnVelocity { MPEValue::minValue() };
|
||||
|
||||
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel
|
||||
position). This dimension can be modulated while the note sounds.
|
||||
|
||||
Note: This value is not aware of the currently used pitchbend range,
|
||||
or an additional master pitchbend that may be simultaneously applied.
|
||||
To compute the actual effective pitchbend of an MPENote, you should
|
||||
probably use the member totalPitchbendInSemitones instead.
|
||||
|
||||
@see totalPitchbendInSemitones, getFrequencyInHertz
|
||||
*/
|
||||
MPEValue pitchbend { MPEValue::centreValue() };
|
||||
|
||||
/** Current pressure with which the note is held down.
|
||||
This dimension can be modulated while the note sounds.
|
||||
*/
|
||||
MPEValue pressure { MPEValue::centreValue() };
|
||||
|
||||
/** Initial value of timbre when the note was triggered.
|
||||
This should never change during the lifetime of an MPENote object.
|
||||
*/
|
||||
MPEValue initialTimbre { MPEValue::centreValue() };
|
||||
|
||||
/** Current value of the note's third expressive dimension, typically
|
||||
encoding some kind of timbre parameter.
|
||||
This dimension can be modulated while the note sounds.
|
||||
*/
|
||||
MPEValue timbre { MPEValue::centreValue() };
|
||||
|
||||
/** The release velocity ("lift") of the note after a note-off has been
|
||||
received.
|
||||
This dimension will only have a meaningful value after a note-off has
|
||||
been received for the note (and keyState is set to MPENote::off or
|
||||
MPENote::sustained). Initially, the value is undefined.
|
||||
*/
|
||||
MPEValue noteOffVelocity { MPEValue::minValue() };
|
||||
|
||||
//==============================================================================
|
||||
/** Current effective pitchbend of the note in units of semitones, relative
|
||||
to initialNote. You should use this to compute the actual effective pitch
|
||||
of the note. This value is computed and set by an MPEInstrument to the
|
||||
sum of the per-note pitchbend value (stored in MPEValue::pitchbend)
|
||||
and the master pitchbend of the MPE zone, weighted with the per-note
|
||||
pitchbend range and master pitchbend range of the zone, respectively.
|
||||
|
||||
@see getFrequencyInHertz
|
||||
*/
|
||||
double totalPitchbendInSemitones;
|
||||
|
||||
/** Current key state. Indicates whether the note key is currently down (pressed)
|
||||
and/or the note is sustained (by a sustain or sostenuto pedal).
|
||||
*/
|
||||
KeyState keyState { MPENote::off };
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the current frequency of the note in Hertz. This is the sum of
|
||||
the initialNote and the totalPitchbendInSemitones, converted to Hertz.
|
||||
*/
|
||||
double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept;
|
||||
|
||||
/** Returns true if two notes are the same, determined by their unique ID. */
|
||||
bool operator== (const MPENote& other) const noexcept;
|
||||
|
||||
/** Returns true if two notes are different notes, determined by their unique ID. */
|
||||
bool operator!= (const MPENote& other) const noexcept;
|
||||
};
|
||||
|
||||
} // namespace juce
|
343
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiser.cpp
vendored
Normal file
343
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiser.cpp
vendored
Normal file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPESynthesiser::MPESynthesiser()
|
||||
{
|
||||
MPEZoneLayout zoneLayout;
|
||||
zoneLayout.setLowerZone (15);
|
||||
setZoneLayout (zoneLayout);
|
||||
}
|
||||
|
||||
MPESynthesiser::MPESynthesiser (MPEInstrument* mpeInstrument) : MPESynthesiserBase (mpeInstrument)
|
||||
{
|
||||
}
|
||||
|
||||
MPESynthesiser::~MPESynthesiser()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiser::startVoice (MPESynthesiserVoice* voice, MPENote noteToStart)
|
||||
{
|
||||
jassert (voice != nullptr);
|
||||
|
||||
voice->currentlyPlayingNote = noteToStart;
|
||||
voice->noteOnTime = lastNoteOnCounter++;
|
||||
voice->noteStarted();
|
||||
}
|
||||
|
||||
void MPESynthesiser::stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff)
|
||||
{
|
||||
jassert (voice != nullptr);
|
||||
|
||||
voice->currentlyPlayingNote = noteToStop;
|
||||
voice->noteStopped (allowTailOff);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiser::noteAdded (MPENote newNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
if (auto* voice = findFreeVoice (newNote, shouldStealVoices))
|
||||
startVoice (voice, newNote);
|
||||
}
|
||||
|
||||
void MPESynthesiser::notePressureChanged (MPENote changedNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isCurrentlyPlayingNote (changedNote))
|
||||
{
|
||||
voice->currentlyPlayingNote = changedNote;
|
||||
voice->notePressureChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::notePitchbendChanged (MPENote changedNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isCurrentlyPlayingNote (changedNote))
|
||||
{
|
||||
voice->currentlyPlayingNote = changedNote;
|
||||
voice->notePitchbendChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::noteTimbreChanged (MPENote changedNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isCurrentlyPlayingNote (changedNote))
|
||||
{
|
||||
voice->currentlyPlayingNote = changedNote;
|
||||
voice->noteTimbreChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::noteKeyStateChanged (MPENote changedNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isCurrentlyPlayingNote (changedNote))
|
||||
{
|
||||
voice->currentlyPlayingNote = changedNote;
|
||||
voice->noteKeyStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::noteReleased (MPENote finishedNote)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto i = voices.size(); --i >= 0;)
|
||||
{
|
||||
auto* voice = voices.getUnchecked (i);
|
||||
|
||||
if (voice->isCurrentlyPlayingNote (finishedNote))
|
||||
stopVoice (voice, finishedNote, true);
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate)
|
||||
{
|
||||
MPESynthesiserBase::setCurrentPlaybackSampleRate (newRate);
|
||||
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
turnOffAllVoices (false);
|
||||
|
||||
for (auto i = voices.size(); --i >= 0;)
|
||||
voices.getUnchecked (i)->setCurrentSampleRate (newRate);
|
||||
}
|
||||
|
||||
void MPESynthesiser::handleMidiEvent (const MidiMessage& m)
|
||||
{
|
||||
if (m.isController())
|
||||
handleController (m.getChannel(), m.getControllerNumber(), m.getControllerValue());
|
||||
else if (m.isProgramChange())
|
||||
handleProgramChange (m.getChannel(), m.getProgramChangeNumber());
|
||||
|
||||
MPESynthesiserBase::handleMidiEvent (m);
|
||||
}
|
||||
|
||||
MPESynthesiserVoice* MPESynthesiser::findFreeVoice (MPENote noteToFindVoiceFor, bool stealIfNoneAvailable) const
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (! voice->isActive())
|
||||
return voice;
|
||||
}
|
||||
|
||||
if (stealIfNoneAvailable)
|
||||
return findVoiceToSteal (noteToFindVoiceFor);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MPESynthesiserVoice* MPESynthesiser::findVoiceToSteal (MPENote noteToStealVoiceFor) const
|
||||
{
|
||||
// This voice-stealing algorithm applies the following heuristics:
|
||||
// - Re-use the oldest notes first
|
||||
// - Protect the lowest & topmost notes, even if sustained, but not if they've been released.
|
||||
|
||||
|
||||
// apparently you are trying to render audio without having any voices...
|
||||
jassert (voices.size() > 0);
|
||||
|
||||
// These are the voices we want to protect (ie: only steal if unavoidable)
|
||||
MPESynthesiserVoice* low = nullptr; // Lowest sounding note, might be sustained, but NOT in release phase
|
||||
MPESynthesiserVoice* top = nullptr; // Highest sounding note, might be sustained, but NOT in release phase
|
||||
|
||||
// this is a list of voices we can steal, sorted by how long they've been running
|
||||
Array<MPESynthesiserVoice*> usableVoices;
|
||||
usableVoices.ensureStorageAllocated (voices.size());
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
jassert (voice->isActive()); // We wouldn't be here otherwise
|
||||
|
||||
usableVoices.add (voice);
|
||||
|
||||
// NB: Using a functor rather than a lambda here due to scare-stories about
|
||||
// compilers generating code containing heap allocations..
|
||||
struct Sorter
|
||||
{
|
||||
bool operator() (const MPESynthesiserVoice* a, const MPESynthesiserVoice* b) const noexcept { return a->noteOnTime < b->noteOnTime; }
|
||||
};
|
||||
|
||||
std::sort (usableVoices.begin(), usableVoices.end(), Sorter());
|
||||
|
||||
if (! voice->isPlayingButReleased()) // Don't protect released notes
|
||||
{
|
||||
auto noteNumber = voice->getCurrentlyPlayingNote().initialNote;
|
||||
|
||||
if (low == nullptr || noteNumber < low->getCurrentlyPlayingNote().initialNote)
|
||||
low = voice;
|
||||
|
||||
if (top == nullptr || noteNumber > top->getCurrentlyPlayingNote().initialNote)
|
||||
top = voice;
|
||||
}
|
||||
}
|
||||
|
||||
// Eliminate pathological cases (ie: only 1 note playing): we always give precedence to the lowest note(s)
|
||||
if (top == low)
|
||||
top = nullptr;
|
||||
|
||||
// If we want to re-use the voice to trigger a new note,
|
||||
// then The oldest note that's playing the same note number is ideal.
|
||||
if (noteToStealVoiceFor.isValid())
|
||||
for (auto* voice : usableVoices)
|
||||
if (voice->getCurrentlyPlayingNote().initialNote == noteToStealVoiceFor.initialNote)
|
||||
return voice;
|
||||
|
||||
// Oldest voice that has been released (no finger on it and not held by sustain pedal)
|
||||
for (auto* voice : usableVoices)
|
||||
if (voice != low && voice != top && voice->isPlayingButReleased())
|
||||
return voice;
|
||||
|
||||
// Oldest voice that doesn't have a finger on it:
|
||||
for (auto* voice : usableVoices)
|
||||
if (voice != low && voice != top
|
||||
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDown
|
||||
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDownAndSustained)
|
||||
return voice;
|
||||
|
||||
// Oldest voice that isn't protected
|
||||
for (auto* voice : usableVoices)
|
||||
if (voice != low && voice != top)
|
||||
return voice;
|
||||
|
||||
// We've only got "protected" voices now: lowest note takes priority
|
||||
jassert (low != nullptr);
|
||||
|
||||
// Duophonic synth: give priority to the bass note:
|
||||
if (top != nullptr)
|
||||
return top;
|
||||
|
||||
return low;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiser::addVoice (MPESynthesiserVoice* const newVoice)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
newVoice->setCurrentSampleRate (getSampleRate());
|
||||
voices.add (newVoice);
|
||||
}
|
||||
|
||||
void MPESynthesiser::clearVoices()
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
voices.clear();
|
||||
}
|
||||
|
||||
MPESynthesiserVoice* MPESynthesiser::getVoice (const int index) const
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
return voices [index];
|
||||
}
|
||||
|
||||
void MPESynthesiser::removeVoice (const int index)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
voices.remove (index);
|
||||
}
|
||||
|
||||
void MPESynthesiser::reduceNumVoices (const int newNumVoices)
|
||||
{
|
||||
// we can't possibly get to a negative number of voices...
|
||||
jassert (newNumVoices >= 0);
|
||||
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
while (voices.size() > newNumVoices)
|
||||
{
|
||||
if (auto* voice = findFreeVoice ({}, true))
|
||||
voices.removeObject (voice);
|
||||
else
|
||||
voices.remove (0); // if there's no voice to steal, kill the oldest voice
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::turnOffAllVoices (bool allowTailOff)
|
||||
{
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
// first turn off all voices (it's more efficient to do this immediately
|
||||
// rather than to go through the MPEInstrument for this).
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
voice->currentlyPlayingNote.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number
|
||||
voice->currentlyPlayingNote.keyState = MPENote::off;
|
||||
|
||||
voice->noteStopped (allowTailOff);
|
||||
}
|
||||
}
|
||||
|
||||
// finally make sure the MPE Instrument also doesn't have any notes anymore.
|
||||
instrument->releaseAllNotes();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiser::renderNextSubBlock (AudioBuffer<float>& buffer, int startSample, int numSamples)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isActive())
|
||||
voice->renderNextBlock (buffer, startSample, numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
void MPESynthesiser::renderNextSubBlock (AudioBuffer<double>& buffer, int startSample, int numSamples)
|
||||
{
|
||||
const ScopedLock sl (voicesLock);
|
||||
|
||||
for (auto* voice : voices)
|
||||
{
|
||||
if (voice->isActive())
|
||||
voice->renderNextBlock (buffer, startSample, numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
312
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiser.h
vendored
Normal file
312
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiser.h
vendored
Normal file
@ -0,0 +1,312 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Base class for an MPE-compatible musical device that can play sounds.
|
||||
|
||||
This class extends MPESynthesiserBase by adding the concept of voices,
|
||||
each of which can play a sound triggered by a MPENote that can be modulated
|
||||
by MPE dimensions like pressure, pitchbend, and timbre, while the note is
|
||||
sounding.
|
||||
|
||||
To create a synthesiser, you'll need to create a subclass of MPESynthesiserVoice
|
||||
which can play back one of these sounds at a time.
|
||||
|
||||
Then you can use the addVoice() methods to give the synthesiser a set of voices
|
||||
it can use to play notes. If you only give it one voice it will be monophonic -
|
||||
the more voices it has, the more polyphony it'll have available.
|
||||
|
||||
Then repeatedly call the renderNextBlock() method to produce the audio (inherited
|
||||
from MPESynthesiserBase). The voices will be started, stopped, and modulated
|
||||
automatically, based on the MPE/MIDI messages that the synthesiser receives.
|
||||
|
||||
Before rendering, be sure to call the setCurrentPlaybackSampleRate() to tell it
|
||||
what the target playback rate is. This value is passed on to the voices so that
|
||||
they can pitch their output correctly.
|
||||
|
||||
@see MPESynthesiserBase, MPESynthesiserVoice, MPENote, MPEInstrument
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MPESynthesiser : public MPESynthesiserBase
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Constructor.
|
||||
You'll need to add some voices before it'll make any sound.
|
||||
|
||||
@see addVoice
|
||||
*/
|
||||
MPESynthesiser();
|
||||
|
||||
/** Constructor to pass to the synthesiser a custom MPEInstrument object
|
||||
to handle the MPE note state, MIDI channel assignment etc.
|
||||
(in case you need custom logic for this that goes beyond MIDI and MPE).
|
||||
The synthesiser will take ownership of this object.
|
||||
|
||||
@see MPESynthesiserBase, MPEInstrument
|
||||
*/
|
||||
MPESynthesiser (MPEInstrument* instrumentToUse);
|
||||
|
||||
/** Destructor. */
|
||||
~MPESynthesiser() override;
|
||||
|
||||
//==============================================================================
|
||||
/** Deletes all voices. */
|
||||
void clearVoices();
|
||||
|
||||
/** Returns the number of voices that have been added. */
|
||||
int getNumVoices() const noexcept { return voices.size(); }
|
||||
|
||||
/** Returns one of the voices that have been added. */
|
||||
MPESynthesiserVoice* getVoice (int index) const;
|
||||
|
||||
/** Adds a new voice to the synth.
|
||||
|
||||
All the voices should be the same class of object and are treated equally.
|
||||
|
||||
The object passed in will be managed by the synthesiser, which will delete
|
||||
it later on when no longer needed. The caller should not retain a pointer to the
|
||||
voice.
|
||||
*/
|
||||
void addVoice (MPESynthesiserVoice* newVoice);
|
||||
|
||||
/** Deletes one of the voices. */
|
||||
void removeVoice (int index);
|
||||
|
||||
/** Reduces the number of voices to newNumVoices.
|
||||
|
||||
This will repeatedly call findVoiceToSteal() and remove that voice, until
|
||||
the total number of voices equals newNumVoices. If newNumVoices is greater than
|
||||
or equal to the current number of voices, this method does nothing.
|
||||
*/
|
||||
void reduceNumVoices (int newNumVoices);
|
||||
|
||||
/** Release all MPE notes and turn off all voices.
|
||||
|
||||
If allowTailOff is true, the voices will be allowed to fade out the notes gracefully
|
||||
(if they can do). If this is false, the notes will all be cut off immediately.
|
||||
|
||||
This method is meant to be called by the user, for example to implement
|
||||
a MIDI panic button in a synth.
|
||||
*/
|
||||
virtual void turnOffAllVoices (bool allowTailOff);
|
||||
|
||||
//==============================================================================
|
||||
/** If set to true, then the synth will try to take over an existing voice if
|
||||
it runs out and needs to play another note.
|
||||
|
||||
The value of this boolean is passed into findFreeVoice(), so the result will
|
||||
depend on the implementation of this method.
|
||||
*/
|
||||
void setVoiceStealingEnabled (bool shouldSteal) noexcept { shouldStealVoices = shouldSteal; }
|
||||
|
||||
/** Returns true if note-stealing is enabled. */
|
||||
bool isVoiceStealingEnabled() const noexcept { return shouldStealVoices; }
|
||||
|
||||
//==============================================================================
|
||||
/** Tells the synthesiser what the sample rate is for the audio it's being used to render.
|
||||
|
||||
This overrides the implementation in MPESynthesiserBase, to additionally
|
||||
propagate the new value to the voices so that they can use it to render the correct
|
||||
pitches.
|
||||
*/
|
||||
void setCurrentPlaybackSampleRate (double newRate) override;
|
||||
|
||||
//==============================================================================
|
||||
/** Handle incoming MIDI events.
|
||||
|
||||
This method will be called automatically according to the MIDI data passed
|
||||
into renderNextBlock(), but you can also call it yourself to manually
|
||||
inject MIDI events.
|
||||
|
||||
This implementation forwards program change messages and non-MPE-related
|
||||
controller messages to handleProgramChange and handleController, respectively,
|
||||
and then simply calls through to MPESynthesiserBase::handleMidiEvent to deal
|
||||
with MPE-related MIDI messages used for MPE notes, zones etc.
|
||||
|
||||
This method can be overridden further if you need to do custom MIDI
|
||||
handling on top of what is provided here.
|
||||
*/
|
||||
void handleMidiEvent (const MidiMessage&) override;
|
||||
|
||||
/** Callback for MIDI controller messages. The default implementation
|
||||
provided here does nothing; override this method if you need custom
|
||||
MIDI controller handling on top of MPE.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock().
|
||||
*/
|
||||
virtual void handleController (int /*midiChannel*/,
|
||||
int /*controllerNumber*/,
|
||||
int /*controllerValue*/) {}
|
||||
|
||||
/** Callback for MIDI program change messages. The default implementation
|
||||
provided here does nothing; override this method if you need to handle
|
||||
those messages.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock().
|
||||
*/
|
||||
virtual void handleProgramChange (int /*midiChannel*/,
|
||||
int /*programNumber*/) {}
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** Attempts to start playing a new note.
|
||||
|
||||
The default method here will find a free voice that is appropriate for
|
||||
playing the given MPENote, and use that voice to start playing the sound.
|
||||
If isNoteStealingEnabled returns true (set this by calling setNoteStealingEnabled),
|
||||
the synthesiser will use the voice stealing algorithm to find a free voice for
|
||||
the note (if no voices are free otherwise).
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
|
||||
will become inconsistent.
|
||||
*/
|
||||
void noteAdded (MPENote newNote) override;
|
||||
|
||||
/** Stops playing a note.
|
||||
|
||||
This will be called 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.
|
||||
|
||||
This will find any voice that is currently playing finishedNote,
|
||||
turn its currently playing note off, and call its noteStopped callback.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
|
||||
will become inconsistent.
|
||||
*/
|
||||
void noteReleased (MPENote finishedNote) override;
|
||||
|
||||
/** Will find any voice that is currently playing changedNote, update its
|
||||
currently playing note, and call its notePressureChanged method.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself.
|
||||
*/
|
||||
void notePressureChanged (MPENote changedNote) override;
|
||||
|
||||
/** Will find any voice that is currently playing changedNote, update its
|
||||
currently playing note, and call its notePitchbendChanged method.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself.
|
||||
*/
|
||||
void notePitchbendChanged (MPENote changedNote) override;
|
||||
|
||||
/** Will find any voice that is currently playing changedNote, update its
|
||||
currently playing note, and call its noteTimbreChanged method.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself.
|
||||
*/
|
||||
void noteTimbreChanged (MPENote changedNote) override;
|
||||
|
||||
/** Will find any voice that is currently playing changedNote, update its
|
||||
currently playing note, and call its noteKeyStateChanged method.
|
||||
|
||||
This method will be called automatically according to the midi data passed into
|
||||
renderNextBlock(). Do not call it yourself.
|
||||
*/
|
||||
void noteKeyStateChanged (MPENote changedNote) override;
|
||||
|
||||
//==============================================================================
|
||||
/** This will simply call renderNextBlock for each currently active
|
||||
voice and fill the buffer with the sum.
|
||||
Override this method if you need to do more work to render your audio.
|
||||
*/
|
||||
void renderNextSubBlock (AudioBuffer<float>& outputAudio,
|
||||
int startSample,
|
||||
int numSamples) override;
|
||||
|
||||
/** This will simply call renderNextBlock for each currently active
|
||||
voice and fill the buffer with the sum. (double-precision version)
|
||||
Override this method if you need to do more work to render your audio.
|
||||
*/
|
||||
void renderNextSubBlock (AudioBuffer<double>& outputAudio,
|
||||
int startSample,
|
||||
int numSamples) override;
|
||||
|
||||
//==============================================================================
|
||||
/** Searches through the voices to find one that's not currently playing, and
|
||||
which can play the given MPE note.
|
||||
|
||||
If all voices are active and stealIfNoneAvailable is false, this returns
|
||||
a nullptr. If all voices are active and stealIfNoneAvailable is true,
|
||||
this will call findVoiceToSteal() to find a voice.
|
||||
|
||||
If you need to find a free voice for something else than playing a note
|
||||
(e.g. for deleting it), you can pass an invalid (default-constructed) MPENote.
|
||||
*/
|
||||
virtual MPESynthesiserVoice* findFreeVoice (MPENote noteToFindVoiceFor,
|
||||
bool stealIfNoneAvailable) const;
|
||||
|
||||
/** Chooses a voice that is most suitable for being re-used to play a new
|
||||
note, or for being deleted by reduceNumVoices.
|
||||
|
||||
The default method will attempt to find the oldest voice that isn't the
|
||||
bottom or top note being played. If that's not suitable for your synth,
|
||||
you can override this method and do something more cunning instead.
|
||||
|
||||
If you pass a valid MPENote for the optional argument, then the note number
|
||||
of that note will be taken into account for finding the ideal voice to steal.
|
||||
If you pass an invalid (default-constructed) MPENote instead, this part of
|
||||
the algorithm will be ignored.
|
||||
*/
|
||||
virtual MPESynthesiserVoice* findVoiceToSteal (MPENote noteToStealVoiceFor = MPENote()) const;
|
||||
|
||||
/** Starts a specified voice and tells it to play a particular MPENote.
|
||||
You should never need to call this, it's called internally by
|
||||
MPESynthesiserBase::instrument via the noteStarted callback,
|
||||
but is protected in case it's useful for some custom subclasses.
|
||||
*/
|
||||
void startVoice (MPESynthesiserVoice* voice, MPENote noteToStart);
|
||||
|
||||
/** Stops a given voice and tells it to stop playing a particular MPENote
|
||||
(which should be the same note it is actually playing).
|
||||
You should never need to call this, it's called internally by
|
||||
MPESynthesiserBase::instrument via the noteReleased callback,
|
||||
but is protected in case it's useful for some custom subclasses.
|
||||
*/
|
||||
void stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff);
|
||||
|
||||
//==============================================================================
|
||||
OwnedArray<MPESynthesiserVoice> voices;
|
||||
CriticalSection voicesLock;
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
bool shouldStealVoices = false;
|
||||
uint32 lastNoteOnCounter = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)
|
||||
};
|
||||
|
||||
} // namespace juce
|
376
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserBase.cpp
vendored
Normal file
376
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserBase.cpp
vendored
Normal file
@ -0,0 +1,376 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPESynthesiserBase::MPESynthesiserBase()
|
||||
: instrument (new MPEInstrument)
|
||||
{
|
||||
instrument->addListener (this);
|
||||
}
|
||||
|
||||
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument* inst)
|
||||
: instrument (inst)
|
||||
{
|
||||
jassert (instrument != nullptr);
|
||||
instrument->addListener (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
|
||||
{
|
||||
return instrument->getZoneLayout();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
|
||||
{
|
||||
instrument->setZoneLayout (newLayout);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
|
||||
{
|
||||
instrument->enableLegacyMode (pitchbendRange, channelRange);
|
||||
}
|
||||
|
||||
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
|
||||
{
|
||||
return instrument->isLegacyModeEnabled();
|
||||
}
|
||||
|
||||
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
|
||||
{
|
||||
return instrument->getLegacyModeChannelRange();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
|
||||
{
|
||||
instrument->setLegacyModeChannelRange (channelRange);
|
||||
}
|
||||
|
||||
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
|
||||
{
|
||||
return instrument->getLegacyModePitchbendRange();
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
|
||||
{
|
||||
instrument->setLegacyModePitchbendRange (pitchbendRange);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setPressureTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setPitchbendTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
|
||||
{
|
||||
instrument->setTimbreTrackingMode (modeToUse);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m)
|
||||
{
|
||||
instrument->processNextMidiEvent (m);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
template <typename floatType>
|
||||
void MPESynthesiserBase::renderNextBlock (AudioBuffer<floatType>& outputAudio,
|
||||
const MidiBuffer& inputMidi,
|
||||
int startSample,
|
||||
int numSamples)
|
||||
{
|
||||
// you must set the sample rate before using this!
|
||||
jassert (sampleRate != 0);
|
||||
|
||||
const ScopedLock sl (noteStateLock);
|
||||
|
||||
auto prevSample = startSample;
|
||||
const auto endSample = startSample + numSamples;
|
||||
|
||||
for (auto it = inputMidi.findNextSamplePosition (startSample); it != inputMidi.cend(); ++it)
|
||||
{
|
||||
const auto metadata = *it;
|
||||
|
||||
if (metadata.samplePosition >= endSample)
|
||||
break;
|
||||
|
||||
const auto smallBlockAllowed = (prevSample == startSample && ! subBlockSubdivisionIsStrict);
|
||||
const auto thisBlockSize = smallBlockAllowed ? 1 : minimumSubBlockSize;
|
||||
|
||||
if (metadata.samplePosition >= prevSample + thisBlockSize)
|
||||
{
|
||||
renderNextSubBlock (outputAudio, prevSample, metadata.samplePosition - prevSample);
|
||||
prevSample = metadata.samplePosition;
|
||||
}
|
||||
|
||||
handleMidiEvent (metadata.getMessage());
|
||||
}
|
||||
|
||||
if (prevSample < endSample)
|
||||
renderNextSubBlock (outputAudio, prevSample, endSample - prevSample);
|
||||
}
|
||||
|
||||
// explicit instantiation for supported float types:
|
||||
template void MPESynthesiserBase::renderNextBlock<float> (AudioBuffer<float>&, const MidiBuffer&, int, int);
|
||||
template void MPESynthesiserBase::renderNextBlock<double> (AudioBuffer<double>&, const MidiBuffer&, int, int);
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::setCurrentPlaybackSampleRate (const double newRate)
|
||||
{
|
||||
if (sampleRate != newRate)
|
||||
{
|
||||
const ScopedLock sl (noteStateLock);
|
||||
instrument->releaseAllNotes();
|
||||
sampleRate = newRate;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPESynthesiserBase::setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict) noexcept
|
||||
{
|
||||
jassert (numSamples > 0); // it wouldn't make much sense for this to be less than 1
|
||||
minimumSubBlockSize = numSamples;
|
||||
subBlockSubdivisionIsStrict = shouldBeStrict;
|
||||
}
|
||||
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
namespace
|
||||
{
|
||||
class MpeSynthesiserBaseTests : public UnitTest
|
||||
{
|
||||
enum class CallbackKind { process, midi };
|
||||
|
||||
struct StartAndLength
|
||||
{
|
||||
StartAndLength (int s, int l) : start (s), length (l) {}
|
||||
|
||||
int start = 0;
|
||||
int length = 0;
|
||||
|
||||
std::tuple<const int&, const int&> tie() const noexcept { return std::tie (start, length); }
|
||||
|
||||
bool operator== (const StartAndLength& other) const noexcept { return tie() == other.tie(); }
|
||||
bool operator!= (const StartAndLength& other) const noexcept { return tie() != other.tie(); }
|
||||
|
||||
bool operator< (const StartAndLength& other) const noexcept { return tie() < other.tie(); }
|
||||
};
|
||||
|
||||
struct Events
|
||||
{
|
||||
std::vector<StartAndLength> blocks;
|
||||
std::vector<MidiMessage> messages;
|
||||
std::vector<CallbackKind> order;
|
||||
};
|
||||
|
||||
class MockSynthesiser : public MPESynthesiserBase
|
||||
{
|
||||
public:
|
||||
Events events;
|
||||
|
||||
void handleMidiEvent (const MidiMessage& m) override
|
||||
{
|
||||
events.messages.emplace_back (m);
|
||||
events.order.emplace_back (CallbackKind::midi);
|
||||
}
|
||||
|
||||
private:
|
||||
using MPESynthesiserBase::renderNextSubBlock;
|
||||
|
||||
void renderNextSubBlock (AudioBuffer<float>&,
|
||||
int startSample,
|
||||
int numSamples) override
|
||||
{
|
||||
events.blocks.push_back ({ startSample, numSamples });
|
||||
events.order.emplace_back (CallbackKind::process);
|
||||
}
|
||||
};
|
||||
|
||||
static MidiBuffer makeTestBuffer (const int bufferLength)
|
||||
{
|
||||
MidiBuffer result;
|
||||
|
||||
for (int i = 0; i != bufferLength; ++i)
|
||||
result.addEvent ({}, i);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
MpeSynthesiserBaseTests()
|
||||
: UnitTest ("MPE Synthesiser Base", UnitTestCategories::midi) {}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
const auto sumBlockLengths = [] (const std::vector<StartAndLength>& b)
|
||||
{
|
||||
const auto addBlock = [] (int acc, const StartAndLength& info) { return acc + info.length; };
|
||||
return std::accumulate (b.begin(), b.end(), 0, addBlock);
|
||||
};
|
||||
|
||||
beginTest ("Rendering sparse subblocks works");
|
||||
{
|
||||
const int blockSize = 512;
|
||||
const auto midi = [&] { MidiBuffer b; b.addEvent ({}, blockSize / 2); return b; }();
|
||||
AudioBuffer<float> audio (1, blockSize);
|
||||
|
||||
const auto processEvents = [&] (int start, int length)
|
||||
{
|
||||
MockSynthesiser synth;
|
||||
synth.setMinimumRenderingSubdivisionSize (1, false);
|
||||
synth.setCurrentPlaybackSampleRate (44100);
|
||||
synth.renderNextBlock (audio, midi, start, length);
|
||||
return synth.events;
|
||||
};
|
||||
|
||||
{
|
||||
const auto e = processEvents (0, blockSize);
|
||||
expect (e.blocks.size() == 2);
|
||||
expect (e.messages.size() == 1);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == blockSize);
|
||||
expect (e.order == std::vector<CallbackKind> { CallbackKind::process,
|
||||
CallbackKind::midi,
|
||||
CallbackKind::process });
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Rendering subblocks processes only contained midi events");
|
||||
{
|
||||
const int blockSize = 512;
|
||||
const auto midi = makeTestBuffer (blockSize);
|
||||
AudioBuffer<float> audio (1, blockSize);
|
||||
|
||||
const auto processEvents = [&] (int start, int length)
|
||||
{
|
||||
MockSynthesiser synth;
|
||||
synth.setMinimumRenderingSubdivisionSize (1, false);
|
||||
synth.setCurrentPlaybackSampleRate (44100);
|
||||
synth.renderNextBlock (audio, midi, start, length);
|
||||
return synth.events;
|
||||
};
|
||||
|
||||
{
|
||||
const int subBlockLength = 0;
|
||||
const auto e = processEvents (0, subBlockLength);
|
||||
expect (e.blocks.size() == 0);
|
||||
expect (e.messages.size() == 0);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == subBlockLength);
|
||||
}
|
||||
|
||||
{
|
||||
const int subBlockLength = 0;
|
||||
const auto e = processEvents (1, subBlockLength);
|
||||
expect (e.blocks.size() == 0);
|
||||
expect (e.messages.size() == 0);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == subBlockLength);
|
||||
}
|
||||
|
||||
{
|
||||
const int subBlockLength = 1;
|
||||
const auto e = processEvents (1, subBlockLength);
|
||||
expect (e.blocks.size() == 1);
|
||||
expect (e.messages.size() == 1);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == subBlockLength);
|
||||
expect (e.order == std::vector<CallbackKind> { CallbackKind::midi,
|
||||
CallbackKind::process });
|
||||
}
|
||||
|
||||
{
|
||||
const auto e = processEvents (0, blockSize);
|
||||
expect (e.blocks.size() == blockSize);
|
||||
expect (e.messages.size() == blockSize);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == blockSize);
|
||||
expect (e.order.front() == CallbackKind::midi);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("Subblocks respect their minimum size");
|
||||
{
|
||||
const int blockSize = 512;
|
||||
const auto midi = makeTestBuffer (blockSize);
|
||||
AudioBuffer<float> audio (1, blockSize);
|
||||
|
||||
const auto blockLengthsAreValid = [] (const std::vector<StartAndLength>& info, int minLength, bool strict)
|
||||
{
|
||||
if (info.size() <= 1)
|
||||
return true;
|
||||
|
||||
const auto lengthIsValid = [&] (const StartAndLength& s) { return minLength <= s.length; };
|
||||
const auto begin = strict ? info.begin() : std::next (info.begin());
|
||||
// The final block is allowed to be shorter than the minLength
|
||||
return std::all_of (begin, std::prev (info.end()), lengthIsValid);
|
||||
};
|
||||
|
||||
for (auto strict : { false, true })
|
||||
{
|
||||
for (auto subblockSize : { 1, 16, 32, 64, 1024 })
|
||||
{
|
||||
MockSynthesiser synth;
|
||||
synth.setMinimumRenderingSubdivisionSize (subblockSize, strict);
|
||||
synth.setCurrentPlaybackSampleRate (44100);
|
||||
synth.renderNextBlock (audio, midi, 0, blockSize);
|
||||
|
||||
const auto& e = synth.events;
|
||||
expectWithinAbsoluteError (float (e.blocks.size()),
|
||||
std::ceil ((float) blockSize / (float) subblockSize),
|
||||
1.0f);
|
||||
expect (e.messages.size() == blockSize);
|
||||
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
|
||||
expect (sumBlockLengths (e.blocks) == blockSize);
|
||||
expect (blockLengthsAreValid (e.blocks, subblockSize, strict));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MockSynthesiser synth;
|
||||
synth.setMinimumRenderingSubdivisionSize (32, true);
|
||||
synth.setCurrentPlaybackSampleRate (44100);
|
||||
synth.renderNextBlock (audio, MidiBuffer{}, 0, 16);
|
||||
|
||||
expect (synth.events.blocks == std::vector<StartAndLength> { { 0, 16 } });
|
||||
expect (synth.events.order == std::vector<CallbackKind> { CallbackKind::process });
|
||||
expect (synth.events.messages.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MpeSynthesiserBaseTests mpeSynthesiserBaseTests;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
215
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserBase.h
vendored
Normal file
215
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserBase.h
vendored
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Derive from this class to create a basic audio generator capable of MPE.
|
||||
Implement the callbacks of MPEInstrument::Listener (noteAdded, notePressureChanged
|
||||
etc.) to let your audio generator know that MPE notes were triggered, modulated,
|
||||
or released. What to do inside them, and how that influences your audio generator,
|
||||
is up to you!
|
||||
|
||||
This class uses an instance of MPEInstrument internally to handle the MPE
|
||||
note state logic.
|
||||
|
||||
This class is a very low-level base class for an MPE instrument. If you need
|
||||
something more sophisticated, have a look at MPESynthesiser. This class extends
|
||||
MPESynthesiserBase by adding the concept of voices that can play notes,
|
||||
a voice stealing algorithm, and much more.
|
||||
|
||||
@see MPESynthesiser, MPEInstrument
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
struct JUCE_API MPESynthesiserBase : public MPEInstrument::Listener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Constructor. */
|
||||
MPESynthesiserBase();
|
||||
|
||||
/** Constructor.
|
||||
|
||||
If you use this constructor, the synthesiser will take ownership of the
|
||||
provided instrument object, and will use it internally to handle the
|
||||
MPE note state logic.
|
||||
This is useful if you want to use an instance of your own class derived
|
||||
from MPEInstrument for the MPE logic.
|
||||
*/
|
||||
MPESynthesiserBase (MPEInstrument* instrument);
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the synthesiser's internal MPE zone layout.
|
||||
This happens by value, to enforce thread-safety and class invariants.
|
||||
*/
|
||||
MPEZoneLayout getZoneLayout() const noexcept;
|
||||
|
||||
/** Re-sets the synthesiser's internal MPE zone layout to the one passed in.
|
||||
As a side effect, this will discard all currently playing notes,
|
||||
call noteReleased for all of them, and disable legacy mode (if previously enabled).
|
||||
*/
|
||||
void setZoneLayout (MPEZoneLayout newLayout);
|
||||
|
||||
//==============================================================================
|
||||
/** Tells the synthesiser what the sample rate is for the audio it's being
|
||||
used to render.
|
||||
*/
|
||||
virtual void setCurrentPlaybackSampleRate (double sampleRate);
|
||||
|
||||
/** Returns the current target sample rate at which rendering is being done.
|
||||
Subclasses may need to know this so that they can pitch things correctly.
|
||||
*/
|
||||
double getSampleRate() const noexcept { return sampleRate; }
|
||||
|
||||
//==============================================================================
|
||||
/** Creates the next block of audio output.
|
||||
|
||||
Call this to make sound. This will chop up the AudioBuffer into subBlock
|
||||
pieces separated by events in the MIDI buffer, and then call
|
||||
renderNextSubBlock on each one of them. In between you will get calls
|
||||
to noteAdded/Changed/Finished, where you can update parameters that
|
||||
depend on those notes to use for your audio rendering.
|
||||
|
||||
@param outputAudio Buffer into which audio will be rendered
|
||||
@param inputMidi MIDI events to process
|
||||
@param startSample The first sample to process in both buffers
|
||||
@param numSamples The number of samples to process
|
||||
*/
|
||||
template <typename floatType>
|
||||
void renderNextBlock (AudioBuffer<floatType>& outputAudio,
|
||||
const MidiBuffer& inputMidi,
|
||||
int startSample,
|
||||
int numSamples);
|
||||
|
||||
//==============================================================================
|
||||
/** Handle incoming MIDI events (called from renderNextBlock).
|
||||
|
||||
The default implementation provided here simply forwards everything
|
||||
to MPEInstrument::processNextMidiEvent, where it is used to update the
|
||||
MPE notes, zones etc. MIDI messages not relevant for MPE are ignored.
|
||||
|
||||
This method can be overridden if you need to do custom MIDI handling
|
||||
on top of MPE. The MPESynthesiser class overrides this to implement
|
||||
callbacks for MIDI program changes and non-MPE-related MIDI controller
|
||||
messages.
|
||||
*/
|
||||
virtual void handleMidiEvent (const MidiMessage&);
|
||||
|
||||
//==============================================================================
|
||||
/** Sets a minimum limit on the size to which audio sub-blocks will be divided when rendering.
|
||||
|
||||
When rendering, the audio blocks that are passed into renderNextBlock() will be split up
|
||||
into smaller blocks that lie between all the incoming midi messages, and it is these smaller
|
||||
sub-blocks that are rendered with multiple calls to renderVoices().
|
||||
|
||||
Obviously in a pathological case where there are midi messages on every sample, then
|
||||
renderVoices() could be called once per sample and lead to poor performance, so this
|
||||
setting allows you to set a lower limit on the block size.
|
||||
|
||||
The default setting is 32, which means that midi messages are accurate to about < 1ms
|
||||
accuracy, which is probably fine for most purposes, but you may want to increase or
|
||||
decrease this value for your synth.
|
||||
|
||||
If shouldBeStrict is true, the audio sub-blocks will strictly never be smaller than numSamples.
|
||||
|
||||
If shouldBeStrict is false (default), the first audio sub-block in the buffer is allowed
|
||||
to be smaller, to make sure that the first MIDI event in a buffer will always be sample-accurate
|
||||
(this can sometimes help to avoid quantisation or phasing issues).
|
||||
*/
|
||||
void setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict = false) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Puts the synthesiser into legacy mode.
|
||||
|
||||
@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);
|
||||
|
||||
//==============================================================================
|
||||
using TrackingMode = MPEInstrument::TrackingMode;
|
||||
|
||||
/** 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);
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** Implement this method to render your audio inside.
|
||||
@see renderNextBlock
|
||||
*/
|
||||
virtual void renderNextSubBlock (AudioBuffer<float>& outputAudio,
|
||||
int startSample,
|
||||
int numSamples) = 0;
|
||||
|
||||
/** Implement this method if you want to render 64-bit audio as well;
|
||||
otherwise leave blank.
|
||||
*/
|
||||
virtual void renderNextSubBlock (AudioBuffer<double>& /*outputAudio*/,
|
||||
int /*startSample*/,
|
||||
int /*numSamples*/) {}
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** @internal */
|
||||
std::unique_ptr<MPEInstrument> instrument;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
CriticalSection noteStateLock;
|
||||
double sampleRate = 0.0;
|
||||
int minimumSubBlockSize = 32;
|
||||
bool subBlockSubdivisionIsStrict = false;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserBase)
|
||||
};
|
||||
|
||||
} // namespace juce
|
50
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserVoice.cpp
vendored
Normal file
50
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserVoice.cpp
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPESynthesiserVoice::MPESynthesiserVoice()
|
||||
{
|
||||
}
|
||||
|
||||
MPESynthesiserVoice::~MPESynthesiserVoice()
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPESynthesiserVoice::isCurrentlyPlayingNote (MPENote note) const noexcept
|
||||
{
|
||||
return isActive() && currentlyPlayingNote.noteID == note.noteID;
|
||||
}
|
||||
|
||||
bool MPESynthesiserVoice::isPlayingButReleased() const noexcept
|
||||
{
|
||||
return isActive() && currentlyPlayingNote.keyState == MPENote::off;
|
||||
}
|
||||
|
||||
void MPESynthesiserVoice::clearCurrentNote() noexcept
|
||||
{
|
||||
currentlyPlayingNote = MPENote();
|
||||
}
|
||||
|
||||
} // namespace juce
|
191
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserVoice.h
vendored
Normal file
191
deps/juce/modules/juce_audio_basics/mpe/juce_MPESynthesiserVoice.h
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Represents an MPE voice that an MPESynthesiser can use to play a sound.
|
||||
|
||||
A voice plays a single sound at a time, and a synthesiser holds an array of
|
||||
voices so that it can play polyphonically.
|
||||
|
||||
@see MPESynthesiser, MPENote
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MPESynthesiserVoice
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Constructor. */
|
||||
MPESynthesiserVoice();
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~MPESynthesiserVoice();
|
||||
|
||||
/** Returns the MPENote that this voice is currently playing.
|
||||
Returns an invalid MPENote if no note is playing
|
||||
(you can check this using MPENote::isValid() or MPEVoice::isActive()).
|
||||
*/
|
||||
MPENote getCurrentlyPlayingNote() const noexcept { return currentlyPlayingNote; }
|
||||
|
||||
/** Returns true if the voice is currently playing the given MPENote
|
||||
(as identified by the note's initial note number and MIDI channel).
|
||||
*/
|
||||
bool isCurrentlyPlayingNote (MPENote note) const noexcept;
|
||||
|
||||
/** Returns true if this voice is currently busy playing a sound.
|
||||
By default this just checks whether getCurrentlyPlayingNote()
|
||||
returns a valid MPE note, but can be overridden for more advanced checking.
|
||||
*/
|
||||
virtual bool isActive() const { return currentlyPlayingNote.isValid(); }
|
||||
|
||||
/** Returns true if a voice is sounding in its release phase. **/
|
||||
bool isPlayingButReleased() const noexcept;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that a new note has started on it.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
*/
|
||||
virtual void noteStarted() = 0;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that its currently playing note has stopped.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
|
||||
If allowTailOff is false or the voice doesn't want to tail-off, then it must stop all
|
||||
sound immediately, and must call clearCurrentNote() to reset the state of this voice
|
||||
and allow the synth to reassign it another sound.
|
||||
|
||||
If allowTailOff is true and the voice decides to do a tail-off, then it's allowed to
|
||||
begin fading out its sound, and it can stop playing until it's finished. As soon as it
|
||||
finishes playing (during the rendering callback), it must make sure that it calls
|
||||
clearCurrentNote().
|
||||
*/
|
||||
virtual void noteStopped (bool allowTailOff) = 0;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that its currently playing note
|
||||
has changed its pressure value.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
*/
|
||||
virtual void notePressureChanged() = 0;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that its currently playing note
|
||||
has changed its pitchbend value.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
|
||||
Note: You can call currentlyPlayingNote.getFrequencyInHertz() to find out the effective frequency
|
||||
of the note, as a sum of the initial note number, the per-note pitchbend and the master pitchbend.
|
||||
*/
|
||||
virtual void notePitchbendChanged() = 0;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that its currently playing note
|
||||
has changed its timbre value.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
*/
|
||||
virtual void noteTimbreChanged() = 0;
|
||||
|
||||
/** Called by the MPESynthesiser to let the voice know that its currently playing note
|
||||
has changed its key state.
|
||||
This typically happens when a sustain or sostenuto pedal is pressed or released (on
|
||||
an MPE channel relevant for this note), or if the note key is lifted while the sustained
|
||||
or sostenuto pedal is still held down.
|
||||
This will be called during the rendering callback, so must be fast and thread-safe.
|
||||
*/
|
||||
virtual void noteKeyStateChanged() = 0;
|
||||
|
||||
/** Renders the next block of data for this voice.
|
||||
|
||||
The output audio data must be added to the current contents of the buffer provided.
|
||||
Only the region of the buffer between startSample and (startSample + numSamples)
|
||||
should be altered by this method.
|
||||
|
||||
If the voice is currently silent, it should just return without doing anything.
|
||||
|
||||
If the sound that the voice is playing finishes during the course of this rendered
|
||||
block, it must call clearCurrentNote(), to tell the synthesiser that it has finished.
|
||||
|
||||
The size of the blocks that are rendered can change each time it is called, and may
|
||||
involve rendering as little as 1 sample at a time. In between rendering callbacks,
|
||||
the voice's methods will be called to tell it about note and controller events.
|
||||
*/
|
||||
virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
|
||||
int startSample,
|
||||
int numSamples) = 0;
|
||||
|
||||
/** Renders the next block of 64-bit data for this voice.
|
||||
|
||||
Support for 64-bit audio is optional. You can choose to not override this method if
|
||||
you don't need it (the default implementation simply does nothing).
|
||||
*/
|
||||
virtual void renderNextBlock (AudioBuffer<double>& /*outputBuffer*/,
|
||||
int /*startSample*/,
|
||||
int /*numSamples*/) {}
|
||||
|
||||
/** Changes the voice's reference sample rate.
|
||||
|
||||
The rate is set so that subclasses know the output rate and can set their pitch
|
||||
accordingly.
|
||||
|
||||
This method is called by the synth, and subclasses can access the current rate with
|
||||
the currentSampleRate member.
|
||||
*/
|
||||
virtual void setCurrentSampleRate (double newRate) { currentSampleRate = newRate; }
|
||||
|
||||
/** Returns the current target sample rate at which rendering is being done.
|
||||
Subclasses may need to know this so that they can pitch things correctly.
|
||||
*/
|
||||
double getSampleRate() const noexcept { return currentSampleRate; }
|
||||
|
||||
/** This will be set to an incrementing counter value in MPESynthesiser::startVoice()
|
||||
and can be used to determine the order in which voices started.
|
||||
*/
|
||||
uint32 noteOnTime = 0;
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
/** Resets the state of this voice after a sound has finished playing.
|
||||
|
||||
The subclass must call this when it finishes playing a note and becomes available
|
||||
to play new ones.
|
||||
|
||||
It must either call it in the stopNote() method, or if the voice is tailing off,
|
||||
then it should call it later during the renderNextBlock method, as soon as it
|
||||
finishes its tail-off.
|
||||
|
||||
It can also be called at any time during the render callback if the sound happens
|
||||
to have finished, e.g. if it's playing a sample and the sample finishes.
|
||||
*/
|
||||
void clearCurrentNote() noexcept;
|
||||
|
||||
//==============================================================================
|
||||
double currentSampleRate = 0.0;
|
||||
MPENote currentlyPlayingNote;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
friend class MPESynthesiser;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserVoice)
|
||||
};
|
||||
|
||||
} // namespace juce
|
494
deps/juce/modules/juce_audio_basics/mpe/juce_MPEUtils.cpp
vendored
Normal file
494
deps/juce/modules/juce_audio_basics/mpe/juce_MPEUtils.cpp
vendored
Normal file
@ -0,0 +1,494 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPEChannelAssigner::MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse)
|
||||
: zone (new MPEZoneLayout::Zone (zoneToUse)),
|
||||
channelIncrement (zone->isLowerZone() ? 1 : -1),
|
||||
numChannels (zone->numMemberChannels),
|
||||
firstChannel (zone->getFirstMemberChannel()),
|
||||
lastChannel (zone->getLastMemberChannel()),
|
||||
midiChannelLastAssigned (firstChannel - channelIncrement)
|
||||
{
|
||||
// must be an active MPE zone!
|
||||
jassert (numChannels > 0);
|
||||
}
|
||||
|
||||
MPEChannelAssigner::MPEChannelAssigner (Range<int> channelRange)
|
||||
: isLegacy (true),
|
||||
channelIncrement (1),
|
||||
numChannels (channelRange.getLength()),
|
||||
firstChannel (channelRange.getStart()),
|
||||
lastChannel (channelRange.getEnd() - 1),
|
||||
midiChannelLastAssigned (firstChannel - channelIncrement)
|
||||
{
|
||||
// must have at least one channel!
|
||||
jassert (! channelRange.isEmpty());
|
||||
}
|
||||
|
||||
int MPEChannelAssigner::findMidiChannelForNewNote (int noteNumber) noexcept
|
||||
{
|
||||
if (numChannels <= 1)
|
||||
return firstChannel;
|
||||
|
||||
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
|
||||
{
|
||||
if (midiChannels[ch].isFree() && midiChannels[ch].lastNotePlayed == noteNumber)
|
||||
{
|
||||
midiChannelLastAssigned = ch;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto ch = midiChannelLastAssigned + channelIncrement; ; ch += channelIncrement)
|
||||
{
|
||||
if (ch == lastChannel + channelIncrement) // loop wrap-around
|
||||
ch = firstChannel;
|
||||
|
||||
if (midiChannels[ch].isFree())
|
||||
{
|
||||
midiChannelLastAssigned = ch;
|
||||
midiChannels[ch].notes.add (noteNumber);
|
||||
return ch;
|
||||
}
|
||||
|
||||
if (ch == midiChannelLastAssigned)
|
||||
break; // no free channels!
|
||||
}
|
||||
|
||||
midiChannelLastAssigned = findMidiChannelPlayingClosestNonequalNote (noteNumber);
|
||||
midiChannels[midiChannelLastAssigned].notes.add (noteNumber);
|
||||
|
||||
return midiChannelLastAssigned;
|
||||
}
|
||||
|
||||
void MPEChannelAssigner::noteOff (int noteNumber, int midiChannel)
|
||||
{
|
||||
const auto removeNote = [] (MidiChannel& ch, int noteNum)
|
||||
{
|
||||
if (ch.notes.removeAllInstancesOf (noteNum) > 0)
|
||||
{
|
||||
ch.lastNotePlayed = noteNum;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (midiChannel >= 0 && midiChannel <= 16)
|
||||
{
|
||||
removeNote (midiChannels[midiChannel], noteNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& ch : midiChannels)
|
||||
{
|
||||
if (removeNote (ch, noteNumber))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MPEChannelAssigner::allNotesOff()
|
||||
{
|
||||
for (auto& ch : midiChannels)
|
||||
{
|
||||
if (ch.notes.size() > 0)
|
||||
ch.lastNotePlayed = ch.notes.getLast();
|
||||
|
||||
ch.notes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
int MPEChannelAssigner::findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept
|
||||
{
|
||||
auto channelWithClosestNote = firstChannel;
|
||||
int closestNoteDistance = 127;
|
||||
|
||||
for (auto ch = firstChannel; (isLegacy || zone->isLowerZone() ? ch <= lastChannel : ch >= lastChannel); ch += channelIncrement)
|
||||
{
|
||||
for (auto note : midiChannels[ch].notes)
|
||||
{
|
||||
auto noteDistance = std::abs (note - noteNumber);
|
||||
|
||||
if (noteDistance > 0 && noteDistance < closestNoteDistance)
|
||||
{
|
||||
closestNoteDistance = noteDistance;
|
||||
channelWithClosestNote = ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channelWithClosestNote;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEChannelRemapper::MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap)
|
||||
: zone (zoneToRemap),
|
||||
channelIncrement (zone.isLowerZone() ? 1 : -1),
|
||||
firstChannel (zone.getFirstMemberChannel()),
|
||||
lastChannel (zone.getLastMemberChannel())
|
||||
{
|
||||
// must be an active MPE zone!
|
||||
jassert (zone.numMemberChannels > 0);
|
||||
zeroArrays();
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept
|
||||
{
|
||||
auto channel = message.getChannel();
|
||||
|
||||
if (! zone.isUsingChannelAsMemberChannel (channel))
|
||||
return;
|
||||
|
||||
if (channel == zone.getMasterChannel() && (message.isResetAllControllers() || message.isAllNotesOff()))
|
||||
{
|
||||
clearSource (mpeSourceID);
|
||||
return;
|
||||
}
|
||||
|
||||
auto sourceAndChannelID = (((uint32) mpeSourceID << 5) | (uint32) (channel));
|
||||
|
||||
if (messageIsNoteData (message))
|
||||
{
|
||||
++counter;
|
||||
|
||||
// fast path - no remap
|
||||
if (applyRemapIfExisting (channel, sourceAndChannelID, message))
|
||||
return;
|
||||
|
||||
// find existing remap
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
if (applyRemapIfExisting (chan, sourceAndChannelID, message))
|
||||
return;
|
||||
|
||||
// no remap necessary
|
||||
if (sourceAndChannel[channel] == notMPE)
|
||||
{
|
||||
lastUsed[channel] = counter;
|
||||
sourceAndChannel[channel] = sourceAndChannelID;
|
||||
return;
|
||||
}
|
||||
|
||||
// remap source & channel to new channel
|
||||
auto chan = getBestChanToReuse();
|
||||
|
||||
sourceAndChannel[chan] = sourceAndChannelID;
|
||||
lastUsed[chan] = counter;
|
||||
message.setChannel (chan);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::reset() noexcept
|
||||
{
|
||||
for (auto& s : sourceAndChannel)
|
||||
s = notMPE;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::clearChannel (int channel) noexcept
|
||||
{
|
||||
sourceAndChannel[channel] = notMPE;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::clearSource (uint32 mpeSourceID)
|
||||
{
|
||||
for (auto& s : sourceAndChannel)
|
||||
{
|
||||
if (uint32 (s >> 5) == mpeSourceID)
|
||||
{
|
||||
s = notMPE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MPEChannelRemapper::applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept
|
||||
{
|
||||
if (sourceAndChannel[channel] == sourceAndChannelID)
|
||||
{
|
||||
if (m.isNoteOff())
|
||||
sourceAndChannel[channel] = notMPE;
|
||||
else
|
||||
lastUsed[channel] = counter;
|
||||
|
||||
m.setChannel (channel);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int MPEChannelRemapper::getBestChanToReuse() const noexcept
|
||||
{
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
if (sourceAndChannel[chan] == notMPE)
|
||||
return chan;
|
||||
|
||||
auto bestChan = firstChannel;
|
||||
auto bestLastUse = counter;
|
||||
|
||||
for (int chan = firstChannel; (zone.isLowerZone() ? chan <= lastChannel : chan >= lastChannel); chan += channelIncrement)
|
||||
{
|
||||
if (lastUsed[chan] < bestLastUse)
|
||||
{
|
||||
bestLastUse = lastUsed[chan];
|
||||
bestChan = chan;
|
||||
}
|
||||
}
|
||||
|
||||
return bestChan;
|
||||
}
|
||||
|
||||
void MPEChannelRemapper::zeroArrays()
|
||||
{
|
||||
for (int i = 0; i < 17; ++i)
|
||||
{
|
||||
sourceAndChannel[i] = 0;
|
||||
lastUsed[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
struct MPEUtilsUnitTests : public UnitTest
|
||||
{
|
||||
MPEUtilsUnitTests()
|
||||
: UnitTest ("MPE Utilities", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("MPEChannelAssigner");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
// lower
|
||||
{
|
||||
layout.setLowerZone (15);
|
||||
|
||||
// lower zone
|
||||
MPEChannelAssigner channelAssigner (layout.getLowerZone());
|
||||
|
||||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
for (int ch = 2; ch <= 16; ++ch)
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
|
||||
|
||||
// check that note-offs are processed
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
||||
// last note played
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2);
|
||||
|
||||
// normal assignment
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4);
|
||||
}
|
||||
|
||||
// upper
|
||||
{
|
||||
layout.setUpperZone (15);
|
||||
|
||||
// upper zone
|
||||
MPEChannelAssigner channelAssigner (layout.getUpperZone());
|
||||
|
||||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
for (int ch = 15; ch >= 1; --ch)
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
|
||||
|
||||
// check that note-offs are processed
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
||||
// last note played
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15);
|
||||
|
||||
// normal assignment
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13);
|
||||
}
|
||||
|
||||
// legacy
|
||||
{
|
||||
MPEChannelAssigner channelAssigner;
|
||||
|
||||
// check that channels are assigned in correct order
|
||||
int noteNum = 60;
|
||||
for (int ch = 1; ch <= 16; ++ch)
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum++), ch);
|
||||
|
||||
// check that note-offs are processed
|
||||
channelAssigner.noteOff (60);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1);
|
||||
|
||||
channelAssigner.noteOff (61);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2);
|
||||
|
||||
// check that assigned channel was last to play note
|
||||
channelAssigner.noteOff (65);
|
||||
channelAssigner.noteOff (66);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
|
||||
|
||||
// find closest channel playing nonequal note
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
|
||||
|
||||
// all notes off
|
||||
channelAssigner.allNotesOff();
|
||||
|
||||
// last note played
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1);
|
||||
|
||||
// normal assignment
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2);
|
||||
expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3);
|
||||
}
|
||||
}
|
||||
|
||||
beginTest ("MPEChannelRemapper");
|
||||
{
|
||||
// 3 different MPE 'sources', constant IDs
|
||||
const int sourceID1 = 0;
|
||||
const int sourceID2 = 1;
|
||||
const int sourceID3 = 2;
|
||||
|
||||
MPEZoneLayout layout;
|
||||
|
||||
{
|
||||
layout.setLowerZone (15);
|
||||
|
||||
// lower zone
|
||||
MPEChannelRemapper channelRemapper (layout.getLowerZone());
|
||||
|
||||
// first source, shouldn't remap
|
||||
for (int ch = 2; ch <= 16; ++ch)
|
||||
{
|
||||
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
|
||||
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
|
||||
expectEquals (noteOn.getChannel(), ch);
|
||||
}
|
||||
|
||||
auto noteOn = MidiMessage::noteOn (2, 60, 1.0f);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
|
||||
expectEquals (noteOn.getChannel(), 2);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
|
||||
expectEquals (noteOn.getChannel(), 3);
|
||||
|
||||
// remap to correct channel for source ID
|
||||
auto noteOff = MidiMessage::noteOff (2, 60, 1.0f);
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
|
||||
expectEquals (noteOff.getChannel(), 3);
|
||||
}
|
||||
|
||||
{
|
||||
layout.setUpperZone (15);
|
||||
|
||||
// upper zone
|
||||
MPEChannelRemapper channelRemapper (layout.getUpperZone());
|
||||
|
||||
// first source, shouldn't remap
|
||||
for (int ch = 15; ch >= 1; --ch)
|
||||
{
|
||||
auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f);
|
||||
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1);
|
||||
expectEquals (noteOn.getChannel(), ch);
|
||||
}
|
||||
|
||||
auto noteOn = MidiMessage::noteOn (15, 60, 1.0f);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2);
|
||||
expectEquals (noteOn.getChannel(), 15);
|
||||
|
||||
// remap onto oldest last-used channel
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3);
|
||||
expectEquals (noteOn.getChannel(), 14);
|
||||
|
||||
// remap to correct channel for source ID
|
||||
auto noteOff = MidiMessage::noteOff (15, 60, 1.0f);
|
||||
channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3);
|
||||
expectEquals (noteOff.getChannel(), 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static MPEUtilsUnitTests MPEUtilsUnitTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
153
deps/juce/modules/juce_audio_basics/mpe/juce_MPEUtils.h
vendored
Normal file
153
deps/juce/modules/juce_audio_basics/mpe/juce_MPEUtils.h
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 handles the assignment of new MIDI notes to member channels of an active
|
||||
MPE zone.
|
||||
|
||||
To use it, create an instance passing in the MPE zone that it should operate on
|
||||
and then call use the findMidiChannelForNewNote() method for all note-on messages
|
||||
and the noteOff() method for all note-off messages.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class MPEChannelAssigner
|
||||
{
|
||||
public:
|
||||
/** Constructor.
|
||||
|
||||
This will assign channels within the range of the specified MPE zone.
|
||||
*/
|
||||
MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse);
|
||||
|
||||
/** Legacy mode constructor.
|
||||
|
||||
This will assign channels within the specified range.
|
||||
*/
|
||||
MPEChannelAssigner (Range<int> channelRange = Range<int> (1, 17));
|
||||
|
||||
/** This method will use a set of rules recommended in the MPE specification to
|
||||
determine which member channel the specified MIDI note should be assigned to
|
||||
and will return this channel number.
|
||||
|
||||
The rules have the following precedence:
|
||||
- find a free channel on which the last note played was the same as the one specified
|
||||
- find the next free channel in round-robin assignment
|
||||
- find the channel number that is currently playing the closest note (but not the same)
|
||||
|
||||
@param noteNumber the MIDI note number to be assigned to a channel
|
||||
@returns the zone's member channel that this note should be assigned to
|
||||
*/
|
||||
int findMidiChannelForNewNote (int noteNumber) noexcept;
|
||||
|
||||
/** You must call this method for all note-offs that you receive so that this class
|
||||
can keep track of the currently playing notes internally.
|
||||
|
||||
You can specify the channel number the note off happened on. If you don't, it will
|
||||
look through all channels to find the registered midi note matching the given note number.
|
||||
*/
|
||||
void noteOff (int noteNumber, int midiChannel = -1);
|
||||
|
||||
/** Call this to clear all currently playing notes. */
|
||||
void allNotesOff();
|
||||
|
||||
private:
|
||||
bool isLegacy = false;
|
||||
std::unique_ptr<MPEZoneLayout::Zone> zone;
|
||||
int channelIncrement, numChannels, firstChannel, lastChannel, midiChannelLastAssigned;
|
||||
|
||||
//==============================================================================
|
||||
struct MidiChannel
|
||||
{
|
||||
Array<int> notes;
|
||||
int lastNotePlayed = -1;
|
||||
bool isFree() const noexcept { return notes.isEmpty(); }
|
||||
};
|
||||
MidiChannel midiChannels[17];
|
||||
|
||||
//==============================================================================
|
||||
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This class handles the logic for remapping MIDI note messages from multiple MPE
|
||||
sources onto a specified MPE zone.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class MPEChannelRemapper
|
||||
{
|
||||
public:
|
||||
/** Used to indicate that a particular source & channel combination is not currently using MPE. */
|
||||
static const uint32 notMPE = 0;
|
||||
|
||||
/** Constructor */
|
||||
MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap);
|
||||
|
||||
//==============================================================================
|
||||
/** Remaps the MIDI channel of the specified MIDI message (if necessary).
|
||||
|
||||
Note that the MidiMessage object passed in will have it's channel changed if it
|
||||
needs to be remapped.
|
||||
|
||||
@param message the message to be remapped
|
||||
@param mpeSourceID the ID of the MPE source of the message. This is up to the
|
||||
user to define and keep constant
|
||||
*/
|
||||
void remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept;
|
||||
|
||||
//==============================================================================
|
||||
/** Resets all the source & channel combinations. */
|
||||
void reset() noexcept;
|
||||
|
||||
/** Clears a specified channel of this MPE zone. */
|
||||
void clearChannel (int channel) noexcept;
|
||||
|
||||
/** Clears all channels in use by a specified source. */
|
||||
void clearSource (uint32 mpeSourceID);
|
||||
|
||||
private:
|
||||
MPEZoneLayout::Zone zone;
|
||||
|
||||
int channelIncrement;
|
||||
int firstChannel, lastChannel;
|
||||
|
||||
uint32 sourceAndChannel[17];
|
||||
uint32 lastUsed[17];
|
||||
uint32 counter = 0;
|
||||
|
||||
//==============================================================================
|
||||
bool applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept;
|
||||
int getBestChanToReuse() const noexcept;
|
||||
|
||||
void zeroArrays();
|
||||
|
||||
//==============================================================================
|
||||
bool messageIsNoteData (const MidiMessage& m) { return (*m.getRawData() & 0xf0) != 0xf0; }
|
||||
};
|
||||
|
||||
} // namespace juce
|
173
deps/juce/modules/juce_audio_basics/mpe/juce_MPEValue.cpp
vendored
Normal file
173
deps/juce/modules/juce_audio_basics/mpe/juce_MPEValue.cpp
vendored
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPEValue::MPEValue() noexcept {}
|
||||
MPEValue::MPEValue (int value) : normalisedValue (value) {}
|
||||
|
||||
//==============================================================================
|
||||
MPEValue MPEValue::from7BitInt (int value) noexcept
|
||||
{
|
||||
jassert (value >= 0 && value <= 127);
|
||||
|
||||
auto valueAs14Bit = value <= 64 ? value << 7
|
||||
: int (jmap<float> (float (value - 64), 0.0f, 63.0f, 0.0f, 8191.0f)) + 8192;
|
||||
|
||||
return { valueAs14Bit };
|
||||
}
|
||||
|
||||
MPEValue MPEValue::from14BitInt (int value) noexcept
|
||||
{
|
||||
jassert (value >= 0 && value <= 16383);
|
||||
return { value };
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); }
|
||||
MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); }
|
||||
MPEValue MPEValue::maxValue() noexcept { return MPEValue::from7BitInt (127); }
|
||||
|
||||
int MPEValue::as7BitInt() const noexcept
|
||||
{
|
||||
return normalisedValue >> 7;
|
||||
}
|
||||
|
||||
int MPEValue::as14BitInt() const noexcept
|
||||
{
|
||||
return normalisedValue;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
float MPEValue::asSignedFloat() const noexcept
|
||||
{
|
||||
return (normalisedValue < 8192)
|
||||
? jmap<float> (float (normalisedValue), 0.0f, 8192.0f, -1.0f, 0.0f)
|
||||
: jmap<float> (float (normalisedValue), 8192.0f, 16383.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float MPEValue::asUnsignedFloat() const noexcept
|
||||
{
|
||||
return jmap<float> (float (normalisedValue), 0.0f, 16383.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool MPEValue::operator== (const MPEValue& other) const noexcept
|
||||
{
|
||||
return normalisedValue == other.normalisedValue;
|
||||
}
|
||||
|
||||
bool MPEValue::operator!= (const MPEValue& other) const noexcept
|
||||
{
|
||||
return ! operator== (other);
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class MPEValueTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
MPEValueTests()
|
||||
: UnitTest ("MPEValue class", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("comparison operator");
|
||||
{
|
||||
MPEValue value1 = MPEValue::from7BitInt (7);
|
||||
MPEValue value2 = MPEValue::from7BitInt (7);
|
||||
MPEValue value3 = MPEValue::from7BitInt (8);
|
||||
|
||||
expect (value1 == value1);
|
||||
expect (value1 == value2);
|
||||
expect (value1 != value3);
|
||||
}
|
||||
|
||||
beginTest ("special values");
|
||||
{
|
||||
expectEquals (MPEValue::minValue().as7BitInt(), 0);
|
||||
expectEquals (MPEValue::minValue().as14BitInt(), 0);
|
||||
|
||||
expectEquals (MPEValue::centreValue().as7BitInt(), 64);
|
||||
expectEquals (MPEValue::centreValue().as14BitInt(), 8192);
|
||||
|
||||
expectEquals (MPEValue::maxValue().as7BitInt(), 127);
|
||||
expectEquals (MPEValue::maxValue().as14BitInt(), 16383);
|
||||
}
|
||||
|
||||
beginTest ("zero/minimum value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
|
||||
}
|
||||
|
||||
beginTest ("maximum value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
beginTest ("centre value");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
|
||||
}
|
||||
|
||||
beginTest ("value halfway between min and centre");
|
||||
{
|
||||
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
|
||||
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void expectValuesConsistent (MPEValue value,
|
||||
int expectedValueAs7BitInt,
|
||||
int expectedValueAs14BitInt,
|
||||
float expectedValueAsSignedFloat,
|
||||
float expectedValueAsUnsignedFloat)
|
||||
{
|
||||
expectEquals (value.as7BitInt(), expectedValueAs7BitInt);
|
||||
expectEquals (value.as14BitInt(), expectedValueAs14BitInt);
|
||||
expectFloatWithinRelativeError (value.asSignedFloat(), expectedValueAsSignedFloat, 0.0001f);
|
||||
expectFloatWithinRelativeError (value.asUnsignedFloat(), expectedValueAsUnsignedFloat, 0.0001f);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void expectFloatWithinRelativeError (float actualValue, float expectedValue, float maxRelativeError)
|
||||
{
|
||||
const float maxAbsoluteError = jmax (1.0f, std::abs (expectedValue)) * maxRelativeError;
|
||||
expect (std::abs (expectedValue - actualValue) < maxAbsoluteError);
|
||||
}
|
||||
};
|
||||
|
||||
static MPEValueTests MPEValueUnitTests;
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
97
deps/juce/modules/juce_audio_basics/mpe/juce_MPEValue.h
vendored
Normal file
97
deps/juce/modules/juce_audio_basics/mpe/juce_MPEValue.h
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 a single value for any of the MPE
|
||||
dimensions of control. It supports values with 7-bit or 14-bit resolutions
|
||||
(corresponding to 1 or 2 MIDI bytes, respectively). It also offers helper
|
||||
functions to query the value in a variety of representations that can be
|
||||
useful in an audio or MIDI context.
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MPEValue
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Default constructor.
|
||||
|
||||
Constructs an MPEValue corresponding to the centre value.
|
||||
*/
|
||||
MPEValue() noexcept;
|
||||
|
||||
/** Constructs an MPEValue from an integer between 0 and 127
|
||||
(using 7-bit precision).
|
||||
*/
|
||||
static MPEValue from7BitInt (int value) noexcept;
|
||||
|
||||
/** Constructs an MPEValue from an integer between 0 and 16383
|
||||
(using 14-bit precision).
|
||||
*/
|
||||
static MPEValue from14BitInt (int value) noexcept;
|
||||
|
||||
/** Constructs an MPEValue corresponding to the centre value. */
|
||||
static MPEValue centreValue() noexcept;
|
||||
|
||||
/** Constructs an MPEValue corresponding to the minimum value. */
|
||||
static MPEValue minValue() noexcept;
|
||||
|
||||
/** Constructs an MPEValue corresponding to the maximum value. */
|
||||
static MPEValue maxValue() noexcept;
|
||||
|
||||
/** Retrieves the current value as an integer between 0 and 127.
|
||||
|
||||
Information will be lost if the value was initialised with a precision
|
||||
higher than 7-bit.
|
||||
*/
|
||||
int as7BitInt() const noexcept;
|
||||
|
||||
/** Retrieves the current value as an integer between 0 and 16383.
|
||||
|
||||
Resolution will be lost if the value was initialised with a precision
|
||||
higher than 14-bit.
|
||||
*/
|
||||
int as14BitInt() const noexcept;
|
||||
|
||||
/** Retrieves the current value mapped to a float between -1.0f and 1.0f. */
|
||||
float asSignedFloat() const noexcept;
|
||||
|
||||
/** Retrieves the current value mapped to a float between 0.0f and 1.0f. */
|
||||
float asUnsignedFloat() const noexcept;
|
||||
|
||||
/** Returns true if two values are equal. */
|
||||
bool operator== (const MPEValue& other) const noexcept;
|
||||
|
||||
/** Returns true if two values are not equal. */
|
||||
bool operator!= (const MPEValue& other) const noexcept;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
MPEValue (int normalisedValue);
|
||||
int normalisedValue = 8192;
|
||||
};
|
||||
|
||||
} // namespace juce
|
386
deps/juce/modules/juce_audio_basics/mpe/juce_MPEZoneLayout.cpp
vendored
Normal file
386
deps/juce/modules/juce_audio_basics/mpe/juce_MPEZoneLayout.cpp
vendored
Normal file
@ -0,0 +1,386 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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
|
||||
{
|
||||
|
||||
MPEZoneLayout::MPEZoneLayout() noexcept {}
|
||||
|
||||
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
|
||||
: lowerZone (other.lowerZone),
|
||||
upperZone (other.upperZone)
|
||||
{
|
||||
}
|
||||
|
||||
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other)
|
||||
{
|
||||
lowerZone = other.lowerZone;
|
||||
upperZone = other.upperZone;
|
||||
|
||||
sendLayoutChangeMessage();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void MPEZoneLayout::sendLayoutChangeMessage()
|
||||
{
|
||||
listeners.call ([this] (Listener& l) { l.zoneLayoutChanged (*this); });
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 15, numMemberChannels);
|
||||
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
|
||||
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
|
||||
|
||||
if (isLower)
|
||||
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
else
|
||||
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
|
||||
|
||||
if (numMemberChannels > 0)
|
||||
{
|
||||
auto totalChannels = lowerZone.numMemberChannels + upperZone.numMemberChannels;
|
||||
|
||||
if (totalChannels >= 15)
|
||||
{
|
||||
if (isLower)
|
||||
upperZone.numMemberChannels = 14 - numMemberChannels;
|
||||
else
|
||||
lowerZone.numMemberChannels = 14 - numMemberChannels;
|
||||
}
|
||||
}
|
||||
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
|
||||
void MPEZoneLayout::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
setZone (true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
|
||||
}
|
||||
|
||||
void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
|
||||
{
|
||||
setZone (false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
|
||||
}
|
||||
|
||||
void MPEZoneLayout::clearAllZones()
|
||||
{
|
||||
lowerZone = { true, 0 };
|
||||
upperZone = { false, 0 };
|
||||
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::processNextMidiEvent (const MidiMessage& message)
|
||||
{
|
||||
if (! message.isController())
|
||||
return;
|
||||
|
||||
MidiRPNMessage rpn;
|
||||
|
||||
if (rpnDetector.parseControllerMessage (message.getChannel(),
|
||||
message.getControllerNumber(),
|
||||
message.getControllerValue(),
|
||||
rpn))
|
||||
{
|
||||
processRpnMessage (rpn);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn)
|
||||
{
|
||||
if (rpn.parameterNumber == MPEMessages::zoneLayoutMessagesRpnNumber)
|
||||
processZoneLayoutRpnMessage (rpn);
|
||||
else if (rpn.parameterNumber == 0)
|
||||
processPitchbendRangeRpnMessage (rpn);
|
||||
}
|
||||
|
||||
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
|
||||
{
|
||||
if (rpn.value < 16)
|
||||
{
|
||||
if (rpn.channel == 1)
|
||||
setLowerZone (rpn.value);
|
||||
else if (rpn.channel == 16)
|
||||
setUpperZone (rpn.value);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
|
||||
{
|
||||
if (zone.masterPitchbendRange != value)
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange);
|
||||
zone.masterPitchbendRange = value;
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value)
|
||||
{
|
||||
if (zone.perNotePitchbendRange != value)
|
||||
{
|
||||
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange);
|
||||
zone.perNotePitchbendRange = value;
|
||||
sendLayoutChangeMessage();
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn)
|
||||
{
|
||||
if (rpn.channel == 1)
|
||||
{
|
||||
updateMasterPitchbend (lowerZone, rpn.value);
|
||||
}
|
||||
else if (rpn.channel == 16)
|
||||
{
|
||||
updateMasterPitchbend (upperZone, rpn.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lowerZone.isUsingChannelAsMemberChannel (rpn.channel))
|
||||
updatePerNotePitchbendRange (lowerZone, rpn.value);
|
||||
else if (upperZone.isUsingChannelAsMemberChannel (rpn.channel))
|
||||
updatePerNotePitchbendRange (upperZone, rpn.value);
|
||||
}
|
||||
}
|
||||
|
||||
void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
|
||||
{
|
||||
for (const auto metadata : buffer)
|
||||
processNextMidiEvent (metadata.getMessage());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept
|
||||
{
|
||||
listeners.add (listenerToAdd);
|
||||
}
|
||||
|
||||
void MPEZoneLayout::removeListener (Listener* const listenerToRemove) noexcept
|
||||
{
|
||||
listeners.remove (listenerToRemove);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void MPEZoneLayout::checkAndLimitZoneParameters (int minValue, int maxValue,
|
||||
int& valueToCheckAndLimit) noexcept
|
||||
{
|
||||
if (valueToCheckAndLimit < minValue || valueToCheckAndLimit > maxValue)
|
||||
{
|
||||
// if you hit this, one of the parameters you supplied for this zone
|
||||
// was not within the allowed range!
|
||||
// we fit this back into the allowed range here to maintain a valid
|
||||
// state for the zone, but probably the resulting zone is not what you
|
||||
// wanted it to be!
|
||||
jassertfalse;
|
||||
|
||||
valueToCheckAndLimit = jlimit (minValue, maxValue, valueToCheckAndLimit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
#if JUCE_UNIT_TESTS
|
||||
|
||||
class MPEZoneLayoutTests : public UnitTest
|
||||
{
|
||||
public:
|
||||
MPEZoneLayoutTests()
|
||||
: UnitTest ("MPEZoneLayout class", UnitTestCategories::midi)
|
||||
{}
|
||||
|
||||
void runTest() override
|
||||
{
|
||||
beginTest ("initialisation");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("adding zones");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
layout.setLowerZone (7);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
|
||||
layout.setUpperZone (7);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 7);
|
||||
|
||||
layout.setLowerZone (3);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 3);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 7);
|
||||
|
||||
layout.setUpperZone (3);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 3);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 3);
|
||||
|
||||
layout.setLowerZone (15);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 15);
|
||||
}
|
||||
|
||||
beginTest ("clear all zones");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
|
||||
layout.setLowerZone (7);
|
||||
layout.setUpperZone (2);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
|
||||
layout.clearAllZones();
|
||||
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("process MIDI buffers");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
MidiBuffer buffer;
|
||||
|
||||
buffer = MPEMessages::setLowerZone (7);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
|
||||
buffer = MPEMessages::setUpperZone (7);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 7);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 7);
|
||||
|
||||
{
|
||||
buffer = MPEMessages::setLowerZone (10);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 10);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 4);
|
||||
|
||||
|
||||
buffer = MPEMessages::setLowerZone (10, 33, 44);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 10);
|
||||
expectEquals (layout.getLowerZone().perNotePitchbendRange, 33);
|
||||
expectEquals (layout.getLowerZone().masterPitchbendRange, 44);
|
||||
}
|
||||
|
||||
{
|
||||
buffer = MPEMessages::setUpperZone (10);
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 4);
|
||||
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 10);
|
||||
|
||||
buffer = MPEMessages::setUpperZone (10, 33, 44);
|
||||
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expectEquals (layout.getUpperZone().numMemberChannels, 10);
|
||||
expectEquals (layout.getUpperZone().perNotePitchbendRange, 33);
|
||||
expectEquals (layout.getUpperZone().masterPitchbendRange, 44);
|
||||
}
|
||||
|
||||
buffer = MPEMessages::clearAllZones();
|
||||
layout.processNextMidiBuffer (buffer);
|
||||
|
||||
expect (! layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
}
|
||||
|
||||
beginTest ("process individual MIDI messages");
|
||||
{
|
||||
MPEZoneLayout layout;
|
||||
|
||||
layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg
|
||||
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1
|
||||
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2
|
||||
layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg
|
||||
layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3
|
||||
layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg
|
||||
|
||||
expect (layout.getLowerZone().isActive());
|
||||
expect (! layout.getUpperZone().isActive());
|
||||
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
|
||||
expectEquals (layout.getLowerZone().numMemberChannels, 3);
|
||||
expectEquals (layout.getLowerZone().perNotePitchbendRange, 48);
|
||||
expectEquals (layout.getLowerZone().masterPitchbendRange, 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static MPEZoneLayoutTests MPEZoneLayoutUnitTests;
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
225
deps/juce/modules/juce_audio_basics/mpe/juce_MPEZoneLayout.h
vendored
Normal file
225
deps/juce/modules/juce_audio_basics/mpe/juce_MPEZoneLayout.h
vendored
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - 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 the current MPE zone layout of a device capable of handling MPE.
|
||||
|
||||
An MPE device can have up to two zones: a lower zone with master channel 1 and
|
||||
allocated MIDI channels increasing from channel 2, and an upper zone with master
|
||||
channel 16 and allocated MIDI channels decreasing from channel 15. MPE mode is
|
||||
enabled on a device when one of these zones is active and disabled when both
|
||||
are inactive.
|
||||
|
||||
Use the MPEMessages helper class to convert the zone layout represented
|
||||
by this object to MIDI message sequences that you can send to an Expressive
|
||||
MIDI device to set its zone layout, add zones etc.
|
||||
|
||||
@see MPEInstrument
|
||||
|
||||
@tags{Audio}
|
||||
*/
|
||||
class JUCE_API MPEZoneLayout
|
||||
{
|
||||
public:
|
||||
/** Default constructor.
|
||||
|
||||
This will create a layout with inactive lower and upper zones, representing
|
||||
a device with MPE mode disabled.
|
||||
|
||||
You can set the lower or upper MPE zones using the setZone() method.
|
||||
|
||||
@see setZone
|
||||
*/
|
||||
MPEZoneLayout() noexcept;
|
||||
|
||||
/** Copy constuctor.
|
||||
This will not copy the listeners registered to the MPEZoneLayout.
|
||||
*/
|
||||
MPEZoneLayout (const MPEZoneLayout& other);
|
||||
|
||||
/** Copy assignment operator.
|
||||
This will not copy the listeners registered to the MPEZoneLayout.
|
||||
*/
|
||||
MPEZoneLayout& operator= (const MPEZoneLayout& other);
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
This struct represents an MPE zone.
|
||||
|
||||
It can either be a lower or an upper zone, where:
|
||||
- A lower zone encompasses master channel 1 and an arbitrary number of ascending
|
||||
MIDI channels, increasing from channel 2.
|
||||
- An upper zone encompasses master channel 16 and an arbitrary number of descending
|
||||
MIDI channels, decreasing from channel 15.
|
||||
|
||||
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
|
||||
master pitchbends, respectively.
|
||||
*/
|
||||
struct Zone
|
||||
{
|
||||
Zone (const Zone& other) = default;
|
||||
|
||||
bool isLowerZone() const noexcept { return lowerZone; }
|
||||
bool isUpperZone() const noexcept { return ! lowerZone; }
|
||||
|
||||
bool isActive() const noexcept { return numMemberChannels > 0; }
|
||||
|
||||
int getMasterChannel() const noexcept { return lowerZone ? 1 : 16; }
|
||||
int getFirstMemberChannel() const noexcept { return lowerZone ? 2 : 15; }
|
||||
int getLastMemberChannel() const noexcept { return lowerZone ? (1 + numMemberChannels)
|
||||
: (16 - numMemberChannels); }
|
||||
|
||||
bool isUsingChannelAsMemberChannel (int channel) const noexcept
|
||||
{
|
||||
return lowerZone ? (channel > 1 && channel <= 1 + numMemberChannels)
|
||||
: (channel < 16 && channel >= 16 - numMemberChannels);
|
||||
}
|
||||
|
||||
bool isUsing (int channel) const noexcept
|
||||
{
|
||||
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
|
||||
}
|
||||
|
||||
bool operator== (const Zone& other) const noexcept { return lowerZone == other.lowerZone
|
||||
&& numMemberChannels == other.numMemberChannels
|
||||
&& perNotePitchbendRange == other.perNotePitchbendRange
|
||||
&& masterPitchbendRange == other.masterPitchbendRange; }
|
||||
|
||||
bool operator!= (const Zone& other) const noexcept { return ! operator== (other); }
|
||||
|
||||
int numMemberChannels;
|
||||
int perNotePitchbendRange;
|
||||
int masterPitchbendRange;
|
||||
|
||||
private:
|
||||
friend class MPEZoneLayout;
|
||||
|
||||
Zone (bool lower, int memberChans = 0, int perNotePb = 48, int masterPb = 2) noexcept
|
||||
: numMemberChannels (memberChans),
|
||||
perNotePitchbendRange (perNotePb),
|
||||
masterPitchbendRange (masterPb),
|
||||
lowerZone (lower)
|
||||
{
|
||||
}
|
||||
|
||||
bool lowerZone;
|
||||
};
|
||||
|
||||
/** Sets the lower zone of this layout. */
|
||||
void setLowerZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2) noexcept;
|
||||
|
||||
/** Sets the upper zone of this layout. */
|
||||
void setUpperZone (int numMemberChannels = 0,
|
||||
int perNotePitchbendRange = 48,
|
||||
int masterPitchbendRange = 2) noexcept;
|
||||
|
||||
/** Returns a struct representing the lower MPE zone. */
|
||||
const Zone getLowerZone() const noexcept { return lowerZone; }
|
||||
|
||||
/** Returns a struct representing the upper MPE zone. */
|
||||
const Zone getUpperZone() const noexcept { return upperZone; }
|
||||
|
||||
/** Clears the lower and upper zones of this layout, making them both inactive
|
||||
and disabling MPE mode.
|
||||
*/
|
||||
void clearAllZones();
|
||||
|
||||
//==============================================================================
|
||||
/** Pass incoming MIDI messages to an object of this class if you want the
|
||||
zone layout to properly react to MPE RPN messages like an
|
||||
MPE device.
|
||||
|
||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
|
||||
set the per-note or master pitchbend ranges.
|
||||
|
||||
Any other MIDI messages will be ignored by this class.
|
||||
|
||||
@see MPEMessages
|
||||
*/
|
||||
void processNextMidiEvent (const MidiMessage& message);
|
||||
|
||||
/** Pass incoming MIDI buffers to an object of this class if you want the
|
||||
zone layout to properly react to MPE RPN messages like an
|
||||
MPE device.
|
||||
|
||||
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
|
||||
set the per-note or master pitchbend ranges.
|
||||
|
||||
Any other MIDI messages will be ignored by this class.
|
||||
|
||||
@see MPEMessages
|
||||
*/
|
||||
void processNextMidiBuffer (const MidiBuffer& buffer);
|
||||
|
||||
//==============================================================================
|
||||
/** Listener class. Derive from this class to allow your class to be
|
||||
notified about changes to the zone layout.
|
||||
*/
|
||||
class Listener
|
||||
{
|
||||
public:
|
||||
/** Destructor. */
|
||||
virtual ~Listener() = default;
|
||||
|
||||
/** Implement this callback to be notified about any changes to this
|
||||
MPEZoneLayout. Will be called whenever a zone is added, zones are
|
||||
removed, or any zone's master or note pitchbend ranges change.
|
||||
*/
|
||||
virtual void zoneLayoutChanged (const MPEZoneLayout& layout) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a listener. */
|
||||
void addListener (Listener* const listenerToAdd) noexcept;
|
||||
|
||||
/** Removes a listener. */
|
||||
void removeListener (Listener* const listenerToRemove) noexcept;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Zone lowerZone { true, 0 };
|
||||
Zone upperZone { false, 0 };
|
||||
|
||||
MidiRPNDetector rpnDetector;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
//==============================================================================
|
||||
void setZone (bool, int, int, int) noexcept;
|
||||
|
||||
void processRpnMessage (MidiRPNMessage);
|
||||
void processZoneLayoutRpnMessage (MidiRPNMessage);
|
||||
void processPitchbendRangeRpnMessage (MidiRPNMessage);
|
||||
|
||||
void updateMasterPitchbend (Zone&, int);
|
||||
void updatePerNotePitchbendRange (Zone&, int);
|
||||
|
||||
void sendLayoutChangeMessage();
|
||||
void checkAndLimitZoneParameters (int, int, int&) noexcept;
|
||||
};
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user