chiark / gitweb /
wav file support for disorder-decode
authorrjk@greenend.org.uk <>
Fri, 28 Sep 2007 18:56:29 +0000 (19:56 +0100)
committerrjk@greenend.org.uk <>
Fri, 28 Sep 2007 18:56:29 +0000 (19:56 +0100)
CHANGES
debian/etc.disorder.config
doc/disorder-decode.8.in
doc/disorder_config.5.in
examples/config.sample.in
lib/Makefile.am
lib/wav.c [new file with mode: 0644]
lib/wav.h [new file with mode: 0644]
plugins/Makefile.am
plugins/tracklength.c
server/decode.c

diff --git a/CHANGES b/CHANGES
index ab023a3857c647ff0c1bda95a72f465c2e74ce59..01af4e7e33405ac84db88443dc04c76eb0788058 100644 (file)
--- 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
index e1eec1dac65f469f4961c14a070a0ecd39290f9a..1301d2730768df3e98f922ef28e245da0e83d3be 100644 (file)
@@ -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
index 82f3f8d2bfc4478d89721a2e3680f74fef57441e..522dad26e6c238cce452209cc403a2f4448225c9 100644 (file)
@@ -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)
index dd9c580e2fa918b259c1476d7ea7dfa6157fcb18..4cde04fcf7bd388c7453a47c6cdca0580e6f50c7 100644 (file)
@@ -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
index a3ccec32808b85d194c2267c80df6beb7fd6952e..86e97bf399f7f272a8652bb1d40b900c98b91804 100644 (file)
@@ -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.
index 0101aa29a6acced59fca5d245794c66bc96f21c5..0216f05d7d3515389241398e13d59e4299b75dc2 100644 (file)
@@ -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 (file)
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 <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:
+*/
diff --git a/lib/wav.h b/lib/wav.h
new file mode 100644 (file)
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:
+*/
index 0f6ca75e72247fc1b9fa54694cce7a8f1ef309e2..cbd5b749d8e0cecb456b620676c986a6cc65caea 100644 (file)
@@ -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)
 
index 100b218daf201e67dfb70fbe56d733d7dbaa56d8..c851f18299f72dd385dd02725b2af491d29a5279 100644 (file)
@@ -36,6 +36,7 @@
 #include <disorder.h>
 
 #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;
 }
 
index f462f58c3c6a15dccda69423430954925eba28ad..d1be3483d9e05de56c5f784f213de5b9e4fb3939 100644 (file)
@@ -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 }
 };