X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/763d5e6ad88ef3ba1cd1d7742d060e4f1e54c6b8..346ba8d53eb0c9cead1d3818eedc1b004ea938af:/server/speaker.c diff --git a/server/speaker.c b/server/speaker.c index 6296169..1cdbd28 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder - * Copyright (C) 2005, 2006 Richard Kettlewell + * Copyright (C) 2005, 2006, 2007 Richard Kettlewell * * 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 @@ -40,8 +40,10 @@ #include #include #include +#include #include -#include +#include +#include #include "configuration.h" #include "syscalls.h" @@ -51,6 +53,16 @@ #include "speaker.h" #include "user.h" +#if API_ALSA +#include +#endif + +#ifdef WORDS_BIGENDIAN +# define MACHINE_AO_FMT AO_FMT_BIG +#else +# define MACHINE_AO_FMT AO_FMT_LITTLE +#endif + #define BUFFER_SECONDS 5 /* How many seconds of input to * buffer. */ @@ -76,13 +88,18 @@ static struct track { static time_t last_report; /* when we last reported */ static int paused; /* pause status */ -static snd_pcm_t *pcm; /* current pcm handle */ static ao_sample_format pcm_format; /* current format if aodev != 0 */ static size_t bpf; /* bytes per frame */ static struct pollfd fds[NFDS]; /* if we need more than that */ static int fdno; /* fd number */ -static snd_pcm_uframes_t pcm_bufsize; /* buffer size */ +static size_t bufsize; /* buffer size */ +#if API_ALSA +static snd_pcm_t *pcm; /* current pcm handle */ +static snd_pcm_uframes_t last_pcm_bufsize; /* last seen buffer size */ +#endif +static int ready; /* ready to send audio */ static int forceplay; /* frames to force play */ +static int kidfd = -1; /* child process input */ static const struct option options[] = { { "help", no_argument, 0, 'h' }, @@ -228,10 +245,11 @@ static int formats_equal(const ao_sample_format *a, /* Close the sound device. */ static void idle(void) { - int err; - D(("idle")); - if(pcm) { +#if API_ALSA + if(!config->speaker_command && pcm) { + int err; + if((err = snd_pcm_nonblock(pcm, 0)) < 0) fatal(0, "error calling snd_pcm_nonblock: %d", err); D(("draining pcm")); @@ -242,6 +260,8 @@ static void idle(void) { forceplay = 0; D(("released audio device")); } +#endif + ready = 0; } /* Abandon the current track */ @@ -259,24 +279,135 @@ static void abandon(void) { forceplay = 0; } +#if API_ALSA +static void log_params(snd_pcm_hw_params_t *hwparams, + snd_pcm_sw_params_t *swparams) { + snd_pcm_uframes_t f; + unsigned u; + + return; /* too verbose */ + if(hwparams) { + /* TODO */ + } + if(swparams) { + snd_pcm_sw_params_get_silence_size(swparams, &f); + info("sw silence_size=%lu", (unsigned long)f); + snd_pcm_sw_params_get_silence_threshold(swparams, &f); + info("sw silence_threshold=%lu", (unsigned long)f); + snd_pcm_sw_params_get_sleep_min(swparams, &u); + info("sw sleep_min=%lu", (unsigned long)u); + snd_pcm_sw_params_get_start_threshold(swparams, &f); + info("sw start_threshold=%lu", (unsigned long)f); + snd_pcm_sw_params_get_stop_threshold(swparams, &f); + info("sw stop_threshold=%lu", (unsigned long)f); + snd_pcm_sw_params_get_xfer_align(swparams, &f); + info("sw xfer_align=%lu", (unsigned long)f); + } +} +#endif + +static void soxargs(const char ***pp, char **qq, ao_sample_format *ao) { + int n; + + *(*pp)++ = "-t.raw"; + *(*pp)++ = "-s"; + *(*pp)++ = *qq; n = sprintf(*qq, "-r%d", ao->rate); *qq += n + 1; + *(*pp)++ = *qq; n = sprintf(*qq, "-c%d", ao->channels); *qq += n + 1; + /* sox 12.17.9 insists on -b etc; CVS sox insists on - etc; both are + * deployed! */ + switch(config->sox_generation) { + case 0: + if(ao->bits != 8 + && ao->byte_format != AO_FMT_NATIVE + && ao->byte_format != MACHINE_AO_FMT) { + *(*pp)++ = "-x"; + } + switch(ao->bits) { + case 8: *(*pp)++ = "-b"; break; + case 16: *(*pp)++ = "-w"; break; + case 32: *(*pp)++ = "-l"; break; + case 64: *(*pp)++ = "-d"; break; + default: fatal(0, "cannot handle sample size %d", (int)ao->bits); + } + break; + case 1: + switch(ao->byte_format) { + case AO_FMT_NATIVE: break; + case AO_FMT_BIG: *(*pp)++ = "-B"; break; + case AO_FMT_LITTLE: *(*pp)++ = "-L"; break; + } + *(*pp)++ = *qq; n = sprintf(*qq, "-%d", ao->bits/8); *qq += n + 1; + break; + } +} + /* Make sure the sound device is open and has the right sample format. Return * 0 on success and -1 on error. */ static int activate(void) { - int err; - snd_pcm_hw_params_t *hwparams; - snd_pcm_sw_params_t *swparams; - int sample_format = 0; - unsigned rate; - /* If we don't know the format yet we cannot start. */ if(!playing->got_format) { D((" - not got format for %s", playing->id)); return -1; } + if(kidfd >= 0) { + if(!formats_equal(&playing->format, &config->sample_format)) { + char argbuf[1024], *q = argbuf; + const char *av[18], **pp = av; + int soxpipe[2]; + pid_t soxkid; + *pp++ = "sox"; + soxargs(&pp, &q, &playing->format); + *pp++ = "-"; + soxargs(&pp, &q, &config->sample_format); + *pp++ = "-"; + *pp++ = 0; + if(debugging) { + for(pp = av; *pp; pp++) + D(("sox arg[%d] = %s", pp - av, *pp)); + D(("end args")); + } + xpipe(soxpipe); + soxkid = xfork(); + if(soxkid == 0) { + xdup2(playing->fd, 0); + xdup2(soxpipe[1], 1); + fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK); + close(soxpipe[0]); + close(soxpipe[1]); + close(playing->fd); + execvp("sox", (char **)av); + _exit(1); + } + D(("forking sox for format conversion (kid = %d)", soxkid)); + close(playing->fd); + close(soxpipe[1]); + playing->fd = soxpipe[0]; + playing->format = config->sample_format; + ready = 0; + } + if(!ready) { + pcm_format = config->sample_format; + bufsize = 3 * FRAMES; + bpf = bytes_per_frame(&config->sample_format); + D(("acquired audio device")); + ready = 1; + } + return 0; + } + if(config->speaker_command) + return -1; +#if API_ALSA /* If we need to change format then close the current device. */ if(pcm && !formats_equal(&playing->format, &pcm_format)) idle(); if(!pcm) { + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; + snd_pcm_uframes_t pcm_bufsize; + int err; + int sample_format = 0; + unsigned rate; + D(("snd_pcm_open")); if((err = snd_pcm_open(&pcm, config->device, @@ -329,14 +460,16 @@ static int activate(void) { playing->format.channels, err); goto fatal; } - pcm_bufsize = 3 * FRAMES; + bufsize = 3 * FRAMES; + pcm_bufsize = bufsize; if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams, &pcm_bufsize)) < 0) fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d", 3 * FRAMES, err); - if(pcm_bufsize != 3 * FRAMES) + if(pcm_bufsize != 3 * FRAMES && pcm_bufsize != last_pcm_bufsize) info("asked for PCM buffer of %d frames, got %d", 3 * FRAMES, (int)pcm_bufsize); + last_pcm_bufsize = pcm_bufsize; if((err = snd_pcm_hw_params(pcm, hwparams)) < 0) fatal(0, "error calling snd_pcm_hw_params: %d", err); D(("set up sw params")); @@ -351,6 +484,8 @@ static int activate(void) { pcm_format = playing->format; bpf = bytes_per_frame(&pcm_format); D(("acquired audio device")); + log_params(hwparams, swparams); + ready = 1; } return 0; fatal: @@ -361,6 +496,7 @@ error: snd_pcm_close(pcm); pcm = 0; } +#endif return -1; } @@ -373,10 +509,27 @@ static void maybe_finished(void) { abandon(); } +static void fork_kid(void) { + pid_t kid; + int pfd[2]; + if(kidfd != -1) close(kidfd); + xpipe(pfd); + kid = xfork(); + if(!kid) { + xdup2(pfd[0], 0); + close(pfd[0]); + close(pfd[1]); + execl("/bin/sh", "sh", "-c", config->speaker_command, (char *)0); + fatal(errno, "error execing /bin/sh"); + } + close(pfd[0]); + kidfd = pfd[1]; + D(("forked kid %d, fd = %d", kid, kidfd)); +} + static void play(size_t frames) { - snd_pcm_sframes_t written_frames; - size_t avail_bytes, avail_frames, written_bytes; - int err; + size_t avail_bytes, written_frames; + ssize_t written_bytes; if(activate()) { if(playing) @@ -403,30 +556,61 @@ static void play(size_t frames) { avail_bytes = playing->size - playing->start; else avail_bytes = playing->used; - avail_frames = avail_bytes / bpf; - if(avail_frames > frames) - avail_frames = frames; - if(!avail_frames) - return; - written_frames = snd_pcm_writei(pcm, - playing->buffer + playing->start, - avail_frames); - D(("actually play %zu frames, wrote %d", - avail_frames, (int)written_frames)); - if(written_frames < 0) { - switch(written_frames) { - case -EPIPE: /* underrun */ - error(0, "snd_pcm_writei reports underrun"); - if((err = snd_pcm_prepare(pcm)) < 0) - fatal(0, "error calling snd_pcm_prepare: %d", err); - return; - case -EAGAIN: + + if(!config->speaker_command) { +#if API_ALSA + snd_pcm_sframes_t pcm_written_frames; + size_t avail_frames; + int err; + + avail_frames = avail_bytes / bpf; + if(avail_frames > frames) + avail_frames = frames; + if(!avail_frames) return; - default: - fatal(0, "error calling snd_pcm_writei: %d", (int)written_frames); + pcm_written_frames = snd_pcm_writei(pcm, + playing->buffer + playing->start, + avail_frames); + D(("actually play %zu frames, wrote %d", + avail_frames, (int)pcm_written_frames)); + if(pcm_written_frames < 0) { + switch(pcm_written_frames) { + case -EPIPE: /* underrun */ + error(0, "snd_pcm_writei reports underrun"); + if((err = snd_pcm_prepare(pcm)) < 0) + fatal(0, "error calling snd_pcm_prepare: %d", err); + return; + case -EAGAIN: + return; + default: + fatal(0, "error calling snd_pcm_writei: %d", + (int)pcm_written_frames); + } + } + written_frames = pcm_written_frames; + written_bytes = written_frames * bpf; +#else + assert(!"reached"); +#endif + } else { + if(avail_bytes > frames * bpf) + avail_bytes = frames * bpf; + written_bytes = write(kidfd, playing->buffer + playing->start, + avail_bytes); + D(("actually play %zu bytes, wrote %d", + avail_bytes, (int)written_bytes)); + if(written_bytes < 0) { + switch(errno) { + case EPIPE: + error(0, "hmm, kid died; trying another"); + fork_kid(); + return; + case EAGAIN: + return; + } } + written_frames = written_bytes / bpf; /* good enough */ } - written_bytes = written_frames * bpf; playing->start += written_bytes; playing->used -= written_bytes; playing->played += written_frames; @@ -451,6 +635,16 @@ static void report(void) { time(&last_report); } +static void reap(int __attribute__((unused)) sig) { + pid_t kid; + int st; + + do + kid = waitpid(-1, &st, WNOHANG); + while(kid > 0); + signal(SIGCHLD, reap); +} + static int addfd(int fd, int events) { if(fdno < NFDS) { fds[fdno].fd = fd; @@ -461,10 +655,12 @@ static int addfd(int fd, int events) { } int main(int argc, char **argv) { - int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, err; - unsigned short alsa_revents; + int n, fd, stdin_slot, alsa_slots, kid_slot; struct track *t; struct speaker_message sm; +#if API_ALSA + int alsa_nslots = -1, err; +#endif set_progname(argv); mem_init(0); @@ -488,6 +684,8 @@ int main(int argc, char **argv) { if(config_read()) fatal(0, "cannot read configuration"); /* ignore SIGPIPE */ signal(SIGPIPE, SIG_IGN); + /* reap kids */ + signal(SIGCHLD, reap); /* set nice value */ xnice(config->nice_speaker); /* change user */ @@ -495,6 +693,15 @@ int main(int argc, char **argv) { /* make sure we're not root, whatever the config says */ if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root"); info("started"); + if(config->speaker_command) + fork_kid(); + else { +#if API_ALSA + /* ok */ +#else + fatal(0, "invoked speaker but no speaker_command and no known sound API"); + #endif + } while(getppid() != 1) { fdno = 0; /* Always ready for commands from the main server. */ @@ -507,18 +714,40 @@ int main(int argc, char **argv) { playing->slot = -1; /* If forceplay is set then wait until it succeeds before waiting on the * sound device. */ - if(pcm && !forceplay) { - alsa_slots = fdno; - alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno); - fdno += alsa_nslots; - } else - alsa_slots = -1; + alsa_slots = -1; + kid_slot = -1; + if(ready && !forceplay) { + if(config->speaker_command) { + if(kidfd >= 0) + kid_slot = addfd(kidfd, POLLOUT); + } else { +#if API_ALSA + int retry = 3; + + alsa_slots = fdno; + do { + retry = 0; + alsa_nslots = snd_pcm_poll_descriptors(pcm, &fds[fdno], NFDS - fdno); + if((alsa_nslots <= 0 + || !(fds[alsa_slots].events & POLLOUT)) + && snd_pcm_state(pcm) == SND_PCM_STATE_XRUN) { + error(0, "underrun detected after call to snd_pcm_poll_descriptors()"); + if((err = snd_pcm_prepare(pcm))) + fatal(0, "error calling snd_pcm_prepare: %d", err); + } else + break; + } while(retry-- > 0); + if(alsa_nslots >= 0) + fdno += alsa_nslots; +#endif + } + } /* If any other tracks don't have a full buffer, try to read sample data * from them. */ for(t = tracks; t; t = t->next) if(t != playing) { if(!t->eof && t->used < t->size) { - t->slot = addfd(t->fd, POLLIN); + t->slot = addfd(t->fd, POLLIN | POLLHUP); } else t->slot = -1; } @@ -530,12 +759,19 @@ int main(int argc, char **argv) { } /* Play some sound before doing anything else */ if(alsa_slots != -1) { +#if API_ALSA + unsigned short alsa_revents; + if((err = snd_pcm_poll_descriptors_revents(pcm, &fds[alsa_slots], alsa_nslots, &alsa_revents)) < 0) fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err); - if(alsa_revents & POLLOUT) + if(alsa_revents & (POLLOUT | POLLERR)) + play(3 * FRAMES); +#endif + } else if(kid_slot != -1) { + if(fds[kid_slot].revents & (POLLOUT | POLLERR)) play(3 * FRAMES); } else { /* Some attempt to play must have failed */ @@ -561,7 +797,7 @@ int main(int argc, char **argv) { t = findtrack(sm.id, 1); if(fd != -1) acquire(t, fd); playing = t; - play(pcm_bufsize); + play(bufsize); report(); break; case SM_PAUSE: @@ -574,7 +810,7 @@ int main(int argc, char **argv) { if(paused) { paused = 0; if(playing) - play(pcm_bufsize); + play(bufsize); } report(); break; @@ -604,16 +840,16 @@ int main(int argc, char **argv) { } /* Read in any buffered data */ for(t = tracks; t; t = t->next) - if(t->slot != -1 && (fds[t->slot].revents & POLLIN)) + if(t->slot != -1 && (fds[t->slot].revents & (POLLIN | POLLHUP))) fill(t); /* We might be able to play now */ - if(pcm && forceplay && playing && !paused) + if(ready && forceplay && playing && !paused) play(forceplay); /* Maybe we finished playing a track somewhere in the above */ maybe_finished(); /* If we don't need the sound device for now then close it for the benefit * of anyone else who wants it. */ - if((!playing || paused) && pcm) + if((!playing || paused) && ready) idle(); /* If we've not reported out state for a second do so now. */ if(time(0) > last_report)