diff --git a/configure.ac b/configure.ac index 173b9ed..708380c 100644 --- a/configure.ac +++ b/configure.ac @@ -486,7 +486,7 @@ AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \ bat/Makefile bat/tests/Makefile bat/tests/asound_state/Makefile \ aplay/Makefile include/Makefile iecset/Makefile utils/Makefile \ utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \ - seq/aplaymidi/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \ + seq/aplaymidi/Makefile seq/aplaymidi2/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \ seq/aseqsend/Makefile speaker-test/Makefile speaker-test/samples/Makefile \ alsaloop/Makefile alsa-info/Makefile \ axfer/Makefile axfer/test/Makefile \ diff --git a/seq/Makefile.am b/seq/Makefile.am index b0f628a..a617126 100644 --- a/seq/Makefile.am +++ b/seq/Makefile.am @@ -1 +1 @@ -SUBDIRS=aconnect aplaymidi aseqdump aseqnet aseqsend +SUBDIRS=aconnect aplaymidi aplaymidi2 aseqdump aseqnet aseqsend diff --git a/seq/aplaymidi2/Makefile.am b/seq/aplaymidi2/Makefile.am new file mode 100644 index 0000000..0c5c743 --- /dev/null +++ b/seq/aplaymidi2/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aplaymidi2.1 + +bin_PROGRAMS = aplaymidi2 +man_MANS = aplaymidi2.1 diff --git a/seq/aplaymidi2/aplaymidi2.1 b/seq/aplaymidi2/aplaymidi2.1 new file mode 100644 index 0000000..99be608 --- /dev/null +++ b/seq/aplaymidi2/aplaymidi2.1 @@ -0,0 +1,71 @@ +.TH APLAYMIDI2 1 "4 July 2024" + +.SH NAME +aplaymidi2 \- play MIDI Clip Files + +.SH SYNOPSIS +.B aplaymidi2 +\-p client:port[,...] midi2file ... + +.SH DESCRIPTION +.B aplaymidi2 +is a command-line utility that plays the specified MIDI Clip file(s) to one +or more ALSA sequencer ports. + +.SH OPTIONS + +.TP +.I \-h, \-\-help +Prints a list of options. + +.TP +.I \-V, \-\-version +Prints the current version. + +.TP +.I \-p, \-\-port=client:port,... +Sets the sequencer port(s) to which the events in the MIDI Clip file(s) are +sent. + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +Multiple ports can be specified to allow playback of MIDI Clip file(s) that +contain events for multiple devices (ports) corresponding to the +multiple UMP Groups. + +For compatibility with +.B pmidi(1), +the port specification is taken from the +.I ALSA_OUTPUT_PORTS +environment variable if none is given on the command line. + +.B aplaymidi2 +supports only basic UMP events: in addition to the standard MIDI1 and +MIDI2 CVMs and 7bit SysEx, only the following are supported: +DCTPQ, DC, Set Tempo, Start Clip and End Clip. +Lyrics and other meta data in Flex Data are skipped, so far. + +The multiple output ports are useful when the given MIDI Clip file +contains the UMP packets for multiple Groups. +When the destination port is a UMP MIDI 2.0 port, the single +connection should suffice, though, since a MIDI 2.0 port can process +the inputs for multiple Groups. For other cases (e.g. connecting to a +legacy MIDI port), you would need to specify the destination port per +Group. If undefined, it's sent to the first destination port as +default. + +.TP +.I \-d, \-\-delay=seconds +Specifies how long to wait after the end of each MIDI Clip file, +to allow the last notes to die away. +Default is 2 seconds. + +.SH SEE ALSO +pmidi(1) +.br +aplaymidi(1) + +.SH AUTHOR +Takashi Iwai diff --git a/seq/aplaymidi2/aplaymidi2.c b/seq/aplaymidi2/aplaymidi2.c new file mode 100644 index 0000000..435b7de --- /dev/null +++ b/seq/aplaymidi2/aplaymidi2.c @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * aplaymidi2.c - simple player of a MIDI Clip File over ALSA sequencer + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "version.h" + +static snd_seq_t *seq; +static int client; +static int port_count; +static snd_seq_addr_t ports[16]; +static int queue; +static int end_delay = 2; + +static unsigned int _current_tempo = 50000000; /* default 120 bpm */ +static unsigned int tempo_base = 10; +static unsigned int current_tick; + +/* 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)); +} + +/* open and initialize the sequencer client */ +static void init_seq(void) +{ + int err; + + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + check_snd("open sequencer", err); + + err = snd_seq_set_client_name(seq, "aplaymidi2"); + check_snd("set client name", err); + + client = snd_seq_client_id(seq); + check_snd("get client id", client); + + err = snd_seq_set_client_midi_version(seq, SND_SEQ_CLIENT_UMP_MIDI_2_0); + check_snd("set midi version", err); +} + +/* 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; + if (port_count > 16) + fatal("Too many ports specified"); + + 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); +} + +/* create a source port to send from */ +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, "aplaymidi2"); + + 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); +} + +/* create a queue */ +static void create_queue(void) +{ + if (!snd_seq_has_queue_tempo_base(seq)) + tempo_base = 1000; + + queue = snd_seq_alloc_named_queue(seq, "aplaymidi2"); + check_snd("create queue", queue); +} + +/* connect to destination ports */ +static void connect_ports(void) +{ + int i, err; + + 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)); + } +} + +/* read 32bit word and convert to native endian: + * return 0 on success, -1 on error + */ +static int read_word(FILE *file, uint32_t *dest) +{ + uint32_t v; + + if (fread(&v, 4, 1, file) != 1) + return -1; + *dest = be32toh(v); + return 0; +} + +/* read a UMP packet: return the number of packets, -1 on error */ +static int read_ump_packet(FILE *file, uint32_t *buf) +{ + snd_ump_msg_hdr_t *h = (snd_ump_msg_hdr_t *)buf; + + int i, num; + + if (read_word(file, buf) < 0) + return -1; + num = snd_ump_packet_length(h->type); + for (i = 1; i < num; i++) { + if (read_word(file, buf + i) < 0) + return -1; + } + return num; +} + +/* read the file header and verify it's MIDI Clip File: return 0 on success */ +static int verify_file_header(FILE *file) +{ + unsigned char buf[8]; + + if (fread(buf, 1, 8, file) != 8) + return -1; + if (memcmp(buf, "SMF2CLIP", 8)) + return -1; + return 0; +} + +/* return the current tempo, corrected to be sent to host */ +static int current_tempo(void) +{ + if (tempo_base != 10) + return _current_tempo / 100; /* down to us */ + return _current_tempo; +} + +/* send a timer event */ +static void send_timer_event(unsigned int type, unsigned int val) +{ + snd_seq_ump_event_t ev = { + .type = type, + .flags = SND_SEQ_TIME_STAMP_TICK | SND_SEQ_EVENT_LENGTH_FIXED, + }; + + ev.queue = queue; + ev.source.port = 0; + ev.time.tick = current_tick; + + 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 = val; + + snd_seq_ump_event_output(seq, &ev); +} + +/* set DCTPQ */ +static void set_dctpq(unsigned int ppq) +{ + snd_seq_queue_tempo_t *queue_tempo; + + snd_seq_queue_tempo_alloca(&queue_tempo); + snd_seq_queue_tempo_set_tempo(queue_tempo, current_tempo()); + snd_seq_queue_tempo_set_ppq(queue_tempo, ppq); + snd_seq_queue_tempo_set_tempo_base(queue_tempo, tempo_base); + + if (snd_seq_set_queue_tempo(seq, queue, queue_tempo) < 0) + errormsg("Cannot set queue tempo (%d)", queue); +} + +/* set DC */ +static void set_dc(unsigned int ticks) +{ + current_tick += ticks; +} + +/* set tempo event */ +static void set_tempo(unsigned int tempo) +{ + _current_tempo = tempo; + send_timer_event(SND_SEQ_EVENT_TEMPO, current_tempo()); +} + +/* start clip */ +static void start_clip(void) +{ + if (snd_seq_start_queue(seq, queue, NULL) < 0) + errormsg("Cannot start queue (%d)", queue); +} + +/* end clip */ +static void end_clip(void) +{ + send_timer_event(SND_SEQ_EVENT_STOP, 0); +} + +/* send a UMP packet */ +static void send_ump(const uint32_t *ump, int len) +{ + snd_seq_ump_event_t ev = { + .flags = SND_SEQ_TIME_STAMP_TICK | SND_SEQ_EVENT_LENGTH_FIXED | + SND_SEQ_EVENT_UMP, + }; + int group; + + memcpy(ev.ump, ump, len * 4); + + ev.queue = queue; + ev.source.port = 0; + ev.time.tick = current_tick; + group = snd_ump_msg_group(ump); + if (group >= port_count) + ev.dest = ports[0]; + else + ev.dest = ports[group]; + + snd_seq_ump_event_output(seq, &ev); +} + +/* play the given MIDI Clip File content */ +static void play_midi(FILE *file) +{ + uint32_t ump[4]; + int len; + + current_tick = 0; + + while ((len = read_ump_packet(file, ump)) > 0) { + const snd_ump_msg_hdr_t *h = (snd_ump_msg_hdr_t *)ump; + + if (h->type == SND_UMP_MSG_TYPE_UTILITY) { + const snd_ump_msg_utility_t *uh = + (const snd_ump_msg_utility_t *)ump; + switch (h->status) { + case SND_UMP_UTILITY_MSG_STATUS_DCTPQ: + set_dctpq(uh->dctpq.ticks); + continue; + case SND_UMP_UTILITY_MSG_STATUS_DC: + set_dc(uh->dctpq.ticks); + continue; + } + } else if (h->type == SND_UMP_MSG_TYPE_FLEX_DATA) { + const snd_ump_msg_flex_data_t *fh = + (const snd_ump_msg_flex_data_t *)ump; + if (fh->meta.status_bank == SND_UMP_FLEX_DATA_MSG_BANK_SETUP && + fh->meta.status == SND_UMP_FLEX_DATA_MSG_STATUS_SET_TEMPO) { + set_tempo(fh->set_tempo.tempo); + continue; + } + } else if (h->type == SND_UMP_MSG_TYPE_STREAM) { + const snd_ump_msg_stream_t *sh = + (const snd_ump_msg_stream_t *)ump; + switch (sh->gen.status) { + case SND_UMP_STREAM_MSG_STATUS_START_CLIP: + start_clip(); + continue; + case SND_UMP_STREAM_MSG_STATUS_END_CLIP: + end_clip(); + continue; + } + } else if (h->type == SND_UMP_MSG_TYPE_MIDI1_CHANNEL_VOICE || + h->type == SND_UMP_MSG_TYPE_DATA || + h->type == SND_UMP_MSG_TYPE_MIDI2_CHANNEL_VOICE) { + send_ump(ump, len); + } + } + + snd_seq_drain_output(seq); + snd_seq_sync_output_queue(seq); + + /* give the last notes time to die away */ + if (end_delay > 0) + sleep(end_delay); +} + +static void play_file(const char *file_name) +{ + FILE *file; + + if (!strcmp(file_name, "-")) + file = stdin; + else + file = fopen(file_name, "rb"); + if (!file) { + errormsg("Cannot open %s - %s", file_name, strerror(errno)); + return; + } + + if (verify_file_header(file) < 0) { + errormsg("%s is not a MIDI Clip File", file_name); + goto error; + } + + play_midi(file); + + error: + if (file != stdin) + fclose(file); +} + +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" + "-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("aplaymidi2 version " SND_UTIL_VERSION_STR); +} + +int main(int argc, char *argv[]) +{ + static const struct option long_options[] = { + {"help", 0, NULL, 'h'}, + {"version", 0, NULL, 'V'}, + {"port", 1, NULL, 'p'}, + {"delay", 1, NULL, 'd'}, + {0} + }; + int c; + + init_seq(); + + while ((c = getopt_long(argc, argv, "hVp:d:", + long_options, NULL)) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; + case 'V': + version(); + return 0; + case 'p': + parse_ports(optarg); + break; + case 'd': + end_delay = atoi(optarg); + break; + default: + usage(argv[0]); + return 1; + } + } + + + 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++) + play_file(argv[optind]); + + snd_seq_close(seq); + return 0; +}