mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-09 23:55:42 +01:00
4cb3e3a7b5
After UMP support was added inb399fb8
ev.type setting was inadvertently dropped in the code path handling tempo meta event. This is causing tempo meta events to not be handled at all. Moreover, snd_seq_ev_set_fixed is also missing so MIDI files with variable events such as SYSEX before the tempo meta event usually are causing a segfault. Fixes:b399fb85a9
("aplaymidi: Add UMP support") Closes: https://github.com/alsa-project/alsa-utils/issues/241 Signed-off-by: Takashi Iwai <tiwai@suse.de>
1069 lines
26 KiB
C
1069 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) {
|
|
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;
|
|
}
|
|
#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;
|
|
}
|