diff --git a/.gitignore b/.gitignore index 2c12cfc..eaad794 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ build/ src/.DS_Store examples/.DS_Store .DS_Store -test/xcode \ No newline at end of file +test/xcode +.development diff --git a/examples/ErrorCallback/ErrorCallback.ino b/examples/ErrorCallback/ErrorCallback.ino new file mode 100644 index 0000000..bd280ec --- /dev/null +++ b/examples/ErrorCallback/ErrorCallback.ino @@ -0,0 +1,36 @@ +#include + +// Simple tutorial on how to receive and send MIDI messages. +// Here, when receiving any message on channel 4, the Arduino +// will blink a led and play back a note for 1 second. + +MIDI_CREATE_DEFAULT_INSTANCE(); +USING_NAMESPACE_MIDI + +void handleError(int8_t err) +{ + if (bitRead(err, ErrorActiveSensingTimeout)) + Serial.println("ActiveSensing Timeout"); + else + Serial.println("ActiveSensing OK"); + + digitalWrite(LED_BUILTIN, (err == 0)? LOW : HIGH); +} + +void setup() +{ + Serial.begin(115200); + while (!Serial) {} + Serial.println("booting"); + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); + + MIDI.setHandleError(handleError); + MIDI.begin(1); +} + +void loop() +{ + MIDI.read(); +} diff --git a/src/MIDI.h b/src/MIDI.h index 76964c0..880afa1 100644 --- a/src/MIDI.h +++ b/src/MIDI.h @@ -102,6 +102,7 @@ public: inline void sendSongPosition(unsigned inBeats); inline void sendSongSelect(DataByte inSongNumber); inline void sendTuneRequest(); + inline void sendRealTime(MidiType inType); inline void beginRpn(unsigned inNumber, @@ -168,6 +169,7 @@ public: public: inline void setHandleMessage(void (*fptr)(const MidiMessage&)) { mMessageCallback = fptr; }; + inline void setHandleError(ErrorCallback fptr) { mErrorCallback = fptr; } inline void setHandleNoteOff(NoteOffCallback fptr) { mNoteOffCallback = fptr; } inline void setHandleNoteOn(NoteOnCallback fptr) { mNoteOnCallback = fptr; } inline void setHandleAfterTouchPoly(AfterTouchPolyCallback fptr) { mAfterTouchPolyCallback = fptr; } @@ -193,6 +195,7 @@ private: void launchCallback(); void (*mMessageCallback)(const MidiMessage& message) = nullptr; + ErrorCallback mErrorCallback = nullptr; NoteOffCallback mNoteOffCallback = nullptr; NoteOnCallback mNoteOnCallback = nullptr; AfterTouchPolyCallback mAfterTouchPolyCallback = nullptr; @@ -251,12 +254,23 @@ private: Thru::Mode mThruFilterMode : 7; MidiMessage mMessage; + int8_t mLastError; + unsigned long mLastMessageSentTime; + unsigned long mLastMessageReceivedTime; unsigned long mSenderActiveSensingPeriodicity; + bool mReceiverActiveSensingActivated; private: inline StatusByte getStatus(MidiType inType, Channel inChannel) const; + + inline void UpdateLastSentTime() + { + if (mSenderActiveSensingPeriodicity) + mLastMessageSentTime = Platform::now(); + }; + }; // ----------------------------------------------------------------------------- diff --git a/src/MIDI.hpp b/src/MIDI.hpp index 9cd234c..3fe87bb 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -42,8 +42,10 @@ inline MidiInterface::MidiInterface(Transport& in , mCurrentNrpnNumber(0xffff) , mLastMessageSentTime(0) , mSenderActiveSensingPeriodicity(0) + , mReceiverActiveSensingActivated(false) , mThruActivated(false) , mThruFilterMode(Thru::Full) + , mLastError(0) { } @@ -138,6 +140,7 @@ void MidiInterface::send(const MidiMessage& inMes } } mTransport.endTransmission(); + UpdateLastSentTime(); } @@ -199,15 +202,13 @@ void MidiInterface::send(MidiType inType, } mTransport.endTransmission(); + UpdateLastSentTime(); } } else if (inType >= Clock && inType <= SystemReset) { sendRealTime(inType); // System Real-time and 1 byte. } - - if (mSenderActiveSensingPeriodicity) - mLastMessageSentTime = Platform::now(); } // ----------------------------------------------------------------------------- @@ -371,7 +372,8 @@ void MidiInterface::sendSysEx(unsigned inLength, mTransport.write(MidiType::SystemExclusiveEnd); mTransport.endTransmission(); - } + UpdateLastSentTime(); + } if (Settings::UseRunningStatus) mRunningStatus_TX = InvalidType; @@ -486,6 +488,7 @@ void MidiInterface::sendRealTime(MidiType inType) { mTransport.write((byte)inType); mTransport.endTransmission(); + UpdateLastSentTime(); } break; default: @@ -690,6 +693,7 @@ inline bool MidiInterface::read() template inline bool MidiInterface::read(Channel inChannel) { + #ifndef RegionActiveSending // Active Sensing. This message is intended to be sent // repeatedly to tell the receiver that a connection is alive. Use // of this message is optional. When initially received, the @@ -700,18 +704,49 @@ inline bool MidiInterface::read(Channel inChannel // normal (non- active sensing) operation. if ((mSenderActiveSensingPeriodicity > 0) && (Platform::now() - mLastMessageSentTime) > mSenderActiveSensingPeriodicity) { - sendRealTime(ActiveSensing); + sendActiveSensing(); mLastMessageSentTime = Platform::now(); } + if (mReceiverActiveSensingActivated && (mLastMessageReceivedTime + ActiveSensingTimeout < Platform::now())) + { + mReceiverActiveSensingActivated = false; + + bitSet(mLastError, ErrorActiveSensingTimeout); + if (mErrorCallback) + mErrorCallback(mLastError); + } + #endif + if (inChannel >= MIDI_CHANNEL_OFF) return false; // MIDI Input disabled. if (!parse()) return false; + + #ifndef RegionActiveSending + if (mMessage.type == ActiveSensing) + { + // When an ActiveSensing message is received, the time keeping is activated. + // When a timeout occurs, an error message is send and time keeping ends. + mReceiverActiveSensingActivated = true; + + if (bitRead(mLastError, ErrorActiveSensingTimeout)) + { + bitClear(mLastError, ErrorActiveSensingTimeout); + if (mErrorCallback) + mErrorCallback(mLastError); + } + } + + // Keep the time of the last received message, so we can check for the timeout + if (mReceiverActiveSensingActivated) + mLastMessageReceivedTime = Platform::now(); + + #endif handleNullVelocityNoteOnAsNoteOff(); - + const bool channelMatch = inputFilter(inChannel); if (channelMatch) launchCallback(); @@ -730,6 +765,8 @@ bool MidiInterface::parse() if (mTransport.available() == 0) return false; // No data available. + bitClear(mLastError, ErrorParse); + // Parsing algorithm: // Get a byte from the serial buffer. // If there is no pending message to be recomposed, start a new one. @@ -742,7 +779,7 @@ bool MidiInterface::parse() const byte extracted = mTransport.read(); // Ignore Undefined - if (extracted == 0xf9 || extracted == 0xfd) + if (extracted == Undefined_F9 || extracted == Undefined_FD) return (Settings::Use1ByteParsing) ? false : parse(); if (mPendingMessageIndex == 0) @@ -825,6 +862,10 @@ bool MidiInterface::parse() case InvalidType: default: // This is obviously wrong. Let's get the hell out'a here. + bitSet(mLastError, ErrorParse); + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); return false; break; @@ -919,6 +960,10 @@ bool MidiInterface::parse() else { // Well well well.. error. + bitSet(mLastError, ErrorParse); + if (mErrorCallback) + mErrorCallback(mLastError); + resetInput(); return false; } @@ -977,10 +1022,8 @@ bool MidiInterface::parse() mMessage.channel = 0; mMessage.data1 = mPendingMessage[1]; - // Save data2 only if applicable - mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; - + mMessage.data2 = mPendingMessageExpectedLength == 3 ? mPendingMessage[2] : 0; mMessage.length = mPendingMessageExpectedLength; // Reset local variables @@ -1239,7 +1282,7 @@ template void MidiInterface::launchCallback() { if (mMessageCallback != 0) mMessageCallback(mMessage); - + // The order is mixed to allow frequent messages to trigger their callback faster. switch (mMessage.type) { diff --git a/src/midi_Defs.h b/src/midi_Defs.h index b1228b2..d7b460f 100644 --- a/src/midi_Defs.h +++ b/src/midi_Defs.h @@ -51,6 +51,10 @@ BEGIN_MIDI_NAMESPACE #define MIDI_PITCHBEND_MIN -8192 #define MIDI_PITCHBEND_MAX 8191 +/*! Receiving Active Sensing +*/ +static const uint16_t ActiveSensingTimeout = 300; + // ----------------------------------------------------------------------------- // Type definitions @@ -59,9 +63,15 @@ typedef byte DataByte; typedef byte Channel; typedef byte FilterMode; +// ----------------------------------------------------------------------------- +// Errors +static const uint8_t ErrorParse = 0; +static const uint8_t ErrorActiveSensingTimeout = 1; + // ----------------------------------------------------------------------------- // Aliasing +using ErrorCallback = void (*)(int8_t); using NoteOffCallback = void (*)(Channel channel, byte note, byte velocity); using NoteOnCallback = void (*)(Channel channel, byte note, byte velocity); using AfterTouchPolyCallback = void (*)(Channel channel, byte note, byte velocity); @@ -99,12 +109,17 @@ enum MidiType: uint8_t TimeCodeQuarterFrame = 0xF1, ///< System Common - MIDI Time Code Quarter Frame SongPosition = 0xF2, ///< System Common - Song Position Pointer SongSelect = 0xF3, ///< System Common - Song Select + Undefined_F4 = 0xF4, + Undefined_F5 = 0xF5, TuneRequest = 0xF6, ///< System Common - Tune Request SystemExclusiveEnd = 0xF7, ///< System Exclusive End Clock = 0xF8, ///< System Real Time - Timing Clock + Undefined_F9 = 0xF9, + Tick = Undefined_F9, Start = 0xFA, ///< System Real Time - Start Continue = 0xFB, ///< System Real Time - Continue Stop = 0xFC, ///< System Real Time - Stop + Undefined_FD = 0xFD, ActiveSensing = 0xFE, ///< System Real Time - Active Sensing SystemReset = 0xFF, ///< System Real Time - System Reset }; diff --git a/src/midi_Settings.h b/src/midi_Settings.h index d5ef1c8..bb2d013 100644 --- a/src/midi_Settings.h +++ b/src/midi_Settings.h @@ -70,8 +70,10 @@ struct DefaultSettings termination, the receiver will turn off all voices and return to normal (non- active sensing) operation. - Setting this field to 0 will disable MIDI active sensing. - Typical value is 300 (ms) - an Active Sensing command is send every 300ms. + Typical value is 250 (ms) - an Active Sensing command is send every 250ms. + (All Roland devices send Active Sensing every 250ms) + + Setting this field to 0 will disable sending MIDI active sensing. */ static const uint16_t SenderActiveSensingPeriodicity = 0;