chiark / gitweb /
speaker protocol redesign to cope with libao re-opening
authorRichard Kettlewell <rjk@greenend.org.uk>
Fri, 28 Sep 2007 13:21:10 +0000 (14:21 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Fri, 28 Sep 2007 13:21:10 +0000 (14:21 +0100)
18 files changed:
.bzrignore
driver/disorder.c
lib/configuration.c
lib/configuration.h
lib/mixer.c
lib/plugin.c
lib/speaker-protocol.h
server/Makefile.am
server/api-client.c
server/cgimain.c
server/disorderd.c
server/normalize.c [new file with mode: 0644]
server/play.c
server/speaker-alsa.c
server/speaker-command.c
server/speaker-network.c
server/speaker.c
server/speaker.h

index dbd4af4..c07af06 100644 (file)
@@ -104,3 +104,4 @@ debian/disorder-playrtp
 *.bz2
 debian/disobedience
 server/disorder-decode
+server/disorder-normalize
index bdcb147..fb05b59 100644 (file)
@@ -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
  * 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 <config.h>
+#include "types.h"
 
 #include <string.h>
 #include <stdlib.h>
 #include <ao/ao.h>
 #include <ao/plugin.h>
 
+#include "speaker-protocol.h"
+
 /* extra declarations to help out lazy <ao/plugin.h> */
 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;
index 47ba9f4..486a2ab 100644 (file)
@@ -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 */
index eabbcce..84b1eb8 100644 (file)
@@ -24,7 +24,7 @@
 #ifndef CONFIGURATION_H
 #define CONFIGURATION_H
 
-#include <ao/ao.h>
+#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;
index 5ef30a2..4fd4a9a 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include <config.h>
+#include "types.h"
 
 #include <stdio.h>
 #include <string.h>
index 946f499..57496e6 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include <config.h>
+#include "types.h"
 
 #include <dlfcn.h>
 #include <unistd.h>
index eb2a1ae..8809f0f 100644 (file)
@@ -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 */
 
 /*
index d15d3f4..548ac2a 100644 (file)
@@ -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                   \
index 086de15..1e0ed7a 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include <config.h>
+#include "types.h"
 
 #include <stdio.h>
 #include <errno.h>
index 467ed59..231ece1 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include <config.h>
+#include "types.h"
 
 #include <stdio.h>
 #include <errno.h>
index f2ebb9a..1100140 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 #include <config.h>
+#include "types.h"
 
 #include <stdio.h>
 #include <getopt.h>
diff --git a/server/normalize.c b/server/normalize.c
new file mode 100644 (file)
index 0000000..7e5fbe8
--- /dev/null
@@ -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 <config.h>
+#include "types.h"
+
+#include <locale.h>
+#include <errno.h>
+#include <unistd.h>
+#include <syslog.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#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 -<n> 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:
+*/
index 94567c4..3b3b928 100644 (file)
@@ -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 <config.h>
+#include "types.h"
 
 #include <sys/types.h>
 #include <sys/time.h>
@@ -34,6 +35,7 @@
 #include <stdio.h>
 #include <pcre.h>
 #include <ao/ao.h>
+#include <sys/wait.h>
 
 #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();
index fa9e6c3..80395f3 100644 (file)
@@ -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;
index 8a8f63c..088d0cd 100644 (file)
@@ -25,6 +25,7 @@
 
 #include <unistd.h>
 #include <poll.h>
+#include <errno.h>
 
 #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,
index 5b1ccce..59630ff 100644 (file)
@@ -32,6 +32,7 @@
 #include <assert.h>
 #include <net/if.h>
 #include <ifaddrs.h>
+#include <errno.h>
 
 #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,
index 1dc90e4..aa09c02 100644 (file)
  * 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, <a
- * href="http://sox.sourceforge.net/">sox</a> 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 -<n> 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 */
index 02de4b2..9a48ca6 100644 (file)
 # 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
  * 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 */