From 00c2dc084d015385fe859b9f9b9ab3b01ec8709c Mon Sep 17 00:00:00 2001 From: submodify Date: Sun, 14 Sep 2025 16:51:38 +0100 Subject: [PATCH 1/4] parse() reworked to properly deal with SysEx input A reworking of parse() to properly deal with SysEx start & stop bytes as per Midi guidelines. --- src/MIDI.hpp | 469 ++++++++++++++++++++------------------------------- 1 file changed, 183 insertions(+), 286 deletions(-) diff --git a/src/MIDI.hpp b/src/MIDI.hpp index be7c28a1..452a9e9d 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -828,301 +828,198 @@ inline bool MidiInterface::read(Channel inChannel // Private method: MIDI parser template -bool MidiInterface::parse() -{ - if (mTransport.available() == 0) - return false; // No data available. - - // clear the ErrorParse bit - mLastError &= ~(1UL << ErrorParse); - - // Parsing algorithm: - // Get a byte from the serial buffer. - // If there is no pending message to be recomposed, start a new one. - // - Find type and channel (if pertinent) - // - Look for other bytes in buffer, call parser recursively, - // until the message is assembled or the buffer is empty. - // Else, add the extracted byte to the pending message, and check validity. - // When the message is done, store it. - - const byte extracted = mTransport.read(); - - // Ignore Undefined - if (extracted == Undefined_FD) - return (Settings::Use1ByteParsing) ? false : parse(); - - if (mPendingMessageIndex == 0) - { - // Start a new pending message +bool MidiInterface::parse() { + + if (mTransport.available() == 0) + return false; + + mLastError &= ~(1UL << ErrorParse); // Clear ErrorParse bit + /*Possible Errors: + > SysEx Stop byte received with no pending SysEx Start. + > Unsupported Status byte. + > Data received without a valid Status byte or Running Status. + > Warning SysEx split warning. + .... could potentially add an error for when SysEx is aborted due to receiving a new non-realtime status byte. + */ + + const byte extracted = mTransport.read(); + + if (extracted >= 0x80) { + // Lets try get a valid Status byte. Non-realtime status overides any current Status + const MidiType pendingType = getTypeFromStatusByte(extracted); + switch (pendingType) { + // Realtime + case Start: + case Continue: + case Stop: + case Clock: + case Tick: + case ActiveSensing: + case SystemReset: + case TuneRequest: + // Handle message now + mMessage.type = pendingType; + mMessage.channel = 0; + mMessage.data1 = 0; + mMessage.data2 = 0; + mMessage.length = 1; + mMessage.valid = true; + return true; + break; + // 2 byte messages + case ProgramChange: + case AfterTouchChannel: + case TimeCodeQuarterFrame: + case SongSelect: mPendingMessage[0] = extracted; - - // Check for running status first - if (isChannelMessage(getTypeFromStatusByte(mRunningStatus_RX))) - { - // Only these types allow Running Status - - // If the status byte is not received, prepend it - // to the pending message - if (extracted < 0x80) - { - mPendingMessage[0] = mRunningStatus_RX; - mPendingMessage[1] = extracted; - mPendingMessageIndex = 1; - } - // Else: well, we received another status byte, - // so the running status does not apply here. - // It will be updated upon completion of this message. - } - - const MidiType pendingType = getTypeFromStatusByte(mPendingMessage[0]); - - switch (pendingType) - { - // 1 byte messages - case Start: - case Continue: - case Stop: - case Clock: - case Tick: - case ActiveSensing: - case SystemReset: - case TuneRequest: - // Handle the message type directly here. - mMessage.type = pendingType; - mMessage.channel = 0; - mMessage.data1 = 0; - mMessage.data2 = 0; - mMessage.valid = true; - - // Do not reset all input attributes, Running Status must remain unchanged. - // We still need to reset these - mPendingMessageIndex = 0; - mPendingMessageExpectedLength = 0; - - return true; - break; - - // 2 bytes messages - case ProgramChange: - case AfterTouchChannel: - case TimeCodeQuarterFrame: - case SongSelect: - mPendingMessageExpectedLength = 2; - break; - - // 3 bytes messages - case NoteOn: - case NoteOff: - case ControlChange: - case PitchBend: - case AfterTouchPoly: - case SongPosition: - mPendingMessageExpectedLength = 3; - break; - - case SystemExclusiveStart: - case SystemExclusiveEnd: - // The message can be any length - // between 3 and MidiMessage::sSysExMaxSize bytes - mPendingMessageExpectedLength = MidiMessage::sSysExMaxSize; - mRunningStatus_RX = InvalidType; - mMessage.sysexArray[0] = pendingType; - break; - - case InvalidType: - default: - // This is obviously wrong. Let's get the hell out'a here. - mLastError |= 1UL << ErrorParse; // set the ErrorParse bit - if (mErrorCallback) - mErrorCallback(mLastError); // LCOV_EXCL_LINE - - resetInput(); - return false; - break; - } - - if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) - { - // Reception complete - mMessage.type = pendingType; - mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); - mMessage.data1 = mPendingMessage[1]; - mMessage.data2 = 0; // Completed new message has 1 data byte - mMessage.length = 1; - - mPendingMessageIndex = 0; - mPendingMessageExpectedLength = 0; + mPendingMessageExpectedLength = 2; + break; + // 3 byte messages + case NoteOn: + case NoteOff: + case ControlChange: + case PitchBend: + case AfterTouchPoly: + case SongPosition: + mPendingMessage[0] = extracted; + mPendingMessageExpectedLength = 3; + break; + // SysEx + case SystemExclusiveStart: + mPendingMessage[0] = SystemExclusive; + mPendingMessageExpectedLength = MidiMessage::sSysExMaxSize; + mMessage.sysexArray[0] = SystemExclusiveStart; + mLastError &= ~(1UL << WarningSplitSysEx); // Reset Warning Split SysEx bit + break; + case SystemExclusiveEnd: + if (mPendingMessage[0] == SystemExclusive) { + mMessage.sysexArray[mPendingMessageIndex++] = SystemExclusiveEnd; // Post Inc pending index here for correct lenght data + mMessage.type = SystemExclusive; + mMessage.data1 = mPendingMessageIndex & 0xff; // LSB + mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB + mMessage.channel = 0; + mMessage.length = mPendingMessageIndex; + if (mMessage.sysexArray[0] == SystemExclusiveEnd) { + // This is the last chunk of a split SysEx message, and is NOT a valid SysEx message (it starts with 0xF7) + mMessage.valid = false; // SysEx message is split so this not technically valid + launchCallback(); // Lets notify callback to deal with this + resetInput(); // Restart message + return false; + } else { + // We are in a valid SysEx message that hasn't overrun (starts with 0xF0) so lets complete it mMessage.valid = true; - + resetInput(); // Restart message return true; + } + } else { // Looks like a SysEx End without a Sysex Start + mLastError |= 1UL << ErrorParse; // Error: SysEx Stop byte received with no pending SysEx Start. + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); // Restart message + return false; } - else - { - // Waiting for more data - mPendingMessageIndex++; - } - - return (Settings::Use1ByteParsing) ? false : parse(); + break; + // Unsupported + default: + mPendingMessage[0] = InvalidType; + mLastError |= 1UL << ErrorParse; // Error: Unsupported Status byte. + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); // Restart message + return false; + break; + } + mPendingMessageIndex = 1; // If we are here, we have a valid Status! Lets try get some Data for it.... + mRunningStatus_RX = InvalidType; // Lets also reset Running Status until we have a complete message + return (Settings::Use1ByteParsing) ? false : parse(); + + } else { + // Lets get some data... First off.. check for Status Byte, or use Running Status + if (mPendingMessageIndex == 0) { + if (mRunningStatus_RX) { + // Yay! We have Running Status + mPendingMessage[0] = mRunningStatus_RX; + mPendingMessageIndex = 1; + } else { + // ooops.... No Status Byte... No Running Status... lets ignore this data + mLastError |= 1UL << ErrorParse; // Error: Data received without a valid Status byte or Running Status. + if (mErrorCallback) + mErrorCallback(mLastError); + return false; + } } - else - { - // First, test if this is a status byte - if (extracted >= 0x80) - { - // Reception of status bytes in the middle of an uncompleted message - // are allowed only for interleaved Real Time message or EOX - switch (extracted) - { - case Clock: - case Start: - case Tick: - case Continue: - case Stop: - case ActiveSensing: - case SystemReset: - - // Here we will have to extract the one-byte message, - // pass it to the structure for being read outside - // the MIDI class, and recompose the message it was - // interleaved into. Oh, and without killing the running status.. - // This is done by leaving the pending message as is, - // it will be completed on next calls. - - mMessage.type = (MidiType)extracted; - mMessage.data1 = 0; - mMessage.data2 = 0; - mMessage.channel = 0; - mMessage.length = 1; - mMessage.valid = true; - - return true; - - // Exclusive - case SystemExclusiveStart: - case SystemExclusiveEnd: - if ((mMessage.sysexArray[0] == SystemExclusiveStart) - || (mMessage.sysexArray[0] == SystemExclusiveEnd)) - { - // Store the last byte (EOX) - mMessage.sysexArray[mPendingMessageIndex++] = extracted; - mMessage.type = SystemExclusive; - - // Get length - mMessage.data1 = mPendingMessageIndex & 0xff; // LSB - mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB - mMessage.channel = 0; - mMessage.length = mPendingMessageIndex; - mMessage.valid = true; - - resetInput(); - - return true; - } - else - { - // Well well well.. error. - mLastError |= 1UL << ErrorParse; // set the error bits - if (mErrorCallback) - mErrorCallback(mLastError); // LCOV_EXCL_LINE - - resetInput(); - return false; - } - // LCOV_EXCL_START - Coverage blind spot - default: - break; - // LCOV_EXCL_STOP - } - } - - // Add extracted data byte to pending message - if ((mPendingMessage[0] == SystemExclusiveStart) - || (mPendingMessage[0] == SystemExclusiveEnd)) - mMessage.sysexArray[mPendingMessageIndex] = extracted; - else - mPendingMessage[mPendingMessageIndex] = extracted; - - // Now we are going to check if we have reached the end of the message - if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) - { - // SysEx larger than the allocated buffer size, - // Split SysEx like so: - // first: 0xF0 .... 0xF0 - // midlle: 0xF7 .... 0xF0 - // last: 0xF7 .... 0xF7 - if ((mPendingMessage[0] == SystemExclusiveStart) - || (mPendingMessage[0] == SystemExclusiveEnd)) - { - auto lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; - mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; - mMessage.type = SystemExclusive; - - // Get length - mMessage.data1 = Settings::SysExMaxSize & 0xff; // LSB - mMessage.data2 = byte(Settings::SysExMaxSize >> 8); // MSB - mMessage.channel = 0; - mMessage.length = Settings::SysExMaxSize; - mMessage.valid = true; - - // No need to check against the inputChannel, - // SysEx ignores input channel - launchCallback(); - - mMessage.sysexArray[0] = SystemExclusiveEnd; - mMessage.sysexArray[1] = lastByte; - - mPendingMessageIndex = 2; - - return false; - } - - mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); - - if (isChannelMessage(mMessage.type)) - mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); - else - mMessage.channel = 0; - - mMessage.data1 = mPendingMessage[1]; - // Save data2 only if applicable - mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; - mMessage.length = mPendingMessageExpectedLength; - - // Reset local variables - mPendingMessageIndex = 0; - mPendingMessageExpectedLength = 0; - - mMessage.valid = true; - // Activate running status (if enabled for the received type) - switch (mMessage.type) - { - case NoteOff: - case NoteOn: - case AfterTouchPoly: - case ControlChange: - case ProgramChange: - case AfterTouchChannel: - case PitchBend: - // Running status enabled: store it from received message - mRunningStatus_RX = mPendingMessage[0]; - break; - - default: - // No running status - mRunningStatus_RX = InvalidType; - break; - } - return true; + // Status or Running Status is good so add extracted data byte to pending message + if (mPendingMessage[0] == SystemExclusive) + mMessage.sysexArray[mPendingMessageIndex] = extracted; + else + mPendingMessage[mPendingMessageIndex] = extracted; + + // Now we are going to check if we have reached the end of the message + if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) { + // SysEx larger than the allocated buffer size, + // Split SysEx like so: + // first: 0xF0 .... 0xF0 + // middle: 0xF7 .... 0xF0 + // last: 0xF7 .... 0xF7 + // ***** If the buffer has overrun, this SysEx message can now no longer be considered a valid Midi message and must be dealt with via callbacks only! **** + if (mPendingMessage[0] == SystemExclusive) { + // Warn at start of SysEx split + if (mMessage.sysexArray[0] == SystemExclusiveStart) { + mLastError |= 1UL << WarningSplitSysEx; // We have this error already defined so may as well use it + if (mErrorCallback) + mErrorCallback(mLastError); } - else - { - // Then update the index of the pending message. - mPendingMessageIndex++; + auto lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; + mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; + mMessage.type = SystemExclusive; + + // Get length + mMessage.data1 = Settings::SysExMaxSize & 0xff; // LSB + mMessage.data2 = byte(Settings::SysExMaxSize >> 8); // MSB + mMessage.channel = 0; + mMessage.length = Settings::SysExMaxSize; + mMessage.valid = false; // SysEx message is split so this is not technically valid + + // Notify callback to deal with the SysEx data chunk + launchCallback(); - return (Settings::Use1ByteParsing) ? false : parse(); - } + // Prep next SysEx data chunk to start with 0xF7 + mMessage.sysexArray[0] = SystemExclusiveEnd; + mMessage.sysexArray[1] = lastByte; + mPendingMessageIndex = 2; + // SysEx buffer has overrun so parse() will no longer return true, and will need to be dealt with via callbacks only + return false; + } + + // Pending message is complete so lets save it + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + + if (isChannelMessage(mMessage.type)) { + mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); + // Message will be completed soon so lets update RunningStatus now as this is obviously a valid Channel Message. + mRunningStatus_RX = mPendingMessage[0]; + } else + mMessage.channel = 0; + + mMessage.data1 = mPendingMessage[1]; + // Save data2 only if applicable + mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; + mMessage.length = mPendingMessageExpectedLength; + mMessage.valid = true; + + // Reset index for next message + mPendingMessageIndex = 0; + //mPendingMessageExpectedLength = 0; // <-- No need to reset this as its valid still for Running Status, or will be updated on next Status byte received. + + return true; + } else { + // We need more data... + mPendingMessageIndex++; + + return (Settings::Use1ByteParsing) ? false : parse(); } + } } // Private method, see midi_Settings.h for documentation From 92a8931ec1aebd2b5854a2a2ae12bb5059466f28 Mon Sep 17 00:00:00 2001 From: submodify Date: Wed, 17 Sep 2025 01:01:09 +0100 Subject: [PATCH 2/4] Update MIDI.hpp Fixed formatting --- src/MIDI.hpp | 363 ++++++++++++++++++++++++++------------------------- 1 file changed, 182 insertions(+), 181 deletions(-) diff --git a/src/MIDI.hpp b/src/MIDI.hpp index 452a9e9d..207503e4 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -828,198 +828,199 @@ inline bool MidiInterface::read(Channel inChannel // Private method: MIDI parser template -bool MidiInterface::parse() { - - if (mTransport.available() == 0) - return false; - - mLastError &= ~(1UL << ErrorParse); // Clear ErrorParse bit - /*Possible Errors: - > SysEx Stop byte received with no pending SysEx Start. - > Unsupported Status byte. - > Data received without a valid Status byte or Running Status. - > Warning SysEx split warning. - .... could potentially add an error for when SysEx is aborted due to receiving a new non-realtime status byte. - */ - - const byte extracted = mTransport.read(); - - if (extracted >= 0x80) { - // Lets try get a valid Status byte. Non-realtime status overides any current Status - const MidiType pendingType = getTypeFromStatusByte(extracted); - switch (pendingType) { - // Realtime - case Start: - case Continue: - case Stop: - case Clock: - case Tick: - case ActiveSensing: - case SystemReset: - case TuneRequest: - // Handle message now - mMessage.type = pendingType; - mMessage.channel = 0; - mMessage.data1 = 0; - mMessage.data2 = 0; - mMessage.length = 1; - mMessage.valid = true; - return true; - break; - // 2 byte messages - case ProgramChange: - case AfterTouchChannel: - case TimeCodeQuarterFrame: - case SongSelect: - mPendingMessage[0] = extracted; - mPendingMessageExpectedLength = 2; - break; - // 3 byte messages - case NoteOn: - case NoteOff: - case ControlChange: - case PitchBend: - case AfterTouchPoly: - case SongPosition: - mPendingMessage[0] = extracted; - mPendingMessageExpectedLength = 3; - break; - // SysEx - case SystemExclusiveStart: - mPendingMessage[0] = SystemExclusive; - mPendingMessageExpectedLength = MidiMessage::sSysExMaxSize; - mMessage.sysexArray[0] = SystemExclusiveStart; - mLastError &= ~(1UL << WarningSplitSysEx); // Reset Warning Split SysEx bit - break; - case SystemExclusiveEnd: - if (mPendingMessage[0] == SystemExclusive) { - mMessage.sysexArray[mPendingMessageIndex++] = SystemExclusiveEnd; // Post Inc pending index here for correct lenght data - mMessage.type = SystemExclusive; - mMessage.data1 = mPendingMessageIndex & 0xff; // LSB - mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB - mMessage.channel = 0; - mMessage.length = mPendingMessageIndex; - if (mMessage.sysexArray[0] == SystemExclusiveEnd) { - // This is the last chunk of a split SysEx message, and is NOT a valid SysEx message (it starts with 0xF7) - mMessage.valid = false; // SysEx message is split so this not technically valid - launchCallback(); // Lets notify callback to deal with this - resetInput(); // Restart message - return false; - } else { - // We are in a valid SysEx message that hasn't overrun (starts with 0xF0) so lets complete it - mMessage.valid = true; - resetInput(); // Restart message - return true; - } - } else { // Looks like a SysEx End without a Sysex Start - mLastError |= 1UL << ErrorParse; // Error: SysEx Stop byte received with no pending SysEx Start. - if (mErrorCallback) - mErrorCallback(mLastError); - resetInput(); // Restart message - return false; - } - break; - // Unsupported - default: - mPendingMessage[0] = InvalidType; - mLastError |= 1UL << ErrorParse; // Error: Unsupported Status byte. - if (mErrorCallback) - mErrorCallback(mLastError); - resetInput(); // Restart message - return false; - break; - } - mPendingMessageIndex = 1; // If we are here, we have a valid Status! Lets try get some Data for it.... - mRunningStatus_RX = InvalidType; // Lets also reset Running Status until we have a complete message - return (Settings::Use1ByteParsing) ? false : parse(); - - } else { - // Lets get some data... First off.. check for Status Byte, or use Running Status - if (mPendingMessageIndex == 0) { - if (mRunningStatus_RX) { - // Yay! We have Running Status - mPendingMessage[0] = mRunningStatus_RX; - mPendingMessageIndex = 1; - } else { - // ooops.... No Status Byte... No Running Status... lets ignore this data - mLastError |= 1UL << ErrorParse; // Error: Data received without a valid Status byte or Running Status. - if (mErrorCallback) - mErrorCallback(mLastError); +bool MidiInterface::parse() +{ + + if (mTransport.available() == 0) return false; - } - } - // Status or Running Status is good so add extracted data byte to pending message - if (mPendingMessage[0] == SystemExclusive) - mMessage.sysexArray[mPendingMessageIndex] = extracted; - else - mPendingMessage[mPendingMessageIndex] = extracted; - - // Now we are going to check if we have reached the end of the message - if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) { - // SysEx larger than the allocated buffer size, - // Split SysEx like so: - // first: 0xF0 .... 0xF0 - // middle: 0xF7 .... 0xF0 - // last: 0xF7 .... 0xF7 - // ***** If the buffer has overrun, this SysEx message can now no longer be considered a valid Midi message and must be dealt with via callbacks only! **** - if (mPendingMessage[0] == SystemExclusive) { - // Warn at start of SysEx split - if (mMessage.sysexArray[0] == SystemExclusiveStart) { - mLastError |= 1UL << WarningSplitSysEx; // We have this error already defined so may as well use it - if (mErrorCallback) - mErrorCallback(mLastError); + mLastError &= ~(1UL << ErrorParse); // Clear ErrorParse bit + /*Possible Errors: + > SysEx Stop byte received with no pending SysEx Start. + > Unsupported Status byte. + > Data received without a valid Status byte or Running Status. + > Warning SysEx split warning. + .... could potentially add an error for when SysEx is aborted due to receiving a new non-realtime status byte. + */ + + const byte extracted = mTransport.read(); + + if (extracted >= 0x80) { + // Lets try get a valid Status byte. Non-realtime status overrides any current Status + const MidiType pendingType = getTypeFromStatusByte(extracted); + switch (pendingType) { + // Realtime + case Start: + case Continue: + case Stop: + case Clock: + case Tick: + case ActiveSensing: + case SystemReset: + case TuneRequest: + // Handle message now + mMessage.type = pendingType; + mMessage.channel = 0; + mMessage.data1 = 0; + mMessage.data2 = 0; + mMessage.length = 1; + mMessage.valid = true; + return true; + break; + // 2 byte messages + case ProgramChange: + case AfterTouchChannel: + case TimeCodeQuarterFrame: + case SongSelect: + mPendingMessage[0] = extracted; + mPendingMessageExpectedLength = 2; + break; + // 3 byte messages + case NoteOn: + case NoteOff: + case ControlChange: + case PitchBend: + case AfterTouchPoly: + case SongPosition: + mPendingMessage[0] = extracted; + mPendingMessageExpectedLength = 3; + break; + // SysEx + case SystemExclusiveStart: + mPendingMessage[0] = SystemExclusive; + mPendingMessageExpectedLength = MidiMessage::sSysExMaxSize; + mMessage.sysexArray[0] = SystemExclusiveStart; + mLastError &= ~(1UL << WarningSplitSysEx); // Reset Warning Split SysEx bit + break; + case SystemExclusiveEnd: + if (mPendingMessage[0] == SystemExclusive) { + mMessage.sysexArray[mPendingMessageIndex++] = SystemExclusiveEnd; // Post Inc pending index here for correct length data + mMessage.type = SystemExclusive; + mMessage.data1 = mPendingMessageIndex & 0xff; // LSB + mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB + mMessage.channel = 0; + mMessage.length = mPendingMessageIndex; + if (mMessage.sysexArray[0] == SystemExclusiveEnd) { + // This is the last chunk of a split SysEx message, and is NOT a valid SysEx message (it starts with 0xF7) + mMessage.valid = false; // SysEx message is split so this is not technically valid + launchCallback(); // Lets notify callback to deal with this + resetInput(); // Restart message + return false; + } else { + // We are in a valid SysEx message that hasn't overrun (starts with 0xF0) so lets complete it + mMessage.valid = true; + resetInput(); // Restart message + return true; + } + } else { // Looks like a SysEx End without a Sysex Start + mLastError |= 1UL << ErrorParse; // Error: SysEx Stop byte received with no pending SysEx Start. + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); // Restart message + return false; + } + break; + // Unsupported + default: + mPendingMessage[0] = InvalidType; + mLastError |= 1UL << ErrorParse; // Error: Unsupported Status byte. + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); // Restart message + return false; + break; } - auto lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; - mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; - mMessage.type = SystemExclusive; - - // Get length - mMessage.data1 = Settings::SysExMaxSize & 0xff; // LSB - mMessage.data2 = byte(Settings::SysExMaxSize >> 8); // MSB - mMessage.channel = 0; - mMessage.length = Settings::SysExMaxSize; - mMessage.valid = false; // SysEx message is split so this is not technically valid - - // Notify callback to deal with the SysEx data chunk - launchCallback(); + mPendingMessageIndex = 1; // If we are here, we have a valid Status! Lets try get some Data for it.... + mRunningStatus_RX = InvalidType; // Lets also reset Running Status until we have a complete message + return (Settings::Use1ByteParsing) ? false : parse(); - // Prep next SysEx data chunk to start with 0xF7 - mMessage.sysexArray[0] = SystemExclusiveEnd; - mMessage.sysexArray[1] = lastByte; - mPendingMessageIndex = 2; - // SysEx buffer has overrun so parse() will no longer return true, and will need to be dealt with via callbacks only - return false; - } + } else { + // Lets get some data... First off.. check for Status Byte, or use Running Status + if (mPendingMessageIndex == 0) { + if (mRunningStatus_RX) { + // Yay! We have Running Status + mPendingMessage[0] = mRunningStatus_RX; + mPendingMessageIndex = 1; + } else { + // ooops.... No Status Byte... No Running Status... lets ignore this data + mLastError |= 1UL << ErrorParse; // Error: Data received without a valid Status byte or Running Status. + if (mErrorCallback) + mErrorCallback(mLastError); + return false; + } + } - // Pending message is complete so lets save it - mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + // Status or Running Status is good so add extracted data byte to pending message + if (mPendingMessage[0] == SystemExclusive) + mMessage.sysexArray[mPendingMessageIndex] = extracted; + else + mPendingMessage[mPendingMessageIndex] = extracted; + + // Now we are going to check if we have reached the end of the message + if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) { + // SysEx larger than the allocated buffer size, + // Split SysEx like so: + // first: 0xF0 .... 0xF0 + // middle: 0xF7 .... 0xF0 + // last: 0xF7 .... 0xF7 + // ***** If the buffer has overrun, this SysEx message can now no longer be considered a valid Midi message and must be dealt with via callbacks only! **** + if (mPendingMessage[0] == SystemExclusive) { + // Warn at start of SysEx split + if (mMessage.sysexArray[0] == SystemExclusiveStart) { + mLastError |= 1UL << WarningSplitSysEx; // We have this error already defined so may as well use it + if (mErrorCallback) + mErrorCallback(mLastError); + } + auto lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; + mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; + mMessage.type = SystemExclusive; + + // Get length + mMessage.data1 = Settings::SysExMaxSize & 0xff; // LSB + mMessage.data2 = byte(Settings::SysExMaxSize >> 8); // MSB + mMessage.channel = 0; + mMessage.length = Settings::SysExMaxSize; + mMessage.valid = false; // SysEx message is split so this is not technically valid + + // Notify callback to deal with the SysEx data chunk + launchCallback(); + + // Prep next SysEx data chunk to start with 0xF7 + mMessage.sysexArray[0] = SystemExclusiveEnd; + mMessage.sysexArray[1] = lastByte; + mPendingMessageIndex = 2; + // SysEx buffer has overrun so parse() will no longer return true, and will need to be dealt with via callbacks only + return false; + } - if (isChannelMessage(mMessage.type)) { - mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); - // Message will be completed soon so lets update RunningStatus now as this is obviously a valid Channel Message. - mRunningStatus_RX = mPendingMessage[0]; - } else - mMessage.channel = 0; + // Pending message is complete so lets save it + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); - mMessage.data1 = mPendingMessage[1]; - // Save data2 only if applicable - mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; - mMessage.length = mPendingMessageExpectedLength; - mMessage.valid = true; + if (isChannelMessage(mMessage.type)) { + mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); + // Message will be completed soon so lets update RunningStatus now as this is obviously a valid Channel Message. + mRunningStatus_RX = mPendingMessage[0]; + } else + mMessage.channel = 0; - // Reset index for next message - mPendingMessageIndex = 0; - //mPendingMessageExpectedLength = 0; // <-- No need to reset this as its valid still for Running Status, or will be updated on next Status byte received. + mMessage.data1 = mPendingMessage[1]; + // Save data2 only if applicable + mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; + mMessage.length = mPendingMessageExpectedLength; + mMessage.valid = true; - return true; - } else { - // We need more data... - mPendingMessageIndex++; + // Reset index for next message + mPendingMessageIndex = 0; + //mPendingMessageExpectedLength = 0; // <-- No need to reset this as its valid still for Running Status, or will be updated on next Status byte received. - return (Settings::Use1ByteParsing) ? false : parse(); + return true; + } else { + // We need more data... + mPendingMessageIndex++; + + return (Settings::Use1ByteParsing) ? false : parse(); + } } - } } // Private method, see midi_Settings.h for documentation From ab3a004fab29459d2e07d35e0a0c62327c5f6e01 Mon Sep 17 00:00:00 2001 From: submodify Date: Fri, 26 Sep 2025 09:48:25 +0100 Subject: [PATCH 3/4] Tidied up Parse in MIDI.hpp Tidied up Parse a little to reduce duplicate code. Also now deals with Undefined Midi Status bytes correctly. Undefined System Common F4 & F5 received will now reset Parse, and Undefined Realtime FD & F9 will be ignored (although F9 is currently used for Tick which is not really Midi spec). TuneRequest is also now treated as System Common (not Realtime), and will also reset Parse. --- src/MIDI.hpp | 204 +++++++++++++++++++++------------------------------ 1 file changed, 85 insertions(+), 119 deletions(-) diff --git a/src/MIDI.hpp b/src/MIDI.hpp index 207503e4..c1c867f3 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -828,37 +828,29 @@ inline bool MidiInterface::read(Channel inChannel // Private method: MIDI parser template -bool MidiInterface::parse() +bool MidiInterface::parse() { if (mTransport.available() == 0) return false; mLastError &= ~(1UL << ErrorParse); // Clear ErrorParse bit - /*Possible Errors: - > SysEx Stop byte received with no pending SysEx Start. - > Unsupported Status byte. - > Data received without a valid Status byte or Running Status. - > Warning SysEx split warning. - .... could potentially add an error for when SysEx is aborted due to receiving a new non-realtime status byte. - */ const byte extracted = mTransport.read(); if (extracted >= 0x80) { - // Lets try get a valid Status byte. Non-realtime status overrides any current Status - const MidiType pendingType = getTypeFromStatusByte(extracted); + // Get Status including Undefined ones so we can properly deal with them + const MidiType pendingType = extracted < 0xF0 ? MidiType(extracted & 0xF0) : MidiType(extracted); switch (pendingType) { - // Realtime + // One byte realtime will complete now case Start: case Continue: case Stop: case Clock: - case Tick: + case Tick: // <---- Is tick relevant anymore?? .... Not part of the MIDI spec case ActiveSensing: case SystemReset: - case TuneRequest: - // Handle message now + // Might as well quickly deal with these now. mMessage.type = pendingType; mMessage.channel = 0; mMessage.data1 = 0; @@ -866,160 +858,134 @@ bool MidiInterface::parse() mMessage.length = 1; mMessage.valid = true; return true; - break; - // 2 byte messages - case ProgramChange: - case AfterTouchChannel: - case TimeCodeQuarterFrame: - case SongSelect: - mPendingMessage[0] = extracted; - mPendingMessageExpectedLength = 2; - break; - // 3 byte messages case NoteOn: case NoteOff: case ControlChange: case PitchBend: case AfterTouchPoly: case SongPosition: - mPendingMessage[0] = extracted; mPendingMessageExpectedLength = 3; break; - // SysEx + case ProgramChange: + case AfterTouchChannel: + case TimeCodeQuarterFrame: + case SongSelect: + mPendingMessageExpectedLength = 2; + mPendingMessage[2] = 0; // Zero unused data + break; + // One byte common will be completed this run (This will also reset running status as this is not realtime) + case TuneRequest: + mPendingMessageExpectedLength = 1; + mPendingMessage[1] = 0; // Zero unused data + mPendingMessage[2] = 0; // Zero unused data + break; case SystemExclusiveStart: - mPendingMessage[0] = SystemExclusive; mPendingMessageExpectedLength = MidiMessage::sSysExMaxSize; mMessage.sysexArray[0] = SystemExclusiveStart; - mLastError &= ~(1UL << WarningSplitSysEx); // Reset Warning Split SysEx bit break; case SystemExclusiveEnd: - if (mPendingMessage[0] == SystemExclusive) { - mMessage.sysexArray[mPendingMessageIndex++] = SystemExclusiveEnd; // Post Inc pending index here for correct length data - mMessage.type = SystemExclusive; - mMessage.data1 = mPendingMessageIndex & 0xff; // LSB - mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB - mMessage.channel = 0; - mMessage.length = mPendingMessageIndex; - if (mMessage.sysexArray[0] == SystemExclusiveEnd) { - // This is the last chunk of a split SysEx message, and is NOT a valid SysEx message (it starts with 0xF7) - mMessage.valid = false; // SysEx message is split so this is not technically valid - launchCallback(); // Lets notify callback to deal with this - resetInput(); // Restart message - return false; - } else { - // We are in a valid SysEx message that hasn't overrun (starts with 0xF0) so lets complete it - mMessage.valid = true; - resetInput(); // Restart message - return true; - } - } else { // Looks like a SysEx End without a Sysex Start - mLastError |= 1UL << ErrorParse; // Error: SysEx Stop byte received with no pending SysEx Start. - if (mErrorCallback) - mErrorCallback(mLastError); - resetInput(); // Restart message - return false; + if (mPendingMessage[0] == SystemExclusiveStart) { // If were currently doing SysEx + mPendingMessageExpectedLength = ++mPendingMessageIndex; // Update expected lenght as we have an EOX and within Buffer + break; } - break; - // Unsupported default: - mPendingMessage[0] = InvalidType; - mLastError |= 1UL << ErrorParse; // Error: Unsupported Status byte. + mLastError |= 1UL << ErrorParse; // Error: Undefined Status or stray EOX if (mErrorCallback) mErrorCallback(mLastError); - resetInput(); // Restart message + if ((pendingType != Undefined_F9) && (pendingType != Undefined_FD)) { // Dont reset for 0xFD & 0xF9 (Undefined Realtime) + resetInput(); // Input reset for stray EOX and Undefined common F4/F5 + } return false; - break; } - mPendingMessageIndex = 1; // If we are here, we have a valid Status! Lets try get some Data for it.... - mRunningStatus_RX = InvalidType; // Lets also reset Running Status until we have a complete message - return (Settings::Use1ByteParsing) ? false : parse(); + mRunningStatus_RX = InvalidType; // Reset Running Status until valid channel message complete + mPendingMessage[0] = extracted; // Status seems good so lets store in pending + if (extracted != SystemExclusiveEnd) mPendingMessageIndex = 1; // Set PendingMessageIndex to 1 (needs to be unchanged for EOX) } else { - // Lets get some data... First off.. check for Status Byte, or use Running Status + // Check Status if (mPendingMessageIndex == 0) { if (mRunningStatus_RX) { - // Yay! We have Running Status mPendingMessage[0] = mRunningStatus_RX; mPendingMessageIndex = 1; } else { - // ooops.... No Status Byte... No Running Status... lets ignore this data - mLastError |= 1UL << ErrorParse; // Error: Data received without a valid Status byte or Running Status. + mLastError |= 1UL << ErrorParse; // Error: No Status if (mErrorCallback) mErrorCallback(mLastError); return false; } } - - // Status or Running Status is good so add extracted data byte to pending message + // Add Data if (mPendingMessage[0] == SystemExclusive) mMessage.sysexArray[mPendingMessageIndex] = extracted; else mPendingMessage[mPendingMessageIndex] = extracted; - - // Now we are going to check if we have reached the end of the message - if (mPendingMessageIndex >= (mPendingMessageExpectedLength - 1)) { - // SysEx larger than the allocated buffer size, + mPendingMessageIndex++; + } + // Check for a complete message + if (mPendingMessageIndex >= mPendingMessageExpectedLength) { + // Process SysEx + if (mPendingMessage[0] == SystemExclusiveStart || mPendingMessage[0] == SystemExclusiveEnd) { + + mMessage.type = SystemExclusive; + mMessage.data1 = mPendingMessageIndex & 0xff; // LSB + mMessage.data2 = byte(mPendingMessageIndex >> 8); // MSB + mMessage.channel = 0; + mMessage.length = mPendingMessageIndex; + + // SysEx can be larger than the allocated buffer size (must handle with callbacks only) // Split SysEx like so: - // first: 0xF0 .... 0xF0 - // middle: 0xF7 .... 0xF0 - // last: 0xF7 .... 0xF7 - // ***** If the buffer has overrun, this SysEx message can now no longer be considered a valid Midi message and must be dealt with via callbacks only! **** - if (mPendingMessage[0] == SystemExclusive) { - // Warn at start of SysEx split + // first: 0xF0 .... 0xF0 + // midlle: 0xF7 .... 0xF0 + // last: 0xF7 .... 0xF7 + + // SysEx buffer not full... EOX + if (mPendingMessage[0] == SystemExclusiveEnd) { + mMessage.sysexArray[mPendingMessageIndex - 1] = SystemExclusiveEnd; + mPendingMessageIndex = 0; + if (mMessage.sysexArray[0] == SystemExclusiveEnd) { // This is the last chunk of split SysEx + mMessage.valid = false; + launchCallback(); + } else { // Not Split SysEx + mMessage.valid = true; + } + } else { // SysEx buffer full if (mMessage.sysexArray[0] == SystemExclusiveStart) { - mLastError |= 1UL << WarningSplitSysEx; // We have this error already defined so may as well use it if (mErrorCallback) - mErrorCallback(mLastError); + mErrorCallback(1UL << WarningSplitSysEx); // Notify but no need to store this warning? } - auto lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; - mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; - mMessage.type = SystemExclusive; - - // Get length - mMessage.data1 = Settings::SysExMaxSize & 0xff; // LSB - mMessage.data2 = byte(Settings::SysExMaxSize >> 8); // MSB - mMessage.channel = 0; - mMessage.length = Settings::SysExMaxSize; - mMessage.valid = false; // SysEx message is split so this is not technically valid - - // Notify callback to deal with the SysEx data chunk + byte lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; // <--- change from Settings::SysExMaxSize to mPendingMessageIndex? + mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; // <--- change from Settings::SysExMaxSize to mPendingMessageIndex? + mMessage.valid = false; launchCallback(); // Prep next SysEx data chunk to start with 0xF7 mMessage.sysexArray[0] = SystemExclusiveEnd; mMessage.sysexArray[1] = lastByte; mPendingMessageIndex = 2; - // SysEx buffer has overrun so parse() will no longer return true, and will need to be dealt with via callbacks only - return false; } - // Pending message is complete so lets save it - mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); - - if (isChannelMessage(mMessage.type)) { - mMessage.channel = getChannelFromStatusByte(mPendingMessage[0]); - // Message will be completed soon so lets update RunningStatus now as this is obviously a valid Channel Message. - mRunningStatus_RX = mPendingMessage[0]; - } else - mMessage.channel = 0; - - mMessage.data1 = mPendingMessage[1]; - // Save data2 only if applicable - mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; - mMessage.length = mPendingMessageExpectedLength; - mMessage.valid = true; - - // Reset index for next message - mPendingMessageIndex = 0; - //mPendingMessageExpectedLength = 0; // <-- No need to reset this as its valid still for Running Status, or will be updated on next Status byte received. + return mMessage.valid; + } - return true; - } else { - // We need more data... - mPendingMessageIndex++; + // Process message + mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); + if (isChannelMessage(mMessage.type)) { + mMessage.channel = (mPendingMessage[0] & 0x0F) + 1; + mRunningStatus_RX = mPendingMessage[0]; + } else + mMessage.channel = 0; + mMessage.data1 = mPendingMessage[1]; + mMessage.data2 = mPendingMessage[2]; + mMessage.length = mPendingMessageExpectedLength; + mMessage.valid = true; + + // Reset index for next message + mPendingMessageIndex = 0; - return (Settings::Use1ByteParsing) ? false : parse(); - } + return true; + } else { + // We need more input... + return (Settings::Use1ByteParsing) ? false : parse(); } } From bcf31f656f3134032c6efc531b3869c568ab3e7d Mon Sep 17 00:00:00 2001 From: submodify Date: Sun, 12 Oct 2025 13:31:17 +0100 Subject: [PATCH 4/4] Update MIDI.hpp Removed the need the RunningStatus_RX variable, and also resetInput() is not really needed anymore --- src/MIDI.hpp | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/MIDI.hpp b/src/MIDI.hpp index c1c867f3..138ccc47 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -893,26 +893,20 @@ bool MidiInterface::parse() if (mErrorCallback) mErrorCallback(mLastError); if ((pendingType != Undefined_F9) && (pendingType != Undefined_FD)) { // Dont reset for 0xFD & 0xF9 (Undefined Realtime) - resetInput(); // Input reset for stray EOX and Undefined common F4/F5 + mPendingMessageIndex = 0; // Message reset for stray EOX and Undefined common F4/F5 } return false; } - mRunningStatus_RX = InvalidType; // Reset Running Status until valid channel message complete mPendingMessage[0] = extracted; // Status seems good so lets store in pending if (extracted != SystemExclusiveEnd) mPendingMessageIndex = 1; // Set PendingMessageIndex to 1 (needs to be unchanged for EOX) } else { // Check Status if (mPendingMessageIndex == 0) { - if (mRunningStatus_RX) { - mPendingMessage[0] = mRunningStatus_RX; - mPendingMessageIndex = 1; - } else { - mLastError |= 1UL << ErrorParse; // Error: No Status - if (mErrorCallback) - mErrorCallback(mLastError); - return false; - } + mLastError |= 1UL << ErrorParse; // Error: No Status + if (mErrorCallback) + mErrorCallback(mLastError); + return false; } // Add Data if (mPendingMessage[0] == SystemExclusive) @@ -953,8 +947,8 @@ bool MidiInterface::parse() if (mErrorCallback) mErrorCallback(1UL << WarningSplitSysEx); // Notify but no need to store this warning? } - byte lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; // <--- change from Settings::SysExMaxSize to mPendingMessageIndex? - mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; // <--- change from Settings::SysExMaxSize to mPendingMessageIndex? + byte lastByte = mMessage.sysexArray[Settings::SysExMaxSize - 1]; + mMessage.sysexArray[Settings::SysExMaxSize - 1] = SystemExclusiveStart; mMessage.valid = false; launchCallback(); @@ -968,20 +962,20 @@ bool MidiInterface::parse() } // Process message - mMessage.type = getTypeFromStatusByte(mPendingMessage[0]); - if (isChannelMessage(mMessage.type)) { + if (mPendingMessage[0] < 0xF0) { // Channel message + mMessage.type = MidiType(mPendingMessage[0] & 0xF0); mMessage.channel = (mPendingMessage[0] & 0x0F) + 1; - mRunningStatus_RX = mPendingMessage[0]; - } else + mPendingMessageIndex = 1; // Set to 1 for running status + } else { // Common message + mMessage.type = MidiType(mPendingMessage[0]); mMessage.channel = 0; + mPendingMessageIndex = 0; + } mMessage.data1 = mPendingMessage[1]; mMessage.data2 = mPendingMessage[2]; mMessage.length = mPendingMessageExpectedLength; mMessage.valid = true; - // Reset index for next message - mPendingMessageIndex = 0; - return true; } else { // We need more input...