Skip to content

Commit

Permalink
Move MIDI parsing up from ALSA driver to platform independent driver.
Browse files Browse the repository at this point in the history
Aims for more consistent MIDI support across Windows, MacOS, Linux and
to provide a base for adding MIDI drivers for other platforms.
Reworks the MIDIDriverALSAMidi MIDI parsing implementation as a platform
independent version in MIDIDriver::Parser.
Uses MIDIDriver::Parser to provide running status support in MacOS
MIDIDriverCoreMidi.
Collects connected input names at open, ensuring devices indices reported
in events match names in array returned from get_connected_inputs.

Fixes #77035.
Fixes #79811.

With code review changes by: A Thousand Ships (she/her)
<96648715+AThousandShips@users.noreply.github.com>
  • Loading branch information
ibrahn committed Jun 25, 2024
1 parent 6b281c0 commit 607c5ec
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 316 deletions.
199 changes: 139 additions & 60 deletions core/os/midi_driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,88 +38,167 @@ MIDIDriver *MIDIDriver::get_singleton() {
return singleton;
}

void MIDIDriver::set_singleton() {
MIDIDriver::MIDIDriver() {
singleton = this;
}

void MIDIDriver::receive_input_packet(int device_index, uint64_t timestamp, uint8_t *data, uint32_t length) {
Ref<InputEventMIDI> event;
event.instantiate();
event->set_device(device_index);
uint32_t param_position = 1;

if (length >= 1) {
if (data[0] >= 0xF0) {
// channel does not apply to system common messages
event->set_channel(0);
event->set_message(MIDIMessage(data[0]));
last_received_message = data[0];
} else if ((data[0] & 0x80) == 0x00) {
// running status
event->set_channel(last_received_message & 0xF);
event->set_message(MIDIMessage(last_received_message >> 4));
param_position = 0;
MIDIDriver::MessageCategory MIDIDriver::Parser::category(uint8_t p_midi_fragment) {
if (p_midi_fragment >= 0xf8) {
return MessageCategory::RealTime;
} else if (p_midi_fragment >= 0xf0) {
// System Exclusive begin/end are specified as System Common Category
// messages, but we separate them here and give them their own categories
// as their behavior is significantly different.
if (p_midi_fragment == 0xf0) {
return MessageCategory::SysExBegin;
} else if (p_midi_fragment == 0xf7) {
return MessageCategory::SysExEnd;
}
return MessageCategory::SystemCommon;
} else if (p_midi_fragment >= 0x80) {
return MessageCategory::Voice;
}
return MessageCategory::Data;
}

MIDIMessage MIDIDriver::Parser::status_to_msg_enum(uint8_t p_status_byte) {
if (p_status_byte & 0x80) {
if (p_status_byte < 0xf0) {
return MIDIMessage(p_status_byte >> 4);
} else {
event->set_channel(data[0] & 0xF);
event->set_message(MIDIMessage(data[0] >> 4));
param_position = 1;
last_received_message = data[0];
return MIDIMessage(p_status_byte);
}
}
return MIDIMessage::NONE;
}

switch (event->get_message()) {
case MIDIMessage::AFTERTOUCH:
if (length >= 2 + param_position) {
event->set_pitch(data[param_position]);
event->set_pressure(data[param_position + 1]);
}
break;
size_t MIDIDriver::Parser::expected_data(uint8_t p_status_byte) {
return expected_data(status_to_msg_enum(p_status_byte));
}

size_t MIDIDriver::Parser::expected_data(MIDIMessage p_msg_type) {
switch (p_msg_type) {
case MIDIMessage::NOTE_OFF:
case MIDIMessage::NOTE_ON:
case MIDIMessage::AFTERTOUCH:
case MIDIMessage::CONTROL_CHANGE:
if (length >= 2 + param_position) {
event->set_controller_number(data[param_position]);
event->set_controller_value(data[param_position + 1]);
}
break;
case MIDIMessage::PITCH_BEND:
case MIDIMessage::SONG_POSITION_POINTER:
return 2;
case MIDIMessage::PROGRAM_CHANGE:
case MIDIMessage::CHANNEL_PRESSURE:
case MIDIMessage::QUARTER_FRAME:
case MIDIMessage::SONG_SELECT:
return 1;
default:
return 0;
}
}

case MIDIMessage::NOTE_ON:
uint8_t MIDIDriver::Parser::channel(uint8_t p_status_byte) {
if (category(p_status_byte) == MessageCategory::Voice) {
return p_status_byte & 0x0f;
}
return 0;
}

void MIDIDriver::send_event(int p_device_index, uint8_t p_status,
const uint8_t *p_data, size_t p_data_len) {
const MIDIMessage msg = Parser::status_to_msg_enum(p_status);
ERR_FAIL_COND(p_data_len < Parser::expected_data(msg));

Ref<InputEventMIDI> event;
event.instantiate();
event->set_device(p_device_index);
event->set_channel(Parser::channel(p_status));
event->set_message(msg);
switch (msg) {
case MIDIMessage::NOTE_OFF:
if (length >= 2 + param_position) {
event->set_pitch(data[param_position]);
event->set_velocity(data[param_position + 1]);
}
case MIDIMessage::NOTE_ON:
event->set_pitch(p_data[0]);
event->set_velocity(p_data[1]);
break;

case MIDIMessage::PITCH_BEND:
if (length >= 2 + param_position) {
event->set_pitch((data[param_position + 1] << 7) | data[param_position]);
}
case MIDIMessage::AFTERTOUCH:
event->set_pitch(p_data[0]);
event->set_pressure(p_data[1]);
break;
case MIDIMessage::CONTROL_CHANGE:
event->set_controller_number(p_data[0]);
event->set_controller_value(p_data[1]);
break;

case MIDIMessage::PROGRAM_CHANGE:
if (length >= 1 + param_position) {
event->set_instrument(data[param_position]);
}
event->set_instrument(p_data[0]);
break;

case MIDIMessage::CHANNEL_PRESSURE:
if (length >= 1 + param_position) {
event->set_pressure(data[param_position]);
}
event->set_pressure(p_data[0]);
break;
case MIDIMessage::PITCH_BEND:
event->set_pitch((p_data[1] << 7) | p_data[0]);
break;
// QUARTER_FRAME, SONG_POSITION_POINTER, and SONG_SELECT not yet implemented.
default:
break;
}

Input *id = Input::get_singleton();
id->parse_input_event(event);
Input::get_singleton()->parse_input_event(event);
}

PackedStringArray MIDIDriver::get_connected_inputs() {
PackedStringArray list;
return list;
void MIDIDriver::Parser::parse_fragment(uint8_t p_fragment) {
switch (category(p_fragment)) {
case MessageCategory::RealTime:
// Real-Time messages are single byte messages that can
// occur at any point and do not interrupt other messages.
// We pass them straight through.
MIDIDriver::send_event(device_index, p_fragment);
break;

case MessageCategory::SysExBegin:
status_byte = p_fragment;
skipping_sys_ex = true;
break;

case MessageCategory::SysExEnd:
status_byte = 0;
skipping_sys_ex = false;
break;

case MessageCategory::Voice:
case MessageCategory::SystemCommon:
skipping_sys_ex = false; // If we were in SysEx, assume it was aborted.
received_data_len = 0;
status_byte = 0;
ERR_FAIL_COND(expected_data(p_fragment) > DATA_BUFFER_SIZE);
if (expected_data(p_fragment) == 0) {
// No data bytes needed, post it now.
MIDIDriver::send_event(device_index, p_fragment);
} else {
status_byte = p_fragment;
}
break;

case MessageCategory::Data:
// We don't currently process SysEx messages, so ignore their data.
if (!skipping_sys_ex) {
const size_t expected = expected_data(status_byte);
if (received_data_len < expected) {
data_buffer[received_data_len] = p_fragment;
received_data_len++;
if (received_data_len == expected) {
MIDIDriver::send_event(device_index, status_byte,
data_buffer, expected);
received_data_len = 0;
// Voice messages can use 'running status', sending further
// messages without resending their status byte.
// For other messages types we clear the cached status byte.
if (category(status_byte) != MessageCategory::Voice) {
status_byte = 0;
}
}
}
}
break;
}
}

MIDIDriver::MIDIDriver() {
set_singleton();
PackedStringArray MIDIDriver::get_connected_inputs() const {
return connected_input_names;
}
68 changes: 61 additions & 7 deletions core/os/midi_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,73 @@ class MIDIDriver {
static MIDIDriver *singleton;
static uint8_t last_received_message;

protected:
// Categories of message for parser logic.
enum class MessageCategory {
Data,
Voice,
SysExBegin,
SystemCommon, // excluding System Exclusive Begin/End
SysExEnd,
RealTime,
};

// Convert midi data to InputEventMIDI and send it to Input.
// p_data_len is the length of the buffer passed at p_data, this must be
// at least equal to the data required by the passed message type, but
// may be larger. Only the required data will be read.
static void send_event(int p_device_index, uint8_t p_status,
const uint8_t *p_data = nullptr, size_t p_data_len = 0);

class Parser {
public:
Parser() = default;
Parser(int p_device_index) :
device_index{ p_device_index } {}
virtual ~Parser() = default;

// Push a byte of MIDI stream. Any completed messages will be
// forwarded to MIDIDriver::send_event.
void parse_fragment(uint8_t p_fragment);

static MessageCategory category(uint8_t p_midi_fragment);

// If the byte is a Voice Message status byte return the contained
// channel number, otherwise zero.
static uint8_t channel(uint8_t p_status_byte);

// If the byte is a status byte for a message with a fixed number of
// additional data bytes, return the number expected, otherwise zero.
static size_t expected_data(uint8_t p_status_byte);
static size_t expected_data(MIDIMessage p_msg_type);

// If the fragment is a status byte return the message type
// represented, otherwise MIDIMessage::NONE.
static MIDIMessage status_to_msg_enum(uint8_t p_status_byte);

private:
int device_index = 0;

static constexpr size_t DATA_BUFFER_SIZE = 2;

uint8_t status_byte = 0;
uint8_t data_buffer[DATA_BUFFER_SIZE] = { 0 };
size_t received_data_len = 0;
bool skipping_sys_ex = false;
};

PackedStringArray connected_input_names;

public:
static MIDIDriver *get_singleton();
void set_singleton();

MIDIDriver();
virtual ~MIDIDriver() = default;

virtual Error open() = 0;
virtual void close() = 0;

virtual PackedStringArray get_connected_inputs();

static void receive_input_packet(int device_index, uint64_t timestamp, uint8_t *data, uint32_t length);

MIDIDriver();
virtual ~MIDIDriver() {}
PackedStringArray get_connected_inputs() const;
};

#endif // MIDI_DRIVER_H
Loading

0 comments on commit 607c5ec

Please sign in to comment.