From e83d0967d4c0965eb8036248acc20d1bf12ad1d8 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Tue, 4 Sep 2007 22:53:55 +0100 Subject: [PATCH] Install disorderd under launchd in Mac OS X. Organization: Straylight/Edgeware From: Richard Kettlewell 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. --- .bzrignore | 2 + README | 29 +- clients/playrtp.c | 330 ++++++++++++ configure.ac | 6 +- doc/disorder_config.5.in | 19 + lib/Makefile.am | 5 + lib/addr.c | 16 +- lib/configuration.c | 99 +++- lib/configuration.h | 9 +- lib/defs.c | 3 + lib/defs.h | 3 + lib/rtp.h | 42 ++ lib/timeval.h | 50 ++ server/Makefile.am | 20 +- server/disorderd.c | 37 +- server/speaker.c | 507 +++++++++++++------ server/uk.org.greenend.rjk.disorder.plist.in | 26 + 17 files changed, 1033 insertions(+), 170 deletions(-) create mode 100644 clients/playrtp.c create mode 100644 lib/rtp.h create mode 100644 lib/timeval.h create mode 100644 server/uk.org.greenend.rjk.disorder.plist.in diff --git a/.bzrignore b/.bzrignore index 2ca293c..acfbba5 100644 --- a/.bzrignore +++ b/.bzrignore @@ -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 67c80e4..414e8f5 100644 --- 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 index 0000000..7ec35d0 --- /dev/null +++ b/clients/playrtp.c @@ -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 +#include "types.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#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(<, 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: +*/ diff --git a/configure.ac b/configure.ac index c152c92..a06ce68 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index 3ddeb47..299233d 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -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 diff --git a/lib/Makefile.am b/lib/Makefile.am index ece2b3b..212b1ce 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -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 diff --git a/lib/addr.c b/lib/addr.c index f994ac6..16cd610 100644 --- a/lib/addr.c +++ b/lib/addr.c @@ -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; diff --git a/lib/configuration.c b/lib/configuration.c index 845c32a..5881f87 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -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 */ diff --git a/lib/configuration.h b/lib/configuration.h index c304921..5f0b8cc 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -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 */ diff --git a/lib/defs.c b/lib/defs.c index 5e3795f..7a11ebd 100644 --- a/lib/defs.c +++ b/lib/defs.c @@ -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: diff --git a/lib/defs.h b/lib/defs.h index 4f18012..c14bb9b 100644 --- a/lib/defs.h +++ b/lib/defs.h @@ -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 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 index 0000000..cefc990 --- /dev/null +++ b/lib/timeval.h @@ -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: +*/ diff --git a/server/Makefile.am b/server/Makefile.am index 4b01707..906ce98 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -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 diff --git a/server/disorderd.c b/server/disorderd.c index 774ab6d..4a3915a 100644 --- a/server/disorderd.c +++ b/server/disorderd.c @@ -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: */ diff --git a/server/speaker.c b/server/speaker.c index be783af..98ed297 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -44,6 +44,10 @@ #include #include #include +#include +#include +#include +#include #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 @@ -68,6 +75,11 @@ #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 index 0000000..7e878c0 --- /dev/null +++ b/server/uk.org.greenend.rjk.disorder.plist.in @@ -0,0 +1,26 @@ + + + + + EnvironmentVariables + + LANG + en_GB.UTF-8 + LC_ALL + en_GB.UTF-8 + + Label + uk.org.greenend.rjk.disorder + ProgramArguments + + sbindir/disorderd + --foreground + --syslog + + WorkingDirectory + /Users/jukebox + RunAtLoad + + + + -- [mdw]