From ce6c36be6c2df99afd01a7a602debb321322e113 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Fri, 28 Sep 2007 19:56:29 +0100 Subject: [PATCH] wav file support for disorder-decode Organization: Straylight/Edgeware From: rjk@greenend.org.uk <> --- CHANGES | 3 + debian/etc.disorder.config | 2 +- doc/disorder-decode.8.in | 9 +- doc/disorder_config.5.in | 7 +- examples/config.sample.in | 2 +- lib/Makefile.am | 1 + lib/wav.c | 248 +++++++++++++++++++++++++++++++++++++ lib/wav.h | 80 ++++++++++++ plugins/Makefile.am | 2 +- plugins/tracklength.c | 137 +++----------------- server/decode.c | 37 +++++- 11 files changed, 396 insertions(+), 132 deletions(-) create mode 100644 lib/wav.c create mode 100644 lib/wav.h diff --git a/CHANGES b/CHANGES index ab023a3..01af4e7 100644 --- a/CHANGES +++ b/CHANGES @@ -38,6 +38,9 @@ crash when random play was enabled has been fixed. 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 diff --git a/debian/etc.disorder.config b/debian/etc.disorder.config index e1eec1d..1301d27 100644 --- a/debian/etc.disorder.config +++ b/debian/etc.disorder.config @@ -1,7 +1,7 @@ # 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 diff --git a/doc/disorder-decode.8.in b/doc/disorder-decode.8.in index 82f3f8d..522dad2 100644 --- a/doc/disorder-decode.8.in +++ b/doc/disorder-decode.8.in @@ -24,10 +24,17 @@ disorder-decode \- DisOrder audio decoder .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) diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index dd9c580..4cde04f 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -289,7 +289,12 @@ The command is expected to know how to open its own sound device. .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 diff --git a/examples/config.sample.in b/examples/config.sample.in index a3ccec3..86e97bf 100644 --- a/examples/config.sample.in +++ b/examples/config.sample.in @@ -1,7 +1,7 @@ # 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. diff --git a/lib/Makefile.am b/lib/Makefile.am index 0101aa2..0216f05 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -66,6 +66,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ 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 diff --git a/lib/wav.c b/lib/wav.c new file mode 100644 index 0000000..4197068 --- /dev/null +++ b/lib/wav.c @@ -0,0 +1,248 @@ +/* + * 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 +#include "types.h" + +#include +#include +#include +#include + +#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: +*/ diff --git a/lib/wav.h b/lib/wav.h new file mode 100644 index 0000000..93bf770 --- /dev/null +++ b/lib/wav.h @@ -0,0 +1,80 @@ +/* + * 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: +*/ diff --git a/plugins/Makefile.am b/plugins/Makefile.am index 0f6ca75..cbd5b74 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -25,7 +25,7 @@ AM_CPPFLAGS=-I${top_srcdir}/lib 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) diff --git a/plugins/tracklength.c b/plugins/tracklength.c index 100b218..c851f18 100644 --- a/plugins/tracklength.c +++ b/plugins/tracklength.c @@ -36,6 +36,7 @@ #include #include "madshim.h" +#include "wav.h" static void *mmap_file(const char *path, size_t *lengthp) { int fd; @@ -95,131 +96,21 @@ error: } 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; } diff --git a/server/decode.c b/server/decode.c index f462f58..d1be348 100644 --- a/server/decode.c +++ b/server/decode.c @@ -38,6 +38,7 @@ #include "log.h" #include "syscalls.h" #include "defs.h" +#include "wav.h" #include "speaker-protocol.h" /** @brief Encoding lookup table type */ @@ -91,13 +92,14 @@ static inline void output_16(uint16_t n) { 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); @@ -184,7 +186,8 @@ static enum mad_flow mp3_output(void attribute((unused)) *data, 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--) @@ -260,18 +263,44 @@ static void decode_ogg(void) { 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 } }; -- [mdw]