219 lines
7.8 KiB
C++
219 lines
7.8 KiB
C++
#pragma once
|
|
|
|
#include "MIDI_Pipes.hpp"
|
|
#include "MIDI_Sender.hpp"
|
|
#include "MIDI_Staller.hpp"
|
|
#include <AH/Containers/Updatable.hpp>
|
|
#include <Def/Cable.hpp>
|
|
#include <Def/Channel.hpp>
|
|
#include <Def/MIDIAddress.hpp>
|
|
#include <MIDI_Parsers/MIDI_Parser.hpp>
|
|
|
|
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<MIDI_Interface>,
|
|
public AH::Updatable<MIDI_Interface>,
|
|
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<MIDI_Interface>;
|
|
/// 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 <class MIDIInterface_t>
|
|
static void updateIncoming(MIDIInterface_t *iface);
|
|
/// Dispatch the given type of MIDI message from the given interface.
|
|
template <class MIDIInterface_t>
|
|
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 <class MIDIInterface_t>
|
|
static void handleStall(MIDIInterface_t *self);
|
|
using MIDIStaller::handleStall;
|
|
#endif
|
|
|
|
private:
|
|
MIDI_Callbacks *callbacks = nullptr;
|
|
|
|
private:
|
|
static MIDI_Interface *DefaultMIDI_Interface;
|
|
};
|
|
|
|
template <class MIDIInterface_t>
|
|
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 <class MIDIInterface_t>
|
|
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 <class MIDIInterface_t>
|
|
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<void>(staller_name);
|
|
}
|
|
#endif
|
|
|
|
END_CS_NAMESPACE
|