From 937be4c02816d026de3063f32d82fafda1f3c512 Mon Sep 17 00:00:00 2001 Message-Id: <937be4c02816d026de3063f32d82fafda1f3c512.1714849685.git.mdw@distorted.org.uk> From: Mark Wooding Date: Thu, 4 Oct 2007 11:34:39 +0100 Subject: [PATCH] core audio support in speaker Organization: Straylight/Edgeware From: Richard Kettlewell --- doc/disorder_config.5.in | 16 ++- lib/configuration.c | 21 +++- lib/configuration.h | 1 + lib/speaker-protocol.h | 12 +- lib/syscalls.c | 7 ++ lib/syscalls.h | 3 +- server/Makefile.am | 3 +- server/play.c | 8 +- server/speaker-coreaudio.c | 235 +++++++++++++++++++++++++++++++++++++ server/speaker.c | 8 ++ server/speaker.h | 1 + 11 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 server/speaker-coreaudio.c diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index 8fea1ec..1f0b266 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -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 diff --git a/lib/configuration.c b/lib/configuration.c index f75845c..448a8ab 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -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 diff --git a/lib/configuration.h b/lib/configuration.h index 476a917..e4ac7b9 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -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; diff --git a/lib/speaker-protocol.h b/lib/speaker-protocol.h index b271373..ac4fdf0 100644 --- a/lib/speaker-protocol.h +++ b/lib/speaker-protocol.h @@ -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, diff --git a/lib/syscalls.c b/lib/syscalls.c index a84e2eb..f05b644 100644 --- a/lib/syscalls.c +++ b/lib/syscalls.c @@ -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, diff --git a/lib/syscalls.h b/lib/syscalls.h index 2b69181..296084d 100644 --- a/lib/syscalls.h +++ b/lib/syscalls.h @@ -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@. */ diff --git a/server/Makefile.am b/server/Makefile.am index 2b65595..1aef562 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -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 diff --git a/server/play.c b/server/play.c index fbd7f70..35d756b 100644 --- a/server/play.c +++ b/server/play.c @@ -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 index 0000000..75a69dd --- /dev/null +++ b/server/speaker-coreaudio.c @@ -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 + +#if HAVE_COREAUDIO_AUDIOHARDWARE_H + +#include "types.h" + +#include +#include +#include +#include + +#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: +*/ diff --git a/server/speaker.c b/server/speaker.c index 2c742f7..75e9cd7 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -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); diff --git a/server/speaker.h b/server/speaker.h index c1a76e2..4e22d38 100644 --- a/server/speaker.h +++ b/server/speaker.h @@ -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; -- [mdw]