alsa-utils/seq/aplaymidi/aplaymidi.c
Diego E. 'Flameeyes' Pettenò 6232f1c96c Make some static tables and strings constants.
By doing this we move them from the .data section to .rodata setion,
or from .data.rel to .data.rel.ro.

The .rodata section is mapped directly from the on-disk file, which is
always a save, while .data.rel.ro is mapped directly when using
prelink, which is a save in a lot of cases.

Signed-off-by: Diego E. 'Flameeyes' Pettenò <flameeyes@gmail.com>
2008-11-21 13:10:02 +01:00

928 lines
22 KiB
C

/*
* aplaymidi.c - play Standard MIDI Files to sequencer port(s)
*
* Copyright (c) 2004-2006 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"
#define MIDI_BYTES_PER_SEC 3125
/*
* 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 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 > 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 += MIDI_BYTES_PER_SEC;
length -= MIDI_BYTES_PER_SEC;
}
ev->data.ext.len = length;
}
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);
handle_big_sysex(&ev);
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) {
/* 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"
"-d, --delay=seconds delay after song ends\n",
argv0);
}
static void version(void)
{
puts("aplaymidi version " SND_UTIL_VERSION_STR);
}
int main(int argc, char *argv[])
{
static const char short_options[] = "hVlp:d:";
static const 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;
}