Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix MIDI input with ALSA #54309

Merged
merged 1 commit into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 117 additions & 67 deletions drivers/alsamidi/midi_driver_alsamidi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,85 +37,135 @@

#include <errno.h>

static int get_message_size(uint8_t message) {
switch (message & 0xF0) {
case 0x80: // note off
case 0x90: // note on
case 0xA0: // aftertouch
case 0xB0: // continuous controller
case 0xE0: // pitch bend
case 0xF2: // song position pointer
return 3;

case 0xC0: // patch change
case 0xD0: // channel pressure
case 0xF1: // time code quarter frame
case 0xF3: // song select
MIDIDriverALSAMidi::MessageCategory MIDIDriverALSAMidi::msg_category(uint8_t msg_part) {
if (msg_part >= 0xf8) {
return MessageCategory::RealTime;
} else if (msg_part >= 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
// behaviour is significantly different.
if (msg_part == 0xf0) {
return MessageCategory::SysExBegin;
} else if (msg_part == 0xf7) {
return MessageCategory::SysExEnd;
}
return MessageCategory::SystemCommon;
} else if (msg_part >= 0x80) {
return MessageCategory::Voice;
}
return MessageCategory::Data;
}

size_t MIDIDriverALSAMidi::msg_expected_data(uint8_t status_byte) {
if (msg_category(status_byte) == MessageCategory::Voice) {
// Voice messages have a channel number in the status byte, mask it out.
status_byte &= 0xf0;
}

switch (status_byte) {
case 0x80: // Note Off
case 0x90: // Note On
case 0xA0: // Polyphonic Key Pressure (Aftertouch)
case 0xB0: // Control Change (CC)
case 0xE0: // Pitch Bend Change
case 0xF2: // Song Position Pointer
return 2;

case 0xF0: // SysEx start
case 0xF4: // reserved
case 0xF5: // reserved
case 0xF6: // tune request
case 0xF7: // SysEx end
case 0xF8: // timing clock
case 0xF9: // reserved
case 0xFA: // start
case 0xFB: // continue
case 0xFC: // stop
case 0xFD: // reserved
case 0xFE: // active sensing
case 0xFF: // reset
case 0xC0: // Program Change
case 0xD0: // Channel Pressure (Aftertouch)
case 0xF1: // MIDI Time Code Quarter Frame
case 0xF3: // Song Select
return 1;
}

return 256;
return 0;
}

void MIDIDriverALSAMidi::InputConnection::parse_byte(uint8_t byte, MIDIDriverALSAMidi &driver,
uint64_t timestamp) {
switch (msg_category(byte)) {
case MessageCategory::RealTime:
// Real-Time messages are single byte messages that can
// occur at any point.
// We pass them straight through.
driver.receive_input_packet(timestamp, &byte, 1);
break;

case MessageCategory::Data:
// We don't currently forward System Exclusive messages so skip their data.
// Collect any expected data for other message types.
if (!skipping_sys_ex && expected_data > received_data) {
buffer[received_data + 1] = byte;
received_data++;

// Forward a complete message and reset relevant state.
if (received_data == expected_data) {
driver.receive_input_packet(timestamp, buffer, received_data + 1);
received_data = 0;

if (msg_category(buffer[0]) != MessageCategory::Voice) {
// Voice Category messages can be sent with "running status".
// This means they don't resend the status byte until it changes.
// For other categories, we reset expected data, to require a new status byte.
expected_data = 0;
}
}
}
break;

case MessageCategory::SysExBegin:
buffer[0] = byte;
skipping_sys_ex = true;
break;

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

case MessageCategory::Voice:
case MessageCategory::SystemCommon:
buffer[0] = byte;
received_data = 0;
expected_data = msg_expected_data(byte);
skipping_sys_ex = false;
if (expected_data == 0) {
driver.receive_input_packet(timestamp, &byte, 1);
}
break;
}
}

int MIDIDriverALSAMidi::InputConnection::read_in(MIDIDriverALSAMidi &driver, uint64_t timestamp) {
int ret;
do {
uint8_t byte = 0;
ret = snd_rawmidi_read(rawmidi_ptr, &byte, 1);

if (ret < 0) {
if (ret != -EAGAIN) {
ERR_PRINT("snd_rawmidi_read error: " + String(snd_strerror(ret)));
}
} else {
parse_byte(byte, driver, timestamp);
}
} while (ret > 0);

return ret;
}

void MIDIDriverALSAMidi::thread_func(void *p_udata) {
MIDIDriverALSAMidi *md = static_cast<MIDIDriverALSAMidi *>(p_udata);
uint64_t timestamp = 0;
uint8_t buffer[256];
int expected_size = 255;
int bytes = 0;

while (!md->exit_thread.is_set()) {
int ret;

md->lock();

for (int i = 0; i < md->connected_inputs.size(); i++) {
snd_rawmidi_t *midi_in = md->connected_inputs[i];
do {
uint8_t byte = 0;
ret = snd_rawmidi_read(midi_in, &byte, 1);
if (ret < 0) {
if (ret != -EAGAIN) {
ERR_PRINT("snd_rawmidi_read error: " + String(snd_strerror(ret)));
}
} else {
if (byte & 0x80) {
// Flush previous packet if there is any
if (bytes) {
md->receive_input_packet(timestamp, buffer, bytes);
bytes = 0;
}
expected_size = get_message_size(byte);
// After a SysEx start, all bytes are data until a SysEx end, so
// we're going to end the command at the SES, and let the common
// driver ignore the following data bytes.
}
InputConnection *connections = md->connected_inputs.ptrw();
size_t connection_count = md->connected_inputs.size();

if (bytes < 256) {
buffer[bytes++] = byte;
// If we know the size of the current packet receive it if it reached the expected size
if (bytes >= expected_size) {
md->receive_input_packet(timestamp, buffer, bytes);
bytes = 0;
}
}
}
} while (ret > 0);
for (size_t i = 0; i < connection_count; i++) {
connections[i].read_in(*md, timestamp);
}

md->unlock();
Expand All @@ -139,7 +189,7 @@ Error MIDIDriverALSAMidi::open() {
snd_rawmidi_t *midi_in;
int ret = snd_rawmidi_open(&midi_in, nullptr, name, SND_RAWMIDI_NONBLOCK);
if (ret >= 0) {
connected_inputs.insert(i++, midi_in);
connected_inputs.insert(i++, InputConnection(midi_in));
}
}

Expand All @@ -160,7 +210,7 @@ void MIDIDriverALSAMidi::close() {
thread.wait_to_finish();

for (int i = 0; i < connected_inputs.size(); i++) {
snd_rawmidi_t *midi_in = connected_inputs[i];
snd_rawmidi_t *midi_in = connected_inputs[i].rawmidi_ptr;
snd_rawmidi_close(midi_in);
}
connected_inputs.clear();
Expand All @@ -179,7 +229,7 @@ PackedStringArray MIDIDriverALSAMidi::get_connected_inputs() {

lock();
for (int i = 0; i < connected_inputs.size(); i++) {
snd_rawmidi_t *midi_in = connected_inputs[i];
snd_rawmidi_t *midi_in = connected_inputs[i].rawmidi_ptr;
snd_rawmidi_info_t *info;

snd_rawmidi_info_malloc(&info);
Expand Down
38 changes: 37 additions & 1 deletion drivers/alsamidi/midi_driver_alsamidi.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,48 @@ class MIDIDriverALSAMidi : public MIDIDriver {
Thread thread;
Mutex mutex;

Vector<snd_rawmidi_t *> connected_inputs;
class InputConnection {
public:
InputConnection() = default;
InputConnection(snd_rawmidi_t *midi_in) :
rawmidi_ptr{ midi_in } {}

// Read in and parse available data, forwarding any complete messages through the driver.
int read_in(MIDIDriverALSAMidi &driver, uint64_t timestamp);

snd_rawmidi_t *rawmidi_ptr = nullptr;

private:
static const size_t MSG_BUFFER_SIZE = 3;
uint8_t buffer[MSG_BUFFER_SIZE] = { 0 };
size_t expected_data = 0;
size_t received_data = 0;
bool skipping_sys_ex = false;
void parse_byte(uint8_t byte, MIDIDriverALSAMidi &driver, uint64_t timestamp);
};

Vector<InputConnection> connected_inputs;

SafeFlag exit_thread;

static void thread_func(void *p_udata);

enum class MessageCategory {
Data,
Voice,
SysExBegin,
SystemCommon, // excluding System Exclusive Begin/End
SysExEnd,
RealTime,
};

// If the passed byte is a status byte, return the associated message category,
// else return MessageCategory::Data.
static MessageCategory msg_category(uint8_t msg_part);

// Return the number of data bytes expected for the provided status byte.
static size_t msg_expected_data(uint8_t status_byte);

void lock() const;
void unlock() const;

Expand Down