chiark / gitweb /
core audio support in speaker
authorRichard Kettlewell <rjk@greenend.org.uk>
Thu, 4 Oct 2007 10:34:39 +0000 (11:34 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Thu, 4 Oct 2007 10:34:39 +0000 (11:34 +0100)
doc/disorder_config.5.in
lib/configuration.c
lib/configuration.h
lib/speaker-protocol.h
lib/syscalls.c
lib/syscalls.h
server/Makefile.am
server/play.c
server/speaker-coreaudio.c [new file with mode: 0644]
server/speaker.c
server/speaker.h

index 8fea1ec..1f0b266 100644 (file)
@@ -345,6 +345,16 @@ The number of channels.
 .PP
 The default is
 .BR 16/44100/2 .
+.PP
+With the
+.B network
+backend the sample format is forced to
+.B 16/44100/2b
+and with the
+.B coreaudio
+backend it is forced to
+.BR 16/44100/2 ,
+in both cases regardless of what is specified in the configuration file.
 .RE
 .TP
 .B signal \fINAME\fR
@@ -363,10 +373,14 @@ available:
 Use the ALSA API.  This is only available on Linux systems, on which it is the
 default.
 .TP
+.B coreaudio
+Use Apple Core Audio.  This only available on OS X 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.
+is specified, or if no native is available.
 .TP
 .B network
 Transmit audio over the network.  This is the default if
index f75845c..448a8ab 100644 (file)
@@ -450,7 +450,15 @@ static int set_backend(const struct config_state *cs,
     *valuep = BACKEND_COMMAND;
   else if(!strcmp(vec[0], "network"))
     *valuep = BACKEND_NETWORK;
-  else {
+  else if(!strcmp(vec[0], "coreaudio")) {
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+    *valuep = BACKEND_COREAUDIO;
+#else
+    error(0, "%s:%d: Core Audio is not available on this platform",
+         cs->path, cs->line);
+    return -1;
+#endif
+  } else {
     error(0, "%s:%d: invalid '%s' value '%s'",
          cs->path, cs->line, whoami->name, vec[0]);
     return -1;
@@ -1070,6 +1078,8 @@ static void config_postdefaults(struct config *c,
     else {
 #if API_ALSA
       c->speaker_backend = BACKEND_ALSA;
+#elif HAVE_COREAUDIO_AUDIOHARDWARE_H
+      c->speaker_backend = BACKEND_COREAUDIO;
 #else
       c->speaker_backend = BACKEND_COMMAND;
 #endif
@@ -1081,13 +1091,20 @@ static void config_postdefaults(struct config *c,
     if(c->speaker_backend == BACKEND_NETWORK && !c->broadcast.n)
       fatal(0, "speaker_backend is network but broadcast is not set");
   }
-  if(c->speaker_backend) {
+  if(c->speaker_backend == BACKEND_NETWORK) {
     /* Override sample format */
     c->sample_format.rate = 44100;
     c->sample_format.channels = 2;
     c->sample_format.bits = 16;
     c->sample_format.endian = ENDIAN_BIG;
   }
+  if(c->speaker_backend == BACKEND_COREAUDIO) {
+    /* Override sample format */
+    c->sample_format.rate = 44100;
+    c->sample_format.channels = 2;
+    c->sample_format.bits = 16;
+    c->sample_format.endian = ENDIAN_NATIVE;
+  }
 }
 
 /** @brief (Re-)read the config file
index 476a917..e4ac7b9 100644 (file)
@@ -182,6 +182,7 @@ struct config {
 #define BACKEND_ALSA 0                 /**< Use ALSA (Linux only) */
 #define BACKEND_COMMAND 1              /**< Execute a command */
 #define BACKEND_NETWORK 2              /**< Transmit RTP  */
+#define BACKEND_COREAUDIO 3            /**< Use Core Audio (Mac only) */
 
   /** @brief Home directory for state files */
   const char *home;
index b271373..ac4fdf0 100644 (file)
@@ -89,6 +89,12 @@ struct speaker_message {
  */
 #define SM_PLAYING 131
 
+/** @brief Speaker process is ready
+ *
+ * This is sent once at startup when the speaker has finished its
+ * initialization. */
+#define SM_READY 132
+
 void speaker_send(int fd, const struct speaker_message *sm);
 /* Send a message. */
 
@@ -98,6 +104,9 @@ int speaker_recv(int fd, struct speaker_message *sm);
 
 /** @brief One chunk in a stream */
 struct stream_header {
+  /** @brief Number of bytes */
+  uint32_t nbytes;
+
   /** @brief Frames per second */
   uint32_t rate;
 
@@ -116,9 +125,6 @@ struct stream_header {
 #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,
index a84e2eb..f05b644 100644 (file)
@@ -66,6 +66,13 @@ void nonblock(int fd) {
                                        fcntl(fd, F_GETFL)) | O_NONBLOCK));
 }
 
+void blocking(int fd) {
+  mustnotbeminus1("fcntl F_SETFL",
+                 fcntl(fd, F_SETFL,
+                       mustnotbeminus1("fcntl F_GETFL",
+                                       fcntl(fd, F_GETFL)) & ~O_NONBLOCK));
+}
+
 void cloexec(int fd) {
   mustnotbeminus1("fcntl F_SETFD",
                  fcntl(fd, F_SETFD,
index 2b69181..296084d 100644 (file)
@@ -53,8 +53,9 @@ void xgettimeofday(struct timeval *, struct timezone *);
 /* the above all call @fatal@ if the system call fails */
 
 void nonblock(int fd);
+void blocking(int fd);
 void cloexec(int fd);
-/* make @fd@ non-blocking/close-on-exec; call @fatal@ on error. */
+/* make @fd@ non-blocking/blocking/close-on-exec; call @fatal@ on error. */
 
 int mustnotbeminus1(const char *what, int value);
 /* If @value@ is -1, report an error including @what@. */
index 2b65595..1aef562 100644 (file)
@@ -47,9 +47,10 @@ disorder_deadlock_DEPENDENCIES=../lib/libdisorder.a
 disorder_speaker_SOURCES=speaker.c speaker.h \
                         speaker-command.c \
                         speaker-network.c \
+                        speaker-coreaudio.c \
                         speaker-alsa.c
 disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
-       $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT)
+       $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO)
 disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
 
 disorder_decode_SOURCES=decode.c
index fbd7f70..35d756b 100644 (file)
@@ -128,6 +128,7 @@ static int speaker_readable(ev_source *ev, int fd,
 void speaker_setup(ev_source *ev) {
   int sp[2], lfd;
   pid_t pid;
+  struct speaker_message sm;
 
   if(socketpair(PF_UNIX, SOCK_DGRAM, 0, sp) < 0)
     fatal(errno, "error calling socketpair");
@@ -160,8 +161,9 @@ void speaker_setup(ev_source *ev) {
   speaker_fd = sp[1];
   xclose(sp[0]);
   cloexec(speaker_fd);
-  /* Don't need to make speaker_fd nonblocking because speaker_recv() uses
-   * MSG_DONTWAIT. */
+  /* Wait for the speaker to be ready */
+  speaker_recv(speaker_fd, &sm);
+  nonblock(speaker_fd);
   ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0);
 }
 
@@ -383,6 +385,8 @@ static int start(ev_source *ev,
        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] */
+      blocking(np[0]);
+      blocking(np[1]);
       /* Start disorder-normalize */
       if(!(npid = xfork())) {
        if(!xfork()) {
diff --git a/server/speaker-coreaudio.c b/server/speaker-coreaudio.c
new file mode 100644 (file)
index 0000000..75a69dd
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * 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/speaker-coreaudio.c
+ * @brief Support for @ref BACKEND_COREAUDIO
+ *
+ * Core Audio likes to make callbacks from a separate player thread
+ * which then fill in the required number of bytes of audio.  We fit
+ * this into the existing architecture by means of a pipe between the
+ * threads.
+ *
+ * We currently only support 16-bit 44100Hz stereo (and enforce this
+ * in @ref lib/configuration.c.)  There are some nasty bodges in this
+ * code which depend on this and on further assumptions still...
+ *
+ * @todo support @ref config::device
+ */
+
+#include <config.h>
+
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+
+#include "types.h"
+
+#include <poll.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <CoreAudio/AudioHardware.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "speaker-protocol.h"
+#include "speaker.h"
+
+/** @brief Core Audio Device ID */
+static AudioDeviceID adid;
+
+/** @brief Pipe between main and player threads
+ *
+ * We'll write samples to pfd[1] and read them from pfd[0].
+ */
+static int pfd[2];
+
+/** @brief Slot number in poll array */
+static int pfd_slot;
+
+/** @brief Callback from Core Audio */
+static OSStatus adioproc
+    (AudioDeviceID attribute((unused)) inDevice,
+     const AudioTimeStamp attribute((unused)) *inNow,
+     const AudioBufferList attribute((unused)) *inInputData,
+     const AudioTimeStamp attribute((unused)) *inInputTime,
+     AudioBufferList *outOutputData,
+     const AudioTimeStamp attribute((unused)) *inOutputTime,
+     void attribute((unused)) *inClientData) {
+  UInt32 nbuffers = outOutputData->mNumberBuffers;
+  AudioBuffer *ab = outOutputData->mBuffers;
+
+  while(nbuffers > 0) {
+    float *samplesOut = ab->mData;
+    size_t samplesOutLeft = ab->mDataByteSize / sizeof (float);
+    int16_t input[1024], *ptr;
+    size_t bytes;
+    ssize_t bytes_read;
+    size_t samples;
+
+    while(samplesOutLeft > 0) {
+      /* Read some more data */
+      bytes = samplesOutLeft * sizeof (int16_t);
+      if(bytes > sizeof input)
+       bytes = sizeof input;
+      
+      bytes_read = read(pfd[0], input, bytes);
+      if(bytes_read < 0)
+       switch(errno) {
+       case EINTR:
+         continue;             /* just try again */
+       case EAGAIN:
+         return 0;             /* underrun - just play 0s */
+       default:
+         fatal(errno, "read error in core audio thread");
+       }
+      assert(bytes_read % 4 == 0); /* TODO horrible bodge! */
+      samples = bytes_read / sizeof (int16_t);
+      assert(samples <= samplesOutLeft);
+      ptr = input;
+      samplesOutLeft -= samples;
+      while(samples-- > 0)
+       *samplesOut++ = *ptr++ * (0.5 / 32767);
+    }
+    ++ab;
+    --nbuffers;
+  }
+  return 0;
+}
+
+/** @brief Core Audio backend initialization */
+static void coreaudio_init(void) {
+  OSStatus status;
+  UInt32 propertySize;
+  AudioStreamBasicDescription asbd;
+
+  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         %08lx", asbd.mFormatID));
+  D(("mFormatFlags      %08lx", asbd.mFormatFlags));
+  D(("mBytesPerPacket   %08lx", asbd.mBytesPerPacket));
+  D(("mFramesPerPacket  %08lx", asbd.mFramesPerPacket));
+  D(("mBytesPerFrame    %08lx", asbd.mBytesPerFrame));
+  D(("mChannelsPerFrame %08lx", asbd.mChannelsPerFrame));
+  D(("mBitsPerChannel   %08lx", asbd.mBitsPerChannel));
+  D(("mReserved         %08lx", 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);
+  if(socketpair(PF_UNIX, SOCK_STREAM, 0, pfd) < 0)
+    fatal(errno, "error calling socketpair");
+  nonblock(pfd[0]);
+  nonblock(pfd[1]);
+  info("selected Core Audio backend");
+}
+
+/** @brief Core Audio deactivation */
+static void coreaudio_deactivate(void) {
+  const OSStatus status = AudioDeviceStop(adid, adioproc);
+  if(status) {
+    error(0, "AudioDeviceStop: %d", (int)status);
+    device_state = device_error;
+  } else
+    device_state = device_closed;
+}
+
+/** @brief Core Audio backend activation */
+static void coreaudio_activate(void) {
+  const OSStatus status = AudioDeviceStart(adid, adioproc);
+
+  if(status) {
+    error(0, "AudioDeviceStart: %d", (int)status);
+    device_state = device_error;  
+  }
+  device_state = device_open;
+}
+
+/** @brief Play via Core Audio */
+static size_t coreaudio_play(size_t frames) {
+  static size_t leftover;
+
+  size_t bytes = frames * bpf + leftover;
+  ssize_t bytes_written;
+
+  if(leftover)
+    /* There is a partial frame left over from an earlier write.  Try
+     * and finish that off before doing anything else. */
+    bytes = leftover;
+  bytes_written = write(pfd[1], playing->buffer + playing->start, bytes);
+  if(bytes_written < 0)
+    switch(errno) {
+    case EINTR:                        /* interrupted */
+    case EAGAIN:               /* buffer full */
+      return 0;                        /* try later */
+    default:
+      fatal(errno, "error writing to core audio player thread");
+    }
+  if(leftover) {
+    /* We were dealing the leftover bytes of a partial frame */
+    leftover -= bytes_written;
+    return !leftover;
+  }
+  leftover = bytes_written % bpf;
+  return bytes_written / bpf;
+}
+
+/** @brief Fill in poll fd array for Core Audio */
+static void coreaudio_beforepoll(void) {
+  pfd_slot = addfd(pfd[1], POLLOUT);
+}
+
+/** @brief Process poll() results for Core Audio */
+static int coreaudio_ready(void) {
+  return !!(fds[pfd_slot].revents & (POLLOUT|POLLERR));
+}
+
+/** @brief Backend definition for Core Audio */
+const struct speaker_backend coreaudio_backend = {
+  BACKEND_COREAUDIO,
+  0,
+  coreaudio_init,
+  coreaudio_activate,
+  coreaudio_play,
+  coreaudio_deactivate,
+  coreaudio_beforepoll,
+  coreaudio_ready
+};
+
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 2c742f7..75e9cd7 100644 (file)
@@ -377,6 +377,9 @@ static const struct speaker_backend *backends[] = {
 #endif
   &command_backend,
   &network_backend,
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+  &coreaudio_backend,
+#endif
   0
 };
 
@@ -470,6 +473,7 @@ static void mainloop(void) {
       char id[24];
 
       if((fd = accept(listenfd, (struct sockaddr *)&addr, &addrlen)) >= 0) {
+        blocking(fd);
         if(read(fd, &l, sizeof l) < 4) {
           error(errno, "reading length from inbound connection");
           xclose(fd);
@@ -578,6 +582,7 @@ int main(int argc, char **argv) {
   int n;
   struct sockaddr_un addr;
   static const int one = 1;
+  struct speaker_message sm;
 
   set_progname(argv);
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
@@ -632,6 +637,9 @@ int main(int argc, char **argv) {
   xlisten(listenfd, 128);
   nonblock(listenfd);
   info("listening on %s", addr.sun_path);
+  memset(&sm, 0, sizeof sm);
+  sm.type = SM_READY;
+  speaker_send(1, &sm);
   mainloop();
   info("stopped (parent terminated)");
   exit(0);
index c1a76e2..4e22d38 100644 (file)
@@ -212,6 +212,7 @@ extern struct track *playing;
 extern const struct speaker_backend network_backend;
 extern const struct speaker_backend alsa_backend;
 extern const struct speaker_backend command_backend;
+extern const struct speaker_backend coreaudio_backend;
 
 extern struct pollfd fds[NFDS];
 extern int fdno;