From 6d2d327ca57fefaddceba10eb323451f8150e95d Mon Sep 17 00:00:00 2001 Message-Id: <6d2d327ca57fefaddceba10eb323451f8150e95d.1713564698.git.mdw@distorted.org.uk> From: Mark Wooding Date: Fri, 28 Sep 2007 14:21:10 +0100 Subject: [PATCH] speaker protocol redesign to cope with libao re-opening Organization: Straylight/Edgeware From: Richard Kettlewell --- .bzrignore | 1 + driver/disorder.c | 34 +++++-- lib/configuration.c | 47 +++++---- lib/configuration.h | 4 +- lib/mixer.c | 1 + lib/plugin.c | 1 + lib/speaker-protocol.h | 33 +++++++ server/Makefile.am | 6 +- server/api-client.c | 1 + server/cgimain.c | 1 + server/disorderd.c | 1 + server/normalize.c | 204 +++++++++++++++++++++++++++++++++++++++ server/play.c | 52 ++++++++-- server/speaker-alsa.c | 33 +++---- server/speaker-command.c | 8 +- server/speaker-network.c | 20 ++-- server/speaker.c | 201 ++++++-------------------------------- server/speaker.h | 58 +++++------ 18 files changed, 435 insertions(+), 271 deletions(-) create mode 100644 server/normalize.c diff --git a/.bzrignore b/.bzrignore index dbd4af4..c07af06 100644 --- a/.bzrignore +++ b/.bzrignore @@ -104,3 +104,4 @@ debian/disorder-playrtp *.bz2 debian/disobedience server/disorder-decode +server/disorder-normalize diff --git a/driver/disorder.c b/driver/disorder.c index bdcb147..fb05b59 100644 --- a/driver/disorder.c +++ b/driver/disorder.c @@ -1,7 +1,6 @@ - /* * This file is part of DisOrder. - * Copyright (C) 2005 Richard Kettlewell + * Copyright (C) 2005, 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 @@ -18,8 +17,15 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ +/** @file driver/disorder.c + * @brief libao driver used by DisOrder + * + * The output from this driver is expected to be fed to @c + * disorder-normalize to convert to the confnigured target format. + */ #include +#include "types.h" #include #include @@ -29,15 +35,21 @@ #include #include +#include "speaker-protocol.h" + /* extra declarations to help out lazy */ int ao_plugin_test(void); ao_info *ao_plugin_driver_info(void); char *ao_plugin_file_extension(void); -/* private data structure for this driver */ +/** @brief Private data structure for this driver */ struct internal { int fd; /* output file descriptor */ int exit_on_error; /* exit on write error */ + + /** @brief Record of sample format */ + struct stream_header header; + }; /* like write() but never returns EINTR/EAGAIN or short */ @@ -126,10 +138,10 @@ int ao_plugin_open(ao_device *device, ao_sample_format *format) { /* we would like native-order samples */ device->driver_byte_format = AO_FMT_NATIVE; - if(do_write(i->fd, format, sizeof *format) < 0) { - if(i->exit_on_error) exit(-1); - return 0; - } + i->header.rate = format->rate; + i->header.channels = format->channels; + i->header.bits = format->bits; + i->header.endian = ENDIAN_NATIVE; return 1; } @@ -138,6 +150,14 @@ int ao_plugin_play(ao_device *device, const char *output_samples, uint_32 num_bytes) { struct internal *i = device->internal; + /* Fill in and write the header */ + i->header.nbytes = num_bytes; + if(do_write(i->fd, &i->header, sizeof i->header) < 0) { + if(i->exit_on_error) _exit(-1); + return 0; + } + + /* Write the sample data */ if(do_write(i->fd, output_samples, num_bytes) < 0) { if(i->exit_on_error) _exit(-1); return 0; diff --git a/lib/configuration.c b/lib/configuration.c index 47ba9f4..486a2ab 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -278,61 +278,61 @@ static int set_restrict(const struct config_state *cs, } static int parse_sample_format(const struct config_state *cs, - ao_sample_format *ao, + struct stream_header *format, int nvec, char **vec) { char *p = vec[0]; long t; - if (nvec != 1) { + if(nvec != 1) { error(0, "%s:%d: wrong number of arguments", cs->path, cs->line); return -1; } - if (xstrtol(&t, p, &p, 0)) { + 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) { + 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; + if(format) format->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; + case 'l': case 'L': t = ENDIAN_LITTLE; p++; break; + case 'b': case 'B': t = ENDIAN_BIG; p++; break; + default: t = ENDIAN_NATIVE; break; } - if (ao) ao->byte_format = t; - if (*p != '/') { + if(format) format->endian = 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)) { + 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) { + 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 != '/') { + if(format) format->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)) { + if(xstrtol(&t, p, &p, 0)) { error(errno, "%s:%d: converting channels", cs->path, cs->line); return -1; } - if (t < 1 || t > 8) { + 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) { + if(format) format->channels = t; + if(*p) { error(0, "%s:%d: junk after channels", cs->path, cs->line); return -1; } @@ -342,7 +342,7 @@ static int parse_sample_format(const struct config_state *cs, 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), + return parse_sample_format(cs, ADDRESS(cs->config, struct stream_header), nvec, vec); } @@ -971,7 +971,7 @@ static struct config *config_default(void) { c->sample_format.bits = 16; c->sample_format.rate = 44100; c->sample_format.channels = 2; - c->sample_format.byte_format = AO_FMT_NATIVE; + c->sample_format.endian = ENDIAN_NATIVE; c->queue_pad = 10; c->speaker_backend = -1; c->multicast_ttl = 1; @@ -1059,6 +1059,13 @@ static void config_postdefaults(struct config *c) { fatal(0, "speaker_backend is command but speaker_command is not set"); if(c->speaker_backend == BACKEND_NETWORK && !c->broadcast.n) fatal(0, "speaker_backend is network but broadcast is not set"); + if(c->speaker_backend) { + /* Override sample format */ + c->sample_format.rate = 44100; + c->sample_format.channels = 2; + c->sample_format.bits = 16; + c->sample_format.endian = ENDIAN_BIG; + } } /** @brief (Re-)read the config file */ diff --git a/lib/configuration.h b/lib/configuration.h index eabbcce..84b1eb8 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -24,7 +24,7 @@ #ifndef CONFIGURATION_H #define CONFIGURATION_H -#include +#include "speaker-protocol.h" struct real_pcre; @@ -162,7 +162,7 @@ struct config { const char *speaker_command; /** @brief Target sample format */ - ao_sample_format sample_format; + struct stream_header sample_format; /** @brief Sox syntax generation */ long sox_generation; diff --git a/lib/mixer.c b/lib/mixer.c index 5ef30a2..4fd4a9a 100644 --- a/lib/mixer.c +++ b/lib/mixer.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/lib/plugin.c b/lib/plugin.c index 946f499..57496e6 100644 --- a/lib/plugin.c +++ b/lib/plugin.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/lib/speaker-protocol.h b/lib/speaker-protocol.h index eb2a1ae..8809f0f 100644 --- a/lib/speaker-protocol.h +++ b/lib/speaker-protocol.h @@ -104,6 +104,39 @@ int speaker_recv(int fd, struct speaker_message *sm, int *datafd); * on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other * error. */ +/** @brief One chunk in a stream */ +struct stream_header { + /** @brief Frames per second */ + uint32_t rate; + + /** @brief Samples per frames */ + uint8_t channels; + + /** @brief Bits per sample */ + uint8_t bits; + + /** @brief Endianness */ + uint8_t endian; +#define ENDIAN_BIG 1 +#define ENDIAN_LITTLE 2 +#ifdef WORDS_BIGENDIAN +# define ENDIAN_NATIVE ENDIAN_BIG +#else +# define ENDIAN_NATIVE ENDIAN_LITTLE +#endif + + /** @brief Number of bytes */ + uint32_t nbytes; +} attribute((packed)); + +static inline int formats_equal(const struct stream_header *a, + const struct stream_header *b) { + return (a->rate == b->rate + && a->channels == b->channels + && a->bits == b->bits + && a->endian == b->endian); +} + #endif /* SPEAKER_PROTOCOL_H */ /* diff --git a/server/Makefile.am b/server/Makefile.am index d15d3f4..548ac2a 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -19,7 +19,7 @@ # sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \ - disorder-speaker disorder-decode + disorder-speaker disorder-decode disorder-normalize noinst_PROGRAMS=disorder.cgi trackname noinst_DATA=uk.org.greenend.rjk.disorder.plist @@ -57,6 +57,10 @@ disorder_decode_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ $(LIBMAD) disorder_decode_DEPENDENCIES=../lib/libdisorder.a +disorder_normalize_SOURCES=normalize.c +disorder_normalize_LDADD=$(LIBOBJS) ../lib/libdisorder.a $(LIBPCRE) +disorder_normalize_DEPENDENCIES=../lib/libdisorder.a + disorder_rescan_SOURCES=rescan.c \ api.c api-server.c \ trackdb.c trackdb.h exports.c \ diff --git a/server/api-client.c b/server/api-client.c index 086de15..1e0ed7a 100644 --- a/server/api-client.c +++ b/server/api-client.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/server/cgimain.c b/server/cgimain.c index 467ed59..231ece1 100644 --- a/server/cgimain.c +++ b/server/cgimain.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/server/disorderd.c b/server/disorderd.c index f2ebb9a..1100140 100644 --- a/server/disorderd.c +++ b/server/disorderd.c @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include diff --git a/server/normalize.c b/server/normalize.c new file mode 100644 index 0000000..7e5fbe8 --- /dev/null +++ b/server/normalize.c @@ -0,0 +1,204 @@ +/* + * This file is part of DisOrder + * Copyright (C) 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +/** @file server/disorder-normalize.c + * @brief Convert "raw" format output to the configured format + * + * Currently we invoke sox even for trivial conversions such as byte-swapping. + * Ideally we would do all conversion including resampling in this one process + * and eliminate the dependency on sox. + */ + +#include +#include "types.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "syscalls.h" +#include "log.h" +#include "configuration.h" +#include "speaker-protocol.h" + +/** @brief Copy bytes from one file descriptor to another + * @param infd File descriptor read from + * @param outfd File descriptor to write to + * @param n Number of bytes to copy + */ +static void copy(int infd, int outfd, size_t n) { + char buffer[4096], *ptr; + int r, w; + + while(n > 0) { + r = read(infd, buffer, sizeof buffer); + if(r < 0) { + if(errno == EINTR) + continue; + else + fatal(errno, "read error"); + } + if(r == 0) + fatal(0, "unexpected EOF"); + n -= r; + ptr = buffer; + while(r > 0) { + w = write(outfd, ptr, r - (ptr - buffer)); + if(w < 0) + fatal(errno, "write error"); + ptr += w; + } + } +} + +static void soxargs(const char ***pp, char **qq, + const struct stream_header *header) { + *(*pp)++ = "-t.raw"; + *(*pp)++ = "-s"; + *qq += sprintf((char *)(*(*pp)++ = *qq), "-r%d", header->rate) + 1; + *qq += sprintf((char *)(*(*pp)++ = *qq), "-c%d", header->channels) + 1; + /* sox 12.17.9 insists on -b etc; CVS sox insists on - etc; both are + * deployed! */ + switch(config->sox_generation) { + case 0: + if(header->bits != 8 + && header->endian != ENDIAN_NATIVE) + *(*pp)++ = "-x"; + switch(header->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", header->bits); + } + break; + case 1: + if(header->bits != 8 + && header->endian != ENDIAN_NATIVE) + switch(header->endian) { + case ENDIAN_BIG: *(*pp)++ = "-B"; break; + case ENDIAN_LITTLE: *(*pp)++ = "-L"; break; + } + if(header->bits % 8) + fatal(0, "cannot handle sample size %d", header->bits); + *qq += sprintf((char *)(*(*pp)++ = *qq), "-%d", header->bits / 8) + 1; + break; + default: + fatal(0, "unknown sox_generation %ld", config->sox_generation); + } +} + +int main(int argc, char attribute((unused)) **argv) { + struct stream_header header, latest_format; + int n, p[2], outfd = -1; + pid_t pid = -1; + + set_progname(argv); + if(!setlocale(LC_CTYPE, "")) + fatal(errno, "error calling setlocale"); + if(argc > 1) + fatal(0, "not intended to be invoked by users"); + if(config_read()) + fatal(0, "cannot read configuration"); + if(!isatty(2)) { + openlog(progname, LOG_PID, LOG_DAEMON); + log_default = &log_syslog; + } + memset(&latest_format, 0, sizeof latest_format); + for(;;) { + if((n = read(0, &header, sizeof header)) < 0) + fatal(errno, "read error"); + else if(n == 0) + exit(0); + else if((size_t)n < sizeof header) + fatal(0, "short header"); + /* Sanity check the header */ + if(header.rate < 100 || header.rate > 1000000) + fatal(0, "implausible rate %"PRId32"Hz (%#"PRIx32")", + header.rate, header.rate); + if(header.channels < 1 || header.channels > 2) + fatal(0, "unsupported channel count %d", header.channels); + if(header.bits % 8 || !header.bits || header.bits > 64) + fatal(0, "unsupported sample size %d bits", header.bits); + if(header.endian != ENDIAN_BIG && header.endian != ENDIAN_LITTLE) + fatal(0, "unsupported byte order %x", header.bits); + /* Skip empty chunks regardless of their alleged format */ + if(header.nbytes == 0) + continue; + /* If the format has changed we stop/start the converter */ + if(!formats_equal(&header, &latest_format)) { + if(pid != -1) { + /* There's a running converter, stop it */ + xclose(outfd); + if(waitpid(pid, &n, 0) < 0) + fatal(errno, "error calling waitpid"); + if(n) + fatal(0, "sox failed: %#x", n); + pid = -1; + outfd = -1; + } + if(!formats_equal(&header, &config->sample_format)) { + const char *av[32], **pp = av; + char argbuf[1024], *q = argbuf; + + /* Input format doesn't match target, need to start a converter */ + *pp++ = "sox"; + soxargs(&pp, &q, &header); + *pp++ = "-"; /* stdin */ + soxargs(&pp, &q, &config->sample_format); + *pp++ = "-"; /* stdout */ + *pp = 0; + /* This pipe will be sox's stdin */ + xpipe(p); + if(!(pid = xfork())) { + exitfn = _exit; + xdup2(p[0], 0); + xclose(p[0]); + xclose(p[1]); + execvp(av[0], (char **)av); + fatal(errno, "sox"); + } + xclose(p[0]); + outfd = p[1]; + } else + /* Input format matches output, can just copy bytes */ + outfd = 1; + /* Remember current format for next iteration */ + latest_format = header; + } + /* Convert or copy this chunk */ + copy(0, outfd, header.nbytes); + } + if(outfd != -1) + xclose(outfd); + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/server/play.c b/server/play.c index 94567c4..3b3b928 100644 --- a/server/play.c +++ b/server/play.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder. - * Copyright (C) 2004, 2005, 2006 Richard Kettlewell + * Copyright (C) 2004, 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 @@ -19,6 +19,7 @@ */ #include +#include "types.h" #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include "event.h" #include "log.h" @@ -291,7 +293,7 @@ static int start(ev_source *ev, int smop) { int n, lfd; const char *p; - int sp[2]; + int np[2], sp[2]; struct speaker_message sm; char buffer[64]; int optc; @@ -301,7 +303,7 @@ static int start(ev_source *ev, struct timespec ts; const char *waitdevice = 0; const char *const *optv; - pid_t pid; + pid_t pid, npid; memset(&sm, 0, sizeof sm); if(find_player_pid(q->id) > 0) { @@ -365,21 +367,53 @@ static int start(ev_source *ev, xclose(lfd); /* tidy up */ setpgid(0, 0); if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) { - /* Raw format players write down a pipe (in fact a socket) to - * the speaker process. */ + /* "Raw" format players need special treatment: + * 1) their output needs to go via the disorder-normalize process + * 2) the output of that needs to be passed to the disorder-speaker + * process. + */ + /* np will be the pipe to disorder-normalize */ + if(socketpair(PF_UNIX, SOCK_STREAM, 0, np) < 0) + fatal(errno, "error calling socketpair"); + xshutdown(np[0], SHUT_WR); /* normalize reads from np[0] */ + xshutdown(np[1], SHUT_RD); /* decoder writes to np[1] */ + /* sp will be the pipe to disorder-speaker */ sm.type = smop; - strcpy(sm.id, q->id); if(socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0) fatal(errno, "error calling socketpair"); - xshutdown(sp[0], SHUT_WR); - xshutdown(sp[1], SHUT_RD); + xshutdown(sp[0], SHUT_WR); /* speaker reads from sp[0] */ + xshutdown(sp[1], SHUT_RD); /* normalize writes to sp[1] */ + /* Start disorder-normalize */ + if(!(npid = xfork())) { + if(!xfork()) { + xdup2(np[0], 0); + xdup2(sp[1], 1); + xclose(np[0]); + xclose(np[1]); + xclose(sp[0]); + xclose(sp[1]); + execlp("disorder-normalize", "disorder-normalize", (char *)0); + fatal(errno, "executing disorder-normalize"); + } + _exit(0); + } else { + int w; + + while(waitpid(npid, &w, 0) < 0 && errno == EINTR) + ; + } + /* Send the speaker process the file descriptor to read from */ + strcpy(sm.id, q->id); speaker_send(speaker_fd, &sm, sp[0]); /* Pass the file descriptor to the driver in an environment * variable. */ - snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", sp[1]); + snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", np[1]); if(putenv(buffer) < 0) fatal(errno, "error calling putenv"); + /* Close all the FDs we don't need */ xclose(sp[0]); + xclose(sp[1]); + xclose(np[0]); } if(waitdevice) { ao_initialize(); diff --git a/server/speaker-alsa.c b/server/speaker-alsa.c index fa9e6c3..80395f3 100644 --- a/server/speaker-alsa.c +++ b/server/speaker-alsa.c @@ -92,10 +92,6 @@ static void alsa_deactivate(void) { /** @brief ALSA backend activation */ static void alsa_activate(void) { - /* If we need to change format then close the current device. */ - if(pcm && !formats_equal(&playing->format, &device_format)) - alsa_deactivate(); - /* Now if the sound device is open it must have the right format */ if(!pcm) { snd_pcm_hw_params_t *hwparams; snd_pcm_sw_params_t *swparams; @@ -119,21 +115,21 @@ static void alsa_activate(void) { if((err = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) fatal(0, "error from snd_pcm_hw_params_set_access: %d", err); - switch(playing->format.bits) { + switch(config->sample_format.bits) { case 8: sample_format = SND_PCM_FORMAT_S8; break; case 16: - switch(playing->format.byte_format) { - case AO_FMT_NATIVE: sample_format = SND_PCM_FORMAT_S16; break; - case AO_FMT_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break; - case AO_FMT_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break; - error(0, "unrecognized byte format %d", playing->format.byte_format); + switch(config->sample_format.endian) { + case ENDIAN_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break; + case ENDIAN_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break; + default: + error(0, "unrecognized byte format %d", config->sample_format.endian); goto fatal; } break; default: - error(0, "unsupported sample size %d", playing->format.bits); + error(0, "unsupported sample size %d", config->sample_format.bits); goto fatal; } if((err = snd_pcm_hw_params_set_format(pcm, hwparams, @@ -142,18 +138,18 @@ static void alsa_activate(void) { sample_format, err); goto fatal; } - rate = playing->format.rate; + rate = config->sample_format.rate; if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0) { error(0, "error from snd_pcm_hw_params_set_rate (%d): %d", - playing->format.rate, err); + config->sample_format.rate, err); goto fatal; } - if(rate != (unsigned)playing->format.rate) - info("want rate %d, got %u", playing->format.rate, rate); - if((err = snd_pcm_hw_params_set_channels(pcm, hwparams, - playing->format.channels)) < 0) { + if(rate != (unsigned)config->sample_format.rate) + info("want rate %d, got %u", config->sample_format.rate, rate); + if((err = snd_pcm_hw_params_set_channels + (pcm, hwparams, config->sample_format.channels)) < 0) { error(0, "error from snd_pcm_hw_params_set_channels (%d): %d", - playing->format.channels, err); + config->sample_format.channels, err); goto fatal; } pcm_bufsize = 3 * FRAMES; @@ -176,7 +172,6 @@ static void alsa_activate(void) { FRAMES, err); if((err = snd_pcm_sw_params(pcm, swparams)) < 0) fatal(0, "error calling snd_pcm_sw_params: %d", err); - device_format = playing->format; D(("acquired audio device")); log_params(hwparams, swparams); device_state = device_open; diff --git a/server/speaker-command.c b/server/speaker-command.c index 8a8f63c..088d0cd 100644 --- a/server/speaker-command.c +++ b/server/speaker-command.c @@ -25,6 +25,7 @@ #include #include +#include #include "configuration.h" #include "syscalls.h" @@ -52,6 +53,7 @@ static void fork_cmd(void) { xpipe(pfd); cmdpid = xfork(); if(!cmdpid) { + exitfn = _exit; signal(SIGPIPE, SIG_DFL); xdup2(pfd[0], 0); close(pfd[0]); @@ -72,7 +74,7 @@ static void command_init(void) { /** @brief Play to a subprocess */ static size_t command_play(size_t frames) { - size_t bytes = frames * device_bpf; + size_t bytes = frames * bpf; int written_bytes; written_bytes = write(cmdfd, playing->buffer + playing->start, bytes); @@ -90,7 +92,7 @@ static size_t command_play(size_t frames) { fatal(errno, "error writing to subprocess"); } } else - return written_bytes / device_bpf; + return written_bytes / bpf; } /** @brief Update poll array for writing to subprocess */ @@ -111,7 +113,7 @@ static int command_ready(void) { const struct speaker_backend command_backend = { BACKEND_COMMAND, - FIXED_FORMAT, + 0, command_init, 0, /* activate */ command_play, diff --git a/server/speaker-network.c b/server/speaker-network.c index 5b1ccce..59630ff 100644 --- a/server/speaker-network.c +++ b/server/speaker-network.c @@ -32,6 +32,7 @@ #include #include #include +#include #include "configuration.h" #include "syscalls.h" @@ -106,11 +107,6 @@ static void network_init(void) { socklen_t len; char *sockname, *ssockname; - /* Override sample format */ - config->sample_format.rate = 44100; - config->sample_format.channels = 2; - config->sample_format.bits = 16; - config->sample_format.byte_format = AO_FMT_BIG; res = get_address(&config->broadcast, &pref, &sockname); if(!res) exit(-1); if(config->broadcast_from.n) { @@ -200,7 +196,7 @@ static void network_init(void) { static size_t network_play(size_t frames) { struct rtp_header header; struct iovec vec[2]; - size_t bytes = frames * device_bpf, written_frames; + size_t bytes = frames * bpf, written_frames; int written_bytes; /* We transmit using RTP (RFC3550) and attempt to conform to the internet * AVT profile (RFC3551). */ @@ -216,8 +212,8 @@ static size_t network_play(size_t frames) { /* Find the number of microseconds elapsed since rtp_time=0 */ delta = tvsub_us(now, rtp_time_0); assert(delta <= UINT64_MAX / 88200); - target_rtp_time = (delta * playing->format.rate - * playing->format.channels) / 1000000; + target_rtp_time = (delta * config->sample_format.rate + * config->sample_format.channels) / 1000000; /* Overflows at ~6 years uptime with 44100Hz stereo */ /* rtp_time is the number of samples we've played. NB that we play @@ -276,7 +272,7 @@ static size_t network_play(size_t frames) { if(bytes > NETWORK_BYTES - sizeof header) { bytes = NETWORK_BYTES - sizeof header; /* Always send a whole number of frames */ - bytes -= bytes % device_bpf; + bytes -= bytes % bpf; } /* "The RTP clock rate used for generating the RTP timestamp is independent * of the number of channels and the encoding; it equals the number of @@ -302,9 +298,9 @@ static size_t network_play(size_t frames) { } else audio_errors /= 2; written_bytes -= sizeof (struct rtp_header); - written_frames = written_bytes / device_bpf; + written_frames = written_bytes / bpf; /* Advance RTP's notion of the time */ - rtp_time += written_frames * playing->format.channels; + rtp_time += written_frames * config->sample_format.channels; return written_frames; } @@ -345,7 +341,7 @@ static int network_ready(void) { const struct speaker_backend network_backend = { BACKEND_NETWORK, - FIXED_FORMAT, + 0, network_init, 0, /* activate */ network_play, diff --git a/server/speaker.c b/server/speaker.c index 1dc90e4..aa09c02 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -29,15 +29,9 @@ * 8- and 16- bit stereo and mono are supported, with any sample rate (within * the limits that ALSA can deal with.) * - * When communicating with a subprocess, sox is invoked to convert the inbound - * data to a single consistent format. The same applies for network (RTP) - * play, though in that case currently only 44.1KHz 16-bit stereo is supported. - * - * The inbound data starts with a structure defining the data format. Note - * that this is NOT portable between different platforms or even necessarily - * between versions; the speaker is assumed to be built from the same source - * and run on the same host as the main server. + * Inbound data is expected to match @c config->sample_format. In normal use + * this is arranged by the @c disorder-normalize program (see @ref + * server/normalize.c). * * @b Garbage @b Collection. This program deliberately does not use the * garbage collector even though it might be convenient to do so. This is for @@ -89,7 +83,7 @@ struct track *tracks; struct track *playing; /** @brief Number of bytes pre frame */ -size_t device_bpf; +size_t bpf; /** @brief Array of file descriptors for poll() */ struct pollfd fds[NFDS]; @@ -103,14 +97,6 @@ static int paused; /* pause status */ /** @brief The current device state */ enum device_states device_state; -/** @brief The current device sample format - * - * Only meaningful if @ref device_state = @ref device_open or perhaps @ref - * device_error. For @ref FIXED_FORMAT backends, this should always match @c - * config->sample_format. - */ -ao_sample_format device_format; - /** @brief Set when idled * * This is set when the sound device is deliberately closed by idle(). @@ -153,7 +139,7 @@ static void version(void) { } /** @brief Return the number of bytes per frame in @p format */ -static size_t bytes_per_frame(const ao_sample_format *format) { +static size_t bytes_per_frame(const struct stream_header *format) { return format->channels * format->bits / 8; } @@ -170,9 +156,6 @@ static struct track *findtrack(const char *id, int create) { strcpy(t->id, id); t->fd = -1; tracks = t; - /* The initial input buffer will be the sample format. */ - t->buffer = (void *)&t->format; - t->size = sizeof t->format; } return t; } @@ -193,7 +176,6 @@ static struct track *removetrack(const char *id) { static void destroy(struct track *t) { D(("destroy %s", t->id)); if(t->fd != -1) xclose(t->fd); - if(t->buffer != (void *)&t->format) free(t->buffer); free(t); } @@ -206,96 +188,6 @@ static void acquire(struct track *t, int fd) { nonblock(fd); } -/** @brief Return true if A and B denote identical libao formats, else false */ -int formats_equal(const ao_sample_format *a, - const ao_sample_format *b) { - return (a->bits == b->bits - && a->rate == b->rate - && a->channels == b->channels - && a->byte_format == b->byte_format); -} - -/** @brief Compute arguments to sox */ -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; - } -} - -/** @brief Enable format translation - * - * If necessary, replaces a tracks inbound file descriptor with one connected - * to a sox invocation, which performs the required translation. - */ -static void enable_translation(struct track *t) { - if((backend->flags & FIXED_FORMAT) - && !formats_equal(&t->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, &t->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) { - signal(SIGPIPE, SIG_DFL); - xdup2(t->fd, 0); - xdup2(soxpipe[1], 1); - fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK); - close(soxpipe[0]); - close(soxpipe[1]); - close(t->fd); - execvp("sox", (char **)av); - _exit(1); - } - D(("forking sox for format conversion (kid = %d)", soxkid)); - close(t->fd); - close(soxpipe[1]); - t->fd = soxpipe[0]; - t->format = config->sample_format; - } -} - /** @brief Read data into a sample buffer * @param t Pointer to track * @return 0 on success, -1 on EOF @@ -308,19 +200,15 @@ static int fill(struct track *t) { size_t where, left; int n; - D(("fill %s: eof=%d used=%zu size=%zu got_format=%d", - t->id, t->eof, t->used, t->size, t->got_format)); + D(("fill %s: eof=%d used=%zu", + t->id, t->eof, t->used)); if(t->eof) return -1; - if(t->used < t->size) { + if(t->used < sizeof t->buffer) { /* there is room left in the buffer */ - where = (t->start + t->used) % t->size; - if(t->got_format) { - /* We are reading audio data, get as much as we can */ - if(where >= t->start) left = t->size - where; - else left = t->start - where; - } else - /* We are still waiting for the format, only get that */ - left = sizeof (ao_sample_format) - t->used; + where = (t->start + t->used) % sizeof t->buffer; + /* Get as much data as we can */ + if(where >= t->start) left = (sizeof t->buffer) - where; + else left = t->start - where; do { n = read(t->fd, t->buffer + where, left); } while(n < 0 && errno == EINTR); @@ -334,20 +222,6 @@ static int fill(struct track *t) { return -1; } t->used += n; - if(!t->got_format && t->used >= sizeof (ao_sample_format)) { - assert(t->used == sizeof (ao_sample_format)); - /* Check that our assumptions are met. */ - if(t->format.bits & 7) - fatal(0, "bits per sample not a multiple of 8"); - /* If the input format is unsuitable, arrange to translate it */ - enable_translation(t); - /* Make a new buffer for audio data. */ - t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS; - t->buffer = xmalloc(t->size); - t->used = 0; - t->got_format = 1; - D(("got format for %s", t->id)); - } } return 0; } @@ -387,22 +261,10 @@ void abandon(void) { * 0 on success and -1 on error. */ static void activate(void) { - /* If we don't know the format yet we cannot start. */ - if(!playing->got_format) { - D((" - not got format for %s", playing->id)); - return; - } - if(backend->flags & FIXED_FORMAT) - device_format = config->sample_format; - if(backend->activate) { + if(backend->activate) backend->activate(); - } else { - assert(backend->flags & FIXED_FORMAT); - /* ...otherwise device_format not set */ + else device_state = device_open; - } - if(device_state == device_open) - device_bpf = bytes_per_frame(&device_format); } /** @brief Check whether the current track has finished @@ -415,8 +277,7 @@ static void activate(void) { static void maybe_finished(void) { if(playing && playing->eof - && (!playing->got_format - || playing->used < bytes_per_frame(&playing->format))) + && playing->used < bytes_per_frame(&config->sample_format)) abandon(); } @@ -440,26 +301,25 @@ static void play(size_t frames) { /* Make sure there's a track to play and it is not pasued */ if(!playing || paused) return; - /* Make sure the output device is open and has the right sample format */ - if(device_state != device_open - || !formats_equal(&device_format, &playing->format)) { + /* Make sure the output device is open */ + if(device_state != device_open) { activate(); if(device_state != device_open) return; } - D(("play: play %zu/%zu%s %dHz %db %dc", frames, playing->used / device_bpf, + D(("play: play %zu/%zu%s %dHz %db %dc", frames, playing->used / bpf, playing->eof ? " EOF" : "", - playing->format.rate, - playing->format.bits, - playing->format.channels)); + config->sample_format.rate, + config->sample_format.bits, + config->sample_format.channels)); /* Figure out how many frames there are available to write */ - if(playing->start + playing->used > playing->size) + if(playing->start + playing->used > sizeof playing->buffer) /* The ring buffer is currently wrapped, only play up to the wrap point */ - avail_bytes = playing->size - playing->start; + avail_bytes = (sizeof playing->buffer) - playing->start; else /* The ring buffer is not wrapped, can play the lot */ avail_bytes = playing->used; - avail_frames = avail_bytes / device_bpf; + avail_frames = avail_bytes / bpf; /* Only play up to the requested amount */ if(avail_frames > frames) avail_frames = frames; @@ -467,7 +327,7 @@ static void play(size_t frames) { return; /* Play it, Sam */ written_frames = backend->play(avail_frames); - written_bytes = written_frames * device_bpf; + written_bytes = written_frames * bpf; /* written_bytes and written_frames had better both be set and correct by * this point */ playing->start += written_bytes; @@ -475,7 +335,7 @@ static void play(size_t frames) { playing->played += written_frames; /* If the pointer is at the end of the buffer (or the buffer is completely * empty) wrap it back to the start. */ - if(!playing->used || playing->start == playing->size) + if(!playing->used || playing->start == (sizeof playing->buffer)) playing->start = 0; frames -= written_frames; return; @@ -485,11 +345,11 @@ static void play(size_t frames) { static void report(void) { struct speaker_message sm; - if(playing && playing->buffer != (void *)&playing->format) { + if(playing) { memset(&sm, 0, sizeof sm); sm.type = paused ? SM_PAUSED : SM_PLAYING; strcpy(sm.id, playing->id); - sm.data = playing->played / playing->format.rate; + sm.data = playing->played / config->sample_format.rate; speaker_send(1, &sm, 0); } time(&last_report); @@ -551,7 +411,7 @@ static void mainloop(void) { stdin_slot = addfd(0, POLLIN); /* Try to read sample data for the currently playing track if there is * buffer space. */ - if(playing && !playing->eof && playing->used < playing->size) + if(playing && !playing->eof && playing->used < (sizeof playing->buffer)) playing->slot = addfd(playing->fd, POLLIN); else if(playing) playing->slot = -1; @@ -572,7 +432,7 @@ static void mainloop(void) { * nothing important can't be monitored. */ for(t = tracks; t; t = t->next) if(t != playing) { - if(!t->eof && t->used < t->size) { + if(!t->eof && t->used < sizeof t->buffer) { t->slot = addfd(t->fd, POLLIN | POLLHUP); } else t->slot = -1; @@ -702,6 +562,7 @@ int main(int argc, char **argv) { log_default = &log_syslog; } if(config_read()) fatal(0, "cannot read configuration"); + bpf = bytes_per_frame(&config->sample_format); /* ignore SIGPIPE */ signal(SIGPIPE, SIG_IGN); /* reap kids */ diff --git a/server/speaker.h b/server/speaker.h index 02de4b2..9a48ca6 100644 --- a/server/speaker.h +++ b/server/speaker.h @@ -29,13 +29,6 @@ # define MACHINE_AO_FMT AO_FMT_LITTLE #endif -/** @brief How many seconds of input to buffer - * - * While any given connection has this much audio buffered, no more reads will - * be issued for that connection. The decoder will have to wait. - */ -#define BUFFER_SECONDS 5 - /** @brief Minimum number of frames to try to play at once * * The main loop will only attempt to play any audio when this many @@ -70,17 +63,35 @@ * of these but rearranging the queue can cause there to be more. */ struct track { - struct track *next; /* next track */ + /** @brief Next track */ + struct track *next; + + /** @brief Input file descriptor */ int fd; /* input FD */ - char id[24]; /* ID */ - size_t start, used; /* start + bytes used */ - int eof; /* input is at EOF */ - int got_format; /* got format yet? */ - ao_sample_format format; /* sample format */ - unsigned long long played; /* number of frames played */ - char *buffer; /* sample buffer */ - size_t size; /* sample buffer size */ - int slot; /* poll array slot */ + + /** @brief Track ID */ + char id[24]; + + /** @brief Start position of data in buffer */ + size_t start; + + /** @brief Number of bytes of data in buffer */ + size_t used; + + /** @brief Set @c fd is at EOF */ + int eof; + + /** @brief Total number of frames played */ + unsigned long long played; + + /** @brief Slot in @ref fds */ + int slot; + + /** @brief Input buffer + * + * 1Mbyte is enough for nearly 6s of 44100Hz 16-bit stereo + */ + char buffer[1048576]; }; /** @brief Structure of a backend */ @@ -93,12 +104,9 @@ struct speaker_backend { /** @brief Flags * - * Possible values - * - @ref FIXED_FORMAT + * This field is currently not used and must be 0. */ unsigned flags; -/** @brief Lock to configured sample format */ -#define FIXED_FORMAT 0x0001 /** @brief Initialization * @@ -127,9 +135,6 @@ struct speaker_backend { * If it is @ref device_closed then the device should be opened with * the right sample format. * - * If the @ref FIXED_FORMAT flag is not set then @ref device_format - * must be set on success. - * * Some devices are effectively always open and have no error state, * in which case this callback can be NULL. In this case @ref * FIXED_FORMAT must be set. Note that @ref device_state still @@ -203,7 +208,6 @@ enum device_states { }; extern enum device_states device_state; -extern ao_sample_format device_format; extern struct track *tracks; extern struct track *playing; @@ -213,12 +217,10 @@ extern const struct speaker_backend command_backend; extern struct pollfd fds[NFDS]; extern int fdno; -extern size_t device_bpf; +extern size_t bpf; extern int idled; int addfd(int fd, int events); -int formats_equal(const ao_sample_format *a, - const ao_sample_format *b); void abandon(void); #endif /* SPEAKER_H */ -- [mdw]