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