*.bz2
debian/disobedience
server/disorder-decode
+server/disorder-normalize
-
/*
* This file is part of DisOrder.
- * Copyright (C) 2005 Richard Kettlewell
+ * Copyright (C) 2005, 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
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*/
+/** @file driver/disorder.c
+ * @brief libao driver used by DisOrder
+ *
+ * The output from this driver is expected to be fed to @c
+ * disorder-normalize to convert to the confnigured target format.
+ */
#include <config.h>
+#include "types.h"
#include <string.h>
#include <stdlib.h>
#include <ao/ao.h>
#include <ao/plugin.h>
+#include "speaker-protocol.h"
+
/* extra declarations to help out lazy <ao/plugin.h> */
int ao_plugin_test(void);
ao_info *ao_plugin_driver_info(void);
char *ao_plugin_file_extension(void);
-/* private data structure for this driver */
+/** @brief Private data structure for this driver */
struct internal {
int fd; /* output file descriptor */
int exit_on_error; /* exit on write error */
+
+ /** @brief Record of sample format */
+ struct stream_header header;
+
};
/* like write() but never returns EINTR/EAGAIN or short */
/* we would like native-order samples */
device->driver_byte_format = AO_FMT_NATIVE;
- if(do_write(i->fd, format, sizeof *format) < 0) {
- if(i->exit_on_error) exit(-1);
- return 0;
- }
+ i->header.rate = format->rate;
+ i->header.channels = format->channels;
+ i->header.bits = format->bits;
+ i->header.endian = ENDIAN_NATIVE;
return 1;
}
uint_32 num_bytes) {
struct internal *i = device->internal;
+ /* Fill in and write the header */
+ i->header.nbytes = num_bytes;
+ if(do_write(i->fd, &i->header, sizeof i->header) < 0) {
+ if(i->exit_on_error) _exit(-1);
+ return 0;
+ }
+
+ /* Write the sample data */
if(do_write(i->fd, output_samples, num_bytes) < 0) {
if(i->exit_on_error) _exit(-1);
return 0;
}
static int parse_sample_format(const struct config_state *cs,
- ao_sample_format *ao,
+ struct stream_header *format,
int nvec, char **vec) {
char *p = vec[0];
long t;
- if (nvec != 1) {
+ if(nvec != 1) {
error(0, "%s:%d: wrong number of arguments", cs->path, cs->line);
return -1;
}
- if (xstrtol(&t, p, &p, 0)) {
+ if(xstrtol(&t, p, &p, 0)) {
error(errno, "%s:%d: converting bits-per-sample", cs->path, cs->line);
return -1;
}
- if (t != 8 && t != 16) {
+ if(t != 8 && t != 16) {
error(0, "%s:%d: bad bite-per-sample (%ld)", cs->path, cs->line, t);
return -1;
}
- if (ao) ao->bits = t;
+ if(format) format->bits = t;
switch (*p) {
- case 'l': case 'L': t = AO_FMT_LITTLE; p++; break;
- case 'b': case 'B': t = AO_FMT_BIG; p++; break;
- default: t = AO_FMT_NATIVE; break;
+ case 'l': case 'L': t = ENDIAN_LITTLE; p++; break;
+ case 'b': case 'B': t = ENDIAN_BIG; p++; break;
+ default: t = ENDIAN_NATIVE; break;
}
- if (ao) ao->byte_format = t;
- if (*p != '/') {
+ if(format) format->endian = t;
+ if(*p != '/') {
error(errno, "%s:%d: expected `/' after bits-per-sample",
cs->path, cs->line);
return -1;
}
p++;
- if (xstrtol(&t, p, &p, 0)) {
+ if(xstrtol(&t, p, &p, 0)) {
error(errno, "%s:%d: converting sample-rate", cs->path, cs->line);
return -1;
}
- if (t < 1 || t > INT_MAX) {
+ if(t < 1 || t > INT_MAX) {
error(0, "%s:%d: silly sample-rate (%ld)", cs->path, cs->line, t);
return -1;
}
- if (ao) ao->rate = t;
- if (*p != '/') {
+ if(format) format->rate = t;
+ if(*p != '/') {
error(0, "%s:%d: expected `/' after sample-rate",
cs->path, cs->line);
return -1;
}
p++;
- if (xstrtol(&t, p, &p, 0)) {
+ if(xstrtol(&t, p, &p, 0)) {
error(errno, "%s:%d: converting channels", cs->path, cs->line);
return -1;
}
- if (t < 1 || t > 8) {
+ if(t < 1 || t > 8) {
error(0, "%s:%d: silly number (%ld) of channels", cs->path, cs->line, t);
return -1;
}
- if (ao) ao->channels = t;
- if (*p) {
+ if(format) format->channels = t;
+ if(*p) {
error(0, "%s:%d: junk after channels", cs->path, cs->line);
return -1;
}
static int set_sample_format(const struct config_state *cs,
const struct conf *whoami,
int nvec, char **vec) {
- return parse_sample_format(cs, ADDRESS(cs->config, ao_sample_format),
+ return parse_sample_format(cs, ADDRESS(cs->config, struct stream_header),
nvec, vec);
}
c->sample_format.bits = 16;
c->sample_format.rate = 44100;
c->sample_format.channels = 2;
- c->sample_format.byte_format = AO_FMT_NATIVE;
+ c->sample_format.endian = ENDIAN_NATIVE;
c->queue_pad = 10;
c->speaker_backend = -1;
c->multicast_ttl = 1;
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");
+ if(c->speaker_backend) {
+ /* Override sample format */
+ c->sample_format.rate = 44100;
+ c->sample_format.channels = 2;
+ c->sample_format.bits = 16;
+ c->sample_format.endian = ENDIAN_BIG;
+ }
}
/** @brief (Re-)read the config file */
#ifndef CONFIGURATION_H
#define CONFIGURATION_H
-#include <ao/ao.h>
+#include "speaker-protocol.h"
struct real_pcre;
const char *speaker_command;
/** @brief Target sample format */
- ao_sample_format sample_format;
+ struct stream_header sample_format;
/** @brief Sox syntax generation */
long sox_generation;
*/
#include <config.h>
+#include "types.h"
#include <stdio.h>
#include <string.h>
*/
#include <config.h>
+#include "types.h"
#include <dlfcn.h>
#include <unistd.h>
* on EOF, +ve if a message is read, -1 on EAGAIN, terminates on any other
* error. */
+/** @brief One chunk in a stream */
+struct stream_header {
+ /** @brief Frames per second */
+ uint32_t rate;
+
+ /** @brief Samples per frames */
+ uint8_t channels;
+
+ /** @brief Bits per sample */
+ uint8_t bits;
+
+ /** @brief Endianness */
+ uint8_t endian;
+#define ENDIAN_BIG 1
+#define ENDIAN_LITTLE 2
+#ifdef WORDS_BIGENDIAN
+# define ENDIAN_NATIVE ENDIAN_BIG
+#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,
+ const struct stream_header *b) {
+ return (a->rate == b->rate
+ && a->channels == b->channels
+ && a->bits == b->bits
+ && a->endian == b->endian);
+}
+
#endif /* SPEAKER_PROTOCOL_H */
/*
#
sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
- disorder-speaker disorder-decode
+ disorder-speaker disorder-decode disorder-normalize
noinst_PROGRAMS=disorder.cgi trackname
noinst_DATA=uk.org.greenend.rjk.disorder.plist
$(LIBMAD)
disorder_decode_DEPENDENCIES=../lib/libdisorder.a
+disorder_normalize_SOURCES=normalize.c
+disorder_normalize_LDADD=$(LIBOBJS) ../lib/libdisorder.a $(LIBPCRE)
+disorder_normalize_DEPENDENCIES=../lib/libdisorder.a
+
disorder_rescan_SOURCES=rescan.c \
api.c api-server.c \
trackdb.c trackdb.h exports.c \
*/
#include <config.h>
+#include "types.h"
#include <stdio.h>
#include <errno.h>
*/
#include <config.h>
+#include "types.h"
#include <stdio.h>
#include <errno.h>
*/
#include <config.h>
+#include "types.h"
#include <stdio.h>
#include <getopt.h>
--- /dev/null
+/*
+ * 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/disorder-normalize.c
+ * @brief Convert "raw" format output to the configured format
+ *
+ * Currently we invoke sox even for trivial conversions such as byte-swapping.
+ * Ideally we would do all conversion including resampling in this one process
+ * and eliminate the dependency on sox.
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <locale.h>
+#include <errno.h>
+#include <unistd.h>
+#include <syslog.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "syscalls.h"
+#include "log.h"
+#include "configuration.h"
+#include "speaker-protocol.h"
+
+/** @brief Copy bytes from one file descriptor to another
+ * @param infd File descriptor read from
+ * @param outfd File descriptor to write to
+ * @param n Number of bytes to copy
+ */
+static void copy(int infd, int outfd, size_t n) {
+ char buffer[4096], *ptr;
+ int r, w;
+
+ while(n > 0) {
+ r = read(infd, buffer, sizeof buffer);
+ if(r < 0) {
+ if(errno == EINTR)
+ continue;
+ else
+ fatal(errno, "read error");
+ }
+ if(r == 0)
+ fatal(0, "unexpected EOF");
+ n -= r;
+ ptr = buffer;
+ while(r > 0) {
+ w = write(outfd, ptr, r - (ptr - buffer));
+ if(w < 0)
+ fatal(errno, "write error");
+ ptr += w;
+ }
+ }
+}
+
+static void soxargs(const char ***pp, char **qq,
+ const struct stream_header *header) {
+ *(*pp)++ = "-t.raw";
+ *(*pp)++ = "-s";
+ *qq += sprintf((char *)(*(*pp)++ = *qq), "-r%d", header->rate) + 1;
+ *qq += sprintf((char *)(*(*pp)++ = *qq), "-c%d", header->channels) + 1;
+ /* sox 12.17.9 insists on -b etc; CVS sox insists on -<n> etc; both are
+ * deployed! */
+ switch(config->sox_generation) {
+ case 0:
+ if(header->bits != 8
+ && header->endian != ENDIAN_NATIVE)
+ *(*pp)++ = "-x";
+ switch(header->bits) {
+ case 8: *(*pp)++ = "-b"; break;
+ case 16: *(*pp)++ = "-w"; break;
+ case 32: *(*pp)++ = "-l"; break;
+ case 64: *(*pp)++ = "-d"; break;
+ default: fatal(0, "cannot handle sample size %d", header->bits);
+ }
+ break;
+ case 1:
+ if(header->bits != 8
+ && header->endian != ENDIAN_NATIVE)
+ switch(header->endian) {
+ case ENDIAN_BIG: *(*pp)++ = "-B"; break;
+ case ENDIAN_LITTLE: *(*pp)++ = "-L"; break;
+ }
+ if(header->bits % 8)
+ fatal(0, "cannot handle sample size %d", header->bits);
+ *qq += sprintf((char *)(*(*pp)++ = *qq), "-%d", header->bits / 8) + 1;
+ break;
+ default:
+ fatal(0, "unknown sox_generation %ld", config->sox_generation);
+ }
+}
+
+int main(int argc, char attribute((unused)) **argv) {
+ struct stream_header header, latest_format;
+ int n, p[2], outfd = -1;
+ pid_t pid = -1;
+
+ set_progname(argv);
+ if(!setlocale(LC_CTYPE, ""))
+ fatal(errno, "error calling setlocale");
+ if(argc > 1)
+ fatal(0, "not intended to be invoked by users");
+ if(config_read())
+ fatal(0, "cannot read configuration");
+ if(!isatty(2)) {
+ openlog(progname, LOG_PID, LOG_DAEMON);
+ log_default = &log_syslog;
+ }
+ memset(&latest_format, 0, sizeof latest_format);
+ for(;;) {
+ if((n = read(0, &header, sizeof header)) < 0)
+ fatal(errno, "read error");
+ else if(n == 0)
+ exit(0);
+ else if((size_t)n < sizeof header)
+ fatal(0, "short header");
+ /* Sanity check the header */
+ if(header.rate < 100 || header.rate > 1000000)
+ fatal(0, "implausible rate %"PRId32"Hz (%#"PRIx32")",
+ header.rate, header.rate);
+ if(header.channels < 1 || header.channels > 2)
+ fatal(0, "unsupported channel count %d", header.channels);
+ if(header.bits % 8 || !header.bits || header.bits > 64)
+ fatal(0, "unsupported sample size %d bits", header.bits);
+ if(header.endian != ENDIAN_BIG && header.endian != ENDIAN_LITTLE)
+ fatal(0, "unsupported byte order %x", header.bits);
+ /* Skip empty chunks regardless of their alleged format */
+ if(header.nbytes == 0)
+ continue;
+ /* If the format has changed we stop/start the converter */
+ if(!formats_equal(&header, &latest_format)) {
+ if(pid != -1) {
+ /* There's a running converter, stop it */
+ xclose(outfd);
+ if(waitpid(pid, &n, 0) < 0)
+ fatal(errno, "error calling waitpid");
+ if(n)
+ fatal(0, "sox failed: %#x", n);
+ pid = -1;
+ outfd = -1;
+ }
+ if(!formats_equal(&header, &config->sample_format)) {
+ const char *av[32], **pp = av;
+ char argbuf[1024], *q = argbuf;
+
+ /* Input format doesn't match target, need to start a converter */
+ *pp++ = "sox";
+ soxargs(&pp, &q, &header);
+ *pp++ = "-"; /* stdin */
+ soxargs(&pp, &q, &config->sample_format);
+ *pp++ = "-"; /* stdout */
+ *pp = 0;
+ /* This pipe will be sox's stdin */
+ xpipe(p);
+ if(!(pid = xfork())) {
+ exitfn = _exit;
+ xdup2(p[0], 0);
+ xclose(p[0]);
+ xclose(p[1]);
+ execvp(av[0], (char **)av);
+ fatal(errno, "sox");
+ }
+ xclose(p[0]);
+ outfd = p[1];
+ } else
+ /* Input format matches output, can just copy bytes */
+ outfd = 1;
+ /* Remember current format for next iteration */
+ latest_format = header;
+ }
+ /* Convert or copy this chunk */
+ copy(0, outfd, header.nbytes);
+ }
+ if(outfd != -1)
+ xclose(outfd);
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
/*
* 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
*/
#include <config.h>
+#include "types.h"
#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <pcre.h>
#include <ao/ao.h>
+#include <sys/wait.h>
#include "event.h"
#include "log.h"
int smop) {
int n, lfd;
const char *p;
- int sp[2];
+ int np[2], sp[2];
struct speaker_message sm;
char buffer[64];
int optc;
struct timespec ts;
const char *waitdevice = 0;
const char *const *optv;
- pid_t pid;
+ pid_t pid, npid;
memset(&sm, 0, sizeof sm);
if(find_player_pid(q->id) > 0) {
xclose(lfd); /* tidy up */
setpgid(0, 0);
if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
- /* Raw format players write down a pipe (in fact a socket) to
- * the speaker process. */
+ /* "Raw" format players need special treatment:
+ * 1) their output needs to go via the disorder-normalize process
+ * 2) the output of that needs to be passed to the disorder-speaker
+ * process.
+ */
+ /* np will be the pipe to disorder-normalize */
+ if(socketpair(PF_UNIX, SOCK_STREAM, 0, np) < 0)
+ 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] */
+ /* sp will be the pipe to disorder-speaker */
sm.type = smop;
- strcpy(sm.id, q->id);
if(socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0)
fatal(errno, "error calling socketpair");
- xshutdown(sp[0], SHUT_WR);
- xshutdown(sp[1], SHUT_RD);
+ xshutdown(sp[0], SHUT_WR); /* speaker reads from sp[0] */
+ xshutdown(sp[1], SHUT_RD); /* normalize writes to sp[1] */
+ /* Start disorder-normalize */
+ if(!(npid = xfork())) {
+ if(!xfork()) {
+ xdup2(np[0], 0);
+ xdup2(sp[1], 1);
+ xclose(np[0]);
+ xclose(np[1]);
+ xclose(sp[0]);
+ xclose(sp[1]);
+ execlp("disorder-normalize", "disorder-normalize", (char *)0);
+ fatal(errno, "executing disorder-normalize");
+ }
+ _exit(0);
+ } else {
+ int w;
+
+ while(waitpid(npid, &w, 0) < 0 && errno == EINTR)
+ ;
+ }
+ /* Send the speaker process the file descriptor to read from */
+ strcpy(sm.id, q->id);
speaker_send(speaker_fd, &sm, sp[0]);
/* Pass the file descriptor to the driver in an environment
* variable. */
- snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", sp[1]);
+ snprintf(buffer, sizeof buffer, "DISORDER_RAW_FD=%d", np[1]);
if(putenv(buffer) < 0)
fatal(errno, "error calling putenv");
+ /* Close all the FDs we don't need */
xclose(sp[0]);
+ xclose(sp[1]);
+ xclose(np[0]);
}
if(waitdevice) {
ao_initialize();
/** @brief ALSA backend activation */
static void alsa_activate(void) {
- /* If we need to change format then close the current device. */
- if(pcm && !formats_equal(&playing->format, &device_format))
- alsa_deactivate();
- /* Now if the sound device is open it must have the right format */
if(!pcm) {
snd_pcm_hw_params_t *hwparams;
snd_pcm_sw_params_t *swparams;
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) {
+ switch(config->sample_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);
+ switch(config->sample_format.endian) {
+ case ENDIAN_LITTLE: sample_format = SND_PCM_FORMAT_S16_LE; break;
+ case ENDIAN_BIG: sample_format = SND_PCM_FORMAT_S16_BE; break;
+ default:
+ error(0, "unrecognized byte format %d", config->sample_format.endian);
goto fatal;
}
break;
default:
- error(0, "unsupported sample size %d", playing->format.bits);
+ error(0, "unsupported sample size %d", config->sample_format.bits);
goto fatal;
}
if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
sample_format, err);
goto fatal;
}
- rate = playing->format.rate;
+ rate = config->sample_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);
+ config->sample_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) {
+ if(rate != (unsigned)config->sample_format.rate)
+ info("want rate %d, got %u", config->sample_format.rate, rate);
+ if((err = snd_pcm_hw_params_set_channels
+ (pcm, hwparams, config->sample_format.channels)) < 0) {
error(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
- playing->format.channels, err);
+ config->sample_format.channels, err);
goto fatal;
}
pcm_bufsize = 3 * FRAMES;
FRAMES, err);
if((err = snd_pcm_sw_params(pcm, swparams)) < 0)
fatal(0, "error calling snd_pcm_sw_params: %d", err);
- device_format = playing->format;
D(("acquired audio device"));
log_params(hwparams, swparams);
device_state = device_open;
#include <unistd.h>
#include <poll.h>
+#include <errno.h>
#include "configuration.h"
#include "syscalls.h"
xpipe(pfd);
cmdpid = xfork();
if(!cmdpid) {
+ exitfn = _exit;
signal(SIGPIPE, SIG_DFL);
xdup2(pfd[0], 0);
close(pfd[0]);
/** @brief Play to a subprocess */
static size_t command_play(size_t frames) {
- size_t bytes = frames * device_bpf;
+ size_t bytes = frames * bpf;
int written_bytes;
written_bytes = write(cmdfd, playing->buffer + playing->start, bytes);
fatal(errno, "error writing to subprocess");
}
} else
- return written_bytes / device_bpf;
+ return written_bytes / bpf;
}
/** @brief Update poll array for writing to subprocess */
const struct speaker_backend command_backend = {
BACKEND_COMMAND,
- FIXED_FORMAT,
+ 0,
command_init,
0, /* activate */
command_play,
#include <assert.h>
#include <net/if.h>
#include <ifaddrs.h>
+#include <errno.h>
#include "configuration.h"
#include "syscalls.h"
socklen_t len;
char *sockname, *ssockname;
- /* Override sample format */
- config->sample_format.rate = 44100;
- config->sample_format.channels = 2;
- config->sample_format.bits = 16;
- config->sample_format.byte_format = AO_FMT_BIG;
res = get_address(&config->broadcast, &pref, &sockname);
if(!res) exit(-1);
if(config->broadcast_from.n) {
static size_t network_play(size_t frames) {
struct rtp_header header;
struct iovec vec[2];
- size_t bytes = frames * device_bpf, written_frames;
+ size_t bytes = frames * bpf, written_frames;
int written_bytes;
/* We transmit using RTP (RFC3550) and attempt to conform to the internet
* AVT profile (RFC3551). */
/* Find the number of microseconds elapsed since rtp_time=0 */
delta = tvsub_us(now, rtp_time_0);
assert(delta <= UINT64_MAX / 88200);
- target_rtp_time = (delta * playing->format.rate
- * playing->format.channels) / 1000000;
+ target_rtp_time = (delta * config->sample_format.rate
+ * config->sample_format.channels) / 1000000;
/* Overflows at ~6 years uptime with 44100Hz stereo */
/* rtp_time is the number of samples we've played. NB that we play
if(bytes > NETWORK_BYTES - sizeof header) {
bytes = NETWORK_BYTES - sizeof header;
/* Always send a whole number of frames */
- bytes -= bytes % device_bpf;
+ bytes -= 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
} else
audio_errors /= 2;
written_bytes -= sizeof (struct rtp_header);
- written_frames = written_bytes / device_bpf;
+ written_frames = written_bytes / bpf;
/* Advance RTP's notion of the time */
- rtp_time += written_frames * playing->format.channels;
+ rtp_time += written_frames * config->sample_format.channels;
return written_frames;
}
const struct speaker_backend network_backend = {
BACKEND_NETWORK,
- FIXED_FORMAT,
+ 0,
network_init,
0, /* activate */
network_play,
* 8- and 16- bit stereo and mono are supported, with any sample rate (within
* the limits that ALSA can deal with.)
*
- * When communicating with a subprocess, <a
- * href="http://sox.sourceforge.net/">sox</a> is invoked to convert the inbound
- * data to a single consistent format. The same applies for network (RTP)
- * play, though in that case currently only 44.1KHz 16-bit stereo is supported.
- *
- * The inbound data starts with a structure defining the data format. Note
- * that this is NOT portable between different platforms or even necessarily
- * between versions; the speaker is assumed to be built from the same source
- * and run on the same host as the main server.
+ * Inbound data is expected to match @c config->sample_format. In normal use
+ * this is arranged by the @c disorder-normalize program (see @ref
+ * server/normalize.c).
*
* @b Garbage @b Collection. This program deliberately does not use the
* garbage collector even though it might be convenient to do so. This is for
struct track *playing;
/** @brief Number of bytes pre frame */
-size_t device_bpf;
+size_t bpf;
/** @brief Array of file descriptors for poll() */
struct pollfd fds[NFDS];
/** @brief The current device state */
enum device_states device_state;
-/** @brief The current device sample format
- *
- * Only meaningful if @ref device_state = @ref device_open or perhaps @ref
- * device_error. For @ref FIXED_FORMAT backends, this should always match @c
- * config->sample_format.
- */
-ao_sample_format device_format;
-
/** @brief Set when idled
*
* This is set when the sound device is deliberately closed by idle().
}
/** @brief Return the number of bytes per frame in @p format */
-static size_t bytes_per_frame(const ao_sample_format *format) {
+static size_t bytes_per_frame(const struct stream_header *format) {
return format->channels * format->bits / 8;
}
strcpy(t->id, id);
t->fd = -1;
tracks = t;
- /* The initial input buffer will be the sample format. */
- t->buffer = (void *)&t->format;
- t->size = sizeof t->format;
}
return t;
}
static void destroy(struct track *t) {
D(("destroy %s", t->id));
if(t->fd != -1) xclose(t->fd);
- if(t->buffer != (void *)&t->format) free(t->buffer);
free(t);
}
nonblock(fd);
}
-/** @brief Return true if A and B denote identical libao formats, else false */
-int formats_equal(const ao_sample_format *a,
- const ao_sample_format *b) {
- return (a->bits == b->bits
- && a->rate == b->rate
- && a->channels == b->channels
- && a->byte_format == b->byte_format);
-}
-
-/** @brief Compute arguments to sox */
-static void soxargs(const char ***pp, char **qq, ao_sample_format *ao) {
- int n;
-
- *(*pp)++ = "-t.raw";
- *(*pp)++ = "-s";
- *(*pp)++ = *qq; n = sprintf(*qq, "-r%d", ao->rate); *qq += n + 1;
- *(*pp)++ = *qq; n = sprintf(*qq, "-c%d", ao->channels); *qq += n + 1;
- /* sox 12.17.9 insists on -b etc; CVS sox insists on -<n> etc; both are
- * deployed! */
- switch(config->sox_generation) {
- case 0:
- if(ao->bits != 8
- && ao->byte_format != AO_FMT_NATIVE
- && ao->byte_format != MACHINE_AO_FMT) {
- *(*pp)++ = "-x";
- }
- switch(ao->bits) {
- case 8: *(*pp)++ = "-b"; break;
- case 16: *(*pp)++ = "-w"; break;
- case 32: *(*pp)++ = "-l"; break;
- case 64: *(*pp)++ = "-d"; break;
- default: fatal(0, "cannot handle sample size %d", (int)ao->bits);
- }
- break;
- case 1:
- switch(ao->byte_format) {
- case AO_FMT_NATIVE: break;
- case AO_FMT_BIG: *(*pp)++ = "-B"; break;
- case AO_FMT_LITTLE: *(*pp)++ = "-L"; break;
- }
- *(*pp)++ = *qq; n = sprintf(*qq, "-%d", ao->bits/8); *qq += n + 1;
- break;
- }
-}
-
-/** @brief Enable format translation
- *
- * If necessary, replaces a tracks inbound file descriptor with one connected
- * to a sox invocation, which performs the required translation.
- */
-static void enable_translation(struct track *t) {
- if((backend->flags & FIXED_FORMAT)
- && !formats_equal(&t->format, &config->sample_format)) {
- char argbuf[1024], *q = argbuf;
- const char *av[18], **pp = av;
- int soxpipe[2];
- pid_t soxkid;
-
- *pp++ = "sox";
- soxargs(&pp, &q, &t->format);
- *pp++ = "-";
- soxargs(&pp, &q, &config->sample_format);
- *pp++ = "-";
- *pp++ = 0;
- if(debugging) {
- for(pp = av; *pp; pp++)
- D(("sox arg[%d] = %s", pp - av, *pp));
- D(("end args"));
- }
- xpipe(soxpipe);
- soxkid = xfork();
- if(soxkid == 0) {
- signal(SIGPIPE, SIG_DFL);
- xdup2(t->fd, 0);
- xdup2(soxpipe[1], 1);
- fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK);
- close(soxpipe[0]);
- close(soxpipe[1]);
- close(t->fd);
- execvp("sox", (char **)av);
- _exit(1);
- }
- D(("forking sox for format conversion (kid = %d)", soxkid));
- close(t->fd);
- close(soxpipe[1]);
- t->fd = soxpipe[0];
- t->format = config->sample_format;
- }
-}
-
/** @brief Read data into a sample buffer
* @param t Pointer to track
* @return 0 on success, -1 on EOF
size_t where, left;
int n;
- D(("fill %s: eof=%d used=%zu size=%zu got_format=%d",
- t->id, t->eof, t->used, t->size, t->got_format));
+ D(("fill %s: eof=%d used=%zu",
+ t->id, t->eof, t->used));
if(t->eof) return -1;
- if(t->used < t->size) {
+ if(t->used < sizeof t->buffer) {
/* there is room left in the buffer */
- where = (t->start + t->used) % t->size;
- if(t->got_format) {
- /* We are reading audio data, get as much as we can */
- if(where >= t->start) left = t->size - where;
- else left = t->start - where;
- } else
- /* We are still waiting for the format, only get that */
- left = sizeof (ao_sample_format) - t->used;
+ where = (t->start + t->used) % sizeof t->buffer;
+ /* Get as much data as we can */
+ if(where >= t->start) left = (sizeof t->buffer) - where;
+ else left = t->start - where;
do {
n = read(t->fd, t->buffer + where, left);
} while(n < 0 && errno == EINTR);
return -1;
}
t->used += n;
- if(!t->got_format && t->used >= sizeof (ao_sample_format)) {
- assert(t->used == sizeof (ao_sample_format));
- /* Check that our assumptions are met. */
- if(t->format.bits & 7)
- fatal(0, "bits per sample not a multiple of 8");
- /* If the input format is unsuitable, arrange to translate it */
- enable_translation(t);
- /* Make a new buffer for audio data. */
- t->size = bytes_per_frame(&t->format) * t->format.rate * BUFFER_SECONDS;
- t->buffer = xmalloc(t->size);
- t->used = 0;
- t->got_format = 1;
- D(("got format for %s", t->id));
- }
}
return 0;
}
* 0 on success and -1 on error.
*/
static void activate(void) {
- /* If we don't know the format yet we cannot start. */
- if(!playing->got_format) {
- D((" - not got format for %s", playing->id));
- return;
- }
- if(backend->flags & FIXED_FORMAT)
- device_format = config->sample_format;
- if(backend->activate) {
+ if(backend->activate)
backend->activate();
- } else {
- assert(backend->flags & FIXED_FORMAT);
- /* ...otherwise device_format not set */
+ else
device_state = device_open;
- }
- if(device_state == device_open)
- device_bpf = bytes_per_frame(&device_format);
}
/** @brief Check whether the current track has finished
static void maybe_finished(void) {
if(playing
&& playing->eof
- && (!playing->got_format
- || playing->used < bytes_per_frame(&playing->format)))
+ && playing->used < bytes_per_frame(&config->sample_format))
abandon();
}
/* Make sure there's a track to play and it is not pasued */
if(!playing || paused)
return;
- /* Make sure the output device is open and has the right sample format */
- if(device_state != device_open
- || !formats_equal(&device_format, &playing->format)) {
+ /* Make sure the output device is open */
+ if(device_state != device_open) {
activate();
if(device_state != device_open)
return;
}
- D(("play: play %zu/%zu%s %dHz %db %dc", frames, playing->used / device_bpf,
+ D(("play: play %zu/%zu%s %dHz %db %dc", frames, playing->used / bpf,
playing->eof ? " EOF" : "",
- playing->format.rate,
- playing->format.bits,
- playing->format.channels));
+ config->sample_format.rate,
+ config->sample_format.bits,
+ config->sample_format.channels));
/* Figure out how many frames there are available to write */
- if(playing->start + playing->used > playing->size)
+ if(playing->start + playing->used > sizeof playing->buffer)
/* The ring buffer is currently wrapped, only play up to the wrap point */
- avail_bytes = playing->size - playing->start;
+ avail_bytes = (sizeof playing->buffer) - playing->start;
else
/* The ring buffer is not wrapped, can play the lot */
avail_bytes = playing->used;
- avail_frames = avail_bytes / device_bpf;
+ avail_frames = avail_bytes / bpf;
/* Only play up to the requested amount */
if(avail_frames > frames)
avail_frames = frames;
return;
/* Play it, Sam */
written_frames = backend->play(avail_frames);
- written_bytes = written_frames * device_bpf;
+ written_bytes = written_frames * bpf;
/* written_bytes and written_frames had better both be set and correct by
* this point */
playing->start += written_bytes;
playing->played += written_frames;
/* If the pointer is at the end of the buffer (or the buffer is completely
* empty) wrap it back to the start. */
- if(!playing->used || playing->start == playing->size)
+ if(!playing->used || playing->start == (sizeof playing->buffer))
playing->start = 0;
frames -= written_frames;
return;
static void report(void) {
struct speaker_message sm;
- if(playing && playing->buffer != (void *)&playing->format) {
+ if(playing) {
memset(&sm, 0, sizeof sm);
sm.type = paused ? SM_PAUSED : SM_PLAYING;
strcpy(sm.id, playing->id);
- sm.data = playing->played / playing->format.rate;
+ sm.data = playing->played / config->sample_format.rate;
speaker_send(1, &sm, 0);
}
time(&last_report);
stdin_slot = addfd(0, POLLIN);
/* Try to read sample data for the currently playing track if there is
* buffer space. */
- if(playing && !playing->eof && playing->used < playing->size)
+ if(playing && !playing->eof && playing->used < (sizeof playing->buffer))
playing->slot = addfd(playing->fd, POLLIN);
else if(playing)
playing->slot = -1;
* nothing important can't be monitored. */
for(t = tracks; t; t = t->next)
if(t != playing) {
- if(!t->eof && t->used < t->size) {
+ if(!t->eof && t->used < sizeof t->buffer) {
t->slot = addfd(t->fd, POLLIN | POLLHUP);
} else
t->slot = -1;
log_default = &log_syslog;
}
if(config_read()) fatal(0, "cannot read configuration");
+ bpf = bytes_per_frame(&config->sample_format);
/* ignore SIGPIPE */
signal(SIGPIPE, SIG_IGN);
/* reap kids */
# define MACHINE_AO_FMT AO_FMT_LITTLE
#endif
-/** @brief How many seconds of input to buffer
- *
- * While any given connection has this much audio buffered, no more reads will
- * be issued for that connection. The decoder will have to wait.
- */
-#define BUFFER_SECONDS 5
-
/** @brief Minimum number of frames to try to play at once
*
* The main loop will only attempt to play any audio when this many
* of these but rearranging the queue can cause there to be more.
*/
struct track {
- struct track *next; /* next track */
+ /** @brief Next track */
+ struct track *next;
+
+ /** @brief Input file descriptor */
int fd; /* input FD */
- char id[24]; /* ID */
- size_t start, used; /* start + bytes used */
- int eof; /* input is at EOF */
- int got_format; /* got format yet? */
- ao_sample_format format; /* sample format */
- unsigned long long played; /* number of frames played */
- char *buffer; /* sample buffer */
- size_t size; /* sample buffer size */
- int slot; /* poll array slot */
+
+ /** @brief Track ID */
+ char id[24];
+
+ /** @brief Start position of data in buffer */
+ size_t start;
+
+ /** @brief Number of bytes of data in buffer */
+ size_t used;
+
+ /** @brief Set @c fd is at EOF */
+ int eof;
+
+ /** @brief Total number of frames played */
+ unsigned long long played;
+
+ /** @brief Slot in @ref fds */
+ int slot;
+
+ /** @brief Input buffer
+ *
+ * 1Mbyte is enough for nearly 6s of 44100Hz 16-bit stereo
+ */
+ char buffer[1048576];
};
/** @brief Structure of a backend */
/** @brief Flags
*
- * Possible values
- * - @ref FIXED_FORMAT
+ * This field is currently not used and must be 0.
*/
unsigned flags;
-/** @brief Lock to configured sample format */
-#define FIXED_FORMAT 0x0001
/** @brief Initialization
*
* If it is @ref device_closed then the device should be opened with
* the right sample format.
*
- * If the @ref FIXED_FORMAT flag is not set then @ref device_format
- * must be set on success.
- *
* Some devices are effectively always open and have no error state,
* in which case this callback can be NULL. In this case @ref
* FIXED_FORMAT must be set. Note that @ref device_state still
};
extern enum device_states device_state;
-extern ao_sample_format device_format;
extern struct track *tracks;
extern struct track *playing;
extern struct pollfd fds[NFDS];
extern int fdno;
-extern size_t device_bpf;
+extern size_t bpf;
extern int idled;
int addfd(int fd, int events);
-int formats_equal(const ao_sample_format *a,
- const ao_sample_format *b);
void abandon(void);
#endif /* SPEAKER_H */