chiark / gitweb /
Install disorderd under launchd in Mac OS X.
authorRichard Kettlewell <rjk@greenend.org.uk>
Tue, 4 Sep 2007 21:53:55 +0000 (22:53 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Tue, 4 Sep 2007 21:53:55 +0000 (22:53 +0100)
disorder-speaker can now transmit RTP over the network.  Effectively
untested and not yet fully documented.

New playrtp client.  Totally untested, might not even build.

17 files changed:
.bzrignore
README
clients/playrtp.c [new file with mode: 0644]
configure.ac
doc/disorder_config.5.in
lib/Makefile.am
lib/addr.c
lib/configuration.c
lib/configuration.h
lib/defs.c
lib/defs.h
lib/rtp.h [new file with mode: 0644]
lib/timeval.h [new file with mode: 0644]
server/Makefile.am
server/disorderd.c
server/speaker.c
server/uk.org.greenend.rjk.disorder.plist.in [new file with mode: 0644]

index 2ca293c03b8d18b99d0e64c127a4198bd6a279fc..acfbba532527d103a9b921aa6cd5b39b4e23e281 100644 (file)
@@ -93,3 +93,5 @@ TAGS
 ktrace.out
 tests/Makefile
 tests/testroot
+disorder.plist
+server/uk.org.greenend.rjk.disorder.plist
diff --git a/README b/README
index 67c80e4ceb791a1db7eda5290a7707083289595e..414e8f57ea9e63f031390e6ba74ee57aa22794dc 100644 (file)
--- a/README
+++ b/README
@@ -138,10 +138,20 @@ NOTE: If you are upgrading from an earlier version, see README.upgrades.
 
    See disorderd(8) and disorder_config(5) for more details.
 
-6. Make sure the server is started at boot time.  On many Linux systems,
-   examples/disorder.init should be more or less suitable; install it in
-   /etc/init.d, adapting it as necessary, and make appropriate links from
-   /etc/rc[0-6].d.  If you have a BSD style init then you are on your own.
+6. Make sure the server is started at boot time.
+
+   On many Linux systems, examples/disorder.init should be more or less
+   suitable; install it in /etc/init.d, adapting it as necessary, and make
+   appropriate links from /etc/rc[0-6].d.
+
+   For Mac OS X 10.4, a suitable plist file is automatically installed.  The
+   command:
+
+    sudo launchctl list
+
+   ...should show "uk.org.greenend.rjk.disorder" (among other things).
+
+   If you have a some other init system then you are on your own.
 
 7. Make sure the state directory (/var/disorder or /usr/local/var/disorder or
    as determined by configure) exists and is writable by the jukebox user.
@@ -149,10 +159,19 @@ NOTE: If you are upgrading from an earlier version, see README.upgrades.
      mkdir -m 755 /var/disorder
      chown disorder:root /var/disorder
 
-8. Start the server, for instance:
+   If you want to use some other directory you must put use the 'home' command
+   in the configuration file.
+
+8. Start the server.
+
+   On Linux systems with sysv-style init:
 
      /etc/init.d/disorder start
 
+   On Mac OS X 10.4:
+
+     sudo launchctl start uk.org.greenend.rjk.disorder
+
    By default disorderd logs to daemon.*; check your syslog.conf to see where
    this ends up and look for log messages from disorderd there.  If it didn't
    start up correctly there should be an error message.  Correct the problem
diff --git a/clients/playrtp.c b/clients/playrtp.c
new file mode 100644 (file)
index 0000000..7ec35d0
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * 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
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <pthread.h>
+
+#include "log.h"
+#include "mem.h"
+#include "configuration.h"
+#include "addr.h"
+#include "syscalls.h"
+#include "rtp.h"
+#include "debug.h"
+
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+# include <CoreAudio/AudioHardware.h>
+#endif
+
+static int rtpfd;
+
+#define MAXSAMPLES 2048                 /* max samples/frame we'll support */
+/* NB two channels = two samples in this program! */
+#define MINBUFFER 8820                  /* when to stop playing */
+#define READAHEAD 88200                 /* how far to read ahead */
+#define MAXBUFFER (3 * 88200)           /* maximum buffer contents */
+
+struct frame {
+  struct frame *next;                   /* another frame */
+  int nsamples;                         /* number of samples */
+  int nused;                            /* number of samples used so far */
+  uint32_t timestamp;                   /* timestamp from packet */
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+  float samples[MAXSAMPLES];            /* converted sample data */
+#endif
+};
+
+static unsigned long nsamples;          /* total samples available */
+
+static struct frame *frames;            /* received frames in ascending order
+                                         * of timestamp */
+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
+/* lock protecting frame list */
+
+static pthread_cond_t cond = PTHREAD_CONDVAR_INITIALIZER;
+/* signalled whenever we add a new frame */
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "debug", no_argument, 0, 'd' },
+  { 0, 0, 0, 0 }
+};
+
+/* Return true iff a > b in sequence-space arithmetic */
+static inline int gt(const struct frame *a, const struct frame *b) {
+  return (uint32_t)(a->timestamp - b->timestamp) < 0x80000000;
+}
+
+/* Background thread that reads frames over the network and add them to the
+ * list */
+static listen_thread(void attribute((unused)) *arg) {
+  struct frame *f = 0, **ff;
+  int n, i;
+  union {
+    struct rtp_header header;
+    uint8_t bytes[sizeof(uint16_t) * MAXSAMPLES + sizeof (struct rtp_header)];
+  } packet;
+  const uint16_t *const samples = (uint16_t *)(packet.bytes
+                                               + sizeof (struct rtp_header));
+
+  for(;;) {
+    if(!f)
+      f = xmalloc(sizeof *f);
+    n = read(rtpfd, packet.bytes, sizeof packet.bytes);
+    if(n < 0) {
+      switch(errno) {
+      case EINTR:
+        continue;
+      default:
+        fatal(errno, "error reading from socket");
+      }
+    }
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+    /* Convert to target format */
+    switch(packet.header.mtp & 0x7F) {
+    case 10:
+      f->nsamples = (n - sizeof (struct rtp_header)) / sizeof(uint16_t);
+      for(i = 0; i < f->nsamples; ++i)
+        f->samples[i] = (int16_t)ntohs(samples[i]) * (0.5f / 32767);
+      break;
+      /* TODO support other RFC3551 media types (when the speaker does) */
+    default:
+      fatal(0, "unsupported RTP payload type %d", 
+            packet.header.mpt & 0x7F);
+    }
+#endif
+    f->used = 0;
+    f->timestamp = ntohl(packet.header.timestamp);
+    pthread_mutex_lock(&lock);
+    /* Stop reading if we've reached the maximum */
+    while(nsamples >= MAXBUFFER)
+      pthread_cond_wait(&cond, &lock);
+    for(ff = &frames; *ff && !gt(*ff, f); ff = &(*ff)->next)
+      ;
+    f->next = *ff;
+    *ff = f;
+    nsamples += f->nsamples;
+    pthread_cond_broadcast(&cond);
+    pthread_mutex_unlock(&lock);
+    f = 0;
+  }
+}
+
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+static OSStatus adioproc(AudioDeviceID inDevice,
+                         const AudioTimeStamp *inNow,
+                         const AudioBufferList *inInputData,
+                         const AudioTimeStamp *inInputTime,
+                         AudioBufferList *outOutputData,
+                         const AudioTimeStamp *inOutputTime,
+                         void *inClientData) {
+  UInt32 nbuffers = outOutputData->mNumberBuffers;
+  AudioBuffer *ab = outOutputData->mBuffers;
+  float *samplesOut;                    /* where to write samples to */
+  size_t samplesOutLeft;                /* space left */
+  size_t samplesInLeft;
+  size_t samplesToCopy;
+
+  pthread_mutex_lock(&lock);  
+  samplesOut = ab->data;
+  samplesOutLeft = ab->mDataByteSize / sizeof (float);
+  while(frames && nbuffers > 0) {
+    if(frames->used == frames->nsamples) {
+      /* TODO if we dropped a packet then we should introduce a gap here */
+      struct frame *const f = frames;
+      frames = f->next;
+      free(f);
+      pthread_cond_broadcast(&cond);
+      continue;
+    }
+    if(samplesOutLeft == 0) {
+      --nbuffers;
+      ++ab;
+      samplesOut = ab->data;
+      samplesOutLeft = ab->mDataByteSize / sizeof (float);
+      continue;
+    }
+    /* Now: (1) there is some data left to read
+     *      (2) there is some space to put it */
+    samplesInLeft = frames->nsamples - frames->used;
+    samplesToCopy = (samplesInLeft < samplesOutLeft
+                     ? samplesInLeft : samplesOutLeft);
+    memcpy(samplesOut, frame->samples + frames->used, samplesToCopy);
+    frames->used += samplesToCopy;
+    samplesOut += samplesToCopy;
+    samesOutLeft -= samplesToCopy;
+  }
+  pthread_mutex_unlock(&lock);
+  return 0;
+}
+#endif
+
+void play_rtp(void) {
+  pthread_t lt;
+
+  /* We receive and convert audio data in a background thread */
+  pthread_create(&lt, 0, listen_thread, 0);
+#if API_ALSA
+  assert(!"implemented");
+#elif HAVE_COREAUDIO_AUDIOHARDWARE_H
+  {
+    OSStatus status;
+    UInt32 propertySize;
+    AudioDeviceID adid;
+    AudioStreamBasicDescription asbd;
+
+    /* If this looks suspiciously like libao's macosx driver there's an
+     * excellent reason for that... */
+
+    /* TODO report errors as strings not numbers */
+    propertySize = sizeof adid;
+    status = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice,
+                                      &propertySize, &adid);
+    if(status)
+      fatal(0, "AudioHardwareGetProperty: %d", (int)status);
+    if(adid == kAudioDeviceUnknown)
+      fatal(0, "no output device");
+    propertySize = sizeof asbd;
+    status = AudioDeviceGetProperty(adid, 0, false,
+                                    kAudioDevicePropertyStreamFormat,
+                                    &propertySize, &asbd);
+    if(status)
+      fatal(0, "AudioHardwareGetProperty: %d", (int)status);
+    D(("mSampleRate       %f", asbd.mSampleRate));
+    D(("mFormatID         %08"PRIx32, asbd.mFormatID));
+    D(("mFormatFlags      %08"PRIx32, asbd.mFormatFlags));
+    D(("mBytesPerPacket   %08"PRIx32, asbd.mBytesPerPacket));
+    D(("mFramesPerPacket  %08"PRIx32, asbd.mFramesPerPacket));
+    D(("mBytesPerFrame    %08"PRIx32, asbd.mBytesPerFrame));
+    D(("mChannelsPerFrame %08"PRIx32, asbd.mChannelsPerFrame));
+    D(("mBitsPerChannel   %08"PRIx32, asbd.mBitsPerChannel));
+    D(("mReserved         %08"PRIx32, asbd.mReserved));
+    if(asbd.mFormatID != kAudioFormatLinearPCM)
+      fatal(0, "audio device does not support kAudioFormatLinearPCM");
+    status = AudioDeviceAddIOProc(adid, adioproc, 0);
+    if(status)
+      fatal(0, "AudioDeviceAddIOProc: %d", (int)status);
+    pthread_mutex_lock(&lock);
+    for(;;) {
+      /* Wait for the buffer to fill up a bit */
+      while(nsamples < READAHEAD)
+        pthread_cond_wait(&cond, &lock);
+      /* Start playing now */
+      status = AudioDeviceStart(adid, adioproc);
+      if(status)
+        fatal(0, "AudioDeviceStart: %d", (int)status);
+      /* Wait until the buffer empties out */
+      while(nsamples >= MINBUFFER)
+        pthread_cond_wait(&cond, &lock);
+      /* Stop playing for a bit until the buffer re-fills */
+      status = AudioDeviceStop(adid, adioproc);
+      if(status)
+        fatal(0, "AudioDeviceStop: %d", (int)status);
+      /* Go back round */
+    }
+  }
+#else
+# error No known audio API
+#endif
+}
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+         "  disorder-playrtp [OPTIONS] ADDRESS [PORT]\n"
+         "Options:\n"
+         "  --help, -h              Display usage message\n"
+         "  --version, -V           Display version number\n"
+         "  --debug, -d             Turn on debugging\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("disorder-playrtp version %s\n", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n;
+  struct addrinfo *res;
+  struct stringlist sl;
+  const char *sockname;
+
+  static const struct addrinfo prefbind = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+
+  mem_init();
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVd", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'd': debugging = 1; break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  argc -= optind;
+  argv += optind;
+  if(argc < 1 || argc > 2)
+    fatal(0, "usage: disorder-playrtp [OPTIONS] ADDRESS [PORT]");
+  sl.n = argc;
+  sl.s = argv;
+  /* Listen for inbound audio data */
+  if(!(res = get_address(&sl, &pref, &sockname)))
+    exit(1);
+  if((rtpfd = socket(res->ai_family,
+                     res->ai_socktype,
+                     res->ai_protocol)) < 0)
+    fatal(errno, "error creating socket");
+  if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0)
+    fatal(errno, "error binding socket to %s", sockname);
+  play_rtp();
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index c152c92195e7df6d34e30dd2e545e7dd26225c32..a06ce68578a489e504ee5ee8d7a678e4ef5d7831 100644 (file)
@@ -100,12 +100,16 @@ if test "x$FINK" != xnone; then
   AC_CACHE_CHECK([fink install directory],[rjk_cv_finkprefix],[
     rjk_cv_finkprefix="`echo "$FINK" | sed 's,/bin/fink$,,'`"
   ])
+  finkbindir="${rjk_cv_finkprefix}/bin"
   CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/gc -I${rjk_cv_finkprefix}/include"
   if test $want_server = yes; then
     CPPFLAGS="${CPPFLAGS} -I${rjk_cv_finkprefix}/include/db4"
   fi
   LDFLAGS="${LDFLAGS} -L${rjk_cv_finkprefix}/lib"
+else
+  finkbindir=""
 fi
+AC_SUBST([finkbindir])
 
 # Checks for libraries.
 # We save up a list of missing libraries that we can't do without
@@ -175,7 +179,7 @@ RJK_REQUIRE_PCRE_UTF8([-lpcre])
 
 # Checks for header files.
 RJK_FIND_GC_H
-AC_CHECK_HEADERS([inttypes.h])
+AC_CHECK_HEADERS([inttypes.h CoreAudio/AudioHardware.h])
 # Compilation will fail if any of these headers are missing, so we
 # check for them here and fail early.
 # We don't bother checking very standard stuff
index 3ddeb4702224467a56722e345802e1f0235ff52f..299233d7c82325f649bc1413d81f0a1c2e75cb11 100644 (file)
@@ -142,6 +142,25 @@ automatically included, but should include the proper extension.
 .IP
 The default is \fB{/artist}{/album}{/title}{ext}\fR.
 .TP
+.B backend \fINAME\fR
+Selects the backend use by the speaker process.  The following options are
+available:
+.RS
+.TP
+.B alsa
+Use the ALSA API.  This is only available on Linux systems, on which it is the
+default.
+.TP
+.B command
+Execute a command.  This is the default if
+.B speaker_command
+is specified, or (currently) on non-Linux systems.
+.TP
+.B network
+Transmit audio over the network.  This is the default if
+\fBbroadcast\fR is specified.
+.RE
+.TP
 .B channel \fICHANNEL\fR
 The mixer channel that the volume control should use.  Valid names depend on
 your operating system and hardware, but some standard ones that might be useful
index ece2b3b90cacd5cfaafcd7f9b2c110bb952e49b4..212b1ce015a0107932650f793b5f2a3f6910a941 100644 (file)
@@ -49,6 +49,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        asprintf.c fprintf.c snprintf.c                 \
        queue.c queue.h                                 \
        regsub.c regsub.h                               \
+       rtp.h                                           \
        selection.c selection.h                         \
        signame.c signame.h                             \
        sink.c sink.h                                   \
@@ -57,6 +58,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        syscalls.c syscalls.h                           \
        types.h                                         \
        table.c table.h                                 \
+       timeval.h                                       \
        trackname.c trackname.h                         \
        user.h user.c                                   \
        utf8.h utf8.c                                   \
@@ -72,6 +74,9 @@ definitions.h: Makefile
        echo "#define PKGCONFDIR \"${sysconfdir}/\"PACKAGE" >> $@.new
        echo "#define PKGSTATEDIR \"${localstatedir}/\"PACKAGE" >> $@.new
        echo "#define PKGDATADIR \"${pkgdatadir}/\"" >> $@.new
+       echo "#define SBINDIR \"${sbindir}/\"" >> $@.new
+       echo "#define BINDIR \"${bindir}/\"" >> $@.new
+       echo "#define FINKBINDIR \"${finkbindir}/\"" >> $@.new
        mv $@.new $@
 defs.o: definitions.h
 defs.lo: definitions.h
index f994ac6c5d04f2113267a7a9bc1ace54d2050880..16cd610686391c0cf7ec5f09e4e71a82d6c1b56b 100644 (file)
@@ -39,22 +39,28 @@ struct addrinfo *get_address(const struct stringlist *a,
   struct addrinfo *res;
   char *name;
   int rc;
-  
-  if(a->n == 1) {
+
+  switch(a->n) {  
+  case 1:
     byte_xasprintf(&name, "host * service %s", a->s[0]);
     if((rc = getaddrinfo(0, a->s[0], pref, &res))) {
       error(0, "getaddrinfo %s: %s", a->s[0], gai_strerror(rc));
       return 0;
     }
-  } else {
+    break;
+  case 2:
     byte_xasprintf(&name, "host %s service %s", a->s[0], a->s[1]);
     if((rc = getaddrinfo(a->s[0], a->s[1], pref, &res))) {
       error(0, "getaddrinfo %s %s: %s", a->s[0], a->s[1], gai_strerror(rc));
       return 0;
     }
+    break;
+  default:
+    error(0, "invalid network address specification (n=%d)", a->n);
+    return 0;
   }
-  if(!res || res->ai_socktype != SOCK_STREAM) {
-    error(0, "getaddrinfo didn't give us a stream socket");
+  if(!res || (pref && res->ai_socktype != pref->ai_socktype)) {
+    error(0, "getaddrinfo didn't give us a suitable socket address");
     if(res)
       freeaddrinfo(res);
     return 0;
index 845c32a0c7266662a62ba52a290465dfd6671520..5881f87635620d914d08ccf84dcbd9d422865c27 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
@@ -405,6 +405,36 @@ static int set_transform(const struct config_state *cs,
   return 0;
 }
 
+static int set_backend(const struct config_state *cs,
+                      const struct conf *whoami,
+                      int nvec, char **vec) {
+  int *const valuep = ADDRESS(cs->config, int);
+  
+  if(nvec != 1) {
+    error(0, "%s:%d: '%s' requires one argument",
+         cs->path, cs->line, whoami->name);
+    return -1;
+  }
+  if(!strcmp(vec[0], "alsa")) {
+#if API_ALSA
+    *valuep = BACKEND_ALSA;
+#else
+    error(0, "%s:%d: ALSA is not available on this platform",
+         cs->path, cs->line);
+    return -1;
+#endif
+  } else if(!strcmp(vec[0], "command"))
+    *valuep = BACKEND_COMMAND;
+  else if(!strcmp(vec[0], "network"))
+    *valuep = BACKEND_NETWORK;
+  else {
+    error(0, "%s:%d: invalid '%s' value '%s'",
+         cs->path, cs->line, whoami->name, vec[0]);
+    return -1;
+  }
+  return 0;
+}
+
 /* free functions */
 
 static void free_none(struct config attribute((unused)) *c,
@@ -502,7 +532,8 @@ static const struct conftype
   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 };
+  type_transform = { set_transform, free_transformlist },
+  type_backend = { set_backend, free_none };
 
 /* specific validation routine */
 
@@ -720,6 +751,45 @@ static int validate_alias(const struct config_state *cs,
   return 0;
 }
 
+static int validate_addrport(const struct config_state attribute((unused)) *cs,
+                            int nvec,
+                            char attribute((unused)) **vec) {
+  switch(nvec) {
+  case 0:
+    error(0, "%s:%d: missing address",
+         cs->path, cs->line);
+    return -1;
+  case 1:
+    error(0, "%s:%d: missing port name/number",
+         cs->path, cs->line);
+    return -1;
+  case 2:
+    return 0;
+  default:
+    error(0, "%s:%d: expected ADDRESS PORT",
+         cs->path, cs->line);
+    return -1;
+  }
+}
+
+static int validate_address(const struct config_state attribute((unused)) *cs,
+                        int nvec,
+                        char attribute((unused)) **vec) {
+  switch(nvec) {
+  case 0:
+    error(0, "%s:%d: missing address",
+         cs->path, cs->line);
+    return -1;
+  case 1:
+  case 2:
+    return 0;
+  default:
+    error(0, "%s:%d: expected ADDRESS PORT",
+         cs->path, cs->line);
+    return -1;
+  }
+}
+
 /* configuration table */
 
 #define C(x) #x, offsetof(struct config, x)
@@ -728,16 +798,18 @@ static int validate_alias(const struct config_state *cs,
 static const struct conf conf[] = {
   { C(alias),            &type_string,           validate_alias },
   { C(allow),            &type_stringlist_accum, validate_allow },
+  { C(broadcast),        &type_stringlist,       validate_addrport },
+  { C(broadcast_from),   &type_stringlist,       validate_address },
   { C(channel),          &type_string,           validate_channel },
   { C(checkpoint_kbyte), &type_integer,          validate_non_negative },
   { C(checkpoint_min),   &type_integer,          validate_non_negative },
   { C(collection),       &type_collections,      validate_any },
-  { C(connect),          &type_stringlist,       validate_any },
+  { C(connect),          &type_stringlist,       validate_addrport },
   { C(device),           &type_string,           validate_any },
   { C(gap),              &type_integer,          validate_non_negative },
   { C(history),          &type_integer,          validate_positive },
   { C(home),             &type_string,           validate_isdir },
-  { C(listen),           &type_stringlist,       validate_any },
+  { C(listen),           &type_stringlist,       validate_addrport },
   { C(lock),             &type_boolean,          validate_any },
   { C(mixer),            &type_string,           validate_ischr },
   { C(namepart),         &type_namepart,         validate_any },
@@ -756,6 +828,7 @@ static const struct conf conf[] = {
   { C(scratch),          &type_string_accum,     validate_isreg },
   { C(signal),           &type_signal,           validate_any },
   { C(sox_generation),   &type_integer,          validate_non_negative },
+  { C(speaker_backend),  &type_backend,          validate_any },
   { C(speaker_command),  &type_string,           validate_any },
   { C(stopword),         &type_string_accum,     validate_any },
   { C(templates),        &type_string_accum,     validate_isdir },
@@ -875,6 +948,7 @@ static struct config *config_default(void) {
   c->sample_format.channels = 2;
   c->sample_format.byte_format = AO_FMT_NATIVE;
   c->queue_pad = 10;
+  c->speaker_backend = -1;
   return c;
 }
 
@@ -940,6 +1014,23 @@ static void config_postdefaults(struct config *c) {
     for(n = 0; n < NTRANSFORM; ++n)
       set_transform(&cs, whoami, 5, (char **)transform[n]);
   }
+  if(c->speaker_backend == -1) {
+    if(c->speaker_command)
+      c->speaker_backend = BACKEND_COMMAND;
+    else if(c->broadcast.n)
+      c->speaker_backend = BACKEND_NETWORK;
+    else {
+#if API_ALSA
+      c->speaker_backend = BACKEND_ALSA;
+#else
+      c->speaker_backend = BACKEND_COMMAND;
+#endif
+    }
+  }
+  if(c->speaker_backend == BACKEND_COMMAND && !c->speaker_command)
+    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");
 }
 
 /* re-read the config file */
index c3049215e79aaeadb15964c0e9446f4a139b7c28..5f0b8cce96c3a236e1303abdc1ae0e09d220f6b5 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
@@ -102,6 +102,10 @@ struct config {
   const char *speaker_command;         /* command for speaker to run */
   ao_sample_format sample_format;      /* sample format to enforce */
   long sox_generation;                 /* sox syntax generation */
+  int speaker_backend;                 /* speaker backend */
+#define BACKEND_ALSA 0
+#define BACKEND_COMMAND 1
+#define BACKEND_NETWORK 2
   /* shared client/server config */
   const char *home;                    /* home directory for state files */
   /* client config */
@@ -122,6 +126,9 @@ struct config {
   const char *device;                  /* ALSA output device */
   struct transformlist transform;      /* path name transformations */
 
+  struct stringlist broadcast;         /* audio broadcast address */
+  struct stringlist broadcast_from;    /* audio broadcast source address */
+
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
   char **parts;                                /* name part list  */
index 5e3795f997e0810522670e497312af2a81b1cd85..7a11ebdb5cbec8782aba37b604be15bd6ed5f515 100644 (file)
@@ -29,6 +29,9 @@ const char pkglibdir[] = PKGLIBDIR;
 const char pkgconfdir[] = PKGCONFDIR;
 const char pkgstatedir[] = PKGSTATEDIR;
 const char pkgdatadir[] = PKGDATADIR;
+const char bindir[] = BINDIR;
+const char sbindir[] = SBINDIR;
+const char finkbindir[] = FINKBINDIR;
 
 /*
 Local Variables:
index 4f180121d63b6111cabb4b7a24208625db6f230c..c14bb9be16f582ef4a8a06b5eb8bd57fb562587e 100644 (file)
@@ -26,6 +26,9 @@ extern const char pkglibdir[];
 extern const char pkgconfdir[];
 extern const char pkgstatedir[];
 extern const char pkgdatadir[];
+extern const char bindir[];
+extern const char sbindir[];
+extern const char finkbindir[];
 
 #endif /* DEFS_H */
 
diff --git a/lib/rtp.h b/lib/rtp.h
new file mode 100644 (file)
index 0000000..9159bfb
--- /dev/null
+++ b/lib/rtp.h
@@ -0,0 +1,42 @@
+/*
+ * 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
+ */
+
+#ifndef RTP_H
+#define RTP_H
+
+/* RTP is defined in RFC1889 */
+struct attribute((packed)) rtp {
+  uint8_t vpxcc;
+  uint8_t mpt;
+  uint16_t seq;
+  uint32_t timestamp;
+  uint32_t ssrc;
+};
+
+#endif /* RTP_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/timeval.h b/lib/timeval.h
new file mode 100644 (file)
index 0000000..cefc990
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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
+ */
+
+#ifndef TIMEVAL_H
+#define TIMEVAL_H
+
+static inline struct timeval tvsub(const struct timeval a,
+                                   const struct timeval b) {
+  struct timeval r;
+
+  r.tv_sec = a.tv_sec - b.tv_sec;
+  r.tv_usec = a.tv_usec - b.tv_usec;
+  if(r.tv_usec < 0) {
+    r.tv_usec += 1000000;
+    r.tv_sec--;
+  }
+  if(r.tv_usec > 999999) {
+    r.tv_usec -= 1000000;
+    r.tv_sec++;
+  }
+  return r;
+}
+
+#endif /* TIMEVAL_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 4b01707c18d38ad259d8c6e1b9fa76097bf4d0f9..906ce98ecb8e65116466713716571421d590238c 100644 (file)
@@ -1,3 +1,4 @@
+
 #
 # This file is part of DisOrder.
 # Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
@@ -21,6 +22,7 @@
 sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
              disorder-speaker
 noinst_PROGRAMS=disorder.cgi trackname
+noinst_DATA=uk.org.greenend.rjk.disorder.plist
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 
@@ -45,7 +47,7 @@ disorder_deadlock_DEPENDENCIES=../lib/libdisorder.a
 
 disorder_speaker_SOURCES=speaker.c
 disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
-       $(LIBASOUND) $(LIBPCRE) $(LIBICONV)
+       $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT)
 disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
 
 disorder_rescan_SOURCES=rescan.c                        \
@@ -90,3 +92,19 @@ check-help: all
        ./disorder-speaker --help > /dev/null
 
 cgi.o: ../lib/definitions.h
+
+# for Mac OS X >=10.4
+SEDFILES=uk.org.greenend.rjk.disorder.plist
+include ${top_srcdir}/scripts/sedfiles.make
+EXTRA_DIST=uk.org.greenend.rjk.disorder.plist.in
+LAUNCHD=/Library/LaunchDaemons
+
+install-data-hook:
+       @if [ -d ${LAUNCHD} ]; then \
+         echo $(INSTALL) -m 644 uk.org.greenend.rjk.disorder.plist ${LAUNCHD};\
+         $(INSTALL) -m 644 uk.org.greenend.rjk.disorder.plist ${LAUNCHD};\
+         echo launchctl unload ${LAUNCHD} \|\| true;\
+         launchctl unload ${LAUNCHD} || true;\
+         echo launchctl load ${LAUNCHD} \|\| true;\
+         launchctl load ${LAUNCHD} || true;\
+       fi
index 774ab6d1c5db8adfb63f226d70aff74b1f48b367..4a3915aa466e0896c7d329cb9d125b7244e95ce2 100644 (file)
@@ -53,6 +53,7 @@
 #include "user.h"
 #include "mixer.h"
 #include "eventlog.h"
+#include "printf.h"
 
 static ev_source *ev;
 
@@ -69,6 +70,7 @@ static const struct option options[] = {
   { "log", required_argument, 0, 'l' },
   { "pidfile", required_argument, 0, 'P' },
   { "no-initial-rescan", no_argument, 0, 'N' },
+  { "syslog", no_argument, 0, 's' },
   { 0, 0, 0, 0 }
 };
 
@@ -82,6 +84,7 @@ static void help(void) {
          "  --config PATH, -c PATH   Set configuration file\n"
          "  --debug, -d              Turn on debugging\n"
          "  --foreground, -f         Do not become a daemon\n"
+         "  --syslog, -s             Log to syslog even with -f\n"
          "  --pidfile PATH, -P PATH  Leave a pidfile\n");
   xfclose(stdout);
   exit(0);
@@ -178,8 +181,27 @@ static void volumecheck_after(long offset) {
   ev_timeout(ev, 0, &w, volumecheck_again, 0);
 }
 
- int main(int argc, char **argv) {
-  int n, background = 1;
+/* We fix the path to include the bindir and sbindir we were installed into */
+static void fix_path(void) {
+  char *path = getenv("PATH");
+  char *newpath;
+
+  if(!path)
+    error(0, "PATH is not set at all!");
+
+  if(*finkbindir)
+    /* We appear to be a finkized mac; include fink on the path in case the
+     * tools we need are there. */
+    byte_xasprintf(&newpath, "PATH=%s:%s:%s:%s", 
+                  path, bindir, sbindir, finkbindir);
+  else
+    byte_xasprintf(&newpath, "PATH=%s:%s:%s", path, bindir, sbindir);
+  putenv(newpath);
+  info("%s", newpath); 
+}
+
+int main(int argc, char **argv) {
+  int n, background = 1, logsyslog = 0;
   const char *pidfile = 0;
   int initial_rescan = 1;
 
@@ -189,7 +211,7 @@ static void volumecheck_after(long offset) {
   /* garbage-collect PCRE's memory */
   pcre_malloc = xmalloc;
   pcre_free = xfree;
-  while((n = getopt_long(argc, argv, "hVc:dfP:N", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVc:dfP:Ns", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'V': version();
@@ -198,13 +220,21 @@ static void volumecheck_after(long offset) {
     case 'f': background = 0; break;
     case 'P': pidfile = optarg; break;
     case 'N': initial_rescan = 0; break;
+    case 's': logsyslog = 1; break;
     default: fatal(0, "invalid option");
     }
   }
   /* go into background if necessary */
   if(background)
     daemonize(progname, LOG_DAEMON, pidfile);
+  else if(logsyslog) {
+    /* If we're running under some kind of daemon supervisor then we may want
+     * to log to syslog but not to go into background */
+    openlog(progname, LOG_PID, LOG_DAEMON);
+    log_default = &log_syslog;
+  }
   info("process ID %lu", (unsigned long)getpid());
+  fix_path();
   srand(time(0));                      /* don't start the same every time */
   /* create event loop */
   ev = ev_new();
@@ -270,5 +300,6 @@ static void volumecheck_after(long offset) {
 Local Variables:
 c-basic-offset:2
 comment-column:40
+fill-column:79
 End:
 */
index be783afd7c8958f43909720e59af1f3f6990ba81..98ed297a7896467fe0b9de699a76af19e6b9e77e 100644 (file)
 #include <time.h>
 #include <fcntl.h>
 #include <poll.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <gcrypt.h>
+#include <sys/uio.h>
 
 #include "configuration.h"
 #include "syscalls.h"
@@ -52,6 +56,9 @@
 #include "mem.h"
 #include "speaker.h"
 #include "user.h"
+#include "addr.h"
+#include "timeval.h"
+#include "rtp.h"
 
 #if API_ALSA
 #include <alsa/asoundlib.h>
 
 #define FRAMES 4096                     /* Frame batch size */
 
+#define NETWORK_BYTES 1024              /* Bytes to send per network packet */
+/* (don't make this too big or arithmetic will start to overflow) */
+
+#define RTP_AHEAD 2                     /* Max RTP playahead (seconds) */
+
 #define NFDS 256                        /* Max FDs to poll for */
 
 /* Known tracks are kept in a linked list.  We don't normally to have
@@ -99,7 +111,14 @@ static snd_pcm_uframes_t last_pcm_bufsize; /* last seen buffer size */
 #endif
 static int ready;                       /* ready to send audio */
 static int forceplay;                   /* frames to force play */
-static int kidfd = -1;                  /* child process input */
+static int cmdfd = -1;                  /* child process input */
+static int bfd = -1;                    /* broadcast FD */
+static uint32_t rtp_time;               /* RTP timestamp */
+static struct timeval rtp_time_real;    /* corresponding real time */
+static uint16_t rtp_seq;                /* frame sequence number */
+static uint32_t rtp_id;                 /* RTP SSRC */
+static int idled;                       /* set when idled */
+static int audio_errors;                /* audio error counter */
 
 static const struct option options[] = {
   { "help", no_argument, 0, 'h' },
@@ -247,7 +266,7 @@ static int formats_equal(const ao_sample_format *a,
 static void idle(void) {
   D(("idle"));
 #if API_ALSA
-  if(!config->speaker_command && pcm) {
+  if(config->speaker_backend == BACKEND_ALSA && pcm) {
     int  err;
 
     if((err = snd_pcm_nonblock(pcm, 0)) < 0)
@@ -261,6 +280,7 @@ static void idle(void) {
     D(("released audio device"));
   }
 #endif
+  idled = 1;
   ready = 0;
 }
 
@@ -349,7 +369,11 @@ static int activate(void) {
     D((" - not got format for %s", playing->id));
     return -1;
   }
-  if(kidfd >= 0) {
+  switch(config->speaker_backend) {
+  case BACKEND_COMMAND:
+  case BACKEND_NETWORK:
+    /* If we pass audio on to some other agent then we enforce the configured
+     * sample format on the *inbound* audio data. */
     if(!formats_equal(&playing->format, &config->sample_format)) {
       char argbuf[1024], *q = argbuf;
       const char *av[18], **pp = av;
@@ -393,111 +417,112 @@ static int activate(void) {
       ready = 1;
     }
     return 0;
-  }
-  if(config->speaker_command)
-    return -1;
+  case BACKEND_ALSA:
 #if API_ALSA
-  /* If we need to change format then close the current device. */
-  if(pcm && !formats_equal(&playing->format, &pcm_format))
-     idle();
-  if(!pcm) {
-    snd_pcm_hw_params_t *hwparams;
-    snd_pcm_sw_params_t *swparams;
-    snd_pcm_uframes_t pcm_bufsize;
-    int err;
-    int sample_format = 0;
-    unsigned rate;
-
-    D(("snd_pcm_open"));
-    if((err = snd_pcm_open(&pcm,
-                           config->device,
-                           SND_PCM_STREAM_PLAYBACK,
-                           SND_PCM_NONBLOCK))) {
-      error(0, "error from snd_pcm_open: %d", err);
-      goto error;
-    }
-    snd_pcm_hw_params_alloca(&hwparams);
-    D(("set up hw params"));
-    if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
-      fatal(0, "error from snd_pcm_hw_params_any: %d", err);
-    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) {
-    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);
+    /* If we need to change format then close the current device. */
+    if(pcm && !formats_equal(&playing->format, &pcm_format))
+      idle();
+    if(!pcm) {
+      snd_pcm_hw_params_t *hwparams;
+      snd_pcm_sw_params_t *swparams;
+      snd_pcm_uframes_t pcm_bufsize;
+      int err;
+      int sample_format = 0;
+      unsigned rate;
+
+      D(("snd_pcm_open"));
+      if((err = snd_pcm_open(&pcm,
+                             config->device,
+                             SND_PCM_STREAM_PLAYBACK,
+                             SND_PCM_NONBLOCK))) {
+        error(0, "error from snd_pcm_open: %d", err);
+        goto error;
+      }
+      snd_pcm_hw_params_alloca(&hwparams);
+      D(("set up hw params"));
+      if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
+        fatal(0, "error from snd_pcm_hw_params_any: %d", err);
+      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) {
+      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);
+          goto fatal;
+        }
+        break;
+      default:
+        error(0, "unsupported sample size %d", playing->format.bits);
         goto fatal;
       }
-      break;
-    default:
-      error(0, "unsupported sample size %d", playing->format.bits);
-      goto fatal;
-    }
-    if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
-                                           sample_format)) < 0) {
-      error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
-            sample_format, err);
-      goto fatal;
-    }
-    rate = playing->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);
-      goto fatal;
+      if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
+                                             sample_format)) < 0) {
+        error(0, "error from snd_pcm_hw_params_set_format (%d): %d",
+              sample_format, err);
+        goto fatal;
+      }
+      rate = playing->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);
+        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) {
+        error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
+              playing->format.channels, err);
+        goto fatal;
+      }
+      bufsize = 3 * FRAMES;
+      pcm_bufsize = bufsize;
+      if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
+                                                       &pcm_bufsize)) < 0)
+        fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
+              3 * FRAMES, err);
+      if(pcm_bufsize != 3 * FRAMES && pcm_bufsize != last_pcm_bufsize)
+        info("asked for PCM buffer of %d frames, got %d",
+             3 * FRAMES, (int)pcm_bufsize);
+      last_pcm_bufsize = pcm_bufsize;
+      if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
+        fatal(0, "error calling snd_pcm_hw_params: %d", err);
+      D(("set up sw params"));
+      snd_pcm_sw_params_alloca(&swparams);
+      if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
+        fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
+      if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
+        fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
+              FRAMES, err);
+      if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
+        fatal(0, "error calling snd_pcm_sw_params: %d", err);
+      pcm_format = playing->format;
+      bpf = bytes_per_frame(&pcm_format);
+      D(("acquired audio device"));
+      log_params(hwparams, swparams);
+      ready = 1;
     }
-    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) {
-      error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
-            playing->format.channels, err);
-      goto fatal;
+    return 0;
+  fatal:
+    abandon();
+  error:
+    /* We assume the error is temporary and that we'll retry in a bit. */
+    if(pcm) {
+      snd_pcm_close(pcm);
+      pcm = 0;
     }
-    bufsize = 3 * FRAMES;
-    pcm_bufsize = bufsize;
-    if((err = snd_pcm_hw_params_set_buffer_size_near(pcm, hwparams,
-                                                     &pcm_bufsize)) < 0)
-      fatal(0, "error from snd_pcm_hw_params_set_buffer_size (%d): %d",
-            3 * FRAMES, err);
-    if(pcm_bufsize != 3 * FRAMES && pcm_bufsize != last_pcm_bufsize)
-      info("asked for PCM buffer of %d frames, got %d",
-           3 * FRAMES, (int)pcm_bufsize);
-    last_pcm_bufsize = pcm_bufsize;
-    if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
-      fatal(0, "error calling snd_pcm_hw_params: %d", err);
-    D(("set up sw params"));
-    snd_pcm_sw_params_alloca(&swparams);
-    if((err = snd_pcm_sw_params_current(pcm, swparams)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params_current: %d", err);
-    if((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, FRAMES)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params_set_avail_min %d: %d",
-            FRAMES, err);
-    if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
-      fatal(0, "error calling snd_pcm_sw_params: %d", err);
-    pcm_format = playing->format;
-    bpf = bytes_per_frame(&pcm_format);
-    D(("acquired audio device"));
-    log_params(hwparams, swparams);
-    ready = 1;
-  }
-  return 0;
-fatal:
-  abandon();
-error:
-  /* We assume the error is temporary and that we'll retry in a bit. */
-  if(pcm) {
-    snd_pcm_close(pcm);
-    pcm = 0;
-  }
+    return -1;
 #endif
-  return -1;
+  default:
+    assert(!"reached");
+  }
 }
 
 /* Check to see whether the current track has finished playing */
@@ -509,13 +534,13 @@ static void maybe_finished(void) {
     abandon();
 }
 
-static void fork_kid(void) {
-  pid_t kid;
+static void fork_cmd(void) {
+  pid_t cmdpid;
   int pfd[2];
-  if(kidfd != -1) close(kidfd);
+  if(cmdfd != -1) close(cmdfd);
   xpipe(pfd);
-  kid = xfork();
-  if(!kid) {
+  cmdpid = xfork();
+  if(!cmdpid) {
     xdup2(pfd[0], 0);
     close(pfd[0]);
     close(pfd[1]);
@@ -523,13 +548,15 @@ static void fork_kid(void) {
     fatal(errno, "error execing /bin/sh");
   }
   close(pfd[0]);
-  kidfd = pfd[1];
-  D(("forked kid %d, fd = %d", kid, kidfd));
+  cmdfd = pfd[1];
+  D(("forked cmd %d, fd = %d", cmdpid, cmdfd));
 }
 
 static void play(size_t frames) {
   size_t avail_bytes, written_frames;
   ssize_t written_bytes;
+  struct rtp header;
+  struct iovec vec[2];
 
   if(activate()) {
     if(playing)
@@ -557,8 +584,9 @@ static void play(size_t frames) {
   else
     avail_bytes = playing->used;
 
-  if(!config->speaker_command) {
+  switch(config->speaker_backend) {
 #if API_ALSA
+  case BACKEND_ALSA:
     snd_pcm_sframes_t pcm_written_frames;
     size_t avail_frames;
     int err;
@@ -589,28 +617,107 @@ static void play(size_t frames) {
     }
     written_frames = pcm_written_frames;
     written_bytes = written_frames * bpf;
-#else
-    assert(!"reached");
+    break;
 #endif
-  } else {
+  case BACKEND_COMMAND:
     if(avail_bytes > frames * bpf)
       avail_bytes = frames * bpf;
-    written_bytes = write(kidfd, playing->buffer + playing->start,
+    written_bytes = write(cmdfd, 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();
+          error(0, "hmm, command died; trying another");
+          fork_cmd();
           return;
         case EAGAIN:
           return;
       }
     }
     written_frames = written_bytes / bpf; /* good enough */
+    break;
+  case BACKEND_NETWORK:
+    /* We transmit using RTP (RFC3550) and attempt to conform to the internet
+     * AVT profile (RFC3551). */
+    if(rtp_time_real.tv_sec == 0)
+      xgettimeofday(&rtp_time_real, 0);
+    if(idled) {
+      struct timeval now;
+      xgettimeofday(&now, 0);
+      /* There's been a gap.  Fix up the RTP time accordingly. */
+      rtp_time += (((now.tv_sec + now.tv_usec /1000000.0)
+                    - (rtp_time_real.tv_sec + rtp_time_real.tv_usec / 1000000.0)) 
+                   * playing->format.rate * playing->format.channels);
+    }
+    header.vpxcc = 2 << 6;              /* V=2, P=0, X=0, CC=0 */
+    header.seq = htons(rtp_seq++);
+    header.timestamp = htonl(rtp_time);
+    header.ssrc = rtp_id;
+    header.mpt = (idled ? 0x80 : 0x00) | 10;
+    /* 10 = L16 = 16-bit x 2 x 44100KHz.  We ought to deduce this value from
+     * the sample rate (in a library somewhere so that configuration.c can rule
+     * out invalid rates).
+     */
+    idled = 0;
+    if(avail_bytes > NETWORK_BYTES - sizeof header) {
+      avail_bytes = NETWORK_BYTES - sizeof header;
+      avail_bytes -= avail_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
+     * sampling periods per second.  For N-channel encodings, each sampling
+     * period (say, 1/8000 of a second) generates N samples. (This terminology
+     * is standard, but somewhat confusing, as the total number of samples
+     * generated per second is then the sampling rate times the channel
+     * count.)"
+     */
+    vec[0].iov_base = (void *)&header;
+    vec[0].iov_len = sizeof header;
+    vec[1].iov_base = playing->buffer + playing->start;
+    vec[1].iov_len = avail_bytes;
+#if 0
+    {
+      char buffer[3 * sizeof header + 1];
+      size_t n;
+      const uint8_t *ptr = (void *)&header;
+      
+      for(n = 0; n < sizeof header; ++n)
+        sprintf(&buffer[3 * n], "%02x ", *ptr++);
+      info(buffer);
+    }
+#endif
+    do {
+      written_bytes = writev(bfd,
+                             vec,
+                             2);
+    } while(written_bytes < 0 && errno == EINTR);
+    if(written_bytes < 0) {
+      error(errno, "error transmitting audio data");
+      ++audio_errors;
+      if(audio_errors == 10)
+        fatal(0, "too many audio errors");
+      return;
+    }
+    audio_errors /= 2;
+    written_bytes = avail_bytes;
+    written_frames = written_bytes / bpf;
+    /* Advance RTP's notion of the time */
+    rtp_time += written_frames * playing->format.channels;
+    /* Advance the corresponding real time */
+    assert(NETWORK_BYTES <= 2000);      /* else risk overflowing 32 bits */
+    rtp_time_real.tv_usec += written_frames * 1000000 / playing->format.rate;
+    if(rtp_time_real.tv_usec >= 1000000) {
+      ++rtp_time_real.tv_sec;
+      rtp_time_real.tv_usec -= 1000000;
+    }
+    break;
+  default:
+    assert(!"reached");
   }
+  /* written_bytes and written_frames had better both be set and correct by
+   * this point */
   playing->start += written_bytes;
   playing->used -= written_bytes;
   playing->played += written_frames;
@@ -636,12 +743,12 @@ static void report(void) {
 }
 
 static void reap(int __attribute__((unused)) sig) {
-  pid_t kid;
+  pid_t cmdpid;
   int st;
 
   do
-    kid = waitpid(-1, &st, WNOHANG);
-  while(kid > 0);
+    cmdpid = waitpid(-1, &st, WNOHANG);
+  while(cmdpid > 0);
   signal(SIGCHLD, reap);
 }
 
@@ -655,9 +762,33 @@ static int addfd(int fd, int events) {
 }
 
 int main(int argc, char **argv) {
-  int n, fd, stdin_slot, alsa_slots, kid_slot;
+  int n, fd, stdin_slot, alsa_slots, cmdfd_slot, bfd_slot, poke, timeout;
+  struct timeval now, delta;
   struct track *t;
   struct speaker_message sm;
+  struct addrinfo *res, *sres;
+  static const struct addrinfo pref = {
+    0,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+  static const struct addrinfo prefbind = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_DGRAM,
+    IPPROTO_UDP,
+    0,
+    0,
+    0,
+    0
+  };
+  static const int one = 1;
+  char *sockname, *ssockname;
 #if API_ALSA
   int alsa_nslots = -1, err;
 #endif
@@ -691,15 +822,43 @@ int main(int argc, char **argv) {
   become_mortal();
   /* make sure we're not root, whatever the config says */
   if(getuid() == 0 || geteuid() == 0) fatal(0, "do not run as root");
-  info("started");
-  if(config->speaker_command)
-    fork_kid();
-  else {
-#if API_ALSA
-    /* ok */
-#else
-    fatal(0, "invoked speaker but no speaker_command and no known sound API");
- #endif
+  switch(config->speaker_backend) {
+  case BACKEND_ALSA:
+    info("selected ALSA backend");
+  case BACKEND_COMMAND:
+    info("selected command backend");
+    fork_cmd();
+    break;
+  case BACKEND_NETWORK:
+    res = get_address(&config->broadcast, &pref, &sockname);
+    if(!res) return -1;
+    if(config->broadcast_from.n) {
+      sres = get_address(&config->broadcast_from, &prefbind, &ssockname);
+      if(!sres) return -1;
+    } else
+      sres = 0;
+    if((bfd = socket(res->ai_family,
+                     res->ai_socktype,
+                     res->ai_protocol)) < 0)
+      fatal(errno, "error creating broadcast socket");
+    if(setsockopt(bfd, SOL_SOCKET, SO_BROADCAST, &one, sizeof one) < 0)
+      fatal(errno, "error settting SO_BROADCAST on broadcast socket");
+    /* We might well want to set additional broadcast- or multicast-related
+     * options here */
+    if(sres && bind(bfd, sres->ai_addr, sres->ai_addrlen) < 0)
+      fatal(errno, "error binding broadcast socket to %s", ssockname);
+    if(connect(bfd, res->ai_addr, res->ai_addrlen) < 0)
+      fatal(errno, "error connecting broadcast socket to %s", sockname);
+    /* Select an SSRC */
+    gcry_randomize(&rtp_id, sizeof rtp_id, GCRY_STRONG_RANDOM);
+    info("selected network backend, sending to %s", sockname);
+    if(config->sample_format.byte_format != AO_FMT_BIG) {
+      info("forcing big-endian sample format");
+      config->sample_format.byte_format = AO_FMT_BIG;
+    }
+    break;
+  default:
+    fatal(0, "unknown backend %d", config->speaker_backend);
   }
   while(getppid() != 1) {
     fdno = 0;
@@ -714,13 +873,40 @@ int main(int argc, char **argv) {
     /* If forceplay is set then wait until it succeeds before waiting on the
      * sound device. */
     alsa_slots = -1;
-    kid_slot = -1;
+    cmdfd_slot = -1;
+    bfd_slot = -1;
+    /* By default we will wait up to a second before thinking about current
+     * state. */
+    timeout = 1000;
     if(ready && !forceplay) {
-      if(config->speaker_command) {
-        if(kidfd >= 0)
-          kid_slot = addfd(kidfd, POLLOUT);
-      } else {
+      switch(config->speaker_backend) {
+      case BACKEND_COMMAND:
+        /* We send sample data to the subprocess as fast as it can accept it.
+         * This isn't ideal as pause latency can be very high as a result. */
+        if(cmdfd >= 0)
+          cmdfd_slot = addfd(cmdfd, POLLOUT);
+        break;
+      case BACKEND_NETWORK:
+        /* We want to keep the notional playing point somewhere in the near
+         * future.  If it's too near then clients that attempt even the
+         * slightest amount of read-ahead will never catch up, and those that
+         * don't will skip whenever there's a trivial network delay.  If it's
+         * too far ahead then pause latency will be too high.
+         */
+        xgettimeofday(&now, 0);
+        delta = tvsub(rtp_time_real, now);
+        if(delta.tv_sec < RTP_AHEAD) {
+          D(("delta = %ld.%06ld", (long)delta.tv_sec, (long)delta.tv_usec));
+          bfd_slot = addfd(bfd, POLLOUT);
+          if(delta.tv_sec < 0)
+            rtp_time_real = now;        /* catch up */
+        }
+        break;
 #if API_ALSA
+      case BACKEND_ALSA:
+        /* We send sample data to ALSA as fast as it can accept it, relying on
+         * the fact that it has a relatively small buffer to minimize pause
+         * latency. */
         int retry = 3;
         
         alsa_slots = fdno;
@@ -738,7 +924,10 @@ int main(int argc, char **argv) {
         } while(retry-- > 0);
         if(alsa_nslots >= 0)
           fdno += alsa_nslots;
+        break;
 #endif
+      default:
+        assert(!"unknown backend");
       }
     }
     /* If any other tracks don't have a full buffer, try to read sample data
@@ -750,29 +939,47 @@ int main(int argc, char **argv) {
         } else
           t->slot = -1;
       }
-    /* Wait up to a second before thinking about current state */
-    n = poll(fds, fdno, 1000);
+    /* Wait for something interesting to happen */
+    n = poll(fds, fdno, timeout);
     if(n < 0) {
       if(errno == EINTR) continue;
       fatal(errno, "error calling poll");
     }
     /* Play some sound before doing anything else */
-    if(alsa_slots != -1) {
+    poke = 0;
+    switch(config->speaker_backend) {
 #if API_ALSA
-      unsigned short alsa_revents;
-      
-      if((err = snd_pcm_poll_descriptors_revents(pcm,
-                                                 &fds[alsa_slots],
-                                                 alsa_nslots,
-                                                 &alsa_revents)) < 0)
-        fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
-      if(alsa_revents & (POLLOUT | POLLERR))
-        play(3 * FRAMES);
+    case BACKEND_ALSA:
+      if(alsa_slots != -1) {
+        unsigned short alsa_revents;
+        
+        if((err = snd_pcm_poll_descriptors_revents(pcm,
+                                                   &fds[alsa_slots],
+                                                   alsa_nslots,
+                                                   &alsa_revents)) < 0)
+          fatal(0, "error calling snd_pcm_poll_descriptors_revents: %d", err);
+        if(alsa_revents & (POLLOUT | POLLERR))
+          play(3 * FRAMES);
+      } else
+        poke = 1;
+      break;
 #endif
-    } else if(kid_slot != -1) {
-      if(fds[kid_slot].revents & (POLLOUT | POLLERR))
-        play(3 * FRAMES);
-    } else {
+    case BACKEND_COMMAND:
+      if(cmdfd_slot != -1) {
+        if(fds[cmdfd_slot].revents & (POLLOUT | POLLERR))
+          play(3 * FRAMES);
+      } else
+        poke = 1;
+      break;
+    case BACKEND_NETWORK:
+      if(bfd_slot != -1) {
+        if(fds[bfd_slot].revents & (POLLOUT | POLLERR))
+          play(3 * FRAMES);
+      } else
+        poke = 1;
+      break;
+    }
+    if(poke) {
       /* Some attempt to play must have failed */
       if(playing && !paused)
         play(forceplay);
diff --git a/server/uk.org.greenend.rjk.disorder.plist.in b/server/uk.org.greenend.rjk.disorder.plist.in
new file mode 100644 (file)
index 0000000..7e878c0
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>EnvironmentVariables</key>
+       <dict>
+               <key>LANG</key>
+               <string>en_GB.UTF-8</string>
+               <key>LC_ALL</key>
+               <string>en_GB.UTF-8</string>
+       </dict>
+       <key>Label</key>
+       <string>uk.org.greenend.rjk.disorder</string>
+       <key>ProgramArguments</key>
+       <array>
+               <string>sbindir/disorderd</string>
+               <string>--foreground</string>
+               <string>--syslog</string>
+       </array>
+       <key>WorkingDirectory</key>
+       <string>/Users/jukebox</string>
+       <key>RunAtLoad</key>
+       <true/>
+</dict>
+</plist>
+