alsa-utils/seq/aplaymidi2/arecordmidi2.c
Takashi Iwai 16533f81de arecordmidi2: Start queue at starting the stream
The queue should be started at the very same time of the start of the
stream itself in the interactive mode.  Otherwise it'll get bogus long
waits until the start of the clip.

Move the code to start the queue in start_bar(), so that it's always
tied with the start sequence.

Fixes: 1205dd5f6c ("arecordmidi2: Add passive mode and interactive mode")
Signed-off-by: Takashi Iwai <tiwai@suse.de>
2024-07-08 16:26:09 +02:00

557 lines
13 KiB
C

/*
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <getopt.h>
#include <poll.h>
#include <alsa/asoundlib.h>
#include <alsa/ump_msg.h>
#include "aconfig.h"
#include "version.h"
static snd_seq_t *seq;
static int client;
static int port_count;
static snd_seq_addr_t *ports;
static int queue;
static int midi_version = 1;
static int beats = 120;
static int ticks = 384;
static int tempo_base = 10;
static volatile sig_atomic_t stop;
static int ts_num = 4; /* time signature: numerator */
static int ts_div = 4; /* time signature: denominator */
static int last_tick;
/* Parse a decimal number from a command line argument. */
static long arg_parse_decimal_num(const char *str, int *err)
{
long val;
char *endptr;
errno = 0;
val = strtol(str, &endptr, 0);
if (errno > 0) {
*err = -errno;
return 0;
}
if (*endptr != '\0') {
*err = -EINVAL;
return 0;
}
return val;
}
/* 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 a sequencer client */
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);
/* find out our client's id */
client = snd_seq_client_id(seq);
check_snd("get client id", client);
}
/* set up UMP virtual client/port */
static void create_ump_client(void)
{
snd_ump_endpoint_info_t *ep;
snd_ump_block_info_t *blk;
snd_seq_port_info_t *pinfo;
int num_groups;
int i, err;
/* in passive mode, create full 16 groups */
if (port_count)
num_groups = port_count;
else
num_groups = 16;
/* create a UMP Endpoint */
snd_ump_endpoint_info_alloca(&ep);
snd_ump_endpoint_info_set_name(ep, "arecordmidi2");
if (midi_version == 1) {
snd_ump_endpoint_info_set_protocol_caps(ep, SND_UMP_EP_INFO_PROTO_MIDI1);
snd_ump_endpoint_info_set_protocol(ep, SND_UMP_EP_INFO_PROTO_MIDI1);
} else {
snd_ump_endpoint_info_set_protocol_caps(ep, SND_UMP_EP_INFO_PROTO_MIDI2);
snd_ump_endpoint_info_set_protocol(ep, SND_UMP_EP_INFO_PROTO_MIDI2);
}
snd_ump_endpoint_info_set_num_blocks(ep, num_groups);
err = snd_seq_create_ump_endpoint(seq, ep, num_groups);
check_snd("create UMP endpoint", err);
/* create UMP Function Blocks */
snd_ump_block_info_alloca(&blk);
for (i = 0; i < num_groups; i++) {
char blkname[32];
sprintf(blkname, "Group %d", i + 1);
snd_ump_block_info_set_name(blk, blkname);
snd_ump_block_info_set_direction(blk, SND_UMP_DIR_INPUT);
snd_ump_block_info_set_first_group(blk, i);
snd_ump_block_info_set_num_groups(blk, 1);
snd_ump_block_info_set_ui_hint(blk, SND_UMP_BLOCK_UI_HINT_RECEIVER);
err = snd_seq_create_ump_block(seq, i, blk);
check_snd("create UMP block", err);
}
/* toggle timestamping for all input ports */
snd_seq_port_info_alloca(&pinfo);
for (i = 0; i <= num_groups; i++) {
err = snd_seq_get_port_info(seq, i, pinfo);
check_snd("get port info", err);
snd_seq_port_info_set_timestamping(pinfo, 1);
snd_seq_port_info_set_timestamp_queue(pinfo, queue);
snd_seq_set_port_info(seq, i, pinfo);
check_snd("set port info", 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;
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);
}
/* parses time signature specification */
static void time_signature(const char *arg)
{
long x = 0;
char *sep;
x = strtol(arg, &sep, 10);
if (x < 1 || x > 64 || *sep != ':')
fatal("Invalid time signature (%s)", arg);
ts_num = x;
x = strtol(++sep, NULL, 10);
if (x < 1 || x > 64)
fatal("Invalid time signature (%s)", arg);
ts_div = x;
}
/* create a queue, set up the default tempo */
static void create_queue(void)
{
snd_seq_queue_tempo_t *tempo;
if (!snd_seq_has_queue_tempo_base(seq))
tempo_base = 1000;
queue = snd_seq_alloc_named_queue(seq, "arecordmidi2");
check_snd("create queue", queue);
snd_seq_queue_tempo_alloca(&tempo);
if (tempo_base == 1000)
snd_seq_queue_tempo_set_tempo(tempo, 60000000 / beats);
else
snd_seq_queue_tempo_set_tempo(tempo, (unsigned int)(6000000000ULL / beats));
snd_seq_queue_tempo_set_ppq(tempo, ticks);
snd_seq_queue_tempo_set_tempo_base(tempo, tempo_base);
if (snd_seq_set_queue_tempo(seq, queue, tempo) < 0)
fatal("Cannot set queue tempo (%d)", queue);
}
/* connect to the input ports */
static void connect_ports(void)
{
int i, err;
for (i = 0; i < port_count; ++i) {
err = snd_seq_connect_from(seq, i + 1,
ports[i].client, ports[i].port);
check_snd("port connection", err);
}
}
/* write the given UMP packet */
static void write_ump(FILE *file, const void *src)
{
const snd_ump_msg_hdr_t *h = src;
const uint32_t *p = src;
uint32_t v;
int len;
len = snd_ump_packet_length(h->type);
while (len-- > 0) {
v = htobe32(*p++);
fwrite(&v, 4, 1, file);
}
}
/* write a DC message */
static void write_dcs(FILE *file, unsigned int t)
{
snd_ump_msg_dc_t d = {};
d.type = SND_UMP_MSG_TYPE_UTILITY;
d.status = SND_UMP_UTILITY_MSG_STATUS_DC;
d.ticks = t;
write_ump(file, &d);
}
/* write a DCTPQ message */
static void write_dctpq(FILE *file)
{
snd_ump_msg_dctpq_t d = {};
d.type = SND_UMP_MSG_TYPE_UTILITY;
d.status = SND_UMP_UTILITY_MSG_STATUS_DCTPQ;
d.ticks = ticks;
write_ump(file, &d);
}
/* write a Start Clip message */
static void write_start_clip(FILE *file)
{
snd_ump_msg_stream_gen_t d = {};
d.type = SND_UMP_MSG_TYPE_STREAM;
d.status = SND_UMP_STREAM_MSG_STATUS_START_CLIP;
write_ump(file, &d);
}
/* write an End Clip message */
static void write_end_clip(FILE *file)
{
snd_ump_msg_stream_gen_t d = {};
d.type = SND_UMP_MSG_TYPE_STREAM;
d.status = SND_UMP_STREAM_MSG_STATUS_END_CLIP;
write_ump(file, &d);
}
/* write a Set Tempo message */
static void write_tempo(FILE *file)
{
snd_ump_msg_set_tempo_t d = {};
d.type = SND_UMP_MSG_TYPE_FLEX_DATA;
d.group = 0;
d.format = SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE;
d.addrs = SND_UMP_FLEX_DATA_MSG_ADDR_GROUP;
d.status_bank = SND_UMP_FLEX_DATA_MSG_BANK_SETUP;
d.status = SND_UMP_FLEX_DATA_MSG_STATUS_SET_TEMPO;
d.tempo = (unsigned int)(6000000000ULL / beats);
write_ump(file, &d);
}
/* write a Set Time Signature message */
static void write_time_sig(FILE *file)
{
snd_ump_msg_set_time_sig_t d = {};
d.type = SND_UMP_MSG_TYPE_FLEX_DATA;
d.group = 0;
d.format = SND_UMP_FLEX_DATA_MSG_FORMAT_SINGLE;
d.addrs = SND_UMP_FLEX_DATA_MSG_ADDR_GROUP;
d.status_bank = SND_UMP_FLEX_DATA_MSG_BANK_SETUP;
d.status = SND_UMP_FLEX_DATA_MSG_STATUS_SET_TIME_SIGNATURE;
d.numerator = ts_num;
d.denominator = ts_div;
d.num_notes = 8;
write_ump(file, &d);
}
/* record the delta time from the last event */
static void delta_time(FILE *file, const snd_seq_ump_event_t *ev)
{
int diff = ev->time.tick - last_tick;
if (diff <= 0)
return;
if (tempo_base == 1000)
diff *= 100;
write_dcs(file, diff);
last_tick = ev->time.tick;
}
static void record_event(FILE *file, const snd_seq_ump_event_t *ev)
{
/* ignore events without proper timestamps */
if (ev->queue != queue || !snd_seq_ev_is_tick(ev) ||
!snd_seq_ev_is_ump(ev))
return;
delta_time(file, ev);
write_ump(file, ev->ump);
}
/* write MIDI Clip file header and the configuration packets */
static void write_file_header(FILE *file)
{
/* header id */
fwrite("SMF2CLIP", 1, 8, file);
/* clip configuration header */
/* FIXME: add profiles */
/* first DCS */
write_dcs(file, 0);
write_dctpq(file);
}
/* write start bar */
static void start_bar(FILE *file)
{
int err;
/* start the queue */
err = snd_seq_start_queue(seq, queue, NULL);
check_snd("start queue", err);
snd_seq_drain_output(seq);
write_start_clip(file);
write_tempo(file);
write_time_sig(file);
}
static void help(const char *argv0)
{
fprintf(stderr, "Usage: %s [options] outputfile\n"
"\nAvailable options:\n"
" -h,--help this help\n"
" -V,--version show version\n"
" -p,--port=client:port,... source port(s)\n"
" -b,--bpm=beats tempo in beats per minute\n"
" -t,--ticks=ticks resolution in ticks per beat or frame\n"
" -i,--timesig=nn:dd time signature\n"
" -n,--num-events=events fixed number of events to record, then exit\n"
" -u,--ump=version UMP MIDI version (1 or 2)\n"
" -r,--interactive Interactive mode\n",
argv0);
}
static void version(void)
{
fputs("arecordmidi version " SND_UTIL_VERSION_STR "\n", stderr);
}
static void sighandler(int sig ATTRIBUTE_UNUSED)
{
stop = 1;
}
int main(int argc, char *argv[])
{
static const char short_options[] = "hVp:b:t:n:u:r";
static const struct option long_options[] = {
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'V'},
{"port", 1, NULL, 'p'},
{"bpm", 1, NULL, 'b'},
{"ticks", 1, NULL, 't'},
{"timesig", 1, NULL, 'i'},
{"num-events", 1, NULL, 'n'},
{"ump", 1, NULL, 'u'},
{"interactive", 0, NULL, 'r'},
{0}
};
char *filename;
FILE *file;
struct pollfd *pfds;
int npfds;
int c, err;
/* If |num_events| isn't specified, leave it at 0. */
long num_events = 0;
long events_received = 0;
int start = 0;
int interactive = 0;
init_seq();
while ((c = getopt_long(argc, argv, short_options,
long_options, NULL)) != -1) {
switch (c) {
case 'h':
help(argv[0]);
return 0;
case 'V':
version();
return 0;
case 'p':
parse_ports(optarg);
break;
case 'b':
beats = atoi(optarg);
if (beats < 4 || beats > 6000)
fatal("Invalid tempo");
break;
case 't':
ticks = atoi(optarg);
if (ticks < 1 || ticks > 0x7fff)
fatal("Invalid number of ticks");
break;
case 'i':
time_signature(optarg);
break;
case 'n':
err = 0;
num_events = arg_parse_decimal_num(optarg, &err);
if (err != 0) {
fatal("Couldn't parse num_events argument: %s\n",
strerror(-err));
}
if (num_events <= 0)
fatal("num_events must be greater than 0");
break;
case 'u':
midi_version = atoi(optarg);
if (midi_version != 1 && midi_version != 2)
fatal("Invalid MIDI version %d\n", midi_version);
break;
case 'r':
interactive = 1;
break;
default:
help(argv[0]);
return 1;
}
}
if (optind >= argc) {
fputs("Please specify a file to record to.\n", stderr);
return 1;
}
create_queue();
create_ump_client();
if (port_count)
connect_ports();
filename = argv[optind];
file = fopen(filename, "wb");
if (!file)
fatal("Cannot open %s - %s", filename, strerror(errno));
write_file_header(file);
if (interactive) {
printf("Press RETURN to start recording:");
fflush(stdout);
} else {
start_bar(file);
start = 1;
}
err = snd_seq_nonblock(seq, 1);
check_snd("set nonblock mode", err);
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
npfds = snd_seq_poll_descriptors_count(seq, POLLIN);
pfds = alloca(sizeof(*pfds) * (npfds + 1));
for (;;) {
snd_seq_poll_descriptors(seq, pfds, npfds, POLLIN);
if (interactive) {
pfds[npfds].fd = STDIN_FILENO;
pfds[npfds].events = POLLIN | POLLERR | POLLNVAL;
if (poll(pfds, npfds + 1, -1) < 0)
break;
if (pfds[npfds].revents & POLLIN) {
while (!feof(stdin) && getchar() != '\n')
;
if (!start) {
start_bar(file);
start = 1;
printf("Press RETURN to stop recording:");
fflush(stdout);
continue;
} else {
stop = 1;
}
}
} else {
if (poll(pfds, npfds, -1) < 0)
break;
}
do {
snd_seq_ump_event_t *event;
err = snd_seq_ump_event_input(seq, &event);
if (err < 0)
break;
if (start && event) {
record_event(file, event);
events_received++;
}
} while (err > 0);
if (stop)
break;
if (num_events && (events_received >= num_events))
break;
}
if (num_events && events_received < num_events)
fputs("Warning: Received signal before num_events\n", stdout);
write_end_clip(file);
fclose(file);
snd_seq_close(seq);
return 0;
}