To use the node in your project you should have the e/serial-midi library installed. Use the “File → Add Library” menu item in XOD IDE if you don’t have it yet. See Using libraries for more info.
C++ implementation
// Most of the code below is taken from Arduino MIDI Library (https://github.com/FortySevenEffects/arduino_midi_library)
// and modified to work as a XOD node.
enum MidiType {
InvalidType = 0x00, ///< For notifying errors
NoteOff = 0x80, ///< Note Off
NoteOn = 0x90, ///< Note On
AfterTouchPoly = 0xA0, ///< Polyphonic AfterTouch
ControlChange = 0xB0, ///< Control Change / Channel Mode
ProgramChange = 0xC0, ///< Program Change
AfterTouchChannel = 0xD0, ///< Channel (monophonic) AfterTouch
PitchBend = 0xE0, ///< Pitch Bend
SystemExclusive = 0xF0, ///< System Exclusive
TimeCodeQuarterFrame = 0xF1, ///< System Common - MIDI Time Code Quarter Frame
SongPosition = 0xF2, ///< System Common - Song Position Pointer
SongSelect = 0xF3, ///< System Common - Song Select
TuneRequest = 0xF6, ///< System Common - Tune Request
Clock = 0xF8, ///< System Real Time - Timing Clock
Start = 0xFA, ///< System Real Time - Start
Continue = 0xFB, ///< System Real Time - Continue
Stop = 0xFC, ///< System Real Time - Stop
ActiveSensing = 0xFE, ///< System Real Time - Active Sensing
SystemReset = 0xFF, ///< System Real Time - System Reset
};
struct State {
MidiType runningStatus = InvalidType;
uint8_t pendingMessage[3] = {0, 0, 0};
uint8_t pendingMessageExpectedLength = 0;
uint8_t pendingMessageIndex = 0;
};
// clang-format off
{{ GENERATED_CODE }}
// clang-format on
MidiType getTypeFromStatusByte(byte inStatus) {
if ((inStatus < 0x80) ||
(inStatus == 0xf4) ||
(inStatus == 0xf5) ||
(inStatus == 0xf9) ||
(inStatus == 0xfD)) {
// Data bytes and undefined.
return InvalidType;
}
if (inStatus < 0xf0) {
// Channel message, remove channel nibble.
return MidiType(inStatus & 0xf0);
}
return MidiType(inStatus);
}
inline uint8_t getChannelFromStatusByte(byte inStatus) {
return (inStatus & 0x0f) + 1;
}
bool isChannelMessage(MidiType inType) {
return (inType == NoteOff ||
inType == NoteOn ||
inType == ControlChange ||
inType == AfterTouchPoly ||
inType == AfterTouchChannel ||
inType == PitchBend ||
inType == ProgramChange);
}
inline void resetInput(State* state) {
state->pendingMessageIndex = 0;
state->pendingMessageExpectedLength = 0;
state->runningStatus = InvalidType;
}
void evaluate(Context ctx) {
auto uart = getValue<input_MIDI>(ctx);
// TODO: is this really necessary?
if (uart->getBaudRate() != 31250) {
uart->changeBaudRate(31250);
}
if (!isInputDirty<input_RCV>(ctx))
return;
if (!uart->available())
return;
auto state = getState(ctx);
// 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, emit it along with the "DONE" pulse.
uint8_t extracted = 0x00;
if (!uart->readByte(&extracted))
return;
// Ignore Undefined
if (extracted == 0xf9 || extracted == 0xfd)
return;
if (state->pendingMessageIndex == 0) {
// Start a new pending message
state->pendingMessage[0] = extracted;
// Check for running status first
if (isChannelMessage(getTypeFromStatusByte(state->runningStatus))) {
// Only these types allow Running Status
// If the status byte is not received, prepend it
// to the pending message
if (extracted < 0x80) {
state->pendingMessage[0] = state->runningStatus;
state->pendingMessage[1] = extracted;
state->pendingMessageIndex = 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.
}
switch (getTypeFromStatusByte(state->pendingMessage[0])) {
// 1 byte messages
case Start:
case Continue:
case Stop:
case Clock:
case ActiveSensing:
case SystemReset:
case TuneRequest: {
// Handle the message type directly here.
ValueType<output_MSG>::T msg = {};
msg.type = getTypeFromStatusByte(state->pendingMessage[0]);
msg.channel = 0;
msg.data1 = 0;
msg.data2 = 0;
emitValue<output_MSG>(ctx, msg);
emitValue<output_DONE>(ctx, 1);
// Do not reset all input attributes, Running Status must remain unchanged.
// We still need to reset these
state->pendingMessageIndex = 0;
state->pendingMessageExpectedLength = 0;
return;
}
// 2 byte messages
case ProgramChange:
case AfterTouchChannel:
case TimeCodeQuarterFrame:
case SongSelect: {
state->pendingMessageExpectedLength = 2;
break;
}
// 3 byte messages
case NoteOn:
case NoteOff:
case ControlChange:
case PitchBend:
case AfterTouchPoly:
case SongPosition: {
state->pendingMessageExpectedLength = 3;
break;
}
case SystemExclusive: {
// The message can be any length
// between 3 and MidiMessage::sSysExMaxSize bytes
state->pendingMessageExpectedLength = 3;
state->runningStatus = InvalidType;
// TODO: properly handle SysEx messages
break;
}
case InvalidType:
default: {
resetInput(state);
return;
}
}
if (state->pendingMessageIndex >= (state->pendingMessageExpectedLength - 1)) {
// Reception complete
ValueType<output_MSG>::T msg = {};
msg.type = getTypeFromStatusByte(state->pendingMessage[0]);
msg.channel = getChannelFromStatusByte(state->pendingMessage[0]);
msg.data1 = state->pendingMessage[1];
msg.data2 = 0; // Completed new message has 1 data byte
emitValue<output_MSG>(ctx, msg);
emitValue<output_DONE>(ctx, 1);
state->pendingMessageIndex = 0;
state->pendingMessageExpectedLength = 0;
return;
} else {
// Waiting for more data
state->pendingMessageIndex++;
}
// Message is not complete, wait for another call
return;
} 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 Continue:
case Stop:
case ActiveSensing:
case SystemReset:
// Here we will have to extract the one-byte message,
// emit it, 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.
ValueType<output_MSG>::T msg = {};
msg.type = extracted;
msg.channel = 0;
msg.data1= 0;
msg.data2 = 0;
emitValue<output_MSG>(ctx, msg);
emitValue<output_DONE>(ctx, 1);
return;
// End of Exclusive
case 0xf7:
// TODO: properly handle SysEx messages
resetInput(state);
return;
default:
break; // LCOV_EXCL_LINE - Coverage blind spot
}
}
// Add extracted data byte to pending message
if (state->pendingMessage[0] == SystemExclusive) {
// TODO: properly handle SysEx messages
} else {
state->pendingMessage[state->pendingMessageIndex] = extracted;
}
// Now we are going to check if we have reached the end of the message
if (state->pendingMessageIndex >= (state->pendingMessageExpectedLength - 1)) {
// "FML" case: fall down here with an overflown SysEx..
// This means we received the last possible data byte that can fit the buffer.
if (state->pendingMessage[0] == SystemExclusive) {
resetInput(state);
return;
}
ValueType<output_MSG>::T msg = {};
msg.type = getTypeFromStatusByte(state->pendingMessage[0]);
if (isChannelMessage(msg.type))
msg.channel = getChannelFromStatusByte(state->pendingMessage[0]);
else
msg.channel = 0;
msg.data1 = state->pendingMessage[1];
// Save data2 only if applicable
msg.data2 = state->pendingMessageExpectedLength == 3 ? state->pendingMessage[2] : 0;
// Reset local variables
state->pendingMessageIndex = 0;
state->pendingMessageExpectedLength = 0;
// Handle NoteOn meaasges with 0 velocity as NoteOff
if (msg.type == NoteOn && msg.data2 == 0) {
msg.type = NoteOff;
}
emitValue<output_MSG>(ctx, msg);
emitValue<output_DONE>(ctx, 1);
// Activate running status (if enabled for the received type)
switch (msg.type) {
case NoteOff:
case NoteOn:
case AfterTouchPoly:
case ControlChange:
case ProgramChange:
case AfterTouchChannel:
case PitchBend:
// Running status enabled: store it from received message
state->runningStatus = state->pendingMessage[0];
break;
default:
// No running status
state->runningStatus = InvalidType;
break;
}
} else {
// Message is not complete.
// Update the index of the pending message.
state->pendingMessageIndex++;
}
}
}