A new configuration option 'queue_pad' allows the number of random
tracks kept on the queue to be controlled.
+There is a new utility disorder-decode which can decode several common
+audio file formats. The example config file uses it.
+
** Network Play
DisOrder can broadcast audio over a network, allowing it to be played on
# player programs
player *.mp3 execraw disorder-decode
player *.ogg execraw disorder-decode
-player *.wav shell play
+player *.wav execraw disorder-decode
# don't leave a gap between tracks
gap 0
.I PATH
.SH DESCRIPTION
.B disorder-decode
-converts MP3 and OGG files into DisOrders "raw" format. It is
+converts MP3, OGG and WAV files into DisOrders "raw" format. It is
therefore suitable for use as an
.B execraw
player.
+.PP
+It is not intended to be used from the command line.
+.SH LIMITATIONS
+OGG files with multiple bitstreams are not supported.
+.PP
+WAV files with sample sizes that are not a multiple of 8 bits, or any
+kind of compression, are not supported.
.SH "SEE ALSO"
.BR disorderd (8),
.BR disorder_config (5)
.TP
.B execraw \fICOMMAND\fR \fIARGS\fR...
Identical to the \fBexec\fR except that the player is expected to use the
-DisOrder raw player protocol (see notes below).
+DisOrder raw player protocol.
+.BR disorder-decode (8)
+can decode several common audio file formats to this format. If your favourite
+format is not supported, but you have a player which uses libao, there is also
+a libao driver which supports this format; see below for more information about
+this.
.TP
.B shell \fR[\fISHELL\fR] \fICOMMAND\fR
The command is executed using the shell. If \fISHELL\fR is specified then that
# player programs
player *.mp3 execraw disorder-decode
player *.ogg execraw disorder-decode
-player *.wav shell --wait-for-device play
+player *.wav execraw disorder-decode
# use the fs module to list files under /export/mp3. The encoding
# is ISO-8859-1.
utf8.h utf8.c \
vacopy.h \
vector.c vector.h \
+ wav.h wav.c \
words.c words.h casefold.h unicodegc.h \
wstat.c wstat.h \
disorder.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 lib/wav.c
+ * @brief WAV file support
+ *
+ * This is used by the WAV file suppoort in the tracklength plugin and
+ * by disorder-decode (see @ref server/decode.c).
+ */
+
+/* Sources:
+ *
+ * http://www.technology.niagarac.on.ca/courses/comp530/WavFileFormat.html
+ * http://www.borg.com/~jglatt/tech/wave.htm
+ * http://www.borg.com/~jglatt/tech/aboutiff.htm
+ *
+ * These files consists of a header followed by chunks.
+ * Multibyte values are little-endian.
+ *
+ * 12 byte file header:
+ * offset size meaning
+ * 00 4 'RIFF'
+ * 04 4 length of rest of file
+ * 08 4 'WAVE'
+ *
+ * The length includes 'WAVE' but excludes the 1st 8 bytes.
+ *
+ * Chunk header:
+ * 00 4 chunk ID
+ * 04 4 length of rest of chunk
+ *
+ * The stated length may be odd, if so then there is an implicit padding byte
+ * appended to the chunk to make it up to an even length (someone wasn't
+ * think about 32/64-bit worlds).
+ *
+ * Also some files seem to have extra stuff at the end of chunks that nobody
+ * I know of documents. Go figure, but check the length field rather than
+ * deducing the length from the ID.
+ *
+ * Format chunk:
+ * 00 4 'fmt'
+ * 04 4 length of rest of chunk
+ * 08 2 compression (1 = none)
+ * 0a 2 number of channels
+ * 0c 4 samples/second
+ * 10 4 average bytes/second, = (samples/sec) * (bytes/sample)
+ * 14 2 bytes/sample
+ * 16 2 bits/sample point
+ *
+ * 'sample' means 'sample frame' above, i.e. a sample point for each channel.
+ *
+ * Data chunk:
+ * 00 4 'data'
+ * 04 4 length of rest of chunk
+ * 08 ... data
+ *
+ * There is only allowed to be one data chunk. Some people violate this; we
+ * shall encourage people to fix their broken WAV files by not supporting
+ * this violation and because it's easier.
+ *
+ * As to the encoding of the data:
+ *
+ * Firstly, samples up to 8 bits in size are unsigned, larger samples are
+ * signed. Madness.
+ *
+ * Secondly sample points are stored rounded up to a multiple of 8 bits in
+ * size. Marginally saner.
+ *
+ * Written as a single word (of 8, 16, 24, whatever bits) the padding to
+ * implement this happens at the right hand (least significant) end.
+ * e.g. assuming a 9 bit sample:
+ *
+ * | padded sample word |
+ * | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
+ * | 8 7 6 5 4 3 2 1 0 - - - - - - - |
+ *
+ * But this is a little-endian file format so the least significant byte is
+ * the first, which means that the padding is "between" the bits if you
+ * imagine them in their usual order:
+ *
+ * | first byte | second byte |
+ * | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 |
+ * | 0 - - - - - - - | 8 7 6 5 4 3 2 1 |
+ *
+ * Sample points are grouped into sample frames, consisting of as many
+ * samples points as their are channels. It seems that there are standard
+ * orderings of different channels.
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+
+#include "log.h"
+#include "wav.h"
+
+static inline uint16_t get16(const char *ptr) {
+ return (uint8_t)ptr[0] + 256 * (uint8_t)ptr[1];
+}
+
+static inline uint32_t get32(const char *ptr) {
+ return (uint8_t)ptr[0] + 256 * (uint8_t)ptr[1]
+ + 65536 * (uint8_t)ptr[2] + 16777216 * (uint8_t)ptr[3];
+}
+
+/** @brief Open a WAV file
+ * @return 0 on success, an errno value on error
+ */
+int wav_init(struct wavfile *f, const char *path) {
+ int err, n;
+ char header[64];
+ off_t where;
+
+ memset(f, 0, sizeof *f);
+ f->fd = -1;
+ f->data = -1;
+ if((f->fd = open(path, O_RDONLY)) < 0) goto error_errno;
+ /* Read the file header
+ *
+ * offset size meaning
+ * 00 4 'RIFF'
+ * 04 4 length of rest of file
+ * 08 4 'WAVE'
+ * */
+ if((n = pread(f->fd, header, 12, 0)) < 0) goto error_errno;
+ else if(n < 12) goto einval;
+ if(strncmp(header, "RIFF", 4) || strncmp(header + 8, "WAVE", 4))
+ goto einval;
+ f->length = 8 + get32(header + 4);
+ /* Visit all the chunks */
+ for(where = 12; where + 8 <= f->length;) {
+ /* Read the chunk header
+ *
+ * offset size meaning
+ * 00 4 chunk ID
+ * 04 4 length of rest of chunk
+ */
+ if((n = pread(f->fd, header, 8, where)) < 0) goto error_errno;
+ else if(n < 8) goto einval;
+ if(!strncmp(header,"fmt ", 4)) {
+ /* This is the format chunk
+ *
+ * offset size meaning
+ * 00 4 'fmt'
+ * 04 4 length of rest of chunk
+ * 08 2 compression (1 = none)
+ * 0a 2 number of channels
+ * 0c 4 samples/second
+ * 10 4 average bytes/second, = (samples/sec) * (bytes/sample)
+ * 14 2 bytes/sample
+ * 16 2 bits/sample point
+ * 18 ? extra undocumented rubbish
+ */
+ if(get32(header + 4) < 16) goto einval;
+ if((n = pread(f->fd, header + 8, 16, where + 8)) < 0) goto error_errno;
+ else if(n < 16) goto einval;
+ f->channels = get16(header + 0x0A);
+ f->rate = get32(header + 0x0C);
+ f->bits = get16(header + 0x16);
+ } else if(!strncmp(header, "data", 4)) {
+ /* Remember where the data chunk was and how big it is */
+ f->data = where;
+ f->datasize = get32(header + 4);
+ }
+ where += 8 + get32(header + 4);
+ }
+ /* There had better have been a format chunk */
+ if(f->rate == 0) goto einval;
+ /* There had better have been a data chunk */
+ if(f->data == -1) goto einval;
+ return 0;
+einval:
+ err = EINVAL;
+ goto error;
+error_errno:
+ err = errno;
+error:
+ wav_destroy(f);
+ return err;
+}
+
+/** @brief Close a WAV file */
+void wav_destroy(struct wavfile *f) {
+ if(f) {
+ const int save_errno = errno;
+
+ if(f->fd >= 0)
+ close(f->fd);
+ errno = save_errno;
+ }
+}
+
+/** @brief Visit all the data in a WAV file
+ * @param callback Called for successive blocks of data
+ *
+ * @p callback will only ever be passed whole frames.
+ */
+int wav_data(struct wavfile *f,
+ wav_data_callback *callback,
+ void *u) {
+ off_t left = f->datasize;
+ off_t where = f->data + 8;
+ char buffer[4096];
+ int err;
+ ssize_t n;
+ const size_t bytes_per_frame = f->channels * ((f->bits + 7) / 8);
+
+ while(left > 0) {
+ size_t want = (off_t)sizeof buffer > left ? (size_t)left : sizeof buffer;
+
+ want -= want % bytes_per_frame;
+ if((n = pread(f->fd, buffer, want, where)) < 0) return errno;
+ if((size_t)n < want) return EINVAL;
+ if((err = callback(f, buffer, n, u))) return err;
+ where += n;
+ left -= n;
+ }
+ return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
--- /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 lib/wav.h
+ * @brief WAV file support
+ */
+
+#ifndef WAV_H
+#define WAV_H
+
+/** @brief WAV file access structure */
+struct wavfile {
+ /** @brief File descriptor onto file */
+ int fd;
+
+ /** @brief File length */
+ off_t length;
+
+ /** @brief Offset of data chunk */
+ off_t data;
+
+ /** @brief Sample rate (Hz) */
+ int rate;
+
+ /** @brief Number of channels (usually 1 or 2) */
+ int channels;
+
+ /** @brief Bits per sample */
+ int bits;
+
+ /** @brief Size of data chunk in bytes */
+ off_t datasize;
+};
+
+/** @brief Sample data callback from wav_data()
+ * @param f WAV file being read
+ * @param data Pointer to sample data
+ * @param nbytes Number of bytes of data
+ * @param u As passed to wav_data()
+ * @return 0 on success or an errno value on error
+ *
+ * @p nbytes is always a multiple of the frame size and never 0.
+ */
+typedef int wav_data_callback(struct wavfile *f,
+ const char *data,
+ size_t nbytes,
+ void *u);
+
+int wav_init(struct wavfile *f, const char *path);
+void wav_destroy(struct wavfile *f);
+int wav_data(struct wavfile *f,
+ wav_data_callback *callback,
+ void *u);
+
+#endif /* WAV_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
notify_la_SOURCES=notify.c
notify_la_LDFLAGS=-module
-tracklength_la_SOURCES=tracklength.c mad.c madshim.h
+tracklength_la_SOURCES=tracklength.c mad.c madshim.h ../lib/wav.h ../lib/wav.c
tracklength_la_LDFLAGS=-module
tracklength_la_LIBADD=$(LIBVORBISFILE) $(LIBMAD)
#include <disorder.h>
#include "madshim.h"
+#include "wav.h"
static void *mmap_file(const char *path, size_t *lengthp) {
int fd;
}
static long tl_wav(const char *path) {
- size_t length;
- void *base;
- long duration = -1;
- unsigned char *ptr;
- unsigned n, m, data_bytes = 0, samples_per_second = 0;
- unsigned n_channels = 0, bits_per_sample = 0, sample_point_size;
- unsigned sample_frame_size, n_samples;
+ struct wavfile f[1];
+ int err, sample_frame_size;
+ long duration;
- /* Sources:
- *
- * http://www.technology.niagarac.on.ca/courses/comp530/WavFileFormat.html
- * http://www.borg.com/~jglatt/tech/wave.htm
- * http://www.borg.com/~jglatt/tech/aboutiff.htm
- *
- * These files consists of a header followed by chunks.
- * Multibyte values are little-endian.
- *
- * 12 byte file header:
- * offset size meaning
- * 00 4 'RIFF'
- * 04 4 length of rest of file
- * 08 4 'WAVE'
- *
- * The length includes 'WAVE' but excludes the 1st 8 bytes.
- *
- * Chunk header:
- * 00 4 chunk ID
- * 04 4 length of rest of chunk
- *
- * The stated length may be odd, if so then there is an implicit padding byte
- * appended to the chunk to make it up to an even length (someone wasn't
- * think about 32/64-bit worlds).
- *
- * Also some files seem to have extra stuff at the end of chunks that nobody
- * I know of documents. Go figure, but check the length field rather than
- * deducing the length from the ID.
- *
- * Format chunk:
- * 00 4 'fmt'
- * 04 4 length of rest of chunk
- * 08 2 compression (1 = none)
- * 0a 2 number of channels
- * 0c 4 samples/second
- * 10 4 average bytes/second, = (samples/sec) * (bytes/sample)
- * 14 2 bytes/sample
- * 16 2 bits/sample point
- *
- * 'sample' means 'sample frame' above, i.e. a sample point for each channel.
- *
- * Data chunk:
- * 00 4 'data'
- * 04 4 length of rest of chunk
- * 08 ... data
- *
- * There is only allowed to be one data chunk. Some people violate this; we
- * shall encourage people to fix their broken WAV files by not supporting
- * this violation and because it's easier.
- *
- * As to the encoding of the data:
- *
- * Firstly, samples up to 8 bits in size are unsigned, larger samples are
- * signed. Madness.
- *
- * Secondly sample points are stored rounded up to a multiple of 8 bits in
- * size. Marginally saner.
- *
- * Written as a single word (of 8, 16, 24, whatever bits) the padding to
- * implement this happens at the right hand (least significant) end.
- * e.g. assuming a 9 bit sample:
- *
- * | padded sample word |
- * | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
- * | 8 7 6 5 4 3 2 1 0 - - - - - - - |
- *
- * But this is a little-endian file format so the least significant byte is
- * the first, which means that the padding is "between" the bits if you
- * imagine them in their usual order:
- *
- * | first byte | second byte |
- * | 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 |
- * | 0 - - - - - - - | 8 7 6 5 4 3 2 1 |
- *
- * Sample points are grouped into sample frames, consisting of as many
- * samples points as their are channels. It seems that there are standard
- * orderings of different channels.
- *
- * Given all of the above all we need to do is pick up some numbers from the
- * format chunk, and the length of the data chunk, and do some arithmetic.
- */
- if(!(base = mmap_file(path, &length))) return -1;
-#define get16(p) ((p)[0] + 256 * (p)[1])
-#define get32(p) ((p)[0] + 256 * ((p)[1] + 256 * ((p)[2] + 256 * (p)[3])))
- ptr = base;
- if(length < 12) goto out;
- if(strncmp((char *)ptr, "RIFF", 4)) goto out; /* wrong type */
- n = get32(ptr + 4); /* file length */
- if(n > length - 8) goto out; /* truncated */
- ptr += 8; /* skip file header */
- if(n < 4 || strncmp((char *)ptr, "WAVE", 4)) goto out; /* wrong type */
- ptr += 4; /* skip 'WAVE' */
- n -= 4;
- while(n >= 8) {
- m = get32(ptr + 4); /* chunk length */
- if(m > n - 8) goto out; /* truncated */
- if(!strncmp((char *)ptr, "fmt ", 4)) {
- if(samples_per_second) goto out; /* duplicate format chunk! */
- n_channels = get16(ptr + 0x0a);
- samples_per_second = get32(ptr + 0x0c);
- bits_per_sample = get16(ptr + 0x16);
- if(!samples_per_second) goto out; /* bogus! */
- } else if(!strncmp((char *)ptr, "data", 4)) {
- if(data_bytes) goto out; /* multiple data chunks! */
- data_bytes = m; /* remember data size */
- }
- m += 8; /* include chunk header */
- ptr += m; /* skip chunk */
- n -= m;
+ if((err = wav_init(f, path))) {
+ disorder_error(err, "error opening %s", path);
+ return -1;
}
- sample_point_size = (bits_per_sample + 7) / 8;
- sample_frame_size = sample_point_size * n_channels;
- if(!sample_frame_size) goto out; /* bogus or overflow */
- n_samples = data_bytes / sample_frame_size;
- duration = (n_samples + samples_per_second - 1) / samples_per_second;
-out:
- munmap(base, length);
+ sample_frame_size = (f->bits + 7) / 8 * f->channels;
+ if(sample_frame_size) {
+ const long long n_samples = f->datasize / sample_frame_size;
+ duration = (n_samples + f->rate - 1) / f->rate;
+ } else
+ duration = -1;
+ wav_destroy(f);
return duration;
}
#include "log.h"
#include "syscalls.h"
#include "defs.h"
+#include "wav.h"
#include "speaker-protocol.h"
/** @brief Encoding lookup table type */
static void output_header(int rate,
int channels,
int bits,
- int nbytes) {
+ int nbytes,
+ int endian) {
struct stream_header header;
header.rate = rate;
header.bits = bits;
header.channels = channels;
- header.endian = ENDIAN_BIG;
+ header.endian = endian;
header.nbytes = nbytes;
if(fwrite(&header, sizeof header, 1, outputfp) < 1)
fatal(errno, "decoding %s: writing format header", path);
output_header(header->samplerate,
pcm->channels,
16,
- 2 * pcm->channels * pcm->length);
+ 2 * pcm->channels * pcm->length,
+ ENDIAN_BIG);
switch(pcm->channels) {
case 1:
while(n--)
fatal(0, "ov_read %s: %ld", path, n);
if(bitstream > 0)
fatal(0, "only single-bitstream ogg files are supported");
- output_header(vi->rate, vi->channels, 16/*bits*/, n);
+ output_header(vi->rate, vi->channels, 16/*bits*/, n, ENDIAN_BIG);
if(fwrite(buffer, 1, n, outputfp) < (size_t)n)
fatal(errno, "decoding %s: writing sample data", path);
}
}
+/** @brief Sample data callback used by decode_wav() */
+static int wav_write(struct wavfile attribute((unused)) *f,
+ const char *data,
+ size_t nbytes,
+ void attribute((unused)) *u) {
+ if(fwrite(data, 1, nbytes, outputfp) < nbytes)
+ fatal(errno, "decoding %s: writing sample data", path);
+ return 0;
+}
+
+/** @brief WAV file decoder */
+static void decode_wav(void) {
+ struct wavfile f[1];
+ int err;
+
+ if((err = wav_init(f, path)))
+ fatal(err, "opening %s", path);
+ if(f->bits % 8)
+ fatal(err, "%s: unsupported byte size %d", path, f->bits);
+ output_header(f->rate, f->channels, f->bits, f->datasize, ENDIAN_LITTLE);
+ if((err = wav_data(f, wav_write, 0)))
+ fatal(err, "error decoding %s", path);
+}
+
/** @brief Lookup table of decoders */
static const struct decoder decoders[] = {
{ "*.mp3", decode_mp3 },
{ "*.MP3", decode_mp3 },
{ "*.ogg", decode_ogg },
{ "*.OGG", decode_ogg },
+ { "*.wav", decode_wav },
+ { "*.WAV", decode_wav },
{ 0, 0 }
};