RetroArch/midi/drivers/alsa_midi.c
2021-12-04 16:11:02 +01:00

486 lines
13 KiB
C

/* 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 <http://www.gnu.org/licenses/>.
*/
#include <alsa/asoundlib.h>
#include <libretro.h>
#include <lists/string_list.h>
#include <string/stdstring.h>
#include "../midi_driver.h"
#include "../../verbosity.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)) - 0x2000;
}
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
};