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 8fea1ec5788d79534b1d8cd9aaed3cbb73609056..1f0b26623f218f8066ac2adf5d22c1ed0a75e444 100644 (file)
@@ -345,6 +345,16 @@ The number of channels.
 .PP
 The default is
 .BR 16/44100/2 .
 .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
 .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
 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
 .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
 .TP
 .B network
 Transmit audio over the network.  This is the default if
index f75845cbc1468926f335b2df2da3f5e7f51743ec..448a8ab7bbdda475235dafe706732e7aa8361eb8 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;
     *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;
     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;
     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
 #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 == 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;
   }
     /* 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
 }
 
 /** @brief (Re-)read the config file
index 476a917a33391e36f6ebc0a0b0064db0bf7f830f..e4ac7b93568de92d5a0b2403346ce614abe93778 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_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;
 
   /** @brief Home directory for state files */
   const char *home;
index b271373a1a129066cc5b4f5f2c54ebc8935eb4ef..ac4fdf0efc5fbb57a7ecf176c89c964d79fbfe4a 100644 (file)
@@ -89,6 +89,12 @@ struct speaker_message {
  */
 #define SM_PLAYING 131
 
  */
 #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. */
 
 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 One chunk in a stream */
 struct stream_header {
+  /** @brief Number of bytes */
+  uint32_t nbytes;
+
   /** @brief Frames per second */
   uint32_t rate;
 
   /** @brief Frames per second */
   uint32_t rate;
 
@@ -116,9 +125,6 @@ struct stream_header {
 #else
 # define ENDIAN_NATIVE ENDIAN_LITTLE
 #endif
 #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,
 } attribute((packed));
 
 static inline int formats_equal(const struct stream_header *a,
index a84e2eb763f3dee1d1094388d268a826f60db9cd..f05b644c37f825daf6a77c0ff895878c0015ed17 100644 (file)
@@ -66,6 +66,13 @@ void nonblock(int fd) {
                                        fcntl(fd, F_GETFL)) | O_NONBLOCK));
 }
 
                                        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,
 void cloexec(int fd) {
   mustnotbeminus1("fcntl F_SETFD",
                  fcntl(fd, F_SETFD,
index 2b6918101b27e28e9b7f42cf9de861832348f04f..296084d79b4b510a6f41377e90541108916b4d8b 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);
 /* the above all call @fatal@ if the system call fails */
 
 void nonblock(int fd);
+void blocking(int fd);
 void cloexec(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@. */
 
 int mustnotbeminus1(const char *what, int value);
 /* If @value@ is -1, report an error including @what@. */
index 2b655956128aada719294b48f18fd1a5cb932c42..1aef56236c842a6a95a30b869da1367e544d60a4 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 \
 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 \
                         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
 disorder_speaker_DEPENDENCIES=../lib/libdisorder.a
 
 disorder_decode_SOURCES=decode.c
index fbd7f704c6db208435b17af434f5df3adac5e7e1..35d756bc0d0880ebd96b613d2c6fa83612136b8e 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;
 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");
 
   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);
   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);
 }
 
   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] */
        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()) {
       /* 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 2c742f75ef56331b8030c9ae0c9c06102a732553..75e9cd769b2b2e1c94c97bfbff5465fe66370a6e 100644 (file)
@@ -377,6 +377,9 @@ static const struct speaker_backend *backends[] = {
 #endif
   &command_backend,
   &network_backend,
 #endif
   &command_backend,
   &network_backend,
+#if HAVE_COREAUDIO_AUDIOHARDWARE_H
+  &coreaudio_backend,
+#endif
   0
 };
 
   0
 };
 
@@ -470,6 +473,7 @@ static void mainloop(void) {
       char id[24];
 
       if((fd = accept(listenfd, (struct sockaddr *)&addr, &addrlen)) >= 0) {
       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);
         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;
   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");
 
   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);
   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);
   mainloop();
   info("stopped (parent terminated)");
   exit(0);
index c1a76e2ebdfecd367853ab00a5645a995e263c6c..4e22d38a821328e1d4036c373219003df41b20f5 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 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;
 
 extern struct pollfd fds[NFDS];
 extern int fdno;