mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-08 18:45:43 +01:00
feb9c4cdec
config.h may contain defines like _FILE_OFFSET_BITS which influence the system wide include files (off_t types, open -> open64 function usage etc.). Related: https://github.com/alsa-project/alsa-utils/pull/223 Signed-off-by: Jaroslav Kysela <perex@perex.cz>
1067 lines
26 KiB
C
1067 lines
26 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
/* TODO: sequencer queue timer selection */
|
|
|
|
#include "aconfig.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
#include <unistd.h>
|
|
#include <alsa/asoundlib.h>
|
|
#include "version.h"
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
#include <alsa/ump_msg.h>
|
|
#endif
|
|
|
|
/*
|
|
* 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;
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
static int ump_mode;
|
|
#endif
|
|
|
|
/* 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;
|
|
}
|
|
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
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;
|
|
}
|
|
#endif /* HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION */
|
|
|
|
static void play_midi(void)
|
|
{
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
snd_seq_ump_event_t ump_ev;
|
|
#endif
|
|
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) {
|
|
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;
|
|
}
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
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;
|
|
}
|
|
#endif
|
|
|
|
/* 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"
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
"-u, --ump=version UMP output (only version=1 is supported)\n"
|
|
#endif
|
|
"-d, --delay=seconds delay after song ends\n",
|
|
argv0);
|
|
}
|
|
|
|
static void version(void)
|
|
{
|
|
puts("aplaymidi version " SND_UTIL_VERSION_STR);
|
|
}
|
|
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
#define OPTIONS "hVlp:d:u:"
|
|
#else
|
|
#define OPTIONS "hVlp:d:"
|
|
#endif
|
|
|
|
|
|
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'},
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
{"ump", 1, NULL, 'u'},
|
|
#endif
|
|
{"delay", 1, NULL, 'd'},
|
|
{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;
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
case 'u':
|
|
if (strcmp(optarg, "1")) {
|
|
errormsg("Only MIDI 1.0 is supported");
|
|
return 1;
|
|
}
|
|
ump_mode = 1;
|
|
break;
|
|
#endif
|
|
default:
|
|
usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef HAVE_SEQ_CLIENT_INFO_GET_MIDI_VERSION
|
|
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);
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
}
|