alsa-utils/seq/aplaymidi/aplaymidi.c
Takashi Iwai cae569e10e Remove unused variable
Removed unused variable.
2005-05-13 17:30:50 +00:00

889 lines
21 KiB
C

/*
* aplaymidi.c - play Standard MIDI Files to sequencer port(s)
*
* Copyright (c) 2004 Clemens Ladisch <clemens@ladisch.de>
*
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/* TODO: sequencer queue timer selection */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <getopt.h>
#include <unistd.h>
#include <alsa/asoundlib.h>
#include "aconfig.h"
#include "version.h"
/*
* 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;
/* 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 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 play_midi(void)
{
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 < 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.type = event->type;
ev.time.tick = event->tick;
ev.dest = ports[event->port];
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);
break;
case SND_SEQ_EVENT_TEMPO:
snd_seq_ev_set_fixed(&ev);
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;
break;
default:
fatal("Invalid event type %d!", ev.type);
}
/* 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) {
/* 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)
{
fprintf(stderr,
"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"
"-d, --delay=seconds delay after song ends\n",
argv0);
}
static void version(void)
{
fputs("aplaymidi version " SND_UTIL_VERSION_STR "\n", stderr);
}
int main(int argc, char *argv[])
{
static char short_options[] = "hVlp:d:";
static struct option long_options[] = {
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'V'},
{"list", 0, NULL, 'l'},
{"port", 1, NULL, 'p'},
{"delay", 1, NULL, 'd'},
{}
};
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;
default:
usage(argv[0]);
return 1;
}
}
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;
}