From 9d5da57642eeaf8e2b68f18bb1e6f56bfc7510b9 Mon Sep 17 00:00:00 2001 Message-Id: <9d5da57642eeaf8e2b68f18bb1e6f56bfc7510b9.1715145904.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sun, 8 Jul 2007 14:35:16 +0100 Subject: [PATCH] Merge from Mark's branch. revno: 9 committer: mdw@distorted.org.uk branch nick: disorder timestamp: Sat 2007-06-09 12:58:16 +0100 message: server/speaker: Wake up on POLLERR on kidpipe too. Organization: Straylight/Edgeware From: rjk@greenend.org.uk <> Ooops. The theory was that the speaker process would notice an EPIPE on its kid pipe and respawn its kid. Unfortunately I don't understand poll(2) enough, and failed to listen for POLLERR, so in fact the speaker goes into a tailspin if its kid dies. ------------------------------------------------------------ revno: 8 committer: Mark Wooding branch nick: disorder timestamp: Wed 2007-05-30 11:56:52 +0100 message: Support streaming to external process. Introduce two new configuration variables: * speaker_command: Shell command to pipe audio to instead of using an ALSA device. * sample_format: The sample format expected by this process in the form BITS/RATE/CHANNELS -- each integers, except that BITS may be suffixed by `b' or `l' for big- or little-endian respectively. The default is 16/44100/2. If speaker_command is unset, everything is as it used to be. If it's set, however, disorder-speaker will resample its input (using sox) to conform to the desired format if necessary, and pipe the result to the command's stdin. Only one speaker_command is run at a time (respawned automatically if it quits), and it receives the audio data of many tracks. An example command (and the reason I did this grim hack): lame -h -r -s44.1 -b128 -x -mj --preset standard - - | ices -c /etc/disorder/ices.conf >/dev/null re-encodes as an MP3 stream and feeds the result to my IceCast server. --- lib/configuration.c | 83 +++++++++++++++++ lib/configuration.h | 4 + server/speaker.c | 221 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 261 insertions(+), 47 deletions(-) diff --git a/lib/configuration.c b/lib/configuration.c index 698aac7..99e6d40 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -255,6 +255,75 @@ static int set_restrict(const struct config_state *cs, return 0; } +static int parse_sample_format(const struct config_state *cs, + ao_sample_format *ao, + int nvec, char **vec) { + char *p = vec[0]; + long t; + + if (nvec != 1) { + error(0, "%s:%d: wrong number of arguments", cs->path, cs->line); + return -1; + } + if (xstrtol(&t, p, &p, 0)) { + error(errno, "%s:%d: converting bits-per-sample", cs->path, cs->line); + return -1; + } + if (t != 8 && t != 16) { + error(0, "%s:%d: bad bite-per-sample (%ld)", cs->path, cs->line, t); + return -1; + } + if (ao) ao->bits = t; + switch (*p) { + case 'l': case 'L': t = AO_FMT_LITTLE; p++; break; + case 'b': case 'B': t = AO_FMT_BIG; p++; break; + default: t = AO_FMT_NATIVE; break; + } + if (ao) ao->byte_format = t; + if (*p != '/') { + error(errno, "%s:%d: expected `/' after bits-per-sample", + cs->path, cs->line); + return -1; + } + p++; + if (xstrtol(&t, p, &p, 0)) { + error(errno, "%s:%d: converting sample-rate", cs->path, cs->line); + return -1; + } + if (t < 1 || t > INT_MAX) { + error(0, "%s:%d: silly sample-rate (%ld)", cs->path, cs->line, t); + return -1; + } + if (ao) ao->rate = t; + if (*p != '/') { + error(0, "%s:%d: expected `/' after sample-rate", + cs->path, cs->line); + return -1; + } + p++; + if (xstrtol(&t, p, &p, 0)) { + error(errno, "%s:%d: converting channels", cs->path, cs->line); + return -1; + } + if (t < 1 || t > 8) { + error(0, "%s:%d: silly number (%ld) of channels", cs->path, cs->line, t); + return -1; + } + if (ao) ao->channels = t; + if (*p) { + error(0, "%s:%d: junk after channels", cs->path, cs->line); + return -1; + } + return 0; +} + +static int set_sample_format(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + return parse_sample_format(cs, ADDRESS(cs->config, ao_sample_format), + nvec, vec); +} + static int set_namepart(const struct config_state *cs, const struct conf *whoami, int nvec, char **vec) { @@ -430,6 +499,7 @@ static const struct conftype type_integer = { set_integer, free_none }, type_stringlist_accum = { set_stringlist_accum, free_stringlistlist }, type_string_accum = { set_string_accum, free_stringlist }, + type_sample_format = { set_sample_format, free_none }, type_restrict = { set_restrict, free_none }, type_namepart = { set_namepart, free_namepartlist }, type_transform = { set_transform, free_transformlist }; @@ -550,6 +620,12 @@ static int validate_isauser(const struct config_state *cs, return 0; } +static int validate_sample_format(const struct config_state *cs, + int attribute((unused)) nvec, + char **vec) { + return parse_sample_format(cs, 0, nvec, vec); +} + static int validate_channel(const struct config_state *cs, int attribute((unused)) nvec, char **vec) { @@ -675,8 +751,10 @@ static const struct conf conf[] = { { C(prefsync), &type_integer, validate_positive }, { C(refresh), &type_integer, validate_positive }, { C2(restrict, restrictions), &type_restrict, validate_any }, + { C(sample_format), &type_sample_format, validate_sample_format }, { C(scratch), &type_string_accum, validate_isreg }, { C(signal), &type_signal, validate_any }, + { C(speaker_command), &type_string, validate_any }, { C(stopword), &type_string_accum, validate_any }, { C(templates), &type_string_accum, validate_isdir }, { C(transform), &type_transform, validate_any }, @@ -789,6 +867,11 @@ static struct config *config_default(void) { c->lock = 1; c->device = xstrdup("default"); c->nice_rescan = 10; + c->speaker_command = 0; + c->sample_format.bits = 16; + c->sample_format.rate = 44100; + c->sample_format.channels = 2; + c->sample_format.byte_format = AO_FMT_NATIVE; return c; } diff --git a/lib/configuration.h b/lib/configuration.h index 4af3cad..75e066c 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -21,6 +21,8 @@ #ifndef CONFIGURATION_H #define CONFIGURATION_H +#include + struct real_pcre; /* Configuration is kept in a @struct config@; the live configuration @@ -97,6 +99,8 @@ struct config { int lock; /* server takes a lock */ long nice_server; /* nice value for server */ long nice_speaker; /* nice value for speaker */ + const char *speaker_command; /* command for speaker to run */ + ao_sample_format sample_format; /* sample format to enforce */ /* shared client/server config */ const char *home; /* home directory for state files */ /* client config */ diff --git a/server/speaker.c b/server/speaker.c index f850572..d4714bd 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include "configuration.h" @@ -85,7 +86,9 @@ 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 snd_pcm_uframes_t last_pcm_bufsize; /* last seen buffer size */ +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' }, @@ -245,6 +248,7 @@ static void idle(void) { forceplay = 0; D(("released audio device")); } + ready = 0; } /* Abandon the current track */ @@ -287,6 +291,22 @@ static void log_params(snd_pcm_hw_params_t *hwparams, } } +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; + switch(ao->byte_format) { + case AO_FMT_NATIVE: break; + case AO_FMT_BIG: *(*pp)++ = "-B"; + case AO_FMT_LITTLE: *(*pp)++ = "-L"; + } + *(*pp)++ = *qq; n = sprintf(*qq, "-%d", ao->bits/8); *qq += n + 1; + *(*pp)++ = *qq; n = sprintf(*qq, "-c%d", ao->channels); *qq += n + 1; +} + /* 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) { @@ -301,6 +321,51 @@ static int activate(void) { 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; + pcm_bufsize = 3 * FRAMES; + bpf = bytes_per_frame(&config->sample_format); + D(("acquired audio device")); + ready = 1; + } + return 0; + } /* If we need to change format then close the current device. */ if(pcm && !formats_equal(&playing->format, &pcm_format)) idle(); @@ -381,6 +446,7 @@ static int activate(void) { bpf = bytes_per_frame(&pcm_format); D(("acquired audio device")); log_params(hwparams, swparams); + ready = 1; } return 0; fatal: @@ -403,9 +469,28 @@ 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; + size_t avail_bytes, avail_frames; + ssize_t written_bytes; int err; if(activate()) { @@ -433,30 +518,51 @@ 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(kidfd == -1) { + 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); + 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: + return; + default: + fatal(0, "error calling snd_pcm_writei: %d", (int)written_frames); + } + } + written_bytes = written_frames * bpf; + } 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; @@ -481,6 +587,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; @@ -491,7 +607,7 @@ static int addfd(int fd, int events) { } int main(int argc, char **argv) { - int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, err; + int n, fd, stdin_slot, alsa_slots, alsa_nslots = -1, kid_slot, err; unsigned short alsa_revents; struct track *t; struct speaker_message sm; @@ -518,6 +634,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 */ @@ -525,6 +643,7 @@ 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(); while(getppid() != 1) { fdno = 0; /* Always ready for commands from the main server. */ @@ -537,32 +656,37 @@ int main(int argc, char **argv) { playing->slot = -1; /* If forceplay is set then wait until it succeeds before waiting on the * sound device. */ + alsa_slots = -1; + kid_slot = -1; if(pcm && !forceplay) { - 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; - } else - alsa_slots = -1; + if(kidfd >= 0) + kid_slot = addfd(kidfd, POLLOUT); + else { + 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; + } + } /* 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; } @@ -579,7 +703,10 @@ int main(int argc, char **argv) { 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); + } else if(kid_slot != -1) { + if(fds[kid_slot].revents & (POLLOUT | POLLERR)) play(3 * FRAMES); } else { /* Some attempt to play must have failed */ @@ -648,16 +775,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) -- [mdw]