From df1a250beb787e54f81518d2786d78d654f082ba Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sun, 19 Jan 2025 12:14:32 +0000 Subject: Add local aplaymidi version, for adding clock (WIP) --- aplaymidi-mp.c | 1051 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1051 insertions(+) create mode 100644 aplaymidi-mp.c (limited to 'aplaymidi-mp.c') diff --git a/aplaymidi-mp.c b/aplaymidi-mp.c new file mode 100644 index 0000000..8ad9630 --- /dev/null +++ b/aplaymidi-mp.c @@ -0,0 +1,1051 @@ +/* + * aplaymidi.c - play Standard MIDI Files to sequencer port(s) + * + * Copyright (c) 2004-2006 Clemens Ladisch + * + * + * This program 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 Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* TODO: sequencer queue timer selection */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include "version.h" +#include + +/* + * 31.25 kbaud, one start bit, eight data bits, two stop bits. + * (The MIDI spec says one stop bit, but every transmitter uses two, just to be + * sure, so we better not exceed that to avoid overflowing the output buffer.) + */ +#define MIDI_BYTES_PER_SEC (31250 / (1 + 8 + 2)) + +/* + * A MIDI event after being parsed/loaded from the file. + * There could be made a case for using snd_seq_event_t instead. + */ +struct event { + struct event *next; /* linked list */ + + unsigned char type; /* SND_SEQ_EVENT_xxx */ + unsigned char port; /* port index */ + unsigned int tick; + union { + unsigned char d[3]; /* channel and data bytes */ + int tempo; + unsigned int length; /* length of sysex data */ + } data; + unsigned char sysex[0]; +}; + +struct track { + struct event *first_event; /* list of all events in this track */ + int end_tick; /* length of this track */ + + struct event *current_event; /* used while loading and playing */ +}; + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t *ports; +static int queue; +static int end_delay = 2; +static const char *file_name; +static FILE *file; +static int file_offset; /* current offset in input file */ +static int num_tracks; +static struct track *tracks; +static int smpte_timing; +static int ump_mode; +static int add_clock; + +/* prints an error message to stderr */ +static void errormsg(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +/* memory allocation error handling */ +static void check_mem(void *p) +{ + if (!p) + fatal("Out of memory"); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + /* set our name (otherwise it's "Client-xxx") */ + err = snd_seq_set_client_name(seq, "aplaymidi"); + check_snd("set client name", err); + + /* find out who we actually are */ + client = snd_seq_client_id(seq); + check_snd("get client id", client); +} + +/* parses one or more port addresses from the string */ +static void parse_ports(const char *arg) +{ + char *buf, *s, *port_name; + int err; + + /* make a copy of the string because we're going to modify it */ + buf = strdup(arg); + check_mem(buf); + + for (port_name = s = buf; s; port_name = s + 1) { + /* Assume that ports are separated by commas. We don't use + * spaces because those are valid in client names. */ + s = strchr(port_name, ','); + if (s) + *s = '\0'; + + ++port_count; + ports = realloc(ports, port_count * sizeof(snd_seq_addr_t)); + check_mem(ports); + + err = snd_seq_parse_address(seq, &ports[port_count - 1], port_name); + if (err < 0) + fatal("Invalid port %s - %s", port_name, snd_strerror(err)); + } + + free(buf); +} + +static void create_source_port(void) +{ + snd_seq_port_info_t *pinfo; + int err; + + snd_seq_port_info_alloca(&pinfo); + + /* the first created port is 0 anyway, but let's make sure ... */ + snd_seq_port_info_set_port(pinfo, 0); + snd_seq_port_info_set_port_specified(pinfo, 1); + + snd_seq_port_info_set_name(pinfo, "aplaymidi"); + + snd_seq_port_info_set_capability(pinfo, 0); /* sic */ + snd_seq_port_info_set_type(pinfo, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + + err = snd_seq_create_port(seq, pinfo); + check_snd("create port", err); +} + +static void create_queue(void) +{ + queue = snd_seq_alloc_named_queue(seq, "aplaymidi"); + check_snd("create queue", queue); + /* the queue is now locked, which is just fine */ +} + +static void connect_ports(void) +{ + int i, err; + + /* + * We send MIDI events with explicit destination addresses, so we don't + * need any connections to the playback ports. But we connect to those + * anyway to force any underlying RawMIDI ports to remain open while + * we're playing - otherwise, ALSA would reset the port after every + * event. + */ + for (i = 0; i < port_count; ++i) { + err = snd_seq_connect_to(seq, 0, ports[i].client, ports[i].port); + if (err < 0) + fatal("Cannot connect to port %d:%d - %s", + ports[i].client, ports[i].port, snd_strerror(err)); + } +} + +static int read_byte(void) +{ + ++file_offset; + return getc(file); +} + +/* reads a little-endian 32-bit integer */ +static int read_32_le(void) +{ + int value; + value = read_byte(); + value |= read_byte() << 8; + value |= read_byte() << 16; + value |= read_byte() << 24; + return !feof(file) ? value : -1; +} + +/* reads a 4-character identifier */ +static int read_id(void) +{ + return read_32_le(); +} +#define MAKE_ID(c1, c2, c3, c4) ((c1) | ((c2) << 8) | ((c3) << 16) | ((c4) << 24)) + +/* reads a fixed-size big-endian number */ +static int read_int(int bytes) +{ + int c, value = 0; + + do { + c = read_byte(); + if (c == EOF) + return -1; + value = (value << 8) | c; + } while (--bytes); + return value; +} + +/* reads a variable-length number */ +static int read_var(void) +{ + int value, c; + + c = read_byte(); + value = c & 0x7f; + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | (c & 0x7f); + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | (c & 0x7f); + if (c & 0x80) { + c = read_byte(); + value = (value << 7) | c; + if (c & 0x80) + return -1; + } + } + } + return !feof(file) ? value : -1; +} + +/* allocates a new event */ +static struct event *new_event(struct track *track, int sysex_length) +{ + struct event *event; + + event = malloc(sizeof(struct event) + sysex_length); + check_mem(event); + + event->next = NULL; + + /* append at the end of the track's linked list */ + if (track->current_event) + track->current_event->next = event; + else + track->first_event = event; + track->current_event = event; + + return event; +} + +static void skip(int bytes) +{ + while (bytes > 0) + read_byte(), --bytes; +} + +/* reads one complete track from the file */ +static int read_track(struct track *track, int track_end) +{ + int tick = 0; + unsigned char last_cmd = 0; + unsigned char port = 0; + + /* the current file position is after the track ID and length */ + while (file_offset < track_end) { + unsigned char cmd; + struct event *event; + int delta_ticks, len, c; + + delta_ticks = read_var(); + if (delta_ticks < 0) + break; + tick += delta_ticks; + + c = read_byte(); + if (c < 0) + break; + + if (c & 0x80) { + /* have command */ + cmd = c; + if (cmd < 0xf0) + last_cmd = cmd; + } else { + /* running status */ + ungetc(c, file); + file_offset--; + cmd = last_cmd; + if (!cmd) + goto _error; + } + + switch (cmd >> 4) { + /* maps SMF events to ALSA sequencer events */ + static const unsigned char cmd_type[] = { + [0x8] = SND_SEQ_EVENT_NOTEOFF, + [0x9] = SND_SEQ_EVENT_NOTEON, + [0xa] = SND_SEQ_EVENT_KEYPRESS, + [0xb] = SND_SEQ_EVENT_CONTROLLER, + [0xc] = SND_SEQ_EVENT_PGMCHANGE, + [0xd] = SND_SEQ_EVENT_CHANPRESS, + [0xe] = SND_SEQ_EVENT_PITCHBEND + }; + + case 0x8: /* channel msg with 2 parameter bytes */ + case 0x9: + case 0xa: + case 0xb: + case 0xe: + event = new_event(track, 0); + event->type = cmd_type[cmd >> 4]; + event->port = port; + event->tick = tick; + event->data.d[0] = cmd & 0x0f; + event->data.d[1] = read_byte() & 0x7f; + event->data.d[2] = read_byte() & 0x7f; + break; + + case 0xc: /* channel msg with 1 parameter byte */ + case 0xd: + event = new_event(track, 0); + event->type = cmd_type[cmd >> 4]; + event->port = port; + event->tick = tick; + event->data.d[0] = cmd & 0x0f; + event->data.d[1] = read_byte() & 0x7f; + break; + + case 0xf: + switch (cmd) { + case 0xf0: /* sysex */ + case 0xf7: /* continued sysex, or escaped commands */ + len = read_var(); + if (len < 0) + goto _error; + if (cmd == 0xf0) + ++len; + event = new_event(track, len); + event->type = SND_SEQ_EVENT_SYSEX; + event->port = port; + event->tick = tick; + event->data.length = len; + if (cmd == 0xf0) { + event->sysex[0] = 0xf0; + c = 1; + } else { + c = 0; + } + for (; c < len; ++c) + event->sysex[c] = read_byte(); + break; + + case 0xff: /* meta event */ + c = read_byte(); + len = read_var(); + if (len < 0) + goto _error; + + switch (c) { + case 0x21: /* port number */ + if (len < 1) + goto _error; + port = read_byte() % port_count; + skip(len - 1); + break; + + case 0x2f: /* end of track */ + track->end_tick = tick; + skip(track_end - file_offset); + return 1; + + case 0x51: /* tempo */ + if (len < 3) + goto _error; + if (smpte_timing) { + /* SMPTE timing doesn't change */ + skip(len); + } else { + event = new_event(track, 0); + event->type = SND_SEQ_EVENT_TEMPO; + event->port = port; + event->tick = tick; + event->data.tempo = read_byte() << 16; + event->data.tempo |= read_byte() << 8; + event->data.tempo |= read_byte(); + skip(len - 3); + } + break; + + default: /* ignore all other meta events */ + skip(len); + break; + } + break; + + default: /* invalid Fx command */ + goto _error; + } + break; + + default: /* cannot happen */ + goto _error; + } + } +_error: + errormsg("%s: invalid MIDI data (offset %#x)", file_name, file_offset); + return 0; +} + +/* reads an entire MIDI file */ +static int read_smf(void) +{ + int header_len, type, time_division, i, err; + snd_seq_queue_tempo_t *queue_tempo; + + /* the curren position is immediately after the "MThd" id */ + header_len = read_int(4); + if (header_len < 6) { +invalid_format: + errormsg("%s: invalid file format", file_name); + return 0; + } + + type = read_int(2); + if (type != 0 && type != 1) { + errormsg("%s: type %d format is not supported", file_name, type); + return 0; + } + + num_tracks = read_int(2); + if (num_tracks < 1 || num_tracks > 1000) { + errormsg("%s: invalid number of tracks (%d)", file_name, num_tracks); + num_tracks = 0; + return 0; + } + tracks = calloc(num_tracks, sizeof(struct track)); + if (!tracks) { + errormsg("out of memory"); + num_tracks = 0; + return 0; + } + + time_division = read_int(2); + if (time_division < 0) + goto invalid_format; + + /* interpret and set tempo */ + snd_seq_queue_tempo_alloca(&queue_tempo); + smpte_timing = !!(time_division & 0x8000); + if (!smpte_timing) { + /* time_division is ticks per quarter */ + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); /* default: 120 bpm */ + snd_seq_queue_tempo_set_ppq(queue_tempo, time_division); + } else { + /* upper byte is negative frames per second */ + i = 0x80 - ((time_division >> 8) & 0x7f); + /* lower byte is ticks per frame */ + time_division &= 0xff; + /* now pretend that we have quarter-note based timing */ + switch (i) { + case 24: + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 12 * time_division); + break; + case 25: + snd_seq_queue_tempo_set_tempo(queue_tempo, 400000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 10 * time_division); + break; + case 29: /* 30 drop-frame */ + snd_seq_queue_tempo_set_tempo(queue_tempo, 100000000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 2997 * time_division); + break; + case 30: + snd_seq_queue_tempo_set_tempo(queue_tempo, 500000); + snd_seq_queue_tempo_set_ppq(queue_tempo, 15 * time_division); + break; + default: + errormsg("%s: invalid number of SMPTE frames per second (%d)", + file_name, i); + return 0; + } + } + err = snd_seq_set_queue_tempo(seq, queue, queue_tempo); + if (err < 0) { + errormsg("Cannot set queue tempo (%u/%i)", + snd_seq_queue_tempo_get_tempo(queue_tempo), + snd_seq_queue_tempo_get_ppq(queue_tempo)); + return 0; + } + + /* read tracks */ + for (i = 0; i < num_tracks; ++i) { + int len; + + /* search for MTrk chunk */ + for (;;) { + int id = read_id(); + len = read_int(4); + if (feof(file)) { + errormsg("%s: unexpected end of file", file_name); + return 0; + } + if (len < 0 || len >= 0x10000000) { + errormsg("%s: invalid chunk length %d", file_name, len); + return 0; + } + if (id == MAKE_ID('M', 'T', 'r', 'k')) + break; + skip(len); + } + if (!read_track(&tracks[i], file_offset + len)) + return 0; + } + return 1; +} + +static int read_riff(void) +{ + /* skip file length */ + read_byte(); + read_byte(); + read_byte(); + read_byte(); + + /* check file type ("RMID" = RIFF MIDI) */ + if (read_id() != MAKE_ID('R', 'M', 'I', 'D')) { +invalid_format: + errormsg("%s: invalid file format", file_name); + return 0; + } + /* search for "data" chunk */ + for (;;) { + int id = read_id(); + int len = read_32_le(); + if (feof(file)) { +data_not_found: + errormsg("%s: data chunk not found", file_name); + return 0; + } + if (id == MAKE_ID('d', 'a', 't', 'a')) + break; + if (len < 0) + goto data_not_found; + skip((len + 1) & ~1); + } + /* the "data" chunk must contain data in SMF format */ + if (read_id() != MAKE_ID('M', 'T', 'h', 'd')) + goto invalid_format; + return read_smf(); +} + +static void cleanup_file_data(void) +{ + int i; + struct event *event; + + for (i = 0; i < num_tracks; ++i) { + event = tracks[i].first_event; + while (event) { + struct event *next = event->next; + free(event); + event = next; + } + } + num_tracks = 0; + free(tracks); + tracks = NULL; +} + +static void handle_big_sysex(snd_seq_event_t *ev) +{ + unsigned int length; + ssize_t event_size; + int err; + + length = ev->data.ext.len; + if (length > MIDI_BYTES_PER_SEC) + ev->data.ext.len = MIDI_BYTES_PER_SEC; + event_size = snd_seq_event_length(ev); + if (event_size + 1 > (ssize_t)snd_seq_get_output_buffer_size(seq)) { + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + err = snd_seq_set_output_buffer_size(seq, event_size + 1); + check_snd("set output buffer size", err); + } + while (length > MIDI_BYTES_PER_SEC) { + err = snd_seq_event_output(seq, ev); + check_snd("output event", err); + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + err = snd_seq_sync_output_queue(seq); + check_snd("sync output", err); + if (sleep(1)) + fatal("aborted"); + ev->data.ext.ptr = (char *)ev->data.ext.ptr + MIDI_BYTES_PER_SEC; + length -= MIDI_BYTES_PER_SEC; + } + ev->data.ext.len = length; +} + +static int fill_legacy_event(struct event* event, snd_seq_event_t *ev) +{ + ev->type = event->type; + switch (ev->type) { + case SND_SEQ_EVENT_NOTEON: + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: + snd_seq_ev_set_fixed(ev); + ev->data.note.channel = event->data.d[0]; + ev->data.note.note = event->data.d[1]; + ev->data.note.velocity = event->data.d[2]; + break; + case SND_SEQ_EVENT_CONTROLLER: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.param = event->data.d[1]; + ev->data.control.value = event->data.d[2]; + break; + case SND_SEQ_EVENT_PGMCHANGE: + case SND_SEQ_EVENT_CHANPRESS: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.value = event->data.d[1]; + break; + case SND_SEQ_EVENT_PITCHBEND: + snd_seq_ev_set_fixed(ev); + ev->data.control.channel = event->data.d[0]; + ev->data.control.value = ((event->data.d[1]) | + ((event->data.d[2]) << 7)) - 0x2000; + break; + case SND_SEQ_EVENT_SYSEX: + snd_seq_ev_set_variable(ev, event->data.length, event->sysex); + handle_big_sysex(ev); + break; + default: + fatal("Invalid event type %d!", ev->type); + } + return 0; +} + +static unsigned char to_ump_status(unsigned char ev_type) +{ + switch (ev_type) { + case SND_SEQ_EVENT_NOTEON: + return SND_UMP_MSG_NOTE_ON; + case SND_SEQ_EVENT_NOTEOFF: + return SND_UMP_MSG_NOTE_OFF; + case SND_SEQ_EVENT_KEYPRESS: + return SND_UMP_MSG_POLY_PRESSURE; + case SND_SEQ_EVENT_CONTROLLER: + return SND_UMP_MSG_CONTROL_CHANGE; + case SND_SEQ_EVENT_PGMCHANGE: + return SND_UMP_MSG_PROGRAM_CHANGE; + case SND_SEQ_EVENT_CHANPRESS: + return SND_UMP_MSG_CHANNEL_PRESSURE; + case SND_SEQ_EVENT_PITCHBEND: + return SND_UMP_MSG_PITCHBEND; + default: + return 0; + } +} + +static int fill_ump_event(struct event* event, snd_seq_ump_event_t *ump_ev, + const snd_seq_event_t *ev) +{ + snd_ump_msg_midi1_t ump = {}; + unsigned char status = to_ump_status(event->type); + + memcpy(ump_ev, ev, sizeof(*ev)); + if (!status) + return 0; /* handle as is */ + + ump.note_on.type = SND_UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE; + switch (event->type) { + case SND_SEQ_EVENT_NOTEON: + /* correct the note-on with velocity 0 to note-off; + * UMP may handle velocity 0 differently + */ + if (!ev->data.note.velocity) + status = SND_UMP_MSG_NOTE_OFF; + /* fallthrough */ + case SND_SEQ_EVENT_NOTEOFF: + case SND_SEQ_EVENT_KEYPRESS: + ump.note_on.status = status; + ump.note_on.channel = event->data.d[0]; + ump.note_on.note = event->data.d[1]; + ump.note_on.velocity = event->data.d[2]; + break; + case SND_SEQ_EVENT_CONTROLLER: + ump.control_change.status = status; + ump.control_change.channel = event->data.d[0]; + ump.control_change.index = event->data.d[1]; + ump.control_change.data = event->data.d[2]; + break; + case SND_SEQ_EVENT_PGMCHANGE: + ump.program_change.status = status; + ump.program_change.channel = event->data.d[0]; + ump.program_change.program = event->data.d[1]; + break; + case SND_SEQ_EVENT_CHANPRESS: + ump.channel_pressure.status = status; + ump.channel_pressure.channel = event->data.d[0]; + ump.channel_pressure.data = event->data.d[1]; + break; + case SND_SEQ_EVENT_PITCHBEND: + ump.pitchbend.status = status; + ump.pitchbend.channel = event->data.d[0]; + ump.pitchbend.data_msb = event->data.d[2]; + ump.pitchbend.data_lsb = event->data.d[1]; + break; + default: + return 0; /* handle as is */ + } + snd_seq_ev_set_ump_data(ump_ev, &ump, sizeof(ump)); + return 0; +} + +static void play_midi(void) +{ + snd_seq_ump_event_t ump_ev; + snd_seq_event_t ev; + int i, max_tick, err; + + /* calculate length of the entire file */ + max_tick = -1; + for (i = 0; i < num_tracks; ++i) { + if (tracks[i].end_tick > max_tick) + max_tick = tracks[i].end_tick; + } + + /* initialize current position in each track */ + for (i = 0; i < num_tracks; ++i) + tracks[i].current_event = tracks[i].first_event; + + /* common settings for all our events */ + snd_seq_ev_clear(&ev); + ev.queue = queue; + ev.source.port = 0; + ev.flags = SND_SEQ_TIME_STAMP_TICK; + + err = snd_seq_start_queue(seq, queue, NULL); + check_snd("start queue", err); + /* The queue won't be started until the START_QUEUE event is + * actually drained to the kernel, which is exactly what we want. */ + + for (;;) { + struct event* event = NULL; + struct track* event_track = NULL; + int i, min_tick = max_tick + 1; + + /* search next event */ + for (i = 0; i < num_tracks; ++i) { + struct track *track = &tracks[i]; + struct event *e2 = track->current_event; + if (e2 && e2->tick < (unsigned int)min_tick) { + min_tick = e2->tick; + event = e2; + event_track = track; + } + } + if (!event) + break; /* end of song reached */ + + /* advance pointer to next event */ + event_track->current_event = event->next; + + /* output the event */ + ev.time.tick = event->tick; + ev.dest = ports[event->port]; + if (event->type == SND_SEQ_EVENT_TEMPO) { + snd_seq_ev_set_fixed(&ev); + ev.type = event->type; + ev.dest.client = SND_SEQ_CLIENT_SYSTEM; + ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; + ev.data.queue.queue = queue; + ev.data.queue.param.value = event->data.tempo; + } else { + err = fill_legacy_event(event, &ev); + if (err < 0) + continue; + } + + if (ump_mode) { + err = fill_ump_event(event, &ump_ev, &ev); + if (err < 0) + continue; + err = snd_seq_ump_event_output(seq, &ump_ev); + check_snd("output event", err); + continue; + } + + /* this blocks when the output pool has been filled */ + err = snd_seq_event_output(seq, &ev); + check_snd("output event", err); + } + + /* schedule queue stop at end of song */ + snd_seq_ev_set_fixed(&ev); + ev.type = SND_SEQ_EVENT_STOP; + ev.time.tick = max_tick; + ev.dest.client = SND_SEQ_CLIENT_SYSTEM; + ev.dest.port = SND_SEQ_PORT_SYSTEM_TIMER; + ev.data.queue.queue = queue; + err = snd_seq_event_output(seq, &ev); + check_snd("output event", err); + + /* make sure that the sequencer sees all our events */ + err = snd_seq_drain_output(seq); + check_snd("drain output", err); + + /* + * There are three possibilities how to wait until all events have + * been played: + * 1) send an event back to us (like pmidi does), and wait for it; + * 2) wait for the EVENT_STOP notification for our queue which is sent + * by the system timer port (this would require a subscription); + * 3) wait until the output pool is empty. + * The last is the simplest. + */ + err = snd_seq_sync_output_queue(seq); + check_snd("sync output", err); + + /* give the last notes time to die away */ + if (end_delay > 0) + sleep(end_delay); +} + +static void play_file(void) +{ + int ok; + + if (!strcmp(file_name, "-")) + file = stdin; + else + file = fopen(file_name, "rb"); + if (!file) { + errormsg("Cannot open %s - %s", file_name, strerror(errno)); + return; + } + + file_offset = 0; + ok = 0; + + switch (read_id()) { + case MAKE_ID('M', 'T', 'h', 'd'): + ok = read_smf(); + break; + case MAKE_ID('R', 'I', 'F', 'F'): + ok = read_riff(); + break; + default: + errormsg("%s is not a Standard MIDI File", file_name); + break; + } + + if (file != stdin) + fclose(file); + + if (ok) + play_midi(); + + cleanup_file_data(); +} + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + /* port must understand MIDI messages */ + if (!(snd_seq_port_info_get_type(pinfo) + & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) + continue; + /* we need both WRITE and SUBS_WRITE */ + if ((snd_seq_port_info_get_capability(pinfo) + & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) + != (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +static void usage(const char *argv0) +{ + printf( + "Usage: %s -p client:port[,...] [-d delay] midifile ...\n" + "-h, --help this help\n" + "-V, --version print current version\n" + "-l, --list list all possible output ports\n" + "-p, --port=client:port,... set port(s) to play to\n" + "-u, --ump=version UMP output (only version=1 is supported)\n" + "-d, --delay=seconds delay after song ends\n", + "-c, --clock generate clock signal\n", + argv0); +} + +static void version(void) +{ + puts("aplaymidi version " SND_UTIL_VERSION_STR); +} + +#define OPTIONS "hVlp:d:u:" + +int main(int argc, char *argv[]) +{ + static const char short_options[] = OPTIONS; + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"list", 0, NULL, 'l'}, + {"port", 1, NULL, 'p'}, + {"ump", 1, NULL, 'u'}, + {"delay", 1, NULL, 'd'}, + {"clock", 0, NULL, 'c'}, + {0} + }; + int c; + int do_list = 0; + + init_seq(); + + while ((c = getopt_long(argc, argv, short_options, + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'l': + do_list = 1; + break; + case 'p': + parse_ports(optarg); + break; + case 'd': + end_delay = atoi(optarg); + break; + case 'u': + ump_mode = atoi(optarg); + if (ump_mode < 0 || ump_mode > 1) + fatal("Only MIDI 1.0 is supported"); + break; + case 'c': + add_clock = 1; + break; + default: + usage(argv[0]); + return 1; + } + } + + + if (ump_mode) { + int err; + err = snd_seq_set_client_midi_version(seq, SND_SEQ_CLIENT_UMP_MIDI_1_0); + check_snd("set midi version", err); + } + + if (do_list) { + list_ports(); + } else { + if (port_count < 1) { + /* use env var for compatibility with pmidi */ + const char *ports_str = getenv("ALSA_OUTPUT_PORTS"); + if (ports_str) + parse_ports(ports_str); + if (port_count < 1) { + errormsg("Please specify at least one port with --port."); + return 1; + } + } + if (optind >= argc) { + errormsg("Please specify a file to play."); + return 1; + } + + create_source_port(); + create_queue(); + connect_ports(); + + for (; optind < argc; ++optind) { + file_name = argv[optind]; + play_file(); + } + } + snd_seq_close(seq); + return 0; +} -- cgit v1.2.3