| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,346 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 | ||
| { | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| A view of MIDI message data stored in a contiguous buffer. | ||
| Instances of this class do *not* own the midi data bytes that they point to. | ||
| Instead, they expect the midi data to live in a separate buffer that outlives | ||
| the MidiMessageMetadata instance. | ||
| @tags{Audio} | ||
| */ | ||
| struct MidiMessageMetadata final | ||
| { | ||
| MidiMessageMetadata() noexcept = default; | ||
|
|
||
| MidiMessageMetadata (const uint8* dataIn, int numBytesIn, int positionIn) noexcept | ||
| : data (dataIn), numBytes (numBytesIn), samplePosition (positionIn) | ||
| { | ||
| } | ||
|
|
||
| /** Constructs a new MidiMessage instance from the data that this object is viewing. | ||
| Note that MidiMessage owns its data storage, whereas MidiMessageMetadata does not. | ||
| */ | ||
| MidiMessage getMessage() const { return MidiMessage (data, numBytes, samplePosition); } | ||
|
|
||
| /** Pointer to the first byte of a MIDI message. */ | ||
| const uint8* data = nullptr; | ||
|
|
||
| /** The number of bytes in the MIDI message. */ | ||
| int numBytes = 0; | ||
|
|
||
| /** The MIDI message's timestamp. */ | ||
| int samplePosition = 0; | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| An iterator to move over contiguous raw MIDI data, which Allows iterating | ||
| over a MidiBuffer using C++11 range-for syntax. | ||
| In the following example, we log all three-byte messages in a midi buffer. | ||
| @code | ||
| void processBlock (AudioBuffer<float>&, MidiBuffer& midiBuffer) override | ||
| { | ||
| for (const MidiMessageMetadata metadata : midiBuffer) | ||
| if (metadata.numBytes == 3) | ||
| Logger::writeToLog (metadata.getMessage().getDescription()); | ||
| } | ||
| @endcode | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiBufferIterator | ||
| { | ||
| using Ptr = const uint8*; | ||
|
|
||
| public: | ||
| MidiBufferIterator() = default; | ||
|
|
||
| /** Constructs an iterator pointing at the message starting at the byte `dataIn`. | ||
| `dataIn` must point to the start of a valid MIDI message. If it does not, | ||
| calling other member functions on the iterator will result in undefined | ||
| behaviour. | ||
| */ | ||
| explicit MidiBufferIterator (const uint8* dataIn) noexcept | ||
| : data (dataIn) | ||
| { | ||
| } | ||
|
|
||
| using difference_type = std::iterator_traits<Ptr>::difference_type; | ||
| using value_type = MidiMessageMetadata; | ||
| using reference = MidiMessageMetadata; | ||
| using pointer = void; | ||
| using iterator_category = std::input_iterator_tag; | ||
|
|
||
| /** Make this iterator point to the next message in the buffer. */ | ||
| MidiBufferIterator& operator++() noexcept; | ||
|
|
||
| /** Create a copy of this object, make this iterator point to the next message in | ||
| the buffer, then return the copy. | ||
| */ | ||
| MidiBufferIterator operator++ (int) noexcept; | ||
|
|
||
| /** Return true if this iterator points to the same message as another | ||
| iterator instance, otherwise return false. | ||
| */ | ||
| bool operator== (const MidiBufferIterator& other) const noexcept { return data == other.data; } | ||
|
|
||
| /** Return false if this iterator points to the same message as another | ||
| iterator instance, otherwise returns true. | ||
| */ | ||
| bool operator!= (const MidiBufferIterator& other) const noexcept { return ! operator== (other); } | ||
|
|
||
| /** Return an instance of MidiMessageMetadata which describes the message to which | ||
| the iterator is currently pointing. | ||
| */ | ||
| reference operator*() const noexcept; | ||
|
|
||
| private: | ||
| Ptr data = nullptr; | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| Holds a sequence of time-stamped midi events. | ||
| Analogous to the AudioBuffer, this holds a set of midi events with | ||
| integer time-stamps. The buffer is kept sorted in order of the time-stamps. | ||
| If you're working with a sequence of midi events that may need to be manipulated | ||
| or read/written to a midi file, then MidiMessageSequence is probably a more | ||
| appropriate container. MidiBuffer is designed for lower-level streams of raw | ||
| midi data. | ||
| @see MidiMessage | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiBuffer | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Creates an empty MidiBuffer. */ | ||
| MidiBuffer() noexcept = default; | ||
|
|
||
| /** Creates a MidiBuffer containing a single midi message. */ | ||
| explicit MidiBuffer (const MidiMessage& message) noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Removes all events from the buffer. */ | ||
| void clear() noexcept; | ||
|
|
||
| /** Removes all events between two times from the buffer. | ||
| All events for which (start <= event position < start + numSamples) will | ||
| be removed. | ||
| */ | ||
| void clear (int start, int numSamples); | ||
|
|
||
| /** Returns true if the buffer is empty. | ||
| To actually retrieve the events, use a MidiBufferIterator object | ||
| */ | ||
| bool isEmpty() const noexcept; | ||
|
|
||
| /** Counts the number of events in the buffer. | ||
| This is actually quite a slow operation, as it has to iterate through all | ||
| the events, so you might prefer to call isEmpty() if that's all you need | ||
| to know. | ||
| */ | ||
| int getNumEvents() const noexcept; | ||
|
|
||
| /** Adds an event to the buffer. | ||
| The sample number will be used to determine the position of the event in | ||
| the buffer, which is always kept sorted. The MidiMessage's timestamp is | ||
| ignored. | ||
| If an event is added whose sample position is the same as one or more events | ||
| already in the buffer, the new event will be placed after the existing ones. | ||
| To retrieve events, use a MidiBufferIterator object | ||
| */ | ||
| void addEvent (const MidiMessage& midiMessage, int sampleNumber); | ||
|
|
||
| /** Adds an event to the buffer from raw midi data. | ||
| The sample number will be used to determine the position of the event in | ||
| the buffer, which is always kept sorted. | ||
| If an event is added whose sample position is the same as one or more events | ||
| already in the buffer, the new event will be placed after the existing ones. | ||
| The event data will be inspected to calculate the number of bytes in length that | ||
| the midi event really takes up, so maxBytesOfMidiData may be longer than the data | ||
| that actually gets stored. E.g. if you pass in a note-on and a length of 4 bytes, | ||
| it'll actually only store 3 bytes. If the midi data is invalid, it might not | ||
| add an event at all. | ||
| To retrieve events, use a MidiBufferIterator object | ||
| */ | ||
| void addEvent (const void* rawMidiData, | ||
| int maxBytesOfMidiData, | ||
| int sampleNumber); | ||
|
|
||
| /** Adds some events from another buffer to this one. | ||
| @param otherBuffer the buffer containing the events you want to add | ||
| @param startSample the lowest sample number in the source buffer for which | ||
| events should be added. Any source events whose timestamp is | ||
| less than this will be ignored | ||
| @param numSamples the valid range of samples from the source buffer for which | ||
| events should be added - i.e. events in the source buffer whose | ||
| timestamp is greater than or equal to (startSample + numSamples) | ||
| will be ignored. If this value is less than 0, all events after | ||
| startSample will be taken. | ||
| @param sampleDeltaToAdd a value which will be added to the source timestamps of the events | ||
| that are added to this buffer | ||
| */ | ||
| void addEvents (const MidiBuffer& otherBuffer, | ||
| int startSample, | ||
| int numSamples, | ||
| int sampleDeltaToAdd); | ||
|
|
||
| /** Returns the sample number of the first event in the buffer. | ||
| If the buffer's empty, this will just return 0. | ||
| */ | ||
| int getFirstEventTime() const noexcept; | ||
|
|
||
| /** Returns the sample number of the last event in the buffer. | ||
| If the buffer's empty, this will just return 0. | ||
| */ | ||
| int getLastEventTime() const noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Exchanges the contents of this buffer with another one. | ||
| This is a quick operation, because no memory allocating or copying is done, it | ||
| just swaps the internal state of the two buffers. | ||
| */ | ||
| void swapWith (MidiBuffer&) noexcept; | ||
|
|
||
| /** Preallocates some memory for the buffer to use. | ||
| This helps to avoid needing to reallocate space when the buffer has messages | ||
| added to it. | ||
| */ | ||
| void ensureSize (size_t minimumNumBytes); | ||
|
|
||
| /** Get a read-only iterator pointing to the beginning of this buffer. */ | ||
| MidiBufferIterator begin() const noexcept { return cbegin(); } | ||
|
|
||
| /** Get a read-only iterator pointing one past the end of this buffer. */ | ||
| MidiBufferIterator end() const noexcept { return cend(); } | ||
|
|
||
| /** Get a read-only iterator pointing to the beginning of this buffer. */ | ||
| MidiBufferIterator cbegin() const noexcept { return MidiBufferIterator (data.begin()); } | ||
|
|
||
| /** Get a read-only iterator pointing one past the end of this buffer. */ | ||
| MidiBufferIterator cend() const noexcept { return MidiBufferIterator (data.end()); } | ||
|
|
||
| /** Get an iterator pointing to the first event with a timestamp greater-than or | ||
| equal-to `samplePosition`. | ||
| */ | ||
| MidiBufferIterator findNextSamplePosition (int samplePosition) const noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| Used to iterate through the events in a MidiBuffer. | ||
| Note that altering the buffer while an iterator is using it will produce | ||
| undefined behaviour. | ||
| @see MidiBuffer | ||
| */ | ||
| class JUCE_API Iterator | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Creates an Iterator for this MidiBuffer. | ||
| This class has been deprecated in favour of MidiBufferIterator. | ||
| */ | ||
| JUCE_DEPRECATED (Iterator (const MidiBuffer&) noexcept); | ||
|
|
||
| /** Creates a copy of an iterator. */ | ||
| Iterator (const Iterator&) = default; | ||
|
|
||
| /** Destructor. */ | ||
| ~Iterator() noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Repositions the iterator so that the next event retrieved will be the first | ||
| one whose sample position is at greater than or equal to the given position. | ||
| */ | ||
| void setNextSamplePosition (int samplePosition) noexcept; | ||
|
|
||
| /** Retrieves a copy of the next event from the buffer. | ||
| @param result on return, this will be the message. The MidiMessage's timestamp | ||
| is set to the same value as samplePosition. | ||
| @param samplePosition on return, this will be the position of the event, as a | ||
| sample index in the buffer | ||
| @returns true if an event was found, or false if the iterator has reached | ||
| the end of the buffer | ||
| */ | ||
| bool getNextEvent (MidiMessage& result, | ||
| int& samplePosition) noexcept; | ||
|
|
||
| /** Retrieves the next event from the buffer. | ||
| @param midiData on return, this pointer will be set to a block of data containing | ||
| the midi message. Note that to make it fast, this is a pointer | ||
| directly into the MidiBuffer's internal data, so is only valid | ||
| temporarily until the MidiBuffer is altered. | ||
| @param numBytesOfMidiData on return, this is the number of bytes of data used by the | ||
| midi message | ||
| @param samplePosition on return, this will be the position of the event, as a | ||
| sample index in the buffer | ||
| @returns true if an event was found, or false if the iterator has reached | ||
| the end of the buffer | ||
| */ | ||
| bool getNextEvent (const uint8* &midiData, | ||
| int& numBytesOfMidiData, | ||
| int& samplePosition) noexcept; | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| const MidiBuffer& buffer; | ||
| MidiBufferIterator iterator; | ||
| }; | ||
|
|
||
| /** The raw data holding this buffer. | ||
| Obviously access to this data is provided at your own risk. Its internal format could | ||
| change in future, so don't write code that relies on it! | ||
| */ | ||
| Array<uint8> data; | ||
|
|
||
| private: | ||
| JUCE_LEAK_DETECTOR (MidiBuffer) | ||
| }; | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 | ||
| { | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| Reads/writes standard midi format files. | ||
| To read a midi file, create a MidiFile object and call its readFrom() method. You | ||
| can then get the individual midi tracks from it using the getTrack() method. | ||
| To write a file, create a MidiFile object, add some MidiMessageSequence objects | ||
| to it using the addTrack() method, and then call its writeTo() method to stream | ||
| it out. | ||
| @see MidiMessageSequence | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiFile | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Creates an empty MidiFile object. */ | ||
| MidiFile(); | ||
|
|
||
| /** Destructor. */ | ||
| ~MidiFile(); | ||
|
|
||
| /** Creates a copy of another MidiFile. */ | ||
| MidiFile (const MidiFile&); | ||
|
|
||
| /** Copies from another MidiFile object */ | ||
| MidiFile& operator= (const MidiFile&); | ||
|
|
||
| /** Creates a copy of another MidiFile. */ | ||
| MidiFile (MidiFile&&); | ||
|
|
||
| /** Copies from another MidiFile object */ | ||
| MidiFile& operator= (MidiFile&&); | ||
|
|
||
| //============================================================================== | ||
| /** Returns the number of tracks in the file. | ||
| @see getTrack, addTrack | ||
| */ | ||
| int getNumTracks() const noexcept; | ||
|
|
||
| /** Returns a pointer to one of the tracks in the file. | ||
| @returns a pointer to the track, or nullptr if the index is out-of-range | ||
| @see getNumTracks, addTrack | ||
| */ | ||
| const MidiMessageSequence* getTrack (int index) const noexcept; | ||
|
|
||
| /** Adds a midi track to the file. | ||
| This will make its own internal copy of the sequence that is passed-in. | ||
| @see getNumTracks, getTrack | ||
| */ | ||
| void addTrack (const MidiMessageSequence& trackSequence); | ||
|
|
||
| /** Removes all midi tracks from the file. | ||
| @see getNumTracks | ||
| */ | ||
| void clear(); | ||
|
|
||
| /** Returns the raw time format code that will be written to a stream. | ||
| After reading a midi file, this method will return the time-format that | ||
| was read from the file's header. It can be changed using the setTicksPerQuarterNote() | ||
| or setSmpteTimeFormat() methods. | ||
| If the value returned is positive, it indicates the number of midi ticks | ||
| per quarter-note - see setTicksPerQuarterNote(). | ||
| It it's negative, the upper byte indicates the frames-per-second (but negative), and | ||
| the lower byte is the number of ticks per frame - see setSmpteTimeFormat(). | ||
| */ | ||
| short getTimeFormat() const noexcept; | ||
|
|
||
| /** Sets the time format to use when this file is written to a stream. | ||
| If this is called, the file will be written as bars/beats using the | ||
| specified resolution, rather than SMPTE absolute times, as would be | ||
| used if setSmpteTimeFormat() had been called instead. | ||
| @param ticksPerQuarterNote e.g. 96, 960 | ||
| @see setSmpteTimeFormat | ||
| */ | ||
| void setTicksPerQuarterNote (int ticksPerQuarterNote) noexcept; | ||
|
|
||
| /** Sets the time format to use when this file is written to a stream. | ||
| If this is called, the file will be written using absolute times, rather | ||
| than bars/beats as would be the case if setTicksPerBeat() had been called | ||
| instead. | ||
| @param framesPerSecond must be 24, 25, 29 or 30 | ||
| @param subframeResolution the sub-second resolution, e.g. 4 (midi time code), | ||
| 8, 10, 80 (SMPTE bit resolution), or 100. For millisecond | ||
| timing, setSmpteTimeFormat (25, 40) | ||
| @see setTicksPerBeat | ||
| */ | ||
| void setSmpteTimeFormat (int framesPerSecond, | ||
| int subframeResolution) noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Makes a list of all the tempo-change meta-events from all tracks in the midi file. | ||
| Useful for finding the positions of all the tempo changes in a file. | ||
| @param tempoChangeEvents a list to which all the events will be added | ||
| */ | ||
| void findAllTempoEvents (MidiMessageSequence& tempoChangeEvents) const; | ||
|
|
||
| /** Makes a list of all the time-signature meta-events from all tracks in the midi file. | ||
| Useful for finding the positions of all the tempo changes in a file. | ||
| @param timeSigEvents a list to which all the events will be added | ||
| */ | ||
| void findAllTimeSigEvents (MidiMessageSequence& timeSigEvents) const; | ||
|
|
||
| /** Makes a list of all the time-signature meta-events from all tracks in the midi file. | ||
| @param keySigEvents a list to which all the events will be added | ||
| */ | ||
| void findAllKeySigEvents (MidiMessageSequence& keySigEvents) const; | ||
|
|
||
| /** Returns the latest timestamp in any of the tracks. | ||
| (Useful for finding the length of the file). | ||
| */ | ||
| double getLastTimestamp() const; | ||
|
|
||
| //============================================================================== | ||
| /** Reads a midi file format stream. | ||
| After calling this, you can get the tracks that were read from the file by using the | ||
| getNumTracks() and getTrack() methods. | ||
| The timestamps of the midi events in the tracks will represent their positions in | ||
| terms of midi ticks. To convert them to seconds, use the convertTimestampTicksToSeconds() | ||
| method. | ||
| @param sourceStream the source stream | ||
| @param createMatchingNoteOffs if true, any missing note-offs for previous note-ons will | ||
| be automatically added at the end of the file by calling | ||
| MidiMessageSequence::updateMatchedPairs on each track. | ||
| @returns true if the stream was read successfully | ||
| */ | ||
| bool readFrom (InputStream& sourceStream, bool createMatchingNoteOffs = true); | ||
|
|
||
| /** Writes the midi tracks as a standard midi file. | ||
| The midiFileType value is written as the file's format type, which can be 0, 1 | ||
| or 2 - see the midi file spec for more info about that. | ||
| @param destStream the destination stream | ||
| @param midiFileType the type of midi file | ||
| @returns true if the operation succeeded. | ||
| */ | ||
| bool writeTo (OutputStream& destStream, int midiFileType = 1) const; | ||
|
|
||
| /** Converts the timestamp of all the midi events from midi ticks to seconds. | ||
| This will use the midi time format and tempo/time signature info in the | ||
| tracks to convert all the timestamps to absolute values in seconds. | ||
| */ | ||
| void convertTimestampTicksToSeconds(); | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| OwnedArray<MidiMessageSequence> tracks; | ||
| short timeFormat; | ||
|
|
||
| void readNextTrack (const uint8*, int, bool); | ||
| bool writeTrack (OutputStream&, const MidiMessageSequence&) const; | ||
|
|
||
| JUCE_LEAK_DETECTOR (MidiFile) | ||
| }; | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 | ||
| { | ||
|
|
||
| MidiKeyboardState::MidiKeyboardState() | ||
| { | ||
| zerostruct (noteStates); | ||
| } | ||
|
|
||
| MidiKeyboardState::~MidiKeyboardState() | ||
| { | ||
| } | ||
|
|
||
| //============================================================================== | ||
| void MidiKeyboardState::reset() | ||
| { | ||
| const ScopedLock sl (lock); | ||
| zerostruct (noteStates); | ||
| eventsToAdd.clear(); | ||
| } | ||
|
|
||
| bool MidiKeyboardState::isNoteOn (const int midiChannel, const int n) const noexcept | ||
| { | ||
| jassert (midiChannel > 0 && midiChannel <= 16); | ||
|
|
||
| return isPositiveAndBelow (n, 128) | ||
| && (noteStates[n] & (1 << (midiChannel - 1))) != 0; | ||
| } | ||
|
|
||
| bool MidiKeyboardState::isNoteOnForChannels (const int midiChannelMask, const int n) const noexcept | ||
| { | ||
| return isPositiveAndBelow (n, 128) | ||
| && (noteStates[n] & midiChannelMask) != 0; | ||
| } | ||
|
|
||
| void MidiKeyboardState::noteOn (const int midiChannel, const int midiNoteNumber, const float velocity) | ||
| { | ||
| jassert (midiChannel > 0 && midiChannel <= 16); | ||
| jassert (isPositiveAndBelow (midiNoteNumber, 128)); | ||
|
|
||
| const ScopedLock sl (lock); | ||
|
|
||
| if (isPositiveAndBelow (midiNoteNumber, 128)) | ||
| { | ||
| const int timeNow = (int) Time::getMillisecondCounter(); | ||
| eventsToAdd.addEvent (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity), timeNow); | ||
| eventsToAdd.clear (0, timeNow - 500); | ||
|
|
||
| noteOnInternal (midiChannel, midiNoteNumber, velocity); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::noteOnInternal (const int midiChannel, const int midiNoteNumber, const float velocity) | ||
| { | ||
| if (isPositiveAndBelow (midiNoteNumber, 128)) | ||
| { | ||
| noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] | (1 << (midiChannel - 1))); | ||
| listeners.call ([&] (Listener& l) { l.handleNoteOn (this, midiChannel, midiNoteNumber, velocity); }); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::noteOff (const int midiChannel, const int midiNoteNumber, const float velocity) | ||
| { | ||
| const ScopedLock sl (lock); | ||
|
|
||
| if (isNoteOn (midiChannel, midiNoteNumber)) | ||
| { | ||
| const int timeNow = (int) Time::getMillisecondCounter(); | ||
| eventsToAdd.addEvent (MidiMessage::noteOff (midiChannel, midiNoteNumber), timeNow); | ||
| eventsToAdd.clear (0, timeNow - 500); | ||
|
|
||
| noteOffInternal (midiChannel, midiNoteNumber, velocity); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::noteOffInternal (const int midiChannel, const int midiNoteNumber, const float velocity) | ||
| { | ||
| if (isNoteOn (midiChannel, midiNoteNumber)) | ||
| { | ||
| noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] & ~(1 << (midiChannel - 1))); | ||
| listeners.call ([&] (Listener& l) { l.handleNoteOff (this, midiChannel, midiNoteNumber, velocity); }); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::allNotesOff (const int midiChannel) | ||
| { | ||
| const ScopedLock sl (lock); | ||
|
|
||
| if (midiChannel <= 0) | ||
| { | ||
| for (int i = 1; i <= 16; ++i) | ||
| allNotesOff (i); | ||
| } | ||
| else | ||
| { | ||
| for (int i = 0; i < 128; ++i) | ||
| noteOff (midiChannel, i, 0.0f); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::processNextMidiEvent (const MidiMessage& message) | ||
| { | ||
| if (message.isNoteOn()) | ||
| { | ||
| noteOnInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity()); | ||
| } | ||
| else if (message.isNoteOff()) | ||
| { | ||
| noteOffInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity()); | ||
| } | ||
| else if (message.isAllNotesOff()) | ||
| { | ||
| for (int i = 0; i < 128; ++i) | ||
| noteOffInternal (message.getChannel(), i, 0.0f); | ||
| } | ||
| } | ||
|
|
||
| void MidiKeyboardState::processNextMidiBuffer (MidiBuffer& buffer, | ||
| const int startSample, | ||
| const int numSamples, | ||
| const bool injectIndirectEvents) | ||
| { | ||
| const ScopedLock sl (lock); | ||
|
|
||
| for (const auto metadata : buffer) | ||
| processNextMidiEvent (metadata.getMessage()); | ||
|
|
||
| if (injectIndirectEvents) | ||
| { | ||
| const int firstEventToAdd = eventsToAdd.getFirstEventTime(); | ||
| const double scaleFactor = numSamples / (double) (eventsToAdd.getLastEventTime() + 1 - firstEventToAdd); | ||
|
|
||
| for (const auto metadata : eventsToAdd) | ||
| { | ||
| const auto pos = jlimit (0, numSamples - 1, roundToInt ((metadata.samplePosition - firstEventToAdd) * scaleFactor)); | ||
| buffer.addEvent (metadata.getMessage(), startSample + pos); | ||
| } | ||
| } | ||
|
|
||
| eventsToAdd.clear(); | ||
| } | ||
|
|
||
| //============================================================================== | ||
| void MidiKeyboardState::addListener (Listener* listener) | ||
| { | ||
| const ScopedLock sl (lock); | ||
| listeners.add (listener); | ||
| } | ||
|
|
||
| void MidiKeyboardState::removeListener (Listener* listener) | ||
| { | ||
| const ScopedLock sl (lock); | ||
| listeners.remove (listener); | ||
| } | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 a piano keyboard, keeping track of which keys are currently pressed. | ||
| This object can parse a stream of midi events, using them to update its idea | ||
| of which keys are pressed for each individual midi channel. | ||
| When keys go up or down, it can broadcast these events to listener objects. | ||
| It also allows key up/down events to be triggered with its noteOn() and noteOff() | ||
| methods, and midi messages for these events will be merged into the | ||
| midi stream that gets processed by processNextMidiBuffer(). | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiKeyboardState | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| MidiKeyboardState(); | ||
| ~MidiKeyboardState(); | ||
|
|
||
| //============================================================================== | ||
| /** Resets the state of the object. | ||
| All internal data for all the channels is reset, but no events are sent as a | ||
| result. | ||
| If you want to release any keys that are currently down, and to send out note-up | ||
| midi messages for this, use the allNotesOff() method instead. | ||
| */ | ||
| void reset(); | ||
|
|
||
| /** Returns true if the given midi key is currently held down for the given midi channel. | ||
| The channel number must be between 1 and 16. If you want to see if any notes are | ||
| on for a range of channels, use the isNoteOnForChannels() method. | ||
| */ | ||
| bool isNoteOn (int midiChannel, int midiNoteNumber) const noexcept; | ||
|
|
||
| /** Returns true if the given midi key is currently held down on any of a set of midi channels. | ||
| The channel mask has a bit set for each midi channel you want to test for - bit | ||
| 0 = midi channel 1, bit 1 = midi channel 2, etc. | ||
| If a note is on for at least one of the specified channels, this returns true. | ||
| */ | ||
| bool isNoteOnForChannels (int midiChannelMask, int midiNoteNumber) const noexcept; | ||
|
|
||
| /** Turns a specified note on. | ||
| This will cause a suitable midi note-on event to be injected into the midi buffer during the | ||
| next call to processNextMidiBuffer(). | ||
| It will also trigger a synchronous callback to the listeners to tell them that the key has | ||
| gone down. | ||
| */ | ||
| void noteOn (int midiChannel, int midiNoteNumber, float velocity); | ||
|
|
||
| /** Turns a specified note off. | ||
| This will cause a suitable midi note-off event to be injected into the midi buffer during the | ||
| next call to processNextMidiBuffer(). | ||
| It will also trigger a synchronous callback to the listeners to tell them that the key has | ||
| gone up. | ||
| But if the note isn't actually down for the given channel, this method will in fact do nothing. | ||
| */ | ||
| void noteOff (int midiChannel, int midiNoteNumber, float velocity); | ||
|
|
||
| /** This will turn off any currently-down notes for the given midi channel. | ||
| If you pass 0 for the midi channel, it will in fact turn off all notes on all channels. | ||
| Calling this method will make calls to noteOff(), so can trigger synchronous callbacks | ||
| and events being added to the midi stream. | ||
| */ | ||
| void allNotesOff (int midiChannel); | ||
|
|
||
| //============================================================================== | ||
| /** Looks at a key-up/down event and uses it to update the state of this object. | ||
| To process a buffer full of midi messages, use the processNextMidiBuffer() method | ||
| instead. | ||
| */ | ||
| void processNextMidiEvent (const MidiMessage& message); | ||
|
|
||
| /** Scans a midi stream for up/down events and adds its own events to it. | ||
| This will look for any up/down events and use them to update the internal state, | ||
| synchronously making suitable callbacks to the listeners. | ||
| If injectIndirectEvents is true, then midi events to produce the recent noteOn() | ||
| and noteOff() calls will be added into the buffer. | ||
| Only the section of the buffer whose timestamps are between startSample and | ||
| (startSample + numSamples) will be affected, and any events added will be placed | ||
| between these times. | ||
| If you're going to use this method, you'll need to keep calling it regularly for | ||
| it to work satisfactorily. | ||
| To process a single midi event at a time, use the processNextMidiEvent() method | ||
| instead. | ||
| */ | ||
| void processNextMidiBuffer (MidiBuffer& buffer, | ||
| int startSample, | ||
| int numSamples, | ||
| bool injectIndirectEvents); | ||
|
|
||
| //============================================================================== | ||
| /** Receives events from a MidiKeyboardState object. */ | ||
| class JUCE_API Listener | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| virtual ~Listener() = default; | ||
|
|
||
| //============================================================================== | ||
| /** Called when one of the MidiKeyboardState's keys is pressed. | ||
| This will be called synchronously when the state is either processing a | ||
| buffer in its MidiKeyboardState::processNextMidiBuffer() method, or | ||
| when a note is being played with its MidiKeyboardState::noteOn() method. | ||
| Note that this callback could happen from an audio callback thread, so be | ||
| careful not to block, and avoid any UI activity in the callback. | ||
| */ | ||
| virtual void handleNoteOn (MidiKeyboardState* source, | ||
| int midiChannel, int midiNoteNumber, float velocity) = 0; | ||
|
|
||
| /** Called when one of the MidiKeyboardState's keys is released. | ||
| This will be called synchronously when the state is either processing a | ||
| buffer in its MidiKeyboardState::processNextMidiBuffer() method, or | ||
| when a note is being played with its MidiKeyboardState::noteOff() method. | ||
| Note that this callback could happen from an audio callback thread, so be | ||
| careful not to block, and avoid any UI activity in the callback. | ||
| */ | ||
| virtual void handleNoteOff (MidiKeyboardState* source, | ||
| int midiChannel, int midiNoteNumber, float velocity) = 0; | ||
| }; | ||
|
|
||
| /** Registers a listener for callbacks when keys go up or down. | ||
| @see removeListener | ||
| */ | ||
| void addListener (Listener* listener); | ||
|
|
||
| /** Deregisters a listener. | ||
| @see addListener | ||
| */ | ||
| void removeListener (Listener* listener); | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| CriticalSection lock; | ||
| std::atomic<uint16> noteStates[128]; | ||
| MidiBuffer eventsToAdd; | ||
| ListenerList<Listener> listeners; | ||
|
|
||
| void noteOnInternal (int midiChannel, int midiNoteNumber, float velocity); | ||
| void noteOffInternal (int midiChannel, int midiNoteNumber, float velocity); | ||
|
|
||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardState) | ||
| }; | ||
|
|
||
| using MidiKeyboardStateListener = MidiKeyboardState::Listener; | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,306 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 | ||
| { | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| A sequence of timestamped midi messages. | ||
| This allows the sequence to be manipulated, and also to be read from and | ||
| written to a standard midi file. | ||
| @see MidiMessage, MidiFile | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiMessageSequence | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Creates an empty midi sequence object. */ | ||
| MidiMessageSequence(); | ||
|
|
||
| /** Creates a copy of another sequence. */ | ||
| MidiMessageSequence (const MidiMessageSequence&); | ||
|
|
||
| /** Replaces this sequence with another one. */ | ||
| MidiMessageSequence& operator= (const MidiMessageSequence&); | ||
|
|
||
| /** Move constructor */ | ||
| MidiMessageSequence (MidiMessageSequence&&) noexcept; | ||
|
|
||
| /** Move assignment operator */ | ||
| MidiMessageSequence& operator= (MidiMessageSequence&&) noexcept; | ||
|
|
||
| /** Destructor. */ | ||
| ~MidiMessageSequence(); | ||
|
|
||
| //============================================================================== | ||
| /** Structure used to hold midi events in the sequence. | ||
| These structures act as 'handles' on the events as they are moved about in | ||
| the list, and make it quick to find the matching note-offs for note-on events. | ||
| @see MidiMessageSequence::getEventPointer | ||
| */ | ||
| class MidiEventHolder | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Destructor. */ | ||
| ~MidiEventHolder(); | ||
|
|
||
| /** The message itself, whose timestamp is used to specify the event's time. */ | ||
| MidiMessage message; | ||
|
|
||
| /** The matching note-off event (if this is a note-on event). | ||
| If this isn't a note-on, this pointer will be nullptr. | ||
| Use the MidiMessageSequence::updateMatchedPairs() method to keep these | ||
| note-offs up-to-date after events have been moved around in the sequence | ||
| or deleted. | ||
| */ | ||
| MidiEventHolder* noteOffObject = nullptr; | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| friend class MidiMessageSequence; | ||
| MidiEventHolder (const MidiMessage&); | ||
| MidiEventHolder (MidiMessage&&); | ||
| JUCE_LEAK_DETECTOR (MidiEventHolder) | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| /** Clears the sequence. */ | ||
| void clear(); | ||
|
|
||
| /** Returns the number of events in the sequence. */ | ||
| int getNumEvents() const noexcept; | ||
|
|
||
| /** Returns a pointer to one of the events. */ | ||
| MidiEventHolder* getEventPointer (int index) const noexcept; | ||
|
|
||
| /** Iterator for the list of MidiEventHolders */ | ||
| MidiEventHolder** begin() noexcept; | ||
|
|
||
| /** Iterator for the list of MidiEventHolders */ | ||
| MidiEventHolder* const* begin() const noexcept; | ||
|
|
||
| /** Iterator for the list of MidiEventHolders */ | ||
| MidiEventHolder** end() noexcept; | ||
|
|
||
| /** Iterator for the list of MidiEventHolders */ | ||
| MidiEventHolder* const* end() const noexcept; | ||
|
|
||
| /** Returns the time of the note-up that matches the note-on at this index. | ||
| If the event at this index isn't a note-on, it'll just return 0. | ||
| @see MidiMessageSequence::MidiEventHolder::noteOffObject | ||
| */ | ||
| double getTimeOfMatchingKeyUp (int index) const noexcept; | ||
|
|
||
| /** Returns the index of the note-up that matches the note-on at this index. | ||
| If the event at this index isn't a note-on, it'll just return -1. | ||
| @see MidiMessageSequence::MidiEventHolder::noteOffObject | ||
| */ | ||
| int getIndexOfMatchingKeyUp (int index) const noexcept; | ||
|
|
||
| /** Returns the index of an event. */ | ||
| int getIndexOf (const MidiEventHolder* event) const noexcept; | ||
|
|
||
| /** Returns the index of the first event on or after the given timestamp. | ||
| If the time is beyond the end of the sequence, this will return the | ||
| number of events. | ||
| */ | ||
| int getNextIndexAtTime (double timeStamp) const noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Returns the timestamp of the first event in the sequence. | ||
| @see getEndTime | ||
| */ | ||
| double getStartTime() const noexcept; | ||
|
|
||
| /** Returns the timestamp of the last event in the sequence. | ||
| @see getStartTime | ||
| */ | ||
| double getEndTime() const noexcept; | ||
|
|
||
| /** Returns the timestamp of the event at a given index. | ||
| If the index is out-of-range, this will return 0.0 | ||
| */ | ||
| double getEventTime (int index) const noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Inserts a midi message into the sequence. | ||
| The index at which the new message gets inserted will depend on its timestamp, | ||
| because the sequence is kept sorted. | ||
| Remember to call updateMatchedPairs() after adding note-on events. | ||
| @param newMessage the new message to add (an internal copy will be made) | ||
| @param timeAdjustment an optional value to add to the timestamp of the message | ||
| that will be inserted | ||
| @see updateMatchedPairs | ||
| */ | ||
| MidiEventHolder* addEvent (const MidiMessage& newMessage, double timeAdjustment = 0); | ||
|
|
||
| /** Inserts a midi message into the sequence. | ||
| The index at which the new message gets inserted will depend on its timestamp, | ||
| because the sequence is kept sorted. | ||
| Remember to call updateMatchedPairs() after adding note-on events. | ||
| @param newMessage the new message to add (an internal copy will be made) | ||
| @param timeAdjustment an optional value to add to the timestamp of the message | ||
| that will be inserted | ||
| @see updateMatchedPairs | ||
| */ | ||
| MidiEventHolder* addEvent (MidiMessage&& newMessage, double timeAdjustment = 0); | ||
|
|
||
| /** Deletes one of the events in the sequence. | ||
| Remember to call updateMatchedPairs() after removing events. | ||
| @param index the index of the event to delete | ||
| @param deleteMatchingNoteUp whether to also remove the matching note-off | ||
| if the event you're removing is a note-on | ||
| */ | ||
| void deleteEvent (int index, bool deleteMatchingNoteUp); | ||
|
|
||
| /** Merges another sequence into this one. | ||
| Remember to call updateMatchedPairs() after using this method. | ||
| @param other the sequence to add from | ||
| @param timeAdjustmentDelta an amount to add to the timestamps of the midi events | ||
| as they are read from the other sequence | ||
| @param firstAllowableDestTime events will not be added if their time is earlier | ||
| than this time. (This is after their time has been adjusted | ||
| by the timeAdjustmentDelta) | ||
| @param endOfAllowableDestTimes events will not be added if their time is equal to | ||
| or greater than this time. (This is after their time has | ||
| been adjusted by the timeAdjustmentDelta) | ||
| */ | ||
| void addSequence (const MidiMessageSequence& other, | ||
| double timeAdjustmentDelta, | ||
| double firstAllowableDestTime, | ||
| double endOfAllowableDestTimes); | ||
|
|
||
| /** Merges another sequence into this one. | ||
| Remember to call updateMatchedPairs() after using this method. | ||
| @param other the sequence to add from | ||
| @param timeAdjustmentDelta an amount to add to the timestamps of the midi events | ||
| as they are read from the other sequence | ||
| */ | ||
| void addSequence (const MidiMessageSequence& other, | ||
| double timeAdjustmentDelta); | ||
|
|
||
| //============================================================================== | ||
| /** Makes sure all the note-on and note-off pairs are up-to-date. | ||
| Call this after re-ordering messages or deleting/adding messages, and it | ||
| will scan the list and make sure all the note-offs in the MidiEventHolder | ||
| structures are pointing at the correct ones. | ||
| */ | ||
| void updateMatchedPairs() noexcept; | ||
|
|
||
| /** Forces a sort of the sequence. | ||
| You may need to call this if you've manually modified the timestamps of some | ||
| events such that the overall order now needs updating. | ||
| */ | ||
| void sort() noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Copies all the messages for a particular midi channel to another sequence. | ||
| @param channelNumberToExtract the midi channel to look for, in the range 1 to 16 | ||
| @param destSequence the sequence that the chosen events should be copied to | ||
| @param alsoIncludeMetaEvents if true, any meta-events (which don't apply to a specific | ||
| channel) will also be copied across. | ||
| @see extractSysExMessages | ||
| */ | ||
| void extractMidiChannelMessages (int channelNumberToExtract, | ||
| MidiMessageSequence& destSequence, | ||
| bool alsoIncludeMetaEvents) const; | ||
|
|
||
| /** Copies all midi sys-ex messages to another sequence. | ||
| @param destSequence this is the sequence to which any sys-exes in this sequence | ||
| will be added | ||
| @see extractMidiChannelMessages | ||
| */ | ||
| void extractSysExMessages (MidiMessageSequence& destSequence) const; | ||
|
|
||
| /** Removes any messages in this sequence that have a specific midi channel. | ||
| @param channelNumberToRemove the midi channel to look for, in the range 1 to 16 | ||
| */ | ||
| void deleteMidiChannelMessages (int channelNumberToRemove); | ||
|
|
||
| /** Removes any sys-ex messages from this sequence. */ | ||
| void deleteSysExMessages(); | ||
|
|
||
| /** Adds an offset to the timestamps of all events in the sequence. | ||
| @param deltaTime the amount to add to each timestamp. | ||
| */ | ||
| void addTimeToMessages (double deltaTime) noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Scans through the sequence to determine the state of any midi controllers at | ||
| a given time. | ||
| This will create a sequence of midi controller changes that can be | ||
| used to set all midi controllers to the state they would be in at the | ||
| specified time within this sequence. | ||
| As well as controllers, it will also recreate the midi program number | ||
| and pitch bend position. | ||
| @param channelNumber the midi channel to look for, in the range 1 to 16. Controllers | ||
| for other channels will be ignored. | ||
| @param time the time at which you want to find out the state - there are | ||
| no explicit units for this time measurement, it's the same units | ||
| as used for the timestamps of the messages | ||
| @param resultMessages an array to which midi controller-change messages will be added. This | ||
| will be the minimum number of controller changes to recreate the | ||
| state at the required time. | ||
| */ | ||
| void createControllerUpdatesForTime (int channelNumber, double time, | ||
| Array<MidiMessage>& resultMessages); | ||
|
|
||
| //============================================================================== | ||
| /** Swaps this sequence with another one. */ | ||
| void swapWith (MidiMessageSequence&) noexcept; | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| friend class MidiFile; | ||
| OwnedArray<MidiEventHolder> list; | ||
|
|
||
| MidiEventHolder* addEvent (MidiEventHolder*, double); | ||
|
|
||
| JUCE_LEAK_DETECTOR (MidiMessageSequence) | ||
| }; | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,380 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 | ||
| { | ||
|
|
||
| MidiRPNDetector::MidiRPNDetector() noexcept | ||
| { | ||
| } | ||
|
|
||
| MidiRPNDetector::~MidiRPNDetector() noexcept | ||
| { | ||
| } | ||
|
|
||
| bool MidiRPNDetector::parseControllerMessage (int midiChannel, | ||
| int controllerNumber, | ||
| int controllerValue, | ||
| MidiRPNMessage& result) noexcept | ||
| { | ||
| jassert (midiChannel > 0 && midiChannel <= 16); | ||
| jassert (controllerNumber >= 0 && controllerNumber < 128); | ||
| jassert (controllerValue >= 0 && controllerValue < 128); | ||
|
|
||
| return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue, result); | ||
| } | ||
|
|
||
| void MidiRPNDetector::reset() noexcept | ||
| { | ||
| for (int i = 0; i < 16; ++i) | ||
| { | ||
| states[i].parameterMSB = 0xff; | ||
| states[i].parameterLSB = 0xff; | ||
| states[i].resetValue(); | ||
| states[i].isNRPN = false; | ||
| } | ||
| } | ||
|
|
||
| //============================================================================== | ||
| MidiRPNDetector::ChannelState::ChannelState() noexcept | ||
| : parameterMSB (0xff), parameterLSB (0xff), valueMSB (0xff), valueLSB (0xff), isNRPN (false) | ||
| { | ||
| } | ||
|
|
||
| bool MidiRPNDetector::ChannelState::handleController (int channel, | ||
| int controllerNumber, | ||
| int value, | ||
| MidiRPNMessage& result) noexcept | ||
| { | ||
| switch (controllerNumber) | ||
| { | ||
| case 0x62: parameterLSB = uint8 (value); resetValue(); isNRPN = true; break; | ||
| case 0x63: parameterMSB = uint8 (value); resetValue(); isNRPN = true; break; | ||
|
|
||
| case 0x64: parameterLSB = uint8 (value); resetValue(); isNRPN = false; break; | ||
| case 0x65: parameterMSB = uint8 (value); resetValue(); isNRPN = false; break; | ||
|
|
||
| case 0x06: valueMSB = uint8 (value); return sendIfReady (channel, result); | ||
| case 0x26: valueLSB = uint8 (value); break; | ||
|
|
||
| default: break; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| void MidiRPNDetector::ChannelState::resetValue() noexcept | ||
| { | ||
| valueMSB = 0xff; | ||
| valueLSB = 0xff; | ||
| } | ||
|
|
||
| //============================================================================== | ||
| bool MidiRPNDetector::ChannelState::sendIfReady (int channel, MidiRPNMessage& result) noexcept | ||
| { | ||
| if (parameterMSB < 0x80 && parameterLSB < 0x80) | ||
| { | ||
| if (valueMSB < 0x80) | ||
| { | ||
| result.channel = channel; | ||
| result.parameterNumber = (parameterMSB << 7) + parameterLSB; | ||
| result.isNRPN = isNRPN; | ||
|
|
||
| if (valueLSB < 0x80) | ||
| { | ||
| result.value = (valueMSB << 7) + valueLSB; | ||
| result.is14BitValue = true; | ||
| } | ||
| else | ||
| { | ||
| result.value = valueMSB; | ||
| result.is14BitValue = false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| //============================================================================== | ||
| MidiBuffer MidiRPNGenerator::generate (MidiRPNMessage message) | ||
| { | ||
| return generate (message.channel, | ||
| message.parameterNumber, | ||
| message.value, | ||
| message.isNRPN, | ||
| message.is14BitValue); | ||
| } | ||
|
|
||
| MidiBuffer MidiRPNGenerator::generate (int midiChannel, | ||
| int parameterNumber, | ||
| int value, | ||
| bool isNRPN, | ||
| bool use14BitValue) | ||
| { | ||
| jassert (midiChannel > 0 && midiChannel <= 16); | ||
| jassert (parameterNumber >= 0 && parameterNumber < 16384); | ||
| jassert (value >= 0 && value < (use14BitValue ? 16384 : 128)); | ||
|
|
||
| uint8 parameterLSB = uint8 (parameterNumber & 0x0000007f); | ||
| uint8 parameterMSB = uint8 (parameterNumber >> 7); | ||
|
|
||
| uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00; | ||
| uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value); | ||
|
|
||
| uint8 channelByte = uint8 (0xb0 + midiChannel - 1); | ||
|
|
||
| MidiBuffer buffer; | ||
|
|
||
| buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x62 : 0x64, parameterLSB), 0); | ||
| buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x63 : 0x65, parameterMSB), 0); | ||
|
|
||
| // sending the value LSB is optional, but must come before sending the value MSB: | ||
| if (use14BitValue) | ||
| buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0); | ||
|
|
||
| buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0); | ||
|
|
||
| return buffer; | ||
| } | ||
|
|
||
|
|
||
| //============================================================================== | ||
| //============================================================================== | ||
| #if JUCE_UNIT_TESTS | ||
|
|
||
| class MidiRPNDetectorTests : public UnitTest | ||
| { | ||
| public: | ||
| MidiRPNDetectorTests() | ||
| : UnitTest ("MidiRPNDetector class", UnitTestCategories::midi) | ||
| {} | ||
|
|
||
| void runTest() override | ||
| { | ||
| beginTest ("7-bit RPN"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (2, 101, 0, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 100, 7, rpn)); | ||
| expect (detector.parseControllerMessage (2, 6, 42, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 2); | ||
| expectEquals (rpn.parameterNumber, 7); | ||
| expectEquals (rpn.value, 42); | ||
| expect (! rpn.isNRPN); | ||
| expect (! rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("14-bit RPN"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (1, 100, 44, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 101, 2, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 38, 94, rpn)); | ||
| expect (detector.parseControllerMessage (1, 6, 1, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 1); | ||
| expectEquals (rpn.parameterNumber, 300); | ||
| expectEquals (rpn.value, 222); | ||
| expect (! rpn.isNRPN); | ||
| expect (rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("RPNs on multiple channels simultaneously"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (1, 100, 44, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 101, 0, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 101, 2, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 100, 7, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 38, 94, rpn)); | ||
| expect (detector.parseControllerMessage (2, 6, 42, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 2); | ||
| expectEquals (rpn.parameterNumber, 7); | ||
| expectEquals (rpn.value, 42); | ||
| expect (! rpn.isNRPN); | ||
| expect (! rpn.is14BitValue); | ||
|
|
||
| expect (detector.parseControllerMessage (1, 6, 1, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 1); | ||
| expectEquals (rpn.parameterNumber, 300); | ||
| expectEquals (rpn.value, 222); | ||
| expect (! rpn.isNRPN); | ||
| expect (rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("14-bit RPN with value within 7-bit range"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (16, 100, 0 , rpn)); | ||
| expect (! detector.parseControllerMessage (16, 101, 0, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 38, 3, rpn)); | ||
| expect (detector.parseControllerMessage (16, 6, 0, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 16); | ||
| expectEquals (rpn.parameterNumber, 0); | ||
| expectEquals (rpn.value, 3); | ||
| expect (! rpn.isNRPN); | ||
| expect (rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("invalid RPN (wrong order)"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (2, 6, 42, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 101, 0, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 100, 7, rpn)); | ||
| } | ||
|
|
||
| beginTest ("14-bit RPN interspersed with unrelated CC messages"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (16, 3, 80, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 100, 0 , rpn)); | ||
| expect (! detector.parseControllerMessage (16, 4, 81, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 101, 0, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 5, 82, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 5, 83, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 38, 3, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 4, 84, rpn)); | ||
| expect (! detector.parseControllerMessage (16, 3, 85, rpn)); | ||
| expect (detector.parseControllerMessage (16, 6, 0, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 16); | ||
| expectEquals (rpn.parameterNumber, 0); | ||
| expectEquals (rpn.value, 3); | ||
| expect (! rpn.isNRPN); | ||
| expect (rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("14-bit NRPN"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (1, 98, 44, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 99 , 2, rpn)); | ||
| expect (! detector.parseControllerMessage (1, 38, 94, rpn)); | ||
| expect (detector.parseControllerMessage (1, 6, 1, rpn)); | ||
|
|
||
| expectEquals (rpn.channel, 1); | ||
| expectEquals (rpn.parameterNumber, 300); | ||
| expectEquals (rpn.value, 222); | ||
| expect (rpn.isNRPN); | ||
| expect (rpn.is14BitValue); | ||
| } | ||
|
|
||
| beginTest ("reset"); | ||
| { | ||
| MidiRPNDetector detector; | ||
| MidiRPNMessage rpn; | ||
| expect (! detector.parseControllerMessage (2, 101, 0, rpn)); | ||
| detector.reset(); | ||
| expect (! detector.parseControllerMessage (2, 100, 7, rpn)); | ||
| expect (! detector.parseControllerMessage (2, 6, 42, rpn)); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| static MidiRPNDetectorTests MidiRPNDetectorUnitTests; | ||
|
|
||
| //============================================================================== | ||
| class MidiRPNGeneratorTests : public UnitTest | ||
| { | ||
| public: | ||
| MidiRPNGeneratorTests() | ||
| : UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi) | ||
| {} | ||
|
|
||
| void runTest() override | ||
| { | ||
| beginTest ("generating RPN/NRPN"); | ||
| { | ||
| { | ||
| MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true); | ||
| expectContainsRPN (buffer, 1, 23, 1337, true, true); | ||
| } | ||
| { | ||
| MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false); | ||
| expectContainsRPN (buffer, 16, 101, 34, false, false); | ||
| } | ||
| { | ||
| MidiRPNMessage message = { 16, 101, 34, false, false }; | ||
| MidiBuffer buffer = MidiRPNGenerator::generate (message); | ||
| expectContainsRPN (buffer, message); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| void expectContainsRPN (const MidiBuffer& midiBuffer, | ||
| int channel, | ||
| int parameterNumber, | ||
| int value, | ||
| bool isNRPN, | ||
| bool is14BitValue) | ||
| { | ||
| MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue }; | ||
| expectContainsRPN (midiBuffer, expected); | ||
| } | ||
|
|
||
| //============================================================================== | ||
| void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected) | ||
| { | ||
| MidiRPNMessage result = MidiRPNMessage(); | ||
| MidiRPNDetector detector; | ||
|
|
||
| for (const auto metadata : midiBuffer) | ||
| { | ||
| const auto midiMessage = metadata.getMessage(); | ||
|
|
||
| if (detector.parseControllerMessage (midiMessage.getChannel(), | ||
| midiMessage.getControllerNumber(), | ||
| midiMessage.getControllerValue(), | ||
| result)) | ||
| break; | ||
| } | ||
|
|
||
| expectEquals (result.channel, expected.channel); | ||
| expectEquals (result.parameterNumber, expected.parameterNumber); | ||
| expectEquals (result.value, expected.value); | ||
| expect (result.isNRPN == expected.isNRPN); | ||
| expect (result.is14BitValue == expected.is14BitValue); | ||
| } | ||
| }; | ||
|
|
||
| static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests; | ||
|
|
||
| #endif | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| /* | ||
| ============================================================================== | ||
| 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 a MIDI RPN (registered parameter number) or NRPN (non-registered | ||
| parameter number) message. | ||
| @tags{Audio} | ||
| */ | ||
| struct MidiRPNMessage | ||
| { | ||
| /** Midi channel of the message, in the range 1 to 16. */ | ||
| int channel; | ||
|
|
||
| /** The 14-bit parameter index, in the range 0 to 16383 (0x3fff). */ | ||
| int parameterNumber; | ||
|
|
||
| /** The parameter value, in the range 0 to 16383 (0x3fff). | ||
| If the message contains no value LSB, the value will be in the range | ||
| 0 to 127 (0x7f). | ||
| */ | ||
| int value; | ||
|
|
||
| /** True if this message is an NRPN; false if it is an RPN. */ | ||
| bool isNRPN; | ||
|
|
||
| /** True if the value uses 14-bit resolution (LSB + MSB); false if | ||
| the value is 7-bit (MSB only). | ||
| */ | ||
| bool is14BitValue; | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| Parses a stream of MIDI data to assemble RPN and NRPN messages from their | ||
| constituent MIDI CC messages. | ||
| The detector uses the following parsing rules: the parameter number | ||
| LSB/MSB can be sent/received in either order and must both come before the | ||
| parameter value; for the parameter value, LSB always has to be sent/received | ||
| before the value MSB, otherwise it will be treated as 7-bit (MSB only). | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiRPNDetector | ||
| { | ||
| public: | ||
| /** Constructor. */ | ||
| MidiRPNDetector() noexcept; | ||
|
|
||
| /** Destructor. */ | ||
| ~MidiRPNDetector() noexcept; | ||
|
|
||
| /** Resets the RPN detector's internal state, so that it forgets about | ||
| previously received MIDI CC messages. | ||
| */ | ||
| void reset() noexcept; | ||
|
|
||
| //============================================================================== | ||
| /** Takes the next in a stream of incoming MIDI CC messages and returns true | ||
| if it forms the last of a sequence that makes an RPN or NPRN. | ||
| If this returns true, then the RPNMessage object supplied will be | ||
| filled-out with the message's details. | ||
| (If it returns false then the RPNMessage object will be unchanged). | ||
| */ | ||
| bool parseControllerMessage (int midiChannel, | ||
| int controllerNumber, | ||
| int controllerValue, | ||
| MidiRPNMessage& result) noexcept; | ||
|
|
||
| private: | ||
| //============================================================================== | ||
| struct ChannelState | ||
| { | ||
| ChannelState() noexcept; | ||
| bool handleController (int channel, int controllerNumber, | ||
| int value, MidiRPNMessage&) noexcept; | ||
| void resetValue() noexcept; | ||
| bool sendIfReady (int channel, MidiRPNMessage&) noexcept; | ||
|
|
||
| uint8 parameterMSB, parameterLSB, valueMSB, valueLSB; | ||
| bool isNRPN; | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| ChannelState states[16]; | ||
|
|
||
| JUCE_LEAK_DETECTOR (MidiRPNDetector) | ||
| }; | ||
|
|
||
| //============================================================================== | ||
| /** | ||
| Generates an appropriate sequence of MIDI CC messages to represent an RPN | ||
| or NRPN message. | ||
| This sequence (as a MidiBuffer) can then be directly sent to a MidiOutput. | ||
| @tags{Audio} | ||
| */ | ||
| class JUCE_API MidiRPNGenerator | ||
| { | ||
| public: | ||
| //============================================================================== | ||
| /** Generates a MIDI sequence representing the given RPN or NRPN message. */ | ||
| static MidiBuffer generate (MidiRPNMessage message); | ||
|
|
||
| //============================================================================== | ||
| /** Generates a MIDI sequence representing an RPN or NRPN message with the | ||
| given parameters. | ||
| @param channel The MIDI channel of the RPN/NRPN message. | ||
| @param parameterNumber The parameter number, in the range 0 to 16383. | ||
| @param value The parameter value, in the range 0 to 16383, or | ||
| in the range 0 to 127 if sendAs14BitValue is false. | ||
| @param isNRPN Whether you need a MIDI RPN or NRPN sequence (RPN is default). | ||
| @param use14BitValue If true (default), the value will have 14-bit precision | ||
| (two MIDI bytes). If false, instead the value will have | ||
| 7-bit precision (a single MIDI byte). | ||
| */ | ||
| static MidiBuffer generate (int channel, | ||
| int parameterNumber, | ||
| int value, | ||
| bool isNRPN = false, | ||
| bool use14BitValue = true); | ||
| }; | ||
|
|
||
| } // namespace juce |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |