#pragma once #include "MIDI_Pipes.hpp" #include "MIDI_Sender.hpp" #include "MIDI_Staller.hpp" #include #include #include #include #include BEGIN_CS_NAMESPACE constexpr auto MIDI_BAUD = 31250; class MIDI_Callbacks; /** * @brief An abstract class for MIDI interfaces. */ class MIDI_Interface : public TrueMIDI_SinkSource, public MIDI_Sender, public AH::Updatable, protected MIDIStaller { protected: MIDI_Interface() = default; MIDI_Interface(MIDI_Interface &&) = default; public: /// Destructor. virtual ~MIDI_Interface(); /// Initialize the MIDI Interface. void begin() override {} /// Read the MIDI interface and call the callback if a message was received. void update() override = 0; /// @name Default MIDI Interfaces /// @{ /// Set this MIDI interface as the default interface. void setAsDefault(); /// Return the default MIDI interface. If the default MIDI interface was /// configured explicitly using @ref setAsDefault(), that interface is /// returned. If it wasn't set, or if that MIDI interface no longer exists, /// this function returns the newest MIDI interface, the one that was /// constructed most recently. If no MIDI interfaces exist, `nullptr` is /// returned. static MIDI_Interface *getDefault(); /// @} /// @name MIDI Input Callbacks /// @{ /// Set the callbacks that will be called when a MIDI message is received. /// @param cb /// A pointer to the callback object. void setCallbacks(MIDI_Callbacks *cb) { this->callbacks = cb; } /// Set the callbacks that will be called when a MIDI message is received. /// @param cb /// A reference to the callback object. void setCallbacks(MIDI_Callbacks &cb) { setCallbacks(&cb); } /// @} protected: friend class MIDI_Sender; /// Low-level function for sending a MIDI channel voice message. virtual void sendChannelMessageImpl(ChannelMessage) = 0; /// Low-level function for sending a MIDI system common message. virtual void sendSysCommonImpl(SysCommonMessage) = 0; /// Low-level function for sending a system exclusive MIDI message. virtual void sendSysExImpl(SysExMessage) = 0; /// Low-level function for sending a MIDI real-time message. virtual void sendRealTimeImpl(RealTimeMessage) = 0; /// Low-level function for sending any buffered outgoing MIDI messages. virtual void sendNowImpl() = 0; public: #if DISABLE_PIPES virtual MIDIReadEvent read() = 0; virtual ChannelMessage getChannelMessage() const = 0; virtual SysCommonMessage getSysCommonMessage() const = 0; virtual RealTimeMessage getRealTimeMessage() const = 0; virtual SysExMessage getSysExMessage() const = 0; #endif protected: #if !DISABLE_PIPES /// Accept an incoming MIDI Channel message from the source pipe. void sinkMIDIfromPipe(ChannelMessage msg) override { send(msg); } /// Accept an incoming MIDI System Exclusive message from the source pipe. void sinkMIDIfromPipe(SysExMessage msg) override { send(msg); } /// Accept an incoming MIDI System Common message from the source pipe. void sinkMIDIfromPipe(SysCommonMessage msg) override { send(msg); } /// Accept an incoming MIDI Real-Time message from the source pipe. void sinkMIDIfromPipe(RealTimeMessage msg) override { send(msg); } #endif protected: /// Call the channel message callback and send the message to the sink pipe. void onChannelMessage(ChannelMessage message); /// Call the SysEx message callback and send the message to the sink pipe. void onSysExMessage(SysExMessage message); /// Call the System Common message callback and send the message to the sink /// pipe. void onSysCommonMessage(SysCommonMessage message); /// Call the real-time message callback and send the message to the sink /// pipe. void onRealTimeMessage(RealTimeMessage message); public: /// Read, parse and dispatch incoming MIDI messages on the given interface. template static void updateIncoming(MIDIInterface_t *iface); /// Dispatch the given type of MIDI message from the given interface. template static void dispatchIncoming(MIDIInterface_t *iface, MIDIReadEvent event); #if !DISABLE_PIPES /// Un-stall the given MIDI interface. Assumes the interface has been /// stalled because of a chunked SysEx messages. Waits until that message /// is finished. template static void handleStall(MIDIInterface_t *self); using MIDIStaller::handleStall; #endif private: MIDI_Callbacks *callbacks = nullptr; private: static MIDI_Interface *DefaultMIDI_Interface; }; template void MIDI_Interface::updateIncoming(MIDIInterface_t *self) { #if DISABLE_PIPES MIDIReadEvent event = self->read(); while (event != MIDIReadEvent::NO_MESSAGE) { dispatchIncoming(self, event); event = self->read(); } #else MIDIReadEvent event = self->read(); if (event == MIDIReadEvent::NO_MESSAGE) return; if (self->getStaller() == self) self->unstall(self); int16_t size_rem = 512 * 3 / 4; // Don't keep on reading for too long bool chunked = false; // Whether there's an unterminated SysEx chunk while (event != MIDIReadEvent::NO_MESSAGE) { dispatchIncoming(self, event); if (event == MIDIReadEvent::SYSEX_CHUNK) { size_rem -= self->getSysExMessage().length; chunked = true; } else if (event == MIDIReadEvent::SYSEX_MESSAGE) { size_rem -= self->getSysExMessage().length; chunked = false; } else { size_rem -= 3; } if (size_rem < 0) break; event = self->read(); } if (chunked) self->stall(self); #endif // TODO: add logic to detect MIDI messages such as (N)RPN that span over // multiple channel voice messages and that shouldn't be interrupted. // For short messages such as (N)RPN, I suggest waiting with a timeout. } template void MIDI_Interface::dispatchIncoming(MIDIInterface_t *self, MIDIReadEvent event) { switch (event) { case MIDIReadEvent::CHANNEL_MESSAGE: self->onChannelMessage(self->getChannelMessage()); break; case MIDIReadEvent::SYSEX_CHUNK: // fallthrough case MIDIReadEvent::SYSEX_MESSAGE: self->onSysExMessage(self->getSysExMessage()); break; case MIDIReadEvent::SYSCOMMON_MESSAGE: self->onSysCommonMessage(self->getSysCommonMessage()); break; case MIDIReadEvent::REALTIME_MESSAGE: self->onRealTimeMessage(self->getRealTimeMessage()); break; case MIDIReadEvent::NO_MESSAGE: break; // LCOV_EXCL_LINE default: break; // LCOV_EXCL_LINE } } #if !DISABLE_PIPES template void MIDI_Interface::handleStall(MIDIInterface_t *self) { const char *staller_name = self->getStallerName(); DEBUGFN(F("Handling stall. Cause: ") << staller_name); self->unstall(self); unsigned long startTime = millis(); while (millis() - startTime < SYSEX_CHUNK_TIMEOUT) { MIDIReadEvent event = self->read(); dispatchIncoming(self, event); if (event == MIDIReadEvent::SYSEX_CHUNK) startTime = millis(); // reset timeout else if (event == MIDIReadEvent::SYSEX_MESSAGE) return; } DEBUGFN(F("Warning: Unable to un-stall pipes. Cause: ") << staller_name); static_cast(staller_name); } #endif END_CS_NAMESPACE