From eaeb587429d0fefeb39ee52fbe070dc4dd44984f Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 3 May 2019 13:38:25 +0300 Subject: [PATCH] moc: refactor options, add patches Add support for conditional compile options, add pulseaudio and ffmpeg4 support patches. --- pkgs/applications/audio/moc/default.nix | 93 +- pkgs/applications/audio/moc/moc-ffmpeg4.patch | 33 + pkgs/applications/audio/moc/pulseaudio.patch | 800 ++++++++++++++++++ 3 files changed, 917 insertions(+), 9 deletions(-) create mode 100644 pkgs/applications/audio/moc/moc-ffmpeg4.patch create mode 100644 pkgs/applications/audio/moc/pulseaudio.patch diff --git a/pkgs/applications/audio/moc/default.nix b/pkgs/applications/audio/moc/default.nix index ea83a1012ebb..3ed330cc7437 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; - buildInputs = [ - ncurses alsaLib flac libmad speex ffmpeg libvorbis libmpc libsndfile libjack2 - db libmodplug timidity libid3tag libtool + 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; + + 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 000000000000..7499f9c539bc --- /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 000000000000..37d81dddf2bc --- /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 ++ * ++ * 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 ++#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