From c1662ded34792b3e8746292ad573640b2191c8e9 Mon Sep 17 00:00:00 2001 From: Zoran Vuckovic Date: Fri, 28 Sep 2018 16:07:51 +0200 Subject: [PATCH] Add ALSA MIDI driver --- Makefile.common | 1 + configuration.c | 5 + midi/drivers/alsa_midi.c | 484 +++++++++++++++++++++++++++++++++++++++ midi/midi_driver.c | 4 + 4 files changed, 494 insertions(+) create mode 100644 midi/drivers/alsa_midi.c diff --git a/Makefile.common b/Makefile.common index 2622c46af0..71fd4d0b10 100644 --- a/Makefile.common +++ b/Makefile.common @@ -542,6 +542,7 @@ endif ifeq ($(HAVE_ALSA), 1) OBJ += audio/drivers/alsa.o + OBJ += midi/drivers/alsa_midi.o ifeq ($(HAVE_THREADS), 1) OBJ += audio/drivers/alsathread.o diff --git a/configuration.c b/configuration.c index 0914c19c03..b238f37c32 100644 --- a/configuration.c +++ b/configuration.c @@ -298,6 +298,7 @@ enum record_driver_enum enum midi_driver_enum { MIDI_WINMM = RECORD_NULL + 1, + MIDI_ALSA, MIDI_NULL }; @@ -409,6 +410,8 @@ static enum record_driver_enum RECORD_DEFAULT_DRIVER = RECORD_NULL; #ifdef HAVE_WINMM static enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_WINMM; +#elif defined HAVE_ALSA +static enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_ALSA; #else static enum midi_driver_enum MIDI_DEFAULT_DRIVER = MIDI_NULL; #endif @@ -1047,6 +1050,8 @@ const char *config_get_default_midi(void) { case MIDI_WINMM: return "winmm"; + case MIDI_ALSA: + return "alsa"; case MIDI_NULL: break; } diff --git a/midi/drivers/alsa_midi.c b/midi/drivers/alsa_midi.c new file mode 100644 index 0000000000..671434967e --- /dev/null +++ b/midi/drivers/alsa_midi.c @@ -0,0 +1,484 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2018 The RetroArch team + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include + +#include +#include +#include +#include + +#include "../midi_driver.h" + +typedef struct +{ + snd_seq_t *seq; + snd_seq_addr_t in; + snd_seq_addr_t in_dest; + snd_seq_addr_t out; + snd_seq_addr_t out_src; + int out_queue; + snd_seq_real_time_t out_ev_time; /* time of the last output event */ +} alsa_midi_t; + +static const snd_seq_event_type_t alsa_midi_ev_map[8] = +{ + SND_SEQ_EVENT_NOTEOFF, + SND_SEQ_EVENT_NOTEON, + SND_SEQ_EVENT_KEYPRESS, + SND_SEQ_EVENT_CONTROLLER, + SND_SEQ_EVENT_PGMCHANGE, + SND_SEQ_EVENT_CHANPRESS, + SND_SEQ_EVENT_PITCHBEND, + SND_SEQ_EVENT_SYSEX +}; + +static bool alsa_midi_get_avail_ports(struct string_list *ports, unsigned caps) +{ + int r; + snd_seq_t *seq; + snd_seq_client_info_t *client_info; + snd_seq_port_info_t *port_info; + union string_list_elem_attr attr = {0}; + + snd_seq_client_info_alloca(&client_info); + snd_seq_port_info_alloca(&port_info); + + r = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_open failed with error %d.\n", r); + return false; + } + + snd_seq_client_info_set_client(client_info, -1); + + while (snd_seq_query_next_client(seq, client_info) == 0) + { + int client = snd_seq_client_info_get_client(client_info); + + snd_seq_port_info_set_client(port_info, client); + snd_seq_port_info_set_port(port_info, -1); + + while (snd_seq_query_next_port(seq, port_info) == 0) + { + unsigned port_caps = snd_seq_port_info_get_capability(port_info); + unsigned port_type = snd_seq_port_info_get_type(port_info); + + if ((port_type & SND_SEQ_PORT_TYPE_MIDI_GENERIC) && + (port_caps & caps) == caps) + { + const char *port_name = snd_seq_port_info_get_name(port_info); + + if (!string_list_append(ports, port_name, attr)) + { + RARCH_ERR("[MIDI]: string_list_append failed.\n"); + snd_seq_close(seq); + + return false; + } + } + } + } + + snd_seq_close(seq); + + return true; +} + +static bool alsa_midi_get_port(snd_seq_t *seq, const char *name, unsigned caps, + snd_seq_addr_t *addr) +{ + snd_seq_client_info_t *client_info; + snd_seq_port_info_t *port_info; + + snd_seq_client_info_alloca(&client_info); + snd_seq_port_info_alloca(&port_info); + + snd_seq_client_info_set_client(client_info, -1); + while (snd_seq_query_next_client(seq, client_info) == 0) + { + int client_id = snd_seq_client_info_get_client(client_info); + + snd_seq_port_info_set_client(port_info, client_id); + snd_seq_port_info_set_port(port_info, -1); + + while (snd_seq_query_next_port(seq, port_info) == 0) + { + unsigned port_caps = snd_seq_port_info_get_capability(port_info); + unsigned type = snd_seq_port_info_get_type(port_info); + + if ((type & SND_SEQ_PORT_TYPE_MIDI_GENERIC) && (port_caps & caps) == caps) + { + const char *port_name = snd_seq_port_info_get_name(port_info); + + if (string_is_equal(port_name, name)) + { + addr->client = client_id; + addr->port = snd_seq_port_info_get_port(port_info); + + return true; + } + } + } + } + + return false; +} + +static bool alsa_midi_get_avail_inputs(struct string_list *inputs) +{ + return alsa_midi_get_avail_ports(inputs, SND_SEQ_PORT_CAP_READ | + SND_SEQ_PORT_CAP_SUBS_READ); +} + +static bool alsa_midi_get_avail_outputs(struct string_list *outputs) +{ + return alsa_midi_get_avail_ports(outputs, SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE); +} + +static void alsa_midi_free(void *p) +{ + alsa_midi_t *d = (alsa_midi_t*)p; + + if (d) + { + if (d->seq) + snd_seq_close(d->seq); + free(d); + } +} + +static bool alsa_midi_set_input(void *p, const char *input) +{ + int r; + snd_seq_port_subscribe_t *sub; + alsa_midi_t *d = (alsa_midi_t*)p; + + if (!input) + { + if (d->in_dest.port >= 0) + { + snd_seq_delete_simple_port(d->seq, d->in_dest.port); + d->in_dest.port = -1; + } + + return true; + } + + if (!alsa_midi_get_port(d->seq, input, SND_SEQ_PORT_CAP_READ | + SND_SEQ_PORT_CAP_SUBS_READ, &d->in)) + return false; + + r = snd_seq_create_simple_port(d->seq, "in", SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_APPLICATION); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_create_simple_port failed with error %d.\n", r); + return false; + } + + d->in_dest.client = snd_seq_client_id(d->seq); + d->in_dest.port = r; + + snd_seq_port_subscribe_alloca(&sub); + snd_seq_port_subscribe_set_sender(sub, &d->in); + snd_seq_port_subscribe_set_dest(sub, &d->in_dest); + r = snd_seq_subscribe_port(d->seq, sub); + if (r < 0) + RARCH_ERR("[MIDI]: snd_seq_subscribe_port failed with error %d.\n", r); + + return r >= 0; +} + +static bool alsa_midi_set_output(void *p, const char *output) +{ + int r; + alsa_midi_t *d = (alsa_midi_t*)p; + + if (!output) + { + if (d->out_queue >= 0) + { + snd_seq_stop_queue(d->seq, d->out_queue, NULL); + snd_seq_free_queue(d->seq, d->out_queue); + d->out_queue = -1; + } + if (d->out_src.port >= 0) + { + snd_seq_delete_simple_port(d->seq, d->out_src.port); + d->out_src.port = -1; + } + + return true; + } + + if (!alsa_midi_get_port(d->seq, output, SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, &d->out)) + return false; + + r = snd_seq_create_simple_port(d->seq, "out", SND_SEQ_PORT_CAP_READ | + SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_APPLICATION); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_create_simple_port failed with error %d.\n", r); + return false; + } + + d->out_src.client = snd_seq_client_id(d->seq); + d->out_src.port = r; + + r = snd_seq_connect_to(d->seq, d->out_src.port, d->out.client, d->out.port); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_connect_to failed with error %d.\n", r); + return false; + } + + d->out_queue = snd_seq_alloc_queue(d->seq); + if (d->out_queue < 0) + { + RARCH_ERR("[MIDI]: snd_seq_alloc_queue failed with error %d.\n", d->out_queue); + return false; + } + + r = snd_seq_start_queue(d->seq, d->out_queue, NULL); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_start_queue failed with error %d.\n", r); + return false; + } + + return true; +} + +static void *alsa_midi_init(const char *input, const char *output) +{ + int r; + bool err = false; + alsa_midi_t *d = (alsa_midi_t*)calloc(sizeof(alsa_midi_t), 1); + + if (!d) + { + RARCH_ERR("[MIDI]: Out of memory.\n"); + return NULL; + } + + d->in_dest.port = -1; + d->out_src.port = -1; + d->out_queue = -1; + + r = snd_seq_open(&d->seq, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK); + if (r < 0) + { + RARCH_ERR("[MIDI]: snd_seq_open failed with error %d.\n", r); + err = true; + } + else if (!alsa_midi_set_input(d, input)) + err = true; + else if (!alsa_midi_set_output(d, output)) + err = true; + + if (err) + { + alsa_midi_free(d); + d = NULL; + } + + return d; +} + +static bool alsa_midi_read(void *p, midi_event_t *event) +{ + int r; + snd_seq_event_t *ev; + alsa_midi_t *d = (alsa_midi_t*)p; + + r = snd_seq_event_input(d->seq, &ev); + if (r < 0) + { +#ifdef DEBUG + if (r != -EAGAIN) + RARCH_ERR("[MIDI]: snd_seq_event_input failed with error %d.\n", r); +#endif + return false; + } + + if (ev->type == SND_SEQ_EVENT_NOTEOFF) + { + event->data[0] = 0x80 | ev->data.note.channel; + event->data[1] = ev->data.note.note; + event->data[2] = ev->data.note.velocity; + event->data_size = 3; + } + else if (ev->type == SND_SEQ_EVENT_NOTEON) + { + event->data[0] = 0x90 | ev->data.note.channel; + event->data[1] = ev->data.note.note; + event->data[2] = ev->data.note.velocity; + event->data_size = 3; + } + else if (ev->type == SND_SEQ_EVENT_KEYPRESS) + { + event->data[0] = 0xA0 | ev->data.note.channel; + event->data[1] = ev->data.note.note; + event->data[2] = ev->data.note.velocity; + event->data_size = 3; + } + else if (ev->type == SND_SEQ_EVENT_CONTROLLER) + { + event->data[0] = 0xB0 | ev->data.control.channel; + event->data[1] = ev->data.control.param; + event->data[2] = ev->data.control.value; + event->data_size = 3; + } + else if (ev->type == SND_SEQ_EVENT_PGMCHANGE) + { + event->data[0] = 0xC0 | ev->data.control.channel; + event->data[1] = ev->data.control.value; + event->data_size = 2; + } + else if (ev->type == SND_SEQ_EVENT_CHANPRESS) + { + event->data[0] = 0xD0 | ev->data.control.channel; + event->data[1] = ev->data.control.value; + event->data_size = 2; + } + else if (ev->type == SND_SEQ_EVENT_PITCHBEND) + { + event->data[0] = 0xE0 | ev->data.control.channel; + event->data[1] = ev->data.control.value & 127; + event->data[2] = ev->data.control.value >> 7; + event->data_size = 3; + } + else if (ev->type == SND_SEQ_EVENT_SYSEX) + { + if (ev->data.ext.len <= event->data_size) + { + size_t i; + uint8_t *ev_data = (uint8_t*)ev->data.ext.ptr; + + for (i = 0; i < ev->data.ext.len; ++i) + event->data[i] = ev_data[i]; + + event->data_size = ev->data.ext.len; + } +#ifdef DEBUG + else + { + RARCH_ERR("[MIDI]: SysEx event too big.\n"); + r = -1; + } +#endif + } + else + r = -1; + + event->delta_time = 0; + snd_seq_free_event(ev); + + return r >= 0; +} + +static bool alsa_midi_write(void *p, const midi_event_t *event) +{ + int r; + snd_seq_event_t ev; + alsa_midi_t *d = (alsa_midi_t*)p; + + ev.type = alsa_midi_ev_map[(event->data[0] >> 4) & 7]; + ev.flags = SND_SEQ_TIME_STAMP_REAL | SND_SEQ_TIME_MODE_ABS; + ev.queue = d->out_queue; + ev.time.time.tv_sec = d->out_ev_time.tv_sec + event->delta_time / 1000000; + ev.time.time.tv_nsec = d->out_ev_time.tv_nsec + + (event->delta_time % 1000000) * 1000; + if(ev.time.time.tv_nsec >= 1000000000) + { + ev.time.time.tv_sec += 1; + ev.time.time.tv_nsec -= 1000000000; + } + ev.source.port = d->out_src.port; + ev.dest.client = SND_SEQ_ADDRESS_SUBSCRIBERS; + + if (ev.type == SND_SEQ_EVENT_NOTEOFF || ev.type == SND_SEQ_EVENT_NOTEON || + ev.type == SND_SEQ_EVENT_KEYPRESS) + { + ev.data.note.channel = event->data[0] & 0x0F; + ev.data.note.note = event->data[1]; + ev.data.note.velocity = event->data[2]; + } + else if (ev.type == SND_SEQ_EVENT_CONTROLLER) + { + ev.data.control.channel = event->data[0] & 0x0F; + ev.data.control.param = event->data[1]; + ev.data.control.value = event->data[2]; + } + else if (ev.type == SND_SEQ_EVENT_PGMCHANGE || + ev.type == SND_SEQ_EVENT_CHANPRESS) + { + ev.data.control.channel = event->data[0] & 0x0F; + ev.data.control.value = event->data[1]; + } + else if (ev.type == SND_SEQ_EVENT_PITCHBEND) + { + ev.data.control.channel = event->data[0] & 0x0F; + ev.data.control.value = event->data[1] | (event->data[2] << 7); + } + else if (ev.type == SND_SEQ_EVENT_SYSEX) + { + ev.flags |= SND_SEQ_EVENT_LENGTH_VARIABLE; + ev.data.ext.ptr = event->data; + ev.data.ext.len = event->data_size; + } + + r = snd_seq_event_output(d->seq, &ev); +#ifdef DEBUG + if (r < 0) + RARCH_ERR("[MIDI]: snd_seq_event_output failed with error %d.\n", r); +#endif + + d->out_ev_time.tv_sec = ev.time.time.tv_sec; + d->out_ev_time.tv_nsec = ev.time.time.tv_nsec; + + return r >= 0; +} + +static bool alsa_midi_flush(void *p) +{ + int r; + alsa_midi_t *d = (alsa_midi_t*)p; + + r = snd_seq_drain_output(d->seq); +#ifdef DEBUG + if (r < 0) + RARCH_ERR("[MIDI]: snd_seq_drain_output failed with error %d.\n", r); +#endif + + return r == 0; +} + +midi_driver_t midi_alsa = { + "alsa", + alsa_midi_get_avail_inputs, + alsa_midi_get_avail_outputs, + alsa_midi_init, + alsa_midi_free, + alsa_midi_set_input, + alsa_midi_set_output, + alsa_midi_read, + alsa_midi_write, + alsa_midi_flush +}; diff --git a/midi/midi_driver.c b/midi/midi_driver.c index d4f651f810..012a62d39a 100644 --- a/midi/midi_driver.c +++ b/midi/midi_driver.c @@ -27,8 +27,12 @@ extern midi_driver_t midi_null; extern midi_driver_t midi_winmm; +extern midi_driver_t midi_alsa; static midi_driver_t *midi_drivers[] = { +#ifdef HAVE_ALSA + &midi_alsa, +#endif #ifdef HAVE_WINMM &midi_winmm, #endif