summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--pkgs/applications/audio/moc/default.nix93
-rw-r--r--pkgs/applications/audio/moc/moc-ffmpeg4.patch33
-rw-r--r--pkgs/applications/audio/moc/pulseaudio.patch800
3 files changed, 917 insertions, 9 deletions
diff --git a/pkgs/applications/audio/moc/default.nix b/pkgs/applications/audio/moc/default.nix
index ea83a1012eb..3ed330cc743 100644
--- a/pkgs/applications/audio/moc/default.nix
+++ b/pkgs/applications/audio/moc/default.nix
@@ -1,9 +1,35 @@
-{ stdenv, fetchurl, ncurses, pkgconfig, alsaLib, flac, libmad, speex, ffmpeg
-, libvorbis, libmpc, libsndfile, libjack2, db, libmodplug, timidity, libid3tag
-, libtool
+{ stdenv, fetchurl, pkgconfig
+, ncurses, db , popt, libtool
+# Sound sub-systems
+, alsaSupport ? true, alsaLib
+, pulseSupport ? true, libpulseaudio, autoreconfHook
+, jackSupport ? true, libjack2
+, ossSupport ? true
+# Audio formats
+, aacSupport ? true, faad2, libid3tag
+, flacSupport ? true, flac
+, midiSupport ? true, timidity
+, modplugSupport ? true, libmodplug
+, mp3Support ? true, libmad
+, musepackSupport ? true, libmpc, libmpcdec, taglib
+, vorbisSupport ? true, libvorbis
+, speexSupport ? true, speex
+, ffmpegSupport ? true, ffmpeg
+, sndfileSupport ? true, libsndfile
+, wavpackSupport ? true, wavpack
+# Misc
+, withffmpeg4 ? false, ffmpeg_4
+, curlSupport ? true, curl
+, samplerateSupport ? true, libsamplerate
+, withDebug ? false
 }:
 
-stdenv.mkDerivation rec {
+let
+  opt = stdenv.lib.optional;
+  mkFlag = c: f: if c then "--with-${f}" else "--without-${f}";
+
+in stdenv.mkDerivation rec {
+
   name = "moc-${version}";
   version = "2.5.2";
 
@@ -12,18 +38,67 @@ stdenv.mkDerivation rec {
     sha256 = "026v977kwb0wbmlmf6mnik328plxg8wykfx9ryvqhirac0aq39pk";
   };
 
-  nativeBuildInputs = [ pkgconfig ];
+  patches = []
+    ++ opt withffmpeg4 ./moc-ffmpeg4.patch
+    ++ opt pulseSupport ./pulseaudio.patch;
+
+  nativeBuildInputs = [ pkgconfig ]
+    ++ opt pulseSupport autoreconfHook;
+
+  buildInputs = [ ncurses db popt libtool ]
+    # Sound sub-systems
+    ++ opt alsaSupport alsaLib
+    ++ opt pulseSupport libpulseaudio
+    ++ opt jackSupport libjack2
+    # Audio formats
+    ++ opt (aacSupport || mp3Support) libid3tag
+    ++ opt aacSupport faad2
+    ++ opt flacSupport flac
+    ++ opt midiSupport timidity
+    ++ opt modplugSupport libmodplug
+    ++ opt mp3Support libmad
+    ++ opt musepackSupport [ libmpc libmpcdec taglib ]
+    ++ opt vorbisSupport libvorbis
+    ++ opt speexSupport speex
+    ++ opt (ffmpegSupport && !withffmpeg4) ffmpeg
+    ++ opt (ffmpegSupport && withffmpeg4) ffmpeg_4
+    ++ opt sndfileSupport libsndfile
+    ++ opt wavpackSupport wavpack
+    # Misc
+    ++ opt curlSupport curl
+    ++ opt samplerateSupport libsamplerate;
 
-  buildInputs = [
-    ncurses alsaLib flac libmad speex ffmpeg libvorbis libmpc libsndfile libjack2
-    db libmodplug timidity libid3tag libtool
+  configureFlags = [
+    # Sound sub-systems
+    (mkFlag alsaSupport "alsa")
+    (mkFlag pulseSupport "pulse")
+    (mkFlag jackSupport "jack")
+    (mkFlag ossSupport "oss")
+    # Audio formats
+    (mkFlag aacSupport "aac")
+    (mkFlag flacSupport "flac")
+    (mkFlag midiSupport "timidity")
+    (mkFlag modplugSupport "modplug")
+    (mkFlag mp3Support "mp3")
+    (mkFlag musepackSupport "musepack")
+    (mkFlag vorbisSupport "vorbis")
+    (mkFlag speexSupport "speex")
+    (mkFlag ffmpegSupport "ffmpeg")
+    (mkFlag sndfileSupport "sndfile")
+    (mkFlag wavpackSupport "wavpack")
+    # Misc
+    (mkFlag curlSupport "curl")
+    (mkFlag samplerateSupport "samplerate")
+    ("--enable-debug=" + (if withDebug then "yes" else "no"))
+    "--disable-cache"
+    "--without-rcc"
   ];
 
   meta = with stdenv.lib; {
     description = "An ncurses console audio player designed to be powerful and easy to use";
     homepage = http://moc.daper.net/;
     license = licenses.gpl2;
-    maintainers = with maintainers; [ pSub jagajaga ];
+    maintainers = with maintainers; [ aethelz pSub jagajaga ];
     platforms = platforms.linux;
   };
 }
diff --git a/pkgs/applications/audio/moc/moc-ffmpeg4.patch b/pkgs/applications/audio/moc/moc-ffmpeg4.patch
new file mode 100644
index 00000000000..7499f9c539b
--- /dev/null
+++ b/pkgs/applications/audio/moc/moc-ffmpeg4.patch
@@ -0,0 +1,33 @@
+Index: decoder_plugins/ffmpeg/ffmpeg.c
+===================================================================
+--- /decoder_plugins/ffmpeg/ffmpeg.c	(revisiĆ³n: 2963)
++++ /decoder_plugins/ffmpeg/ffmpeg.c	(copia de trabajo)
+@@ -697,7 +697,7 @@
+ 	 * FFmpeg/LibAV in use.  For some versions this will be caught in
+ 	 * *_find_stream_info() above and misreported as an unfound codec
+ 	 * parameters error. */
+-	if (data->codec->capabilities & CODEC_CAP_EXPERIMENTAL) {
++	if (data->codec->capabilities & AV_CODEC_CAP_EXPERIMENTAL) {
+ 		decoder_error (&data->error, ERROR_FATAL, 0,
+ 				"The codec is experimental and may damage MOC: %s",
+ 				data->codec->name);
+@@ -705,8 +705,8 @@
+ 	}
+ 
+ 	set_downmixing (data);
+-	if (data->codec->capabilities & CODEC_CAP_TRUNCATED)
+-		data->enc->flags |= CODEC_FLAG_TRUNCATED;
++	if (data->codec->capabilities & AV_CODEC_CAP_TRUNCATED)
++		data->enc->flags |= AV_CODEC_FLAG_TRUNCATED;
+ 
+ 	if (avcodec_open2 (data->enc, data->codec, NULL) < 0)
+ 	{
+@@ -725,7 +725,7 @@
+ 
+ 	data->sample_width = sfmt_Bps (data->fmt);
+ 
+-	if (data->codec->capabilities & CODEC_CAP_DELAY)
++	if (data->codec->capabilities & AV_CODEC_CAP_DELAY)
+ 		data->delay = true;
+ 	data->seek_broken = is_seek_broken (data);
+ 	data->timing_broken = is_timing_broken (data->ic);
diff --git a/pkgs/applications/audio/moc/pulseaudio.patch b/pkgs/applications/audio/moc/pulseaudio.patch
new file mode 100644
index 00000000000..37d81dddf2b
--- /dev/null
+++ b/pkgs/applications/audio/moc/pulseaudio.patch
@@ -0,0 +1,800 @@
+diff --git a/audio.c b/audio.c
+--- a/audio.c
++++ b/audio.c
+@@ -32,6 +32,9 @@
+ #include "log.h"
+ #include "lists.h"
+ 
++#ifdef HAVE_PULSE
++# include "pulse.h"
++#endif
+ #ifdef HAVE_OSS
+ # include "oss.h"
+ #endif
+@@ -893,6 +896,15 @@
+ 		}
+ #endif
+ 
++#ifdef HAVE_PULSE
++		if (!strcasecmp(name, "pulseaudio")) {
++			pulse_funcs (funcs);
++			printf ("Trying PulseAudio...\n");
++			if (funcs->init(&hw_caps))
++				return;
++		}
++#endif
++
+ #ifdef HAVE_OSS
+ 		if (!strcasecmp(name, "oss")) {
+ 			oss_funcs (funcs);
+diff --git a/configure.in b/configure.in
+--- a/configure.in
++++ b/configure.in
+@@ -162,6 +162,21 @@
+ 		       AC_MSG_ERROR([BerkeleyDB (libdb) not found.]))
+ fi
+ 
++AC_ARG_WITH(pulse, AS_HELP_STRING(--without-pulse,
++				  Compile without PulseAudio support.))
++
++if test "x$with_pulse" != "xno"
++then
++	PKG_CHECK_MODULES(PULSE, [libpulse],
++		 [SOUND_DRIVERS="$SOUND_DRIVERS PULSE"
++		  EXTRA_OBJS="$EXTRA_OBJS pulse.o"
++		  AC_DEFINE([HAVE_PULSE], 1, [Define if you have PulseAudio.])
++		  EXTRA_LIBS="$EXTRA_LIBS $PULSE_LIBS"
++		  CFLAGS="$CFLAGS $PULSE_CFLAGS"],
++		  [true])
++fi
++
++
+ AC_ARG_WITH(oss, AS_HELP_STRING([--without-oss],
+                                 [Compile without OSS support]))
+ 
+diff --git a/options.c b/options.c
+--- a/options.c
++++ b/options.c
+@@ -572,10 +572,11 @@
+ 
+ #ifdef OPENBSD
+ 	add_list ("SoundDriver", "SNDIO:JACK:OSS",
+-	          CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null");
++		  CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null");
++
+ #else
+ 	add_list ("SoundDriver", "Jack:ALSA:OSS",
+-	          CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null");
++	          CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null");
+ #endif
+ 
+ 	add_str  ("JackClientName", "moc", CHECK_NONE);
+diff --git a/pulse.c b/pulse.c
+new file mode 100644
+--- /dev/null
++++ b/pulse.c
+@@ -0,0 +1,705 @@
++/*
++ * MOC - music on console
++ * Copyright (C) 2011 Marien Zwart <marienz@marienz.net>
++ *
++ * 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.
++ *
++ */
++
++/* PulseAudio backend.
++ *
++ * FEATURES:
++ *
++ * Does not autostart a PulseAudio server, but uses an already-started
++ * one, which should be better than alsa-through-pulse.
++ *
++ * Supports control of either our stream's or our entire sink's volume
++ * while we are actually playing. Volume control while paused is
++ * intentionally unsupported: the PulseAudio documentation strongly
++ * suggests not passing in an initial volume when creating a stream
++ * (allowing the server to track this instead), and we do not know
++ * which sink to control if we do not have a stream open.
++ *
++ * IMPLEMENTATION:
++ *
++ * Most client-side (resource allocation) errors are fatal. Failure to
++ * create a server context or stream is not fatal (and MOC should cope
++ * with these failures too), but server communication failures later
++ * on are currently not handled (MOC has no great way for us to tell
++ * it we no longer work, and I am not sure if attempting to reconnect
++ * is worth it or even a good idea).
++ *
++ * The pulse "simple" API is too simple: it combines connecting to the
++ * server and opening a stream into one operation, while I want to
++ * connect to the server when MOC starts (and fall back to a different
++ * backend if there is no server), and I cannot open a stream at that
++ * time since I do not know the audio format yet.
++ *
++ * PulseAudio strongly recommends we use a high-latency connection,
++ * which the MOC frontend code might not expect from its audio
++ * backend. We'll see.
++ *
++ * We map MOC's percentage volumes linearly to pulse's PA_VOLUME_MUTED
++ * (0) .. PA_VOLUME_NORM range. This is what the PulseAudio docs recommend
++ * ( http://pulseaudio.org/wiki/WritingVolumeControlUIs ). It does mean
++ * PulseAudio volumes above PA_VOLUME_NORM do not work well with MOC.
++ *
++ * Comments in audio.h claim "All functions are executed only by one
++ * thread" (referring to the function in the hw_funcs struct). This is
++ * a blatant lie. Most of them are invoked off the "output buffer"
++ * thread (out_buf.c) but at least the "playing" thread (audio.c)
++ * calls audio_close which calls our close function. We can mostly
++ * ignore this problem because we serialize on the pulseaudio threaded
++ * mainloop lock. But it does mean that functions that are normally
++ * only called between open and close (like reset) are sometimes
++ * called without us having a stream. Bulletproof, therefore:
++ * serialize setting/unsetting our global stream using the threaded
++ * mainloop lock, and check for that stream being non-null before
++ * using it.
++ *
++ * I am not convinced there are no further dragons lurking here: can
++ * the "playing" thread(s) close and reopen our output stream while
++ * the "output buffer" thread is sending output there? We can bail if
++ * our stream is simply closed, but we do not currently detect it
++ * being reopened and no longer using the same sample format, which
++ * might have interesting results...
++ *
++ * Also, read_mixer is called from the main server thread (handling
++ * commands). This crashed me once when it got at a stream that was in
++ * the "creating" state and therefore did not have a valid stream
++ * index yet. Fixed by only assigning to the stream global when the
++ * stream is valid.
++ */
++
++#ifdef HAVE_CONFIG_H
++# include "config.h"
++#endif
++
++#define DEBUG
++
++#include <pulse/pulseaudio.h>
++#include "common.h"
++#include "log.h"
++#include "audio.h"
++
++
++/* The pulse mainloop and context are initialized in pulse_init and
++ * destroyed in pulse_shutdown.
++ */
++static pa_threaded_mainloop *mainloop = NULL;
++static pa_context *context = NULL;
++
++/* The stream is initialized in pulse_open and destroyed in pulse_close. */
++static pa_stream *stream = NULL;
++
++static int showing_sink_volume = 0;
++
++/* Callbacks that do nothing but wake up the mainloop. */
++
++static void context_state_callback (pa_context *context ATTR_UNUSED,
++				    void *userdata)
++{
++	pa_threaded_mainloop *m = userdata;
++
++	pa_threaded_mainloop_signal (m, 0);
++}
++
++static void stream_state_callback (pa_stream *stream ATTR_UNUSED,
++				   void *userdata)
++{
++	pa_threaded_mainloop *m = userdata;
++
++	pa_threaded_mainloop_signal (m, 0);
++}
++
++static void stream_write_callback (pa_stream *stream ATTR_UNUSED,
++				   size_t nbytes ATTR_UNUSED, void *userdata)
++{
++	pa_threaded_mainloop *m = userdata;
++
++	pa_threaded_mainloop_signal (m, 0);
++}
++
++/* Initialize pulse mainloop and context. Failure to connect to the
++ * pulse daemon is nonfatal, everything else is fatal (as it
++ * presumably means we ran out of resources).
++ */
++static int pulse_init (struct output_driver_caps *caps)
++{
++	pa_context *c;
++	pa_proplist *proplist;
++
++	assert (!mainloop);
++	assert (!context);
++
++	mainloop = pa_threaded_mainloop_new ();
++	if (!mainloop)
++		fatal ("Cannot create PulseAudio mainloop");
++
++	if (pa_threaded_mainloop_start (mainloop) < 0)
++		fatal ("Cannot start PulseAudio mainloop");
++
++	/* TODO: possibly add more props.
++	 *
++	 * There are a few we could set in proplist.h but nothing I
++	 * expect to be very useful.
++	 *
++	 * http://pulseaudio.org/wiki/ApplicationProperties recommends
++	 * setting at least application.name, icon.name and media.role.
++	 *
++	 * No need to set application.name here, the name passed to
++	 * pa_context_new_with_proplist overrides it.
++	 */
++	proplist = pa_proplist_new ();
++	if (!proplist)
++		fatal ("Cannot allocate PulseAudio proplist");
++
++	pa_proplist_sets (proplist,
++			  PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION);
++	pa_proplist_sets (proplist, PA_PROP_MEDIA_ROLE, "music");
++	pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "net.daper.moc");
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	c = pa_context_new_with_proplist (
++		pa_threaded_mainloop_get_api (mainloop),
++		PACKAGE_NAME, proplist);
++	pa_proplist_free (proplist);
++
++	if (!c)
++		fatal ("Cannot allocate PulseAudio context");
++
++	pa_context_set_state_callback (c, context_state_callback, mainloop);
++
++	/* Ignore return value, rely on state being set properly */
++	pa_context_connect (c, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
++
++	while (1) {
++		pa_context_state_t state = pa_context_get_state (c);
++
++		if (state == PA_CONTEXT_READY)
++			break;
++
++		if (!PA_CONTEXT_IS_GOOD (state)) {
++			error ("PulseAudio connection failed: %s",
++			       pa_strerror (pa_context_errno (c)));
++
++			goto unlock_and_fail;
++		}
++
++		debug ("waiting for context to become ready...");
++		pa_threaded_mainloop_wait (mainloop);
++	}
++
++	/* Only set the global now that the context is actually ready */
++	context = c;
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	/* We just make up the hardware capabilities, since pulse is
++	 * supposed to be abstracting these out. Assume pulse will
++	 * deal with anything we want to throw at it, and that we will
++	 * only want mono or stereo audio.
++	 */
++	caps->min_channels = 1;
++	caps->max_channels = 2;
++	caps->formats = (SFMT_S8 | SFMT_S16 | SFMT_S32 |
++			 SFMT_FLOAT | SFMT_BE | SFMT_LE);
++
++	return 1;
++
++unlock_and_fail:
++
++	pa_context_unref (c);
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	pa_threaded_mainloop_stop (mainloop);
++	pa_threaded_mainloop_free (mainloop);
++	mainloop = NULL;
++
++	return 0;
++}
++
++static void pulse_shutdown (void)
++{
++	pa_threaded_mainloop_lock (mainloop);
++
++	pa_context_disconnect (context);
++	pa_context_unref (context);
++	context = NULL;
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	pa_threaded_mainloop_stop (mainloop);
++	pa_threaded_mainloop_free (mainloop);
++	mainloop = NULL;
++}
++
++static int pulse_open (struct sound_params *sound_params)
++{
++	pa_sample_spec ss;
++	pa_buffer_attr ba;
++	pa_stream *s;
++
++	assert (!stream);
++	/* Initialize everything to -1, which in practice gets us
++	 * about 2 seconds of latency (which is fine). This is not the
++	 * same as passing NULL for this struct, which gets us an
++	 * unnecessarily short alsa-like latency.
++	 */
++	ba.fragsize = (uint32_t) -1;
++	ba.tlength = (uint32_t) -1;
++	ba.prebuf = (uint32_t) -1;
++	ba.minreq = (uint32_t) -1;
++	ba.maxlength = (uint32_t) -1;
++
++	ss.channels = sound_params->channels;
++	ss.rate = sound_params->rate;
++	switch (sound_params->fmt) {
++	case SFMT_U8:
++		ss.format = PA_SAMPLE_U8;
++		break;
++	case SFMT_S16 | SFMT_LE:
++		ss.format = PA_SAMPLE_S16LE;
++		break;
++	case SFMT_S16 | SFMT_BE:
++		ss.format = PA_SAMPLE_S16BE;
++		break;
++	case SFMT_FLOAT | SFMT_LE:
++		ss.format = PA_SAMPLE_FLOAT32LE;
++		break;
++	case SFMT_FLOAT | SFMT_BE:
++		ss.format = PA_SAMPLE_FLOAT32BE;
++		break;
++	case SFMT_S32 | SFMT_LE:
++		ss.format = PA_SAMPLE_S32LE;
++		break;
++	case SFMT_S32 | SFMT_BE:
++		ss.format = PA_SAMPLE_S32BE;
++		break;
++
++	default:
++		fatal ("pulse: got unrequested format");
++	}
++
++	debug ("opening stream");
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	/* TODO: figure out if there are useful stream properties to set.
++	 *
++	 * I do not really see any in proplist.h that we can set from
++	 * here (there are media title/artist/etc props but we do not
++	 * have that data available here).
++	 */
++	s = pa_stream_new (context, "music", &ss, NULL);
++	if (!s)
++		fatal ("pulse: stream allocation failed");
++
++	pa_stream_set_state_callback (s, stream_state_callback, mainloop);
++	pa_stream_set_write_callback (s, stream_write_callback, mainloop);
++
++	/* Ignore return value, rely on failed stream state instead. */
++	pa_stream_connect_playback (
++		s, NULL, &ba,
++		PA_STREAM_INTERPOLATE_TIMING |
++		PA_STREAM_AUTO_TIMING_UPDATE |
++		PA_STREAM_ADJUST_LATENCY,
++		NULL, NULL);
++
++	while (1) {
++		pa_stream_state_t state = pa_stream_get_state (s);
++
++		if (state == PA_STREAM_READY)
++			break;
++
++		if (!PA_STREAM_IS_GOOD (state)) {
++			error ("PulseAudio stream connection failed");
++
++			goto fail;
++		}
++
++		debug ("waiting for stream to become ready...");
++		pa_threaded_mainloop_wait (mainloop);
++	}
++
++	/* Only set the global stream now that it is actually ready */
++	stream = s;
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	return 1;
++
++fail:
++	pa_stream_unref (s);
++
++	pa_threaded_mainloop_unlock (mainloop);
++	return 0;
++}
++
++static void pulse_close (void)
++{
++	debug ("closing stream");
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	pa_stream_disconnect (stream);
++	pa_stream_unref (stream);
++	stream = NULL;
++
++	pa_threaded_mainloop_unlock (mainloop);
++}
++
++static int pulse_play (const char *buff, const size_t size)
++{
++	size_t offset = 0;
++
++	debug ("Got %d bytes to play", (int)size);
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	/* The buffer is usually writable when we get here, and there
++	 * are usually few (if any) writes after the first one. So
++	 * there is no point in doing further writes directly from the
++	 * callback: we can just do all writes from this thread.
++	 */
++
++	/* Break out of the loop if some other thread manages to close
++	 * our stream underneath us.
++	 */
++	while (stream) {
++		size_t towrite = MIN(pa_stream_writable_size (stream),
++				     size - offset);
++		debug ("writing %d bytes", (int)towrite);
++
++		/* We have no working way of dealing with errors
++		 * (see below). */
++		if (pa_stream_write(stream, buff + offset, towrite,
++				    NULL, 0, PA_SEEK_RELATIVE))
++			error ("pa_stream_write failed");
++
++		offset += towrite;
++
++		if (offset >= size)
++			break;
++
++		pa_threaded_mainloop_wait (mainloop);
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	debug ("Done playing!");
++
++	/* We should always return size, calling code does not deal
++	 * well with anything else. Only read the rest if you want to
++	 * know why.
++	 *
++	 * The output buffer reader thread (out_buf.c:read_thread)
++	 * repeatedly loads some 64k/0.1s of audio into a buffer on
++	 * the stack, then calls audio_send_pcm repeatedly until this
++	 * entire buffer has been processed (similar to the loop in
++	 * this function). audio_send_pcm applies the softmixer and
++	 * equalizer, then feeds the result to this function, passing
++	 * through our return value.
++	 *
++	 * So if we return less than size the equalizer/softmixer is
++	 * re-applied to the remaining data, which is silly. Also,
++	 * audio_send_pcm checks for our return value being zero and
++	 * calls fatal() if it is, so try to always process *some*
++	 * data. Also, out_buf.c uses the return value of this
++	 * function from the last run through its inner loop to update
++	 * its time attribute, which means it will be interestingly
++	 * off if that loop ran more than once.
++	 *
++	 * Oh, and alsa.c seems to think it can return -1 to indicate
++	 * failure, which will cause out_buf.c to rewind its buffer
++	 * (to before its start, usually).
++	 */
++	return size;
++}
++
++static void volume_cb (const pa_cvolume *v, void *userdata)
++{
++	int *result = userdata;
++
++	if (v)
++		*result = 100 * pa_cvolume_avg (v) / PA_VOLUME_NORM;
++
++	pa_threaded_mainloop_signal (mainloop, 0);
++}
++
++static void sink_volume_cb (pa_context *c ATTR_UNUSED,
++			    const pa_sink_info *i, int eol ATTR_UNUSED,
++			    void *userdata)
++{
++	volume_cb (i ? &i->volume : NULL, userdata);
++}
++
++static void sink_input_volume_cb (pa_context *c ATTR_UNUSED,
++				  const pa_sink_input_info *i,
++				  int eol ATTR_UNUSED,
++				  void *userdata ATTR_UNUSED)
++{
++	volume_cb (i ? &i->volume : NULL, userdata);
++}
++
++static int pulse_read_mixer (void)
++{
++	pa_operation *op;
++	int result = 0;
++
++	debug ("read mixer");
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	if (stream) {
++		if (showing_sink_volume)
++			op = pa_context_get_sink_info_by_index (
++				context, pa_stream_get_device_index (stream),
++				sink_volume_cb, &result);
++		else
++			op = pa_context_get_sink_input_info (
++				context, pa_stream_get_index (stream),
++				sink_input_volume_cb, &result);
++
++		while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)
++			pa_threaded_mainloop_wait (mainloop);
++
++		pa_operation_unref (op);
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	return result;
++}
++
++static void pulse_set_mixer (int vol)
++{
++	pa_cvolume v;
++	pa_operation *op;
++
++	/* Setting volume for one channel does the right thing. */
++	pa_cvolume_set(&v, 1, vol * PA_VOLUME_NORM / 100);
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	if (stream) {
++		if (showing_sink_volume)
++			op = pa_context_set_sink_volume_by_index (
++				context, pa_stream_get_device_index (stream),
++				&v, NULL, NULL);
++		else
++			op = pa_context_set_sink_input_volume (
++				context, pa_stream_get_index (stream),
++				&v, NULL, NULL);
++
++		pa_operation_unref (op);
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++}
++
++static int pulse_get_buff_fill (void)
++{
++	/* This function is problematic. MOC uses it to for the "time
++	 * remaining" in the UI, but calls it more than once per
++	 * second (after each chunk of audio played, not for each
++	 * playback time update). We have to be fairly accurate here
++	 * for that time remaining to not jump weirdly. But PulseAudio
++	 * cannot give us a 100% accurate value here, as it involves a
++	 * server roundtrip. And if we call this a lot it suggests
++	 * switching to a mode where the value is interpolated, making
++	 * it presumably more inaccurate (see the flags we pass to
++	 * pa_stream_connect_playback).
++	 *
++	 * MOC also contains what I believe to be a race: it calls
++	 * audio_get_buff_fill "soon" (after playing the first chunk)
++	 * after starting playback of the next song, at which point we
++	 * still have part of the previous song buffered. This means
++	 * our position into the new song is negative, which fails an
++	 * assert (in out_buf.c:out_buf_time_get). There is no sane
++	 * way for us to detect this condition. I believe no other
++	 * backend triggers this because the assert sits after an
++	 * implicit float -> int seconds conversion, which means we
++	 * have to be off by at least an entire second to get a
++	 * negative value, and none of the other backends have buffers
++	 * that large (alsa buffers are supposedly a few 100 ms).
++	 */
++	pa_usec_t buffered_usecs = 0;
++	int buffered_bytes = 0;
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	/* Using pa_stream_get_timing_info and returning the distance
++	 * between write_index and read_index would be more obvious,
++	 * but because of how the result is actually used I believe
++	 * using the latency value is slightly more correct, and it
++	 * makes the following crash-avoidance hack more obvious.
++	 */
++
++	/* This function will frequently fail the first time we call
++	 * it (pulse does not have the requested data yet). We ignore
++	 * that and just return 0.
++	 *
++	 * Deal with stream being NULL too, just in case this is
++	 * called in a racy fashion similar to how reset() is.
++	 */
++	if (stream &&
++	    pa_stream_get_latency (stream, &buffered_usecs, NULL) >= 0) {
++		/* Crash-avoidance HACK: floor our latency to at most
++		 * 1 second. It is usually more, but reporting that at
++		 * the start of playback crashes MOC, and we cannot
++		 * sanely detect when reporting it is safe.
++		 */
++		if (buffered_usecs > 1000000)
++			buffered_usecs = 1000000;
++
++		buffered_bytes = pa_usec_to_bytes (
++			buffered_usecs,
++			pa_stream_get_sample_spec (stream));
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	debug ("buffer fill: %d usec / %d bytes",
++	       (int) buffered_usecs, (int) buffered_bytes);
++
++	return buffered_bytes;
++}
++
++static void flush_callback (pa_stream *s ATTR_UNUSED, int success,
++			    void *userdata)
++{
++	int *result = userdata;
++
++	*result = success;
++
++	pa_threaded_mainloop_signal (mainloop, 0);
++}
++
++static int pulse_reset (void)
++{
++	pa_operation *op;
++	int result = 0;
++
++	debug ("reset requested");
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	/* We *should* have a stream here, but MOC is racy, so bulletproof */
++	if (stream) {
++		op = pa_stream_flush (stream, flush_callback, &result);
++
++		while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)
++			pa_threaded_mainloop_wait (mainloop);
++
++		pa_operation_unref (op);
++	} else
++		logit ("pulse_reset() called without a stream");
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	return result;
++}
++
++static int pulse_get_rate (void)
++{
++	/* This is called once right after open. Do not bother making
++	 * this fast. */
++
++	int result;
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	if (stream)
++		result = pa_stream_get_sample_spec (stream)->rate;
++	else {
++		error ("get_rate called without a stream");
++		result = 0;
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	return result;
++}
++
++static void pulse_toggle_mixer_channel (void)
++{
++	showing_sink_volume = !showing_sink_volume;
++}
++
++static void sink_name_cb (pa_context *c ATTR_UNUSED,
++			  const pa_sink_info *i, int eol ATTR_UNUSED,
++			  void *userdata)
++{
++	char **result = userdata;
++
++	if (i && !*result)
++		*result = xstrdup (i->name);
++
++	pa_threaded_mainloop_signal (mainloop, 0);
++}
++
++static void sink_input_name_cb (pa_context *c ATTR_UNUSED,
++				const pa_sink_input_info *i,
++				int eol ATTR_UNUSED,
++				void *userdata)
++{
++	char **result = userdata;
++
++	if (i && !*result)
++		*result = xstrdup (i->name);
++
++	pa_threaded_mainloop_signal (mainloop, 0);
++}
++
++static char *pulse_get_mixer_channel_name (void)
++{
++	char *result = NULL;
++	pa_operation *op;
++
++	pa_threaded_mainloop_lock (mainloop);
++
++	if (stream) {
++		if (showing_sink_volume)
++			op = pa_context_get_sink_info_by_index (
++				context, pa_stream_get_device_index (stream),
++				sink_name_cb, &result);
++		else
++			op = pa_context_get_sink_input_info (
++				context, pa_stream_get_index (stream),
++				sink_input_name_cb, &result);
++
++		while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)
++			pa_threaded_mainloop_wait (mainloop);
++
++		pa_operation_unref (op);
++	}
++
++	pa_threaded_mainloop_unlock (mainloop);
++
++	if (!result)
++		result = xstrdup ("disconnected");
++
++	return result;
++}
++
++void pulse_funcs (struct hw_funcs *funcs)
++{
++	funcs->init = pulse_init;
++	funcs->shutdown = pulse_shutdown;
++	funcs->open = pulse_open;
++	funcs->close = pulse_close;
++	funcs->play = pulse_play;
++	funcs->read_mixer = pulse_read_mixer;
++	funcs->set_mixer = pulse_set_mixer;
++	funcs->get_buff_fill = pulse_get_buff_fill;
++	funcs->reset = pulse_reset;
++	funcs->get_rate = pulse_get_rate;
++	funcs->toggle_mixer_channel = pulse_toggle_mixer_channel;
++	funcs->get_mixer_channel_name = pulse_get_mixer_channel_name;
++}
+diff --git a/pulse.h b/pulse.h
+new file mode 100644
+--- /dev/null
++++ b/pulse.h
+@@ -0,0 +1,14 @@
++#ifndef PULSE_H
++#define PULSE_H
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++void pulse_funcs (struct hw_funcs *funcs);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif