File: [local] / src / usr.bin / aucat / aucat.c (download)
Revision 1.25, Mon Jun 2 17:09:51 2008 UTC (16 years ago) by ratchov
Branch: MAIN
CVS Tags: OPENBSD_4_4_BASE, OPENBSD_4_4 Changes since 1.24: +2 -2 lines
document latest changes: -d flag is replaced by AUCAT_DEBUG
environment variable, new -xX options
bits from eric, ok jakemsr
|
/* $OpenBSD: aucat.c,v 1.25 2008/06/02 17:09:51 ratchov Exp $ */
/*
* Copyright (c) 2008 Alexandre Ratchov <alex@caoua.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* TODO:
*
* (not yet)add a silent/quiet/verbose/whatever flag, but be sure
* that by default the user is notified when one of the following
* (cpu consuming) aproc is created: mix, sub, conv
*
* (hard) use parsable encoding names instead of the lookup
* table. For instance, [s|u]bits[le|be][/bytes{msb|lsb}], example
* s8, s16le, s24le/3msb. This would give names that correspond to
* what use most linux-centric apps, but for which we have an
* algorithm to convert the name to a aparams structure.
*
* (easy) uses {chmin-chmax} instead of chmin:chmax notation for
* channels specification to match the notation used in rmix.
*
* (easy) use comma-separated parameters syntax, example:
* s24le/3msb,{3-6},48000 so we don't have to use three -e, -r, -c
* flags, but only one -p flag that specify one or more parameters.
*
* (hard) dont create mix (sub) if there's only one input (output)
*
* (hard) if all inputs are over, the mixer terminates and closes
* the write end of the device. It should continue writing zeros
* until the recording is over (or be able to stop write end of
* the device)
*
* (hard) implement -n flag (no device) to connect all inputs to
* the outputs.
*
* (hard) ignore input files that are not audible (because channels
* they provide are not used on the output). Similarly ignore
* outputs that are zero filled (because channels they consume are
* not provided).
*
* (easy) do we need -d flag ?
*/
#include <sys/param.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <err.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <varargs.h>
#include "conf.h"
#include "aparams.h"
#include "aproc.h"
#include "abuf.h"
#include "file.h"
#include "dev.h"
/*
* Format for file headers.
*/
#define HDR_AUTO 0 /* guess by looking at the file name */
#define HDR_RAW 1 /* no headers, ie openbsd native ;-) */
#define HDR_WAV 2 /* microsoft riff wave */
int debug_level = 0;
volatile int quit_flag = 0;
/*
* List of allowed encodings and their names.
*/
struct enc {
char *name;
struct aparams par;
} enc_list[] = {
/* name bps, bits, le, sign, msb, unused */
{ "s8", { 1, 8, 1, 1, 1, 0, 0, 0 } },
{ "u8", { 1, 8, 1, 0, 1, 0, 0, 0 } },
{ "s16le", { 2, 16, 1, 1, 1, 0, 0, 0 } },
{ "u16le", { 2, 16, 1, 0, 1, 0, 0, 0 } },
{ "s16be", { 2, 16, 0, 1, 1, 0, 0, 0 } },
{ "u16be", { 2, 16, 0, 0, 1, 0, 0, 0 } },
{ "s24le", { 4, 24, 1, 1, 1, 0, 0, 0 } },
{ "u24le", { 4, 24, 1, 0, 1, 0, 0, 0 } },
{ "s24be", { 4, 24, 0, 1, 1, 0, 0, 0 } },
{ "u24be", { 4, 24, 0, 0, 1, 0, 0, 0 } },
{ "s32le", { 4, 32, 1, 1, 1, 0, 0, 0 } },
{ "u32le", { 4, 32, 1, 0, 1, 0, 0, 0 } },
{ "s32be", { 4, 32, 0, 1, 1, 0, 0, 0 } },
{ "u32be", { 4, 32, 0, 0, 1, 0, 0, 0 } },
{ "s24le3", { 3, 24, 1, 1, 1, 0, 0, 0 } },
{ "u24le3", { 3, 24, 1, 0, 1, 0, 0, 0 } },
{ "s24be3", { 3, 24, 0, 1, 1, 0, 0, 0 } },
{ "u24be3", { 3, 24, 0, 0, 1, 0, 0, 0 } },
{ "s20le3", { 3, 20, 1, 1, 1, 0, 0, 0 } },
{ "u20le3", { 3, 20, 1, 0, 1, 0, 0, 0 } },
{ "s20be3", { 3, 20, 0, 1, 1, 0, 0, 0 } },
{ "u20be3", { 3, 20, 0, 0, 1, 0, 0, 0 } },
{ "s18le3", { 3, 18, 1, 1, 1, 0, 0, 0 } },
{ "u18le3", { 3, 18, 1, 0, 1, 0, 0, 0 } },
{ "s18be3", { 3, 18, 0, 1, 1, 0, 0, 0 } },
{ "u18be3", { 3, 18, 0, 0, 1, 0, 0, 0 } },
{ NULL, { 0, 0, 0, 0, 0, 0, 0, 0 } }
};
/*
* Search an encoding in the above table. On success fill encoding
* part of "par" and return 1, otherwise return 0.
*/
unsigned
enc_lookup(char *name, struct aparams *par)
{
struct enc *e;
for (e = enc_list; e->name != NULL; e++) {
if (strcmp(e->name, name) == 0) {
par->bps = e->par.bps;
par->bits = e->par.bits;
par->sig = e->par.sig;
par->le = e->par.le;
par->msb = e->par.msb;
return 1;
}
}
return 0;
}
void
usage(void)
{
extern char *__progname;
fprintf(stderr,
"usage: %s [-qu] [-C min:max] [-c min:max] [-d level] "
"[-E enc] [-e enc]\n"
"\t[-f device] [-H fmt] [-h fmt] [-i file] [-o file] [-R rate]\n"
"\t[-r rate] [-X policy] [-x policy]\n",
__progname);
}
void
opt_ch(struct aparams *par)
{
if (sscanf(optarg, "%u:%u", &par->cmin, &par->cmax) != 2 ||
par->cmin > CHAN_MAX || par->cmax > CHAN_MAX ||
par->cmin > par->cmax)
err(1, "%s: bad channel range", optarg);
}
void
opt_rate(struct aparams *par)
{
if (sscanf(optarg, "%u", &par->rate) != 1 ||
par->rate < RATE_MIN || par->rate > RATE_MAX)
err(1, "%s: bad sample rate", optarg);
}
void
opt_enc(struct aparams *par)
{
if (!enc_lookup(optarg, par))
err(1, "%s: bad encoding", optarg);
}
int
opt_hdr(void)
{
if (strcmp("auto", optarg) == 0)
return HDR_AUTO;
if (strcmp("raw", optarg) == 0)
return HDR_RAW;
if (strcmp("wav", optarg) == 0)
return HDR_WAV;
err(1, "%s: bad header specification", optarg);
}
int
opt_xrun(void)
{
if (strcmp("ignore", optarg) == 0)
return XRUN_IGNORE;
if (strcmp("sync", optarg) == 0)
return XRUN_SYNC;
if (strcmp("error", optarg) == 0)
return XRUN_ERROR;
errx(1, "%s: onderrun/overrun policy", optarg);
}
/*
* Arguments of -i and -o opations are stored in a list.
*/
struct farg {
SLIST_ENTRY(farg) entry;
struct aparams par; /* last requested format */
unsigned vol; /* last requested volume */
char *name; /* optarg pointer (no need to copy it */
int hdr; /* header format */
int xrun; /* overrun/underrun policy */
int fd; /* file descriptor for I/O */
struct aproc *proc; /* rpipe_xxx our wpipe_xxx */
struct abuf *buf;
};
SLIST_HEAD(farglist, farg);
/*
* Add a farg entry to the given list, corresponding
* to the given file name.
*/
void
opt_file(struct farglist *list,
struct aparams *par, unsigned vol, int hdr, int xrun, char *optarg)
{
struct farg *fa;
size_t namelen;
fa = malloc(sizeof(struct farg));
if (fa == NULL)
err(1, "%s", optarg);
if (hdr == HDR_AUTO) {
namelen = strlen(optarg);
if (namelen >= 4 &&
strcasecmp(optarg + namelen - 4, ".wav") == 0) {
fa->hdr = HDR_WAV;
DPRINTF("%s: assuming wav file format\n", optarg);
} else {
fa->hdr = HDR_RAW;
DPRINTF("%s: assuming headerless file\n", optarg);
}
} else
fa->hdr = hdr;
fa->xrun = xrun;
fa->par = *par;
fa->vol = vol;
fa->name = optarg;
fa->proc = NULL;
SLIST_INSERT_HEAD(list, fa, entry);
}
/*
* Open an input file and setup converter if necessary.
*/
void
newinput(struct farg *fa, struct aparams *npar, unsigned nfr, int quiet_flag)
{
int fd;
struct file *f;
struct aproc *p, *c;
struct abuf *buf, *nbuf;
if (strcmp(fa->name, "-") == 0) {
fd = STDIN_FILENO;
if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0)
warn("stdin");
fa->name = "stdin";
} else {
fd = open(fa->name, O_RDONLY | O_NONBLOCK, 0666);
if (fd < 0)
err(1, "%s", fa->name);
}
f = file_new(fd, fa->name);
if (fa->hdr == HDR_WAV) {
if (!wav_readhdr(fd, &fa->par, &f->rbytes))
exit(1);
}
buf = abuf_new(nfr, aparams_bpf(&fa->par));
p = rpipe_new(f);
aproc_setout(p, buf);
if (!aparams_eq(&fa->par, npar)) {
if (!quiet_flag) {
fprintf(stderr, "%s: ", fa->name);
aparams_print2(&fa->par, npar);
fprintf(stderr, "\n");
}
nbuf = abuf_new(nfr, aparams_bpf(npar));
c = conv_new(fa->name, &fa->par, npar);
aproc_setin(c, buf);
aproc_setout(c, nbuf);
fa->buf = nbuf;
} else
fa->buf = buf;
fa->proc = p;
fa->fd = fd;
}
/*
* Open an output file and setup converter if necessary.
*/
void
newoutput(struct farg *fa, struct aparams *npar, unsigned nfr, int quiet_flag)
{
int fd;
struct file *f;
struct aproc *p, *c;
struct abuf *buf, *nbuf;
if (strcmp(fa->name, "-") == 0) {
fd = STDOUT_FILENO;
if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0)
warn("stdout");
fa->name = "stdout";
} else {
fd = open(fa->name,
O_WRONLY | O_TRUNC | O_CREAT | O_NONBLOCK, 0666);
if (fd < 0)
err(1, "%s", fa->name);
}
f = file_new(fd, fa->name);
if (fa->hdr == HDR_WAV) {
f->wbytes = WAV_DATAMAX;
if (!wav_writehdr(fd, &fa->par))
exit(1);
}
buf = abuf_new(nfr, aparams_bpf(&fa->par));
p = wpipe_new(f);
aproc_setin(p, buf);
if (!aparams_eq(&fa->par, npar)) {
if (!quiet_flag) {
fprintf(stderr, "%s: ", fa->name);
aparams_print2(npar, &fa->par);
fprintf(stderr, "\n");
}
c = conv_new(fa->name, npar, &fa->par);
nbuf = abuf_new(nfr, aparams_bpf(npar));
aproc_setin(c, nbuf);
aproc_setout(c, buf);
fa->buf = nbuf;
} else
fa->buf = buf;
fa->proc = p;
fa->fd = fd;
}
void
sighdl(int s)
{
if (quit_flag)
_exit(1);
quit_flag = 1;
}
int
main(int argc, char **argv)
{
sigset_t sigset;
struct sigaction sa;
int c, u_flag, quiet_flag, ohdr, ihdr, ixrun, oxrun;
struct farg *fa;
struct farglist ifiles, ofiles;
struct aparams ipar, opar, dipar, dopar, cipar, copar;
unsigned ivol, ovol;
unsigned dinfr, donfr, cinfr, confr;
char *devpath, *dbgenv;
unsigned n;
struct aproc *rec, *play, *mix, *sub, *conv;
struct file *dev, *f;
struct abuf *buf, *cbuf;
const char *errstr;
int fd;
dbgenv = getenv("AUCAT_DEBUG");
if (dbgenv) {
debug_level = strtonum(dbgenv, 0, 4, &errstr);
if (errstr)
errx(1, "AUCAT_DEBUG is %s: %s", errstr, dbgenv);
}
aparams_init(&ipar, 0, 1, 44100);
aparams_init(&opar, 0, 1, 44100);
u_flag = quiet_flag = 0;
devpath = NULL;
SLIST_INIT(&ifiles);
SLIST_INIT(&ofiles);
ihdr = ohdr = HDR_AUTO;
ixrun = oxrun = XRUN_IGNORE;
ivol = ovol = MIDI_TO_ADATA(127);
while ((c = getopt(argc, argv, "c:C:e:E:r:R:h:H:x:X:i:o:f:qu"))
!= -1) {
switch (c) {
case 'h':
ihdr = opt_hdr();
break;
case 'H':
ohdr = opt_hdr();
break;
case 'x':
ixrun = opt_xrun();
break;
case 'X':
oxrun = opt_xrun();
break;
case 'c':
opt_ch(&ipar);
break;
case 'C':
opt_ch(&opar);
break;
case 'e':
opt_enc(&ipar);
break;
case 'E':
opt_enc(&opar);
break;
case 'r':
opt_rate(&ipar);
break;
case 'R':
opt_rate(&opar);
break;
case 'i':
opt_file(&ifiles, &ipar, 127, ihdr, ixrun, optarg);
break;
case 'o':
opt_file(&ofiles, &opar, 127, ohdr, oxrun, optarg);
break;
case 'f':
if (devpath)
err(1, "only one -f allowed");
devpath = optarg;
dipar = ipar;
dopar = opar;
break;
case 'q':
quiet_flag = 1;
break;
case 'u':
u_flag = 1;
break;
default:
usage();
exit(1);
}
}
argc -= optind;
argv += optind;
if (!devpath) {
devpath = getenv("AUDIODEVICE");
if (devpath == NULL)
devpath = DEFAULT_DEVICE;
dipar = ipar;
dopar = opar;
}
if (SLIST_EMPTY(&ifiles) && SLIST_EMPTY(&ofiles) && argc > 0) {
/*
* Legacy mode: if no -i or -o options are provided, and
* there are arguments then assume the arguments are files
* to play.
*/
for (c = 0; c < argc; c++)
if (legacy_play(devpath, argv[c]) != 0) {
errx(1, "%s: could not play\n", argv[c]);
}
exit(0);
} else if (argc > 0) {
usage();
exit(1);
}
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sighdl;
if (sigaction(SIGINT, &sa, NULL) < 0)
err(1, "sigaction");
sigemptyset(&sigset);
(void)sigaddset(&sigset, SIGTSTP);
(void)sigaddset(&sigset, SIGCONT);
if (sigprocmask(SIG_BLOCK, &sigset, NULL))
err(1, "sigprocmask");
file_start();
play = rec = mix = sub = NULL;
aparams_init(&cipar, CHAN_MAX, 0, RATE_MIN);
aparams_init(&copar, CHAN_MAX, 0, RATE_MAX);
/*
* Iterate over all inputs and outputs and find the maximum
* sample rate and channel number.
*/
SLIST_FOREACH(fa, &ifiles, entry) {
if (cipar.cmin > fa->par.cmin)
cipar.cmin = fa->par.cmin;
if (cipar.cmax < fa->par.cmax)
cipar.cmax = fa->par.cmax;
if (cipar.rate < fa->par.rate)
cipar.rate = fa->par.rate;
}
SLIST_FOREACH(fa, &ofiles, entry) {
if (copar.cmin > fa->par.cmin)
copar.cmin = fa->par.cmin;
if (copar.cmax < fa->par.cmax)
copar.cmax = fa->par.cmax;
if (copar.rate > fa->par.rate)
copar.rate = fa->par.rate;
}
/*
* Open the device and increase the maximum sample rate.
* channel number to include those used by the device
*/
if (!u_flag) {
dipar = copar;
dopar = cipar;
}
fd = dev_init(devpath,
!SLIST_EMPTY(&ofiles) ? &dipar : NULL,
!SLIST_EMPTY(&ifiles) ? &dopar : NULL, &dinfr, &donfr);
if (fd < 0)
exit(1);
if (!SLIST_EMPTY(&ofiles)) {
if (!quiet_flag) {
fprintf(stderr, "%s: recording ", devpath);
aparams_print(&dipar);
fprintf(stderr, "\n");
}
if (copar.cmin > dipar.cmin)
copar.cmin = dipar.cmin;
if (copar.cmax < dipar.cmax)
copar.cmax = dipar.cmax;
if (copar.rate > dipar.rate)
copar.rate = dipar.rate;
dinfr *= DEFAULT_NBLK;
DPRINTF("%s: using %ums rec buffer\n", devpath,
1000 * dinfr / dipar.rate);
}
if (!SLIST_EMPTY(&ifiles)) {
if (!quiet_flag) {
fprintf(stderr, "%s: playing ", devpath);
aparams_print(&dopar);
fprintf(stderr, "\n");
}
if (cipar.cmin > dopar.cmin)
cipar.cmin = dopar.cmin;
if (cipar.cmax < dopar.cmax)
cipar.cmax = dopar.cmax;
if (cipar.rate < dopar.rate)
cipar.rate = dopar.rate;
donfr *= DEFAULT_NBLK;
DPRINTF("%s: using %ums play buffer\n", devpath,
1000 * donfr / dopar.rate);
}
/*
* Create buffers for the device.
*/
dev = file_new(fd, devpath);
if (!SLIST_EMPTY(&ofiles)) {
rec = rpipe_new(dev);
sub = sub_new();
}
if (!SLIST_EMPTY(&ifiles)) {
play = wpipe_new(dev);
mix = mix_new();
}
/*
* Calculate sizes of buffers using "common" parameters, to
* have roughly the same duration as device buffers.
*/
cinfr = donfr * cipar.rate / dopar.rate;
confr = dinfr * copar.rate / dipar.rate;
/*
* Create buffers for all input and output pipes.
*/
SLIST_FOREACH(fa, &ifiles, entry) {
newinput(fa, &cipar, cinfr, quiet_flag);
if (mix) {
aproc_setin(mix, fa->buf);
fa->buf->xrun = fa->xrun;
}
if (!quiet_flag) {
fprintf(stderr, "%s: reading ", fa->name);
aparams_print(&fa->par);
fprintf(stderr, "\n");
}
}
SLIST_FOREACH(fa, &ofiles, entry) {
newoutput(fa, &copar, confr, quiet_flag);
if (sub) {
aproc_setout(sub, fa->buf);
fa->buf->xrun = fa->xrun;
}
if (!quiet_flag) {
fprintf(stderr, "%s: writing ", fa->name);
aparams_print(&fa->par);
fprintf(stderr, "\n");
}
}
/*
* Connect the multiplexer to the device input.
*/
if (sub) {
buf = abuf_new(dinfr, aparams_bpf(&dipar));
aproc_setout(rec, buf);
if (!aparams_eq(&copar, &dipar)) {
if (!quiet_flag) {
fprintf(stderr, "%s: ", devpath);
aparams_print2(&dipar, &copar);
fprintf(stderr, "\n");
}
conv = conv_new("subconv", &dipar, &copar);
cbuf = abuf_new(confr, aparams_bpf(&copar));
aproc_setin(conv, buf);
aproc_setout(conv, cbuf);
aproc_setin(sub, cbuf);
} else
aproc_setin(sub, buf);
}
/*
* Normalize input levels and connect the mixer to the device
* output.
*/
if (mix) {
n = 0;
SLIST_FOREACH(fa, &ifiles, entry)
n++;
SLIST_FOREACH(fa, &ifiles, entry)
fa->buf->mixvol /= n;
buf = abuf_new(donfr, aparams_bpf(&dopar));
aproc_setin(play, buf);
if (!aparams_eq(&cipar, &dopar)) {
if (!quiet_flag) {
fprintf(stderr, "%s: ", devpath);
aparams_print2(&cipar, &dopar);
fprintf(stderr, "\n");
}
conv = conv_new("mixconv", &cipar, &dopar);
cbuf = abuf_new(cinfr, aparams_bpf(&cipar));
aproc_setout(conv, buf);
aproc_setin(conv, cbuf);
aproc_setout(mix, cbuf);
} else
aproc_setout(mix, buf);
}
/*
* start audio
*/
if (play != NULL) {
if (!quiet_flag)
fprintf(stderr, "filling buffers...\n");
buf = LIST_FIRST(&play->ibuflist);
while (!quit_flag) {
/* no more devices to poll */
if (!file_poll())
break;
/* eof */
if (dev->state & FILE_EOF)
break;
/* device is blocked and play buffer is full */
if ((dev->events & POLLOUT) && !ABUF_WOK(buf))
break;
}
}
if (!quiet_flag)
fprintf(stderr, "starting device...\n");
dev_start(dev->fd);
if (mix)
mix->u.mix.flags |= MIX_DROP;
if (sub)
sub->u.sub.flags |= SUB_DROP;
while (!quit_flag) {
if (!file_poll())
break;
}
if (!quiet_flag)
fprintf(stderr, "draining buffers...\n");
/*
* generate EOF on all files that do input, so
* once buffers are drained, everything will be cleaned
*/
LIST_FOREACH(f, &file_list, entry) {
if ((f->events) & POLLIN || (f->state & FILE_ROK))
file_eof(f);
}
for (;;) {
if (!file_poll())
break;
}
SLIST_FOREACH(fa, &ofiles, entry) {
if (fa->hdr == HDR_WAV)
wav_writehdr(fa->fd, &fa->par);
close(fa->fd);
DPRINTF("%s: closed\n", fa->name);
}
dev_stop(dev->fd);
file_stop();
return 0;
}