chiark / gitweb /
server/gstdecode.c: New program, like `disorder-decode'.
authorMark Wooding <mdw@distorted.org.uk>
Thu, 2 May 2013 03:02:08 +0000 (04:02 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 25 May 2013 13:24:46 +0000 (14:24 +0100)
This uses GStreamer for the decoding, so it should be able to decode
more or less anything you can throw at it, assuming you have the
necessary plugins lying about.

More importantly (from my point of view) it does ReplayGain (optionally,
but by default).  This uses the per-album setting by default, but this
can be overridden.

.gitignore
configure.ac
debian/control
debian/rules
server/Makefile.am
server/gstdecode.c [new file with mode: 0644]

index 48dbf0ac339351f7a3dddc54b915b54753a00fe9..5a35943b58cc48736eeeebaa4c18405312092d67 100644 (file)
@@ -212,3 +212,5 @@ libtests/t-timeval
 /GRTAGS
 /GSYMS
 /GTAGS
+debian/disorder-gstdecode
+server/disorder-gstdecode
index e47aca290efb7473bc1786087a980560ccd1b7c2..e73ea47bbcacaf599d1e40fef78bf8d14f9ea8f7 100644 (file)
@@ -33,6 +33,7 @@ want_gtk=yes
 want_python=yes
 want_tests=yes
 want_server=yes
+want_gstdecode=whatever
 want_cgi=yes
 
 # APIs we want
@@ -242,9 +243,14 @@ AC_ARG_WITH([python],
            [AS_HELP_STRING([--without-python],
                            [do not build Python support])],
            [want_python=$withval])
+AC_ARG_WITH([gstdecode],
+            [AS_HELP_STRING([--with-gstdecode],
+                           [require GStreamer-based decoder])],
+            [want_gstdecode=$withval])
 
 if test $want_server = no; then
   want_cgi=no
+  want_gstdecode=no
 fi
 
 #
@@ -430,6 +436,38 @@ fi
 AC_SUBST([finkdir])
 AC_SUBST([finkbindir])
 
+# Checks for packages.
+case $want_gstdecode in
+  yes | whatever)
+    PKG_CHECK_MODULES([GSTREAMER], [gstreamer-0.10],
+        [have_gstreamer=yes], [have_gstreamer=no])
+    PKG_CHECK_MODULES([GSTREAMER_PLUGINS_BASE],
+        [gstreamer-plugins-base-0.10],
+        [have_gst_plugins_base=yes], [have_gst_plugins_base=no])
+    ;;
+esac
+case $want_gstdecode,$have_gstreamer,$have_gst_plugins_base in
+  whatever,yes,yes | yes,yes,yes) want_gstdecode=yes ;;
+  whatever,*) want_gstdecode=no ;;
+  yes,*)
+    case $have_gstreamer in
+      no) missing_libraries="$missing_libraries gstreamer-0.10" ;;
+    esac
+    case $have_gst_plugins_base in
+      no) missing_libraries="$missing_libraries gstreamer-plugins-base-0.10" ;;
+    esac
+esac
+
+mdw_SAVE_CFLAGS=$CFLAGS
+mdw_SAVE_LIBS=$LIBS
+CFLAGS="$CFLAGS $GSTREAMER_CFLAGS $GSTREAMER_PLUGINS_BASE_CFLAGS"
+LIBS="$LIBS \
+        $GSTREAMER_LIBS $GSTREAMER_PLUGINS_BASE_LIBS
+        -lgstaudio-0.10 -lgstapp-0.10"
+AC_CHECK_FUNCS([gst_audio_info_from_caps])
+CFLAGS=$mdw_SAVE_CFLAGS
+LIBS=$mdw_SAVE_LIBS
+
 # Checks for libraries.
 # We save up a list of missing libraries that we can't do without
 # and report them all at once.
@@ -699,6 +737,7 @@ if test $want_gtk = yes; then
   AC_DEFINE([WITH_GTK], [1], [define if using GTK+])
 fi
 AM_CONDITIONAL([GTK], [test x$want_gtk = xyes])
+AM_CONDITIONAL([GSTDECODE], [test x$want_gstdecode = xyes])
 
 if test "x$GCC" = xyes; then
   # We need LLONG_MAX and annoyingly GCC doesn't always give it to us
index 0c183d31ca3f684b16e541a58233ff5e2ffb3fe2..37eb7c36a1745e6e3fcc96c825b1fb43757d387a 100644 (file)
@@ -3,7 +3,7 @@ Maintainer: Richard Kettlewell <rjk@greenend.org.uk>
 Section: sound
 Priority: optional
 Standards-Version: 3.8.1.0
-Build-Depends: libgc6-dev | libgc-dev, libgcrypt-dev, libdb4.3-dev | libdb4.5-dev | libdb4.7-dev | libdb4.8-dev, libpcre3-dev, libvorbis-dev, libmad0-dev, libasound2-dev, libao-dev, python, libflac-dev, libgtk2.0-dev (>= 2.12.12)
+Build-Depends: libgc6-dev | libgc-dev, libgcrypt-dev, libdb4.3-dev | libdb4.5-dev | libdb4.7-dev | libdb4.8-dev, libpcre3-dev, libvorbis-dev, libmad0-dev, libasound2-dev, libao-dev, python, libflac-dev, libgtk2.0-dev (>= 2.12.12), pkg-config, libgstreamer0.10-dev, libgstreamer-plugins-base0.10-dev
 Vcs-Git: https://code.google.com/p/disorder/
 Homepage: http://www.greenend.org.uk/rjk/disorder/
 
@@ -53,6 +53,22 @@ Description: Network play client for DisOrder
  This package contains the RTP player.  It can play audio streams generated
  by the server, received over a LAN.
 
+Package: disorder-gstdecode
+Architecture: any
+Section: sound
+Priority: extra
+Depends: ${shlibs:Depends}
+Enhances: disorder-server
+Description: GStreamer-based audio decoder for DisOrder
+ DisOrder is a software jukebox.  It can play OGG, MP3, WAV and FLAC files,
+ and other formats with suitable configuration.  It is capable of playing
+ either via a locally attached sound device or over the network using
+ an RTP stream.
+ .
+ This package contains a GStreamer-based audio decoder.  This can (with
+ appropriate GStreamer plugins) decode different kinds of audio files, and
+ also apply ReplayGain to the decoded audio.
+
 Package: disobedience
 Architecture: any
 Section: sound
index 481b2aefd9497f0ab438af77e0e059ae5d0c7ab8..c2c2df6d6c75330f6e249c78e214ad4baac11f9f 100755 (executable)
@@ -174,6 +174,7 @@ pkg-disorder-server: build
        rm -f debian/disorder-server/usr/share/man/man5/disorder_config.5
        rm -f debian/disorder-server/usr/share/man/man5/disorder_preferences.5
        rm -f debian/disorder-server/usr/share/man/man5/disorder_protocol.5
+       rm -f debian/disorder-server/usr/sbin/disorder-gstdecode
        find debian/disorder-server -name '*.la' -print0 | xargs -r0 rm -f
        find debian/disorder-server -name '*.so.0' -print0 | xargs -r0 rm -f
        @for f in debian/disorder-server/usr/lib/disorder/*.so.0.0.0; do \
@@ -256,6 +257,40 @@ pkg-disorder-playrtp: build
        chmod -R g-ws debian/disorder-playrtp
        dpkg --build debian/disorder-playrtp ..
 
+pkg-disorder-gstdecode: build
+       rm -rf debian/disorder-gstdecode
+       $(MKDIR) debian/disorder-gstdecode
+       $(MKDIR) debian/disorder-gstdecode/DEBIAN
+       $(MKDIR) debian/disorder-gstdecode/usr/share/doc/disorder-gstdecode
+       $(INSTALL_DATA) debian/copyright \
+               debian/disorder-gstdecode/usr/share/doc/disorder-gstdecode/copyright
+       $(INSTALL_DATA) debian/changelog \
+               debian/disorder-gstdecode/usr/share/doc/disorder-gstdecode/changelog
+       gzip -9 debian/disorder-gstdecode/usr/share/doc/disorder-gstdecode/changelog*
+       @for f in preinst postinst prerm postrm conffiles templates config; do\
+         if test -e debian/$$f.disorder-gstdecode; then\
+           echo $(INSTALL_SCRIPT) debian/$$f.disorder-gstdecode debian/disorder-gstdecode/DEBIAN/$$f; \
+           $(INSTALL_SCRIPT) debian/$$f.disorder-gstdecode debian/disorder-gstdecode/DEBIAN/$$f; \
+         fi;\
+       done
+       $(MKDIR) debian/disorder-gstdecode/usr/sbin
+       : $(MKDIR) debian/disorder-gstdecode/usr/share/man/man1
+       $(INSTALL_PROGRAM) server/disorder-gstdecode \
+               debian/disorder-gstdecode/usr/sbin/disorder-gstdecode
+       strip --remove-section=.comment debian/disorder-gstdecode/usr/sbin/disorder-gstdecode
+       dpkg-shlibdeps -Tdebian/substvars.disorder-gstdecode \
+               debian/disorder-gstdecode/usr/sbin/*
+       $(INSTALL_DATA) CHANGES.html debian/disorder-gstdecode/usr/share/doc/disorder-gstdecode/CHANGES.html
+       : gzip -9f debian/disorder-gstdecode/usr/share/man/man*/*
+       cd debian/disorder-gstdecode && \
+               find -name DEBIAN -prune -o -type f -print \
+                       | sed 's/^\.\///' \
+                       | xargs md5sum > DEBIAN/md5sums
+       dpkg-gencontrol -isp -pdisorder-gstdecode -Pdebian/disorder-gstdecode -Tdebian/substvars.disorder-gstdecode
+       chown -R root:root debian/disorder-gstdecode
+       chmod -R g-ws debian/disorder-gstdecode
+       dpkg --build debian/disorder-gstdecode ..
+
 pkg-disobedience: build
        rm -rf debian/disobedience
        $(MKDIR) debian/disobedience
@@ -321,7 +356,7 @@ source-check: source
 
 binary: binary-arch binary-indep
 binary-arch: pkg-disorder pkg-disorder-server pkg-disorder-playrtp \
-            pkg-disobedience
+            pkg-disorder-gstdecode pkg-disobedience
 binary-indep: 
 
 clean:
index c6febdca6370c73125caebf1ca7cd754656c6f24..df6f56036131a44f4ce6031510494004c2607a6e 100644 (file)
@@ -22,6 +22,7 @@ sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \
 noinst_PROGRAMS=trackname endian
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
+AM_CFLAGS=
 
 disorderd_SOURCES=disorderd.c api.c api-server.c daemonize.c play.c    \
        server.c server-queue.c queue-ops.c state.c plugin.c            \
@@ -50,6 +51,16 @@ disorder_decode_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBMAD) $(LIBVORBISFILE) $(LIBFLAC)
 disorder_decode_DEPENDENCIES=../lib/libdisorder.a
 
+if GSTDECODE
+AM_CFLAGS+=$(GSTREAMER_CFLAGS)
+sbin_PROGRAMS+=disorder-gstdecode
+disorder_gstdecode_SOURCES=gstdecode.c disorder-server.h
+disorder_gstdecode_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
+       $(GSTREAMER_PLUGINS_BASE_LIBS) -lgstaudio-0.10 -lgstapp-0.10 \
+       $(GSTREAMER_LIBS)
+disorder_gstdecode_DEPENDENCIES=../lib/libdisorder.a
+endif
+
 disorder_normalize_SOURCES=normalize.c disorder-server.h
 disorder_normalize_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(LIBSAMPLERATE)
diff --git a/server/gstdecode.c b/server/gstdecode.c
new file mode 100644 (file)
index 0000000..02f349b
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2013 Mark Wooding
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+/** @file server/gstdecode.c
+ * @brief Decode compressed audio files, and apply ReplayGain.
+ */
+
+#include "disorder-server.h"
+
+#include "speaker-protocol.h"
+
+/* Ugh.  It turns out that libxml tries to define a function called
+ * `attribute', and it's included by GStreamer for some unimaginable reason.
+ * So undefine it here.  We'll want GCC attributes for special effects, but
+ * can take care of ourselves.
+ */
+#undef attribute
+
+#include <glib.h>
+#include <gst/gst.h>
+#include <gst/app/gstappsink.h>
+#include <gst/audio/audio.h>
+
+/* The only application we have for `attribute' is declaring function
+ * arguments as being unused, because we have a lot of callback functions
+ * which are meant to comply with an externally defined interface.
+ */
+#ifdef __GNUC__
+#  define UNUSED __attribute__((unused))
+#endif
+
+#define END ((void *)0)
+#define N(v) (sizeof(v)/sizeof(*(v)))
+
+static FILE *fp;
+static const char *file;
+static GstAppSink *appsink;
+static GstElement *pipeline;
+static GMainLoop *loop;
+
+#define MODES(_) _("off", OFF) _("track", TRACK) _("album", ALBUM)
+enum {
+#define DEFENUM(name, tag) tag,
+  MODES(DEFENUM)
+#undef DEFENUM
+  NMODES
+};
+static const char *const modes[] = {
+#define DEFNAME(name, tag) name,
+  MODES(DEFNAME)
+#undef DEFNAME
+  0
+};
+static int mode = ALBUM;
+
+static struct stream_header hdr;
+
+/* Report the pads of an element ELT, as iterated by IT; WHAT is an adjective
+ * phrase describing the pads for use in the output.
+ */
+static void report_element_pads(const char *what, GstElement *elt,
+                                GstIterator *it)
+{
+  gchar *cs;
+  gpointer pad;
+
+  for(;;) {
+    switch(gst_iterator_next(it, &pad)) {
+    case GST_ITERATOR_DONE:
+      goto done;
+    case GST_ITERATOR_OK:
+      cs = gst_caps_to_string(gst_pad_get_caps(pad));
+      disorder_error(0, "  `%s' %s pad: %s", GST_OBJECT_NAME(elt), what, cs);
+      g_free(cs);
+      g_object_unref(pad);
+      break;
+    case GST_ITERATOR_RESYNC:
+      gst_iterator_resync(it);
+      break;
+    case GST_ITERATOR_ERROR:
+      disorder_error(0, "<failed to enumerate `%s' %s pads>",
+                     GST_OBJECT_NAME(elt), what);
+      goto done;
+    }
+  }
+
+done:
+  gst_iterator_free(it);
+}
+
+/* Link together two elements; fail with an approximately useful error
+ * message if it didn't work.
+ */
+static void link_elements(GstElement *left, GstElement *right)
+{
+  /* Try to link things together. */
+  if(gst_element_link(left, right)) return;
+
+  /* If this didn't work, it's probably for some really hairy reason, so
+   * provide a bunch of debugging information.
+   */
+  disorder_error(0, "failed to link GStreamer elements `%s' and `%s'",
+                 GST_OBJECT_NAME(left), GST_OBJECT_NAME(right));
+  report_element_pads("source", left, gst_element_iterate_src_pads(left));
+  report_element_pads("source", right, gst_element_iterate_sink_pads(right));
+  disorder_fatal(0, "can't decode `%s'", file);
+}
+
+/* The `decoderbin' element (DECODE) has deigned to announce a new PAD.
+ * Maybe we should attach the tag end of our pipeline (starting with the
+ * element U) to it.
+ */
+static void decoder_pad_arrived(GstElement *decode, GstPad *pad, gpointer u)
+{
+  GstElement *tail = u;
+  GstCaps *caps = gst_pad_get_caps(pad);
+  GstStructure *s;
+  guint i, n;
+  const gchar *name;
+
+  /* The input file could be more or less anything, so this could be any kind
+   * of pad.  We're only interested if it's audio, so let's go check.
+   */
+  for(i = 0, n = gst_caps_get_size(caps); i < n; i++) {
+    s = gst_caps_get_structure(caps, i);
+    name = gst_structure_get_name(s);
+    if(strncmp(name, "audio/x-raw-", 12) == 0) goto match;
+  }
+  return;
+
+match:
+  /* Yes, it's audio.  Link the two elements together. */
+  link_elements(decode, tail);
+
+  /* If requested using the environemnt variable `GST_DEBUG_DUMP_DOT_DIR',
+   * write a dump of the now-completed pipeline.
+   */
+  GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipeline),
+                            GST_DEBUG_GRAPH_SHOW_ALL,
+                            "disorder-gstdecode");
+}
+
+/* Prepare the GStreamer pipeline, ready to decode the given FILE.  This sets
+ * up the variables `appsink' and `pipeline'.
+ */
+static void prepare_pipeline(void)
+{
+  GstElement *source = gst_element_factory_make("filesrc", "file");
+  GstElement *decode = gst_element_factory_make("decodebin", "decode");
+  GstElement *convert = gst_element_factory_make("audioconvert", "convert");
+  GstElement *sink = gst_element_factory_make("appsink", "sink");
+  GstElement *tail = sink;
+  GstElement *gain;
+  GstCaps *caps = gst_caps_new_empty();
+  GstCaps *c;
+  static const int widths[] = { 8, 16 };
+  size_t i;
+
+  /* Set up the global variables. */
+  pipeline = gst_pipeline_new("pipe");
+  appsink = GST_APP_SINK(sink);
+
+  /* Configure the various simple elements. */
+  g_object_set(source, "location", file, END);
+  g_object_set(sink, "sync", FALSE, END);
+
+  /* Set up the sink's capabilities. */
+  for(i = 0; i < N(widths); i++) {
+    c = gst_caps_new_simple("audio/x-raw-int",
+                            "width", G_TYPE_INT, widths[i],
+                            "depth", G_TYPE_INT, widths[i],
+                            "channels", GST_TYPE_INT_RANGE, 1, 2,
+                            "signed", G_TYPE_BOOLEAN, TRUE,
+                            "rate", GST_TYPE_INT_RANGE, 100, 1000000,
+                            END);
+    gst_caps_append(caps, c);
+  }
+  gst_app_sink_set_caps(appsink, caps);
+
+  /* Add the various elements into the pipeline.  We'll stitch them together
+   * in pieces, because the pipeline is somewhat dynamic.
+   */
+  gst_bin_add_many(GST_BIN(pipeline), source, decode, convert, sink, END);
+
+  /* Link an audio conversion stage onto the front.  The rest of DisOrder
+   * doesn't handle much of the full panoply of exciting audio formats.
+   */
+  link_elements(convert, tail); tail = convert;
+
+  /* If we're meant to do ReplayGain then insert it into the pipeline before
+   * the converter.
+   */
+  if(mode != OFF) {
+    gain = gst_element_factory_make("rgvolume", "gain");
+    g_object_set(gain, "album-mode", mode == ALBUM, END);
+    gst_bin_add(GST_BIN(pipeline), gain);
+    link_elements(gain, tail); tail = gain;
+  }
+
+  /* Link the source and the decoder together.  The `decodebin' is annoying
+   * and doesn't have any source pads yet, so the best we can do is make two
+   * halves of the chain, and add a hook to stitch them together later.
+   */
+  link_elements(source, decode);
+  g_signal_connect(decode, "pad-added",
+                   G_CALLBACK(decoder_pad_arrived), tail);
+}
+
+/* Respond to a message from the BUS.  The only thing we need worry about
+ * here is errors from the pipeline.
+ */
+static void bus_message(GstBus UNUSED *bus, GstMessage *msg,
+                        gpointer UNUSED u)
+{
+  switch(msg->type) {
+  case GST_MESSAGE_ERROR:
+    disorder_fatal(0, "%s",
+                   gst_structure_get_string(msg->structure, "debug"));
+  default:
+    break;
+  }
+}
+
+/* End of stream.  Stop polling the main loop. */
+static void cb_eos(GstAppSink UNUSED *sink, gpointer UNUSED u)
+  { g_main_loop_quit(loop); }
+
+/* Preroll buffers are prepared when the pipeline moves to the `paused'
+ * state, so that they're ready for immediate playback.  Conveniently, they
+ * also carry format information, which is what we want here.  Stash the
+ * sample format information in the `stream_header' structure ready for
+ * actual buffers of interesting data.
+ */
+static GstFlowReturn cb_preroll(GstAppSink *sink, gpointer UNUSED u)
+{
+  GstBuffer *buf = gst_app_sink_pull_preroll(sink);
+  GstCaps *caps = GST_BUFFER_CAPS(buf);
+
+#ifdef HAVE_GST_AUDIO_INFO_FROM_CAPS
+
+  /* Parse the audio format information out of the caps.  There's a handy
+   * function to do this in later versions of gst-plugins-base, so use that
+   * if it's available.  Once we no longer care about supporting such old
+   * versions we can delete the version which does the job the hard way.
+   */
+
+  GstAudioInfo ai;
+
+  if(!gst_audio_info_from_caps(&ai, caps))
+    disorder_fatal(0, "can't decode `%s': failed to parse audio info", file);
+  hdr.rate = ai.rate;
+  hdr.channels = ai.channels;
+  hdr.bits = ai.finfo->width;
+  hdr.endian = ai.finfo->endianness == G_BIG_ENDIAN ?
+    ENDIAN_BIG : ENDIAN_LITTLE;
+
+#else
+
+  GstStructure *s;
+  const char *ty;
+  gint rate, channels, bits, endian;
+  gboolean signedp;
+
+  /* Make sure that the caps is basically the right shape. */
+  if(!GST_CAPS_IS_SIMPLE(caps)) disorder_fatal(0, "expected simple caps");
+  s = gst_caps_get_structure(caps, 0);
+  ty = gst_structure_get_name(s);
+  if(strcmp(ty, "audio/x-raw-int") != 0)
+    disorder_fatal(0, "unexpected content type `%s'", ty);
+
+  /* Extract fields from the structure. */
+  if(!gst_structure_get(s,
+                        "rate", G_TYPE_INT, &rate,
+                        "channels", G_TYPE_INT, &channels,
+                        "width", G_TYPE_INT, &bits,
+                        "endianness", G_TYPE_INT, &endian,
+                        "signed", G_TYPE_BOOLEAN, &signedp,
+                        END))
+    disorder_fatal(0, "can't decode `%s': failed to parse audio caps", file);
+  hdr.rate = rate; hdr.channels = channels; hdr.bits = bits;
+  hdr.endian = endian == G_BIG_ENDIAN ? ENDIAN_BIG : ENDIAN_LITTLE;
+
+#endif
+
+  gst_buffer_unref(buf);
+  return GST_FLOW_OK;
+}
+
+/* A new buffer of sample data has arrived, so we should pass it on with
+ * appropriate framing.
+ */
+static GstFlowReturn cb_buffer(GstAppSink *sink, gpointer UNUSED u)
+{
+  GstBuffer *buf = gst_app_sink_pull_buffer(sink);
+
+  /* Make sure we actually have a grip on the sample format here. */
+  if(!hdr.rate) disorder_fatal(0, "format unset");
+
+  /* Write out a frame of audio data. */
+  hdr.nbytes = GST_BUFFER_SIZE(buf);
+  if(fwrite(&hdr, sizeof(hdr), 1, fp) != 1 ||
+     fwrite(GST_BUFFER_DATA(buf), 1, hdr.nbytes, fp) != hdr.nbytes)
+    disorder_fatal(errno, "output");
+
+  /* And we're done. */
+  gst_buffer_unref(buf);
+  return GST_FLOW_OK;
+}
+
+static GstAppSinkCallbacks callbacks = {
+  .eos = cb_eos,
+  .new_preroll = cb_preroll,
+  .new_buffer = cb_buffer
+};
+
+/* Decode the audio file.  We're already set up for everything. */
+static void decode(void)
+{
+  GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
+
+  /* Set up the message bus and main loop. */
+  gst_bus_add_signal_watch(bus);
+  loop = g_main_loop_new(0, FALSE);
+  g_signal_connect(bus, "message", G_CALLBACK(bus_message), 0);
+
+  /* Tell the sink to call us when interesting things happen. */
+  gst_app_sink_set_callbacks(appsink, &callbacks, 0, 0);
+
+  /* Set the ball rolling. */
+  gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING);
+
+  /* And wait for the miracle to come. */
+  g_main_loop_run(loop);
+
+  /* Shut down the pipeline.  This isn't strictly necessary, since we're
+   * about to exit very soon, but it's kind of polite.
+   */
+  gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_NULL);
+}
+
+static int getenum(const char *what, const char *s, const char *const *tags)
+{
+  int i;
+
+  for(i = 0; tags[i]; i++)
+    if(strcmp(s, tags[i]) == 0) return i;
+  disorder_fatal(0, "unknown %s `%s'", what, s);
+}
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "replay-gain", required_argument, 0, 'r' },
+  { 0, 0, 0, 0 }
+};
+
+static void help(void)
+{
+  xprintf("Usage:\n"
+          "  disorder-gstdecode [OPTIONS] PATH\n"
+          "Options:\n"
+          "  --help, -h                 Display usage message\n"
+          "  --version, -V              Display version number\n"
+          "  --replay-gain MODE, -r MODE  MODE is `off', `track' or `album'\n"
+          "\n"
+          "Alternative audio decoder for DisOrder.  Only intended to be\n"
+          "used by speaker process, not for normal users.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* Main program. */
+int main(int argc, char *argv[])
+{
+  int n;
+  const char *e;
+
+  /* Initial setup. */
+  set_progname(argv);
+  if(!setlocale(LC_CTYPE, "")) disorder_fatal(errno, "calling setlocale");
+
+  /* Parse command line. */
+  while((n = getopt_long(argc, argv, "hVr:", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version("disorder-gstdecode");
+    case 'r': mode = getenum("ReplayGain mode", optarg, modes); break;
+    default: disorder_fatal(0, "invalid option");
+    }
+  }
+  if(optind >= argc) disorder_fatal(0, "missing filename");
+  file = argv[optind++];
+  if(optind < argc) disorder_fatal(0, "excess arguments");
+
+  /* Set up the GStreamer machinery. */
+  gst_init(0, 0);
+  prepare_pipeline();
+
+  /* Set up the output file. */
+  if((e = getenv("DISORDER_RAW_FD")) != 0) {
+    if((fp = fdopen(atoi(e), "wb")) == 0) disorder_fatal(errno, "fdopen");
+  } else
+    fp = stdout;
+
+  /* Let's go. */
+  decode();
+
+  /* And now we're done. */
+  xfclose(fp);
+  return (0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:77
+indent-tabs-mode:nil
+End:
+*/