mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-10 00:25:43 +01:00
1205dd5f6c
Allow arecordmidi2 running without specifying the source ports via -p option. This will create a UMP Endpoint with the full 16 FBs, and simply reads from the input ports via subscribers. User needs to connect to the ports manually, though. Also, add -r option to run in the interactive mode. In the interactive mode, arecordmidi2 waits for the RETURN key entered from the terminal to start the recording, and the recording ends after another RETURN key. Signed-off-by: Takashi Iwai <tiwai@suse.de>
553 lines
13 KiB
C
553 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)
|
|
{
|
|
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_start_queue(seq, queue, NULL);
|
|
check_snd("start queue", err);
|
|
snd_seq_drain_output(seq);
|
|
|
|
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;
|
|
}
|