Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)

moc: refactor options, add patches

Add support for conditional compile options, add pulseaudio and ffmpeg4
support patches.

Eugene eaeb5874 5ad1e9f0

+917 -9
+84 -9
pkgs/applications/audio/moc/default.nix
··· 1 - { stdenv, fetchurl, ncurses, pkgconfig, alsaLib, flac, libmad, speex, ffmpeg 2 - , libvorbis, libmpc, libsndfile, libjack2, db, libmodplug, timidity, libid3tag 3 - , libtool 4 }: 5 6 - stdenv.mkDerivation rec { 7 name = "moc-${version}"; 8 version = "2.5.2"; 9 ··· 12 sha256 = "026v977kwb0wbmlmf6mnik328plxg8wykfx9ryvqhirac0aq39pk"; 13 }; 14 15 - nativeBuildInputs = [ pkgconfig ]; 16 17 - buildInputs = [ 18 - ncurses alsaLib flac libmad speex ffmpeg libvorbis libmpc libsndfile libjack2 19 - db libmodplug timidity libid3tag libtool 20 ]; 21 22 meta = with stdenv.lib; { 23 description = "An ncurses console audio player designed to be powerful and easy to use"; 24 homepage = http://moc.daper.net/; 25 license = licenses.gpl2; 26 - maintainers = with maintainers; [ pSub jagajaga ]; 27 platforms = platforms.linux; 28 }; 29 }
··· 1 + { stdenv, fetchurl, pkgconfig 2 + , ncurses, db , popt, libtool 3 + # Sound sub-systems 4 + , alsaSupport ? true, alsaLib 5 + , pulseSupport ? true, libpulseaudio, autoreconfHook 6 + , jackSupport ? true, libjack2 7 + , ossSupport ? true 8 + # Audio formats 9 + , aacSupport ? true, faad2, libid3tag 10 + , flacSupport ? true, flac 11 + , midiSupport ? true, timidity 12 + , modplugSupport ? true, libmodplug 13 + , mp3Support ? true, libmad 14 + , musepackSupport ? true, libmpc, libmpcdec, taglib 15 + , vorbisSupport ? true, libvorbis 16 + , speexSupport ? true, speex 17 + , ffmpegSupport ? true, ffmpeg 18 + , sndfileSupport ? true, libsndfile 19 + , wavpackSupport ? true, wavpack 20 + # Misc 21 + , withffmpeg4 ? false, ffmpeg_4 22 + , curlSupport ? true, curl 23 + , samplerateSupport ? true, libsamplerate 24 + , withDebug ? false 25 }: 26 27 + let 28 + opt = stdenv.lib.optional; 29 + mkFlag = c: f: if c then "--with-${f}" else "--without-${f}"; 30 + 31 + in stdenv.mkDerivation rec { 32 + 33 name = "moc-${version}"; 34 version = "2.5.2"; 35 ··· 38 sha256 = "026v977kwb0wbmlmf6mnik328plxg8wykfx9ryvqhirac0aq39pk"; 39 }; 40 41 + patches = [] 42 + ++ opt withffmpeg4 ./moc-ffmpeg4.patch 43 + ++ opt pulseSupport ./pulseaudio.patch; 44 + 45 + nativeBuildInputs = [ pkgconfig ] 46 + ++ opt pulseSupport autoreconfHook; 47 + 48 + buildInputs = [ ncurses db popt libtool ] 49 + # Sound sub-systems 50 + ++ opt alsaSupport alsaLib 51 + ++ opt pulseSupport libpulseaudio 52 + ++ opt jackSupport libjack2 53 + # Audio formats 54 + ++ opt (aacSupport || mp3Support) libid3tag 55 + ++ opt aacSupport faad2 56 + ++ opt flacSupport flac 57 + ++ opt midiSupport timidity 58 + ++ opt modplugSupport libmodplug 59 + ++ opt mp3Support libmad 60 + ++ opt musepackSupport [ libmpc libmpcdec taglib ] 61 + ++ opt vorbisSupport libvorbis 62 + ++ opt speexSupport speex 63 + ++ opt (ffmpegSupport && !withffmpeg4) ffmpeg 64 + ++ opt (ffmpegSupport && withffmpeg4) ffmpeg_4 65 + ++ opt sndfileSupport libsndfile 66 + ++ opt wavpackSupport wavpack 67 + # Misc 68 + ++ opt curlSupport curl 69 + ++ opt samplerateSupport libsamplerate; 70 71 + configureFlags = [ 72 + # Sound sub-systems 73 + (mkFlag alsaSupport "alsa") 74 + (mkFlag pulseSupport "pulse") 75 + (mkFlag jackSupport "jack") 76 + (mkFlag ossSupport "oss") 77 + # Audio formats 78 + (mkFlag aacSupport "aac") 79 + (mkFlag flacSupport "flac") 80 + (mkFlag midiSupport "timidity") 81 + (mkFlag modplugSupport "modplug") 82 + (mkFlag mp3Support "mp3") 83 + (mkFlag musepackSupport "musepack") 84 + (mkFlag vorbisSupport "vorbis") 85 + (mkFlag speexSupport "speex") 86 + (mkFlag ffmpegSupport "ffmpeg") 87 + (mkFlag sndfileSupport "sndfile") 88 + (mkFlag wavpackSupport "wavpack") 89 + # Misc 90 + (mkFlag curlSupport "curl") 91 + (mkFlag samplerateSupport "samplerate") 92 + ("--enable-debug=" + (if withDebug then "yes" else "no")) 93 + "--disable-cache" 94 + "--without-rcc" 95 ]; 96 97 meta = with stdenv.lib; { 98 description = "An ncurses console audio player designed to be powerful and easy to use"; 99 homepage = http://moc.daper.net/; 100 license = licenses.gpl2; 101 + maintainers = with maintainers; [ aethelz pSub jagajaga ]; 102 platforms = platforms.linux; 103 }; 104 }
+33
pkgs/applications/audio/moc/moc-ffmpeg4.patch
···
··· 1 + Index: decoder_plugins/ffmpeg/ffmpeg.c 2 + =================================================================== 3 + --- /decoder_plugins/ffmpeg/ffmpeg.c (revisión: 2963) 4 + +++ /decoder_plugins/ffmpeg/ffmpeg.c (copia de trabajo) 5 + @@ -697,7 +697,7 @@ 6 + * FFmpeg/LibAV in use. For some versions this will be caught in 7 + * *_find_stream_info() above and misreported as an unfound codec 8 + * parameters error. */ 9 + - if (data->codec->capabilities & CODEC_CAP_EXPERIMENTAL) { 10 + + if (data->codec->capabilities & AV_CODEC_CAP_EXPERIMENTAL) { 11 + decoder_error (&data->error, ERROR_FATAL, 0, 12 + "The codec is experimental and may damage MOC: %s", 13 + data->codec->name); 14 + @@ -705,8 +705,8 @@ 15 + } 16 + 17 + set_downmixing (data); 18 + - if (data->codec->capabilities & CODEC_CAP_TRUNCATED) 19 + - data->enc->flags |= CODEC_FLAG_TRUNCATED; 20 + + if (data->codec->capabilities & AV_CODEC_CAP_TRUNCATED) 21 + + data->enc->flags |= AV_CODEC_FLAG_TRUNCATED; 22 + 23 + if (avcodec_open2 (data->enc, data->codec, NULL) < 0) 24 + { 25 + @@ -725,7 +725,7 @@ 26 + 27 + data->sample_width = sfmt_Bps (data->fmt); 28 + 29 + - if (data->codec->capabilities & CODEC_CAP_DELAY) 30 + + if (data->codec->capabilities & AV_CODEC_CAP_DELAY) 31 + data->delay = true; 32 + data->seek_broken = is_seek_broken (data); 33 + data->timing_broken = is_timing_broken (data->ic);
+800
pkgs/applications/audio/moc/pulseaudio.patch
···
··· 1 + diff --git a/audio.c b/audio.c 2 + --- a/audio.c 3 + +++ b/audio.c 4 + @@ -32,6 +32,9 @@ 5 + #include "log.h" 6 + #include "lists.h" 7 + 8 + +#ifdef HAVE_PULSE 9 + +# include "pulse.h" 10 + +#endif 11 + #ifdef HAVE_OSS 12 + # include "oss.h" 13 + #endif 14 + @@ -893,6 +896,15 @@ 15 + } 16 + #endif 17 + 18 + +#ifdef HAVE_PULSE 19 + + if (!strcasecmp(name, "pulseaudio")) { 20 + + pulse_funcs (funcs); 21 + + printf ("Trying PulseAudio...\n"); 22 + + if (funcs->init(&hw_caps)) 23 + + return; 24 + + } 25 + +#endif 26 + + 27 + #ifdef HAVE_OSS 28 + if (!strcasecmp(name, "oss")) { 29 + oss_funcs (funcs); 30 + diff --git a/configure.in b/configure.in 31 + --- a/configure.in 32 + +++ b/configure.in 33 + @@ -162,6 +162,21 @@ 34 + AC_MSG_ERROR([BerkeleyDB (libdb) not found.])) 35 + fi 36 + 37 + +AC_ARG_WITH(pulse, AS_HELP_STRING(--without-pulse, 38 + + Compile without PulseAudio support.)) 39 + + 40 + +if test "x$with_pulse" != "xno" 41 + +then 42 + + PKG_CHECK_MODULES(PULSE, [libpulse], 43 + + [SOUND_DRIVERS="$SOUND_DRIVERS PULSE" 44 + + EXTRA_OBJS="$EXTRA_OBJS pulse.o" 45 + + AC_DEFINE([HAVE_PULSE], 1, [Define if you have PulseAudio.]) 46 + + EXTRA_LIBS="$EXTRA_LIBS $PULSE_LIBS" 47 + + CFLAGS="$CFLAGS $PULSE_CFLAGS"], 48 + + [true]) 49 + +fi 50 + + 51 + + 52 + AC_ARG_WITH(oss, AS_HELP_STRING([--without-oss], 53 + [Compile without OSS support])) 54 + 55 + diff --git a/options.c b/options.c 56 + --- a/options.c 57 + +++ b/options.c 58 + @@ -572,10 +572,11 @@ 59 + 60 + #ifdef OPENBSD 61 + add_list ("SoundDriver", "SNDIO:JACK:OSS", 62 + - CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null"); 63 + + CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null"); 64 + + 65 + #else 66 + add_list ("SoundDriver", "Jack:ALSA:OSS", 67 + - CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null"); 68 + + CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null"); 69 + #endif 70 + 71 + add_str ("JackClientName", "moc", CHECK_NONE); 72 + diff --git a/pulse.c b/pulse.c 73 + new file mode 100644 74 + --- /dev/null 75 + +++ b/pulse.c 76 + @@ -0,0 +1,705 @@ 77 + +/* 78 + + * MOC - music on console 79 + + * Copyright (C) 2011 Marien Zwart <marienz@marienz.net> 80 + + * 81 + + * This program is free software; you can redistribute it and/or modify 82 + + * it under the terms of the GNU General Public License as published by 83 + + * the Free Software Foundation; either version 2 of the License, or 84 + + * (at your option) any later version. 85 + + * 86 + + */ 87 + + 88 + +/* PulseAudio backend. 89 + + * 90 + + * FEATURES: 91 + + * 92 + + * Does not autostart a PulseAudio server, but uses an already-started 93 + + * one, which should be better than alsa-through-pulse. 94 + + * 95 + + * Supports control of either our stream's or our entire sink's volume 96 + + * while we are actually playing. Volume control while paused is 97 + + * intentionally unsupported: the PulseAudio documentation strongly 98 + + * suggests not passing in an initial volume when creating a stream 99 + + * (allowing the server to track this instead), and we do not know 100 + + * which sink to control if we do not have a stream open. 101 + + * 102 + + * IMPLEMENTATION: 103 + + * 104 + + * Most client-side (resource allocation) errors are fatal. Failure to 105 + + * create a server context or stream is not fatal (and MOC should cope 106 + + * with these failures too), but server communication failures later 107 + + * on are currently not handled (MOC has no great way for us to tell 108 + + * it we no longer work, and I am not sure if attempting to reconnect 109 + + * is worth it or even a good idea). 110 + + * 111 + + * The pulse "simple" API is too simple: it combines connecting to the 112 + + * server and opening a stream into one operation, while I want to 113 + + * connect to the server when MOC starts (and fall back to a different 114 + + * backend if there is no server), and I cannot open a stream at that 115 + + * time since I do not know the audio format yet. 116 + + * 117 + + * PulseAudio strongly recommends we use a high-latency connection, 118 + + * which the MOC frontend code might not expect from its audio 119 + + * backend. We'll see. 120 + + * 121 + + * We map MOC's percentage volumes linearly to pulse's PA_VOLUME_MUTED 122 + + * (0) .. PA_VOLUME_NORM range. This is what the PulseAudio docs recommend 123 + + * ( http://pulseaudio.org/wiki/WritingVolumeControlUIs ). It does mean 124 + + * PulseAudio volumes above PA_VOLUME_NORM do not work well with MOC. 125 + + * 126 + + * Comments in audio.h claim "All functions are executed only by one 127 + + * thread" (referring to the function in the hw_funcs struct). This is 128 + + * a blatant lie. Most of them are invoked off the "output buffer" 129 + + * thread (out_buf.c) but at least the "playing" thread (audio.c) 130 + + * calls audio_close which calls our close function. We can mostly 131 + + * ignore this problem because we serialize on the pulseaudio threaded 132 + + * mainloop lock. But it does mean that functions that are normally 133 + + * only called between open and close (like reset) are sometimes 134 + + * called without us having a stream. Bulletproof, therefore: 135 + + * serialize setting/unsetting our global stream using the threaded 136 + + * mainloop lock, and check for that stream being non-null before 137 + + * using it. 138 + + * 139 + + * I am not convinced there are no further dragons lurking here: can 140 + + * the "playing" thread(s) close and reopen our output stream while 141 + + * the "output buffer" thread is sending output there? We can bail if 142 + + * our stream is simply closed, but we do not currently detect it 143 + + * being reopened and no longer using the same sample format, which 144 + + * might have interesting results... 145 + + * 146 + + * Also, read_mixer is called from the main server thread (handling 147 + + * commands). This crashed me once when it got at a stream that was in 148 + + * the "creating" state and therefore did not have a valid stream 149 + + * index yet. Fixed by only assigning to the stream global when the 150 + + * stream is valid. 151 + + */ 152 + + 153 + +#ifdef HAVE_CONFIG_H 154 + +# include "config.h" 155 + +#endif 156 + + 157 + +#define DEBUG 158 + + 159 + +#include <pulse/pulseaudio.h> 160 + +#include "common.h" 161 + +#include "log.h" 162 + +#include "audio.h" 163 + + 164 + + 165 + +/* The pulse mainloop and context are initialized in pulse_init and 166 + + * destroyed in pulse_shutdown. 167 + + */ 168 + +static pa_threaded_mainloop *mainloop = NULL; 169 + +static pa_context *context = NULL; 170 + + 171 + +/* The stream is initialized in pulse_open and destroyed in pulse_close. */ 172 + +static pa_stream *stream = NULL; 173 + + 174 + +static int showing_sink_volume = 0; 175 + + 176 + +/* Callbacks that do nothing but wake up the mainloop. */ 177 + + 178 + +static void context_state_callback (pa_context *context ATTR_UNUSED, 179 + + void *userdata) 180 + +{ 181 + + pa_threaded_mainloop *m = userdata; 182 + + 183 + + pa_threaded_mainloop_signal (m, 0); 184 + +} 185 + + 186 + +static void stream_state_callback (pa_stream *stream ATTR_UNUSED, 187 + + void *userdata) 188 + +{ 189 + + pa_threaded_mainloop *m = userdata; 190 + + 191 + + pa_threaded_mainloop_signal (m, 0); 192 + +} 193 + + 194 + +static void stream_write_callback (pa_stream *stream ATTR_UNUSED, 195 + + size_t nbytes ATTR_UNUSED, void *userdata) 196 + +{ 197 + + pa_threaded_mainloop *m = userdata; 198 + + 199 + + pa_threaded_mainloop_signal (m, 0); 200 + +} 201 + + 202 + +/* Initialize pulse mainloop and context. Failure to connect to the 203 + + * pulse daemon is nonfatal, everything else is fatal (as it 204 + + * presumably means we ran out of resources). 205 + + */ 206 + +static int pulse_init (struct output_driver_caps *caps) 207 + +{ 208 + + pa_context *c; 209 + + pa_proplist *proplist; 210 + + 211 + + assert (!mainloop); 212 + + assert (!context); 213 + + 214 + + mainloop = pa_threaded_mainloop_new (); 215 + + if (!mainloop) 216 + + fatal ("Cannot create PulseAudio mainloop"); 217 + + 218 + + if (pa_threaded_mainloop_start (mainloop) < 0) 219 + + fatal ("Cannot start PulseAudio mainloop"); 220 + + 221 + + /* TODO: possibly add more props. 222 + + * 223 + + * There are a few we could set in proplist.h but nothing I 224 + + * expect to be very useful. 225 + + * 226 + + * http://pulseaudio.org/wiki/ApplicationProperties recommends 227 + + * setting at least application.name, icon.name and media.role. 228 + + * 229 + + * No need to set application.name here, the name passed to 230 + + * pa_context_new_with_proplist overrides it. 231 + + */ 232 + + proplist = pa_proplist_new (); 233 + + if (!proplist) 234 + + fatal ("Cannot allocate PulseAudio proplist"); 235 + + 236 + + pa_proplist_sets (proplist, 237 + + PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION); 238 + + pa_proplist_sets (proplist, PA_PROP_MEDIA_ROLE, "music"); 239 + + pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "net.daper.moc"); 240 + + 241 + + pa_threaded_mainloop_lock (mainloop); 242 + + 243 + + c = pa_context_new_with_proplist ( 244 + + pa_threaded_mainloop_get_api (mainloop), 245 + + PACKAGE_NAME, proplist); 246 + + pa_proplist_free (proplist); 247 + + 248 + + if (!c) 249 + + fatal ("Cannot allocate PulseAudio context"); 250 + + 251 + + pa_context_set_state_callback (c, context_state_callback, mainloop); 252 + + 253 + + /* Ignore return value, rely on state being set properly */ 254 + + pa_context_connect (c, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL); 255 + + 256 + + while (1) { 257 + + pa_context_state_t state = pa_context_get_state (c); 258 + + 259 + + if (state == PA_CONTEXT_READY) 260 + + break; 261 + + 262 + + if (!PA_CONTEXT_IS_GOOD (state)) { 263 + + error ("PulseAudio connection failed: %s", 264 + + pa_strerror (pa_context_errno (c))); 265 + + 266 + + goto unlock_and_fail; 267 + + } 268 + + 269 + + debug ("waiting for context to become ready..."); 270 + + pa_threaded_mainloop_wait (mainloop); 271 + + } 272 + + 273 + + /* Only set the global now that the context is actually ready */ 274 + + context = c; 275 + + 276 + + pa_threaded_mainloop_unlock (mainloop); 277 + + 278 + + /* We just make up the hardware capabilities, since pulse is 279 + + * supposed to be abstracting these out. Assume pulse will 280 + + * deal with anything we want to throw at it, and that we will 281 + + * only want mono or stereo audio. 282 + + */ 283 + + caps->min_channels = 1; 284 + + caps->max_channels = 2; 285 + + caps->formats = (SFMT_S8 | SFMT_S16 | SFMT_S32 | 286 + + SFMT_FLOAT | SFMT_BE | SFMT_LE); 287 + + 288 + + return 1; 289 + + 290 + +unlock_and_fail: 291 + + 292 + + pa_context_unref (c); 293 + + 294 + + pa_threaded_mainloop_unlock (mainloop); 295 + + 296 + + pa_threaded_mainloop_stop (mainloop); 297 + + pa_threaded_mainloop_free (mainloop); 298 + + mainloop = NULL; 299 + + 300 + + return 0; 301 + +} 302 + + 303 + +static void pulse_shutdown (void) 304 + +{ 305 + + pa_threaded_mainloop_lock (mainloop); 306 + + 307 + + pa_context_disconnect (context); 308 + + pa_context_unref (context); 309 + + context = NULL; 310 + + 311 + + pa_threaded_mainloop_unlock (mainloop); 312 + + 313 + + pa_threaded_mainloop_stop (mainloop); 314 + + pa_threaded_mainloop_free (mainloop); 315 + + mainloop = NULL; 316 + +} 317 + + 318 + +static int pulse_open (struct sound_params *sound_params) 319 + +{ 320 + + pa_sample_spec ss; 321 + + pa_buffer_attr ba; 322 + + pa_stream *s; 323 + + 324 + + assert (!stream); 325 + + /* Initialize everything to -1, which in practice gets us 326 + + * about 2 seconds of latency (which is fine). This is not the 327 + + * same as passing NULL for this struct, which gets us an 328 + + * unnecessarily short alsa-like latency. 329 + + */ 330 + + ba.fragsize = (uint32_t) -1; 331 + + ba.tlength = (uint32_t) -1; 332 + + ba.prebuf = (uint32_t) -1; 333 + + ba.minreq = (uint32_t) -1; 334 + + ba.maxlength = (uint32_t) -1; 335 + + 336 + + ss.channels = sound_params->channels; 337 + + ss.rate = sound_params->rate; 338 + + switch (sound_params->fmt) { 339 + + case SFMT_U8: 340 + + ss.format = PA_SAMPLE_U8; 341 + + break; 342 + + case SFMT_S16 | SFMT_LE: 343 + + ss.format = PA_SAMPLE_S16LE; 344 + + break; 345 + + case SFMT_S16 | SFMT_BE: 346 + + ss.format = PA_SAMPLE_S16BE; 347 + + break; 348 + + case SFMT_FLOAT | SFMT_LE: 349 + + ss.format = PA_SAMPLE_FLOAT32LE; 350 + + break; 351 + + case SFMT_FLOAT | SFMT_BE: 352 + + ss.format = PA_SAMPLE_FLOAT32BE; 353 + + break; 354 + + case SFMT_S32 | SFMT_LE: 355 + + ss.format = PA_SAMPLE_S32LE; 356 + + break; 357 + + case SFMT_S32 | SFMT_BE: 358 + + ss.format = PA_SAMPLE_S32BE; 359 + + break; 360 + + 361 + + default: 362 + + fatal ("pulse: got unrequested format"); 363 + + } 364 + + 365 + + debug ("opening stream"); 366 + + 367 + + pa_threaded_mainloop_lock (mainloop); 368 + + 369 + + /* TODO: figure out if there are useful stream properties to set. 370 + + * 371 + + * I do not really see any in proplist.h that we can set from 372 + + * here (there are media title/artist/etc props but we do not 373 + + * have that data available here). 374 + + */ 375 + + s = pa_stream_new (context, "music", &ss, NULL); 376 + + if (!s) 377 + + fatal ("pulse: stream allocation failed"); 378 + + 379 + + pa_stream_set_state_callback (s, stream_state_callback, mainloop); 380 + + pa_stream_set_write_callback (s, stream_write_callback, mainloop); 381 + + 382 + + /* Ignore return value, rely on failed stream state instead. */ 383 + + pa_stream_connect_playback ( 384 + + s, NULL, &ba, 385 + + PA_STREAM_INTERPOLATE_TIMING | 386 + + PA_STREAM_AUTO_TIMING_UPDATE | 387 + + PA_STREAM_ADJUST_LATENCY, 388 + + NULL, NULL); 389 + + 390 + + while (1) { 391 + + pa_stream_state_t state = pa_stream_get_state (s); 392 + + 393 + + if (state == PA_STREAM_READY) 394 + + break; 395 + + 396 + + if (!PA_STREAM_IS_GOOD (state)) { 397 + + error ("PulseAudio stream connection failed"); 398 + + 399 + + goto fail; 400 + + } 401 + + 402 + + debug ("waiting for stream to become ready..."); 403 + + pa_threaded_mainloop_wait (mainloop); 404 + + } 405 + + 406 + + /* Only set the global stream now that it is actually ready */ 407 + + stream = s; 408 + + 409 + + pa_threaded_mainloop_unlock (mainloop); 410 + + 411 + + return 1; 412 + + 413 + +fail: 414 + + pa_stream_unref (s); 415 + + 416 + + pa_threaded_mainloop_unlock (mainloop); 417 + + return 0; 418 + +} 419 + + 420 + +static void pulse_close (void) 421 + +{ 422 + + debug ("closing stream"); 423 + + 424 + + pa_threaded_mainloop_lock (mainloop); 425 + + 426 + + pa_stream_disconnect (stream); 427 + + pa_stream_unref (stream); 428 + + stream = NULL; 429 + + 430 + + pa_threaded_mainloop_unlock (mainloop); 431 + +} 432 + + 433 + +static int pulse_play (const char *buff, const size_t size) 434 + +{ 435 + + size_t offset = 0; 436 + + 437 + + debug ("Got %d bytes to play", (int)size); 438 + + 439 + + pa_threaded_mainloop_lock (mainloop); 440 + + 441 + + /* The buffer is usually writable when we get here, and there 442 + + * are usually few (if any) writes after the first one. So 443 + + * there is no point in doing further writes directly from the 444 + + * callback: we can just do all writes from this thread. 445 + + */ 446 + + 447 + + /* Break out of the loop if some other thread manages to close 448 + + * our stream underneath us. 449 + + */ 450 + + while (stream) { 451 + + size_t towrite = MIN(pa_stream_writable_size (stream), 452 + + size - offset); 453 + + debug ("writing %d bytes", (int)towrite); 454 + + 455 + + /* We have no working way of dealing with errors 456 + + * (see below). */ 457 + + if (pa_stream_write(stream, buff + offset, towrite, 458 + + NULL, 0, PA_SEEK_RELATIVE)) 459 + + error ("pa_stream_write failed"); 460 + + 461 + + offset += towrite; 462 + + 463 + + if (offset >= size) 464 + + break; 465 + + 466 + + pa_threaded_mainloop_wait (mainloop); 467 + + } 468 + + 469 + + pa_threaded_mainloop_unlock (mainloop); 470 + + 471 + + debug ("Done playing!"); 472 + + 473 + + /* We should always return size, calling code does not deal 474 + + * well with anything else. Only read the rest if you want to 475 + + * know why. 476 + + * 477 + + * The output buffer reader thread (out_buf.c:read_thread) 478 + + * repeatedly loads some 64k/0.1s of audio into a buffer on 479 + + * the stack, then calls audio_send_pcm repeatedly until this 480 + + * entire buffer has been processed (similar to the loop in 481 + + * this function). audio_send_pcm applies the softmixer and 482 + + * equalizer, then feeds the result to this function, passing 483 + + * through our return value. 484 + + * 485 + + * So if we return less than size the equalizer/softmixer is 486 + + * re-applied to the remaining data, which is silly. Also, 487 + + * audio_send_pcm checks for our return value being zero and 488 + + * calls fatal() if it is, so try to always process *some* 489 + + * data. Also, out_buf.c uses the return value of this 490 + + * function from the last run through its inner loop to update 491 + + * its time attribute, which means it will be interestingly 492 + + * off if that loop ran more than once. 493 + + * 494 + + * Oh, and alsa.c seems to think it can return -1 to indicate 495 + + * failure, which will cause out_buf.c to rewind its buffer 496 + + * (to before its start, usually). 497 + + */ 498 + + return size; 499 + +} 500 + + 501 + +static void volume_cb (const pa_cvolume *v, void *userdata) 502 + +{ 503 + + int *result = userdata; 504 + + 505 + + if (v) 506 + + *result = 100 * pa_cvolume_avg (v) / PA_VOLUME_NORM; 507 + + 508 + + pa_threaded_mainloop_signal (mainloop, 0); 509 + +} 510 + + 511 + +static void sink_volume_cb (pa_context *c ATTR_UNUSED, 512 + + const pa_sink_info *i, int eol ATTR_UNUSED, 513 + + void *userdata) 514 + +{ 515 + + volume_cb (i ? &i->volume : NULL, userdata); 516 + +} 517 + + 518 + +static void sink_input_volume_cb (pa_context *c ATTR_UNUSED, 519 + + const pa_sink_input_info *i, 520 + + int eol ATTR_UNUSED, 521 + + void *userdata ATTR_UNUSED) 522 + +{ 523 + + volume_cb (i ? &i->volume : NULL, userdata); 524 + +} 525 + + 526 + +static int pulse_read_mixer (void) 527 + +{ 528 + + pa_operation *op; 529 + + int result = 0; 530 + + 531 + + debug ("read mixer"); 532 + + 533 + + pa_threaded_mainloop_lock (mainloop); 534 + + 535 + + if (stream) { 536 + + if (showing_sink_volume) 537 + + op = pa_context_get_sink_info_by_index ( 538 + + context, pa_stream_get_device_index (stream), 539 + + sink_volume_cb, &result); 540 + + else 541 + + op = pa_context_get_sink_input_info ( 542 + + context, pa_stream_get_index (stream), 543 + + sink_input_volume_cb, &result); 544 + + 545 + + while (pa_operation_get_state (op) == PA_OPERATION_RUNNING) 546 + + pa_threaded_mainloop_wait (mainloop); 547 + + 548 + + pa_operation_unref (op); 549 + + } 550 + + 551 + + pa_threaded_mainloop_unlock (mainloop); 552 + + 553 + + return result; 554 + +} 555 + + 556 + +static void pulse_set_mixer (int vol) 557 + +{ 558 + + pa_cvolume v; 559 + + pa_operation *op; 560 + + 561 + + /* Setting volume for one channel does the right thing. */ 562 + + pa_cvolume_set(&v, 1, vol * PA_VOLUME_NORM / 100); 563 + + 564 + + pa_threaded_mainloop_lock (mainloop); 565 + + 566 + + if (stream) { 567 + + if (showing_sink_volume) 568 + + op = pa_context_set_sink_volume_by_index ( 569 + + context, pa_stream_get_device_index (stream), 570 + + &v, NULL, NULL); 571 + + else 572 + + op = pa_context_set_sink_input_volume ( 573 + + context, pa_stream_get_index (stream), 574 + + &v, NULL, NULL); 575 + + 576 + + pa_operation_unref (op); 577 + + } 578 + + 579 + + pa_threaded_mainloop_unlock (mainloop); 580 + +} 581 + + 582 + +static int pulse_get_buff_fill (void) 583 + +{ 584 + + /* This function is problematic. MOC uses it to for the "time 585 + + * remaining" in the UI, but calls it more than once per 586 + + * second (after each chunk of audio played, not for each 587 + + * playback time update). We have to be fairly accurate here 588 + + * for that time remaining to not jump weirdly. But PulseAudio 589 + + * cannot give us a 100% accurate value here, as it involves a 590 + + * server roundtrip. And if we call this a lot it suggests 591 + + * switching to a mode where the value is interpolated, making 592 + + * it presumably more inaccurate (see the flags we pass to 593 + + * pa_stream_connect_playback). 594 + + * 595 + + * MOC also contains what I believe to be a race: it calls 596 + + * audio_get_buff_fill "soon" (after playing the first chunk) 597 + + * after starting playback of the next song, at which point we 598 + + * still have part of the previous song buffered. This means 599 + + * our position into the new song is negative, which fails an 600 + + * assert (in out_buf.c:out_buf_time_get). There is no sane 601 + + * way for us to detect this condition. I believe no other 602 + + * backend triggers this because the assert sits after an 603 + + * implicit float -> int seconds conversion, which means we 604 + + * have to be off by at least an entire second to get a 605 + + * negative value, and none of the other backends have buffers 606 + + * that large (alsa buffers are supposedly a few 100 ms). 607 + + */ 608 + + pa_usec_t buffered_usecs = 0; 609 + + int buffered_bytes = 0; 610 + + 611 + + pa_threaded_mainloop_lock (mainloop); 612 + + 613 + + /* Using pa_stream_get_timing_info and returning the distance 614 + + * between write_index and read_index would be more obvious, 615 + + * but because of how the result is actually used I believe 616 + + * using the latency value is slightly more correct, and it 617 + + * makes the following crash-avoidance hack more obvious. 618 + + */ 619 + + 620 + + /* This function will frequently fail the first time we call 621 + + * it (pulse does not have the requested data yet). We ignore 622 + + * that and just return 0. 623 + + * 624 + + * Deal with stream being NULL too, just in case this is 625 + + * called in a racy fashion similar to how reset() is. 626 + + */ 627 + + if (stream && 628 + + pa_stream_get_latency (stream, &buffered_usecs, NULL) >= 0) { 629 + + /* Crash-avoidance HACK: floor our latency to at most 630 + + * 1 second. It is usually more, but reporting that at 631 + + * the start of playback crashes MOC, and we cannot 632 + + * sanely detect when reporting it is safe. 633 + + */ 634 + + if (buffered_usecs > 1000000) 635 + + buffered_usecs = 1000000; 636 + + 637 + + buffered_bytes = pa_usec_to_bytes ( 638 + + buffered_usecs, 639 + + pa_stream_get_sample_spec (stream)); 640 + + } 641 + + 642 + + pa_threaded_mainloop_unlock (mainloop); 643 + + 644 + + debug ("buffer fill: %d usec / %d bytes", 645 + + (int) buffered_usecs, (int) buffered_bytes); 646 + + 647 + + return buffered_bytes; 648 + +} 649 + + 650 + +static void flush_callback (pa_stream *s ATTR_UNUSED, int success, 651 + + void *userdata) 652 + +{ 653 + + int *result = userdata; 654 + + 655 + + *result = success; 656 + + 657 + + pa_threaded_mainloop_signal (mainloop, 0); 658 + +} 659 + + 660 + +static int pulse_reset (void) 661 + +{ 662 + + pa_operation *op; 663 + + int result = 0; 664 + + 665 + + debug ("reset requested"); 666 + + 667 + + pa_threaded_mainloop_lock (mainloop); 668 + + 669 + + /* We *should* have a stream here, but MOC is racy, so bulletproof */ 670 + + if (stream) { 671 + + op = pa_stream_flush (stream, flush_callback, &result); 672 + + 673 + + while (pa_operation_get_state (op) == PA_OPERATION_RUNNING) 674 + + pa_threaded_mainloop_wait (mainloop); 675 + + 676 + + pa_operation_unref (op); 677 + + } else 678 + + logit ("pulse_reset() called without a stream"); 679 + + 680 + + pa_threaded_mainloop_unlock (mainloop); 681 + + 682 + + return result; 683 + +} 684 + + 685 + +static int pulse_get_rate (void) 686 + +{ 687 + + /* This is called once right after open. Do not bother making 688 + + * this fast. */ 689 + + 690 + + int result; 691 + + 692 + + pa_threaded_mainloop_lock (mainloop); 693 + + 694 + + if (stream) 695 + + result = pa_stream_get_sample_spec (stream)->rate; 696 + + else { 697 + + error ("get_rate called without a stream"); 698 + + result = 0; 699 + + } 700 + + 701 + + pa_threaded_mainloop_unlock (mainloop); 702 + + 703 + + return result; 704 + +} 705 + + 706 + +static void pulse_toggle_mixer_channel (void) 707 + +{ 708 + + showing_sink_volume = !showing_sink_volume; 709 + +} 710 + + 711 + +static void sink_name_cb (pa_context *c ATTR_UNUSED, 712 + + const pa_sink_info *i, int eol ATTR_UNUSED, 713 + + void *userdata) 714 + +{ 715 + + char **result = userdata; 716 + + 717 + + if (i && !*result) 718 + + *result = xstrdup (i->name); 719 + + 720 + + pa_threaded_mainloop_signal (mainloop, 0); 721 + +} 722 + + 723 + +static void sink_input_name_cb (pa_context *c ATTR_UNUSED, 724 + + const pa_sink_input_info *i, 725 + + int eol ATTR_UNUSED, 726 + + void *userdata) 727 + +{ 728 + + char **result = userdata; 729 + + 730 + + if (i && !*result) 731 + + *result = xstrdup (i->name); 732 + + 733 + + pa_threaded_mainloop_signal (mainloop, 0); 734 + +} 735 + + 736 + +static char *pulse_get_mixer_channel_name (void) 737 + +{ 738 + + char *result = NULL; 739 + + pa_operation *op; 740 + + 741 + + pa_threaded_mainloop_lock (mainloop); 742 + + 743 + + if (stream) { 744 + + if (showing_sink_volume) 745 + + op = pa_context_get_sink_info_by_index ( 746 + + context, pa_stream_get_device_index (stream), 747 + + sink_name_cb, &result); 748 + + else 749 + + op = pa_context_get_sink_input_info ( 750 + + context, pa_stream_get_index (stream), 751 + + sink_input_name_cb, &result); 752 + + 753 + + while (pa_operation_get_state (op) == PA_OPERATION_RUNNING) 754 + + pa_threaded_mainloop_wait (mainloop); 755 + + 756 + + pa_operation_unref (op); 757 + + } 758 + + 759 + + pa_threaded_mainloop_unlock (mainloop); 760 + + 761 + + if (!result) 762 + + result = xstrdup ("disconnected"); 763 + + 764 + + return result; 765 + +} 766 + + 767 + +void pulse_funcs (struct hw_funcs *funcs) 768 + +{ 769 + + funcs->init = pulse_init; 770 + + funcs->shutdown = pulse_shutdown; 771 + + funcs->open = pulse_open; 772 + + funcs->close = pulse_close; 773 + + funcs->play = pulse_play; 774 + + funcs->read_mixer = pulse_read_mixer; 775 + + funcs->set_mixer = pulse_set_mixer; 776 + + funcs->get_buff_fill = pulse_get_buff_fill; 777 + + funcs->reset = pulse_reset; 778 + + funcs->get_rate = pulse_get_rate; 779 + + funcs->toggle_mixer_channel = pulse_toggle_mixer_channel; 780 + + funcs->get_mixer_channel_name = pulse_get_mixer_channel_name; 781 + +} 782 + diff --git a/pulse.h b/pulse.h 783 + new file mode 100644 784 + --- /dev/null 785 + +++ b/pulse.h 786 + @@ -0,0 +1,14 @@ 787 + +#ifndef PULSE_H 788 + +#define PULSE_H 789 + + 790 + +#ifdef __cplusplus 791 + +extern "C" { 792 + +#endif 793 + + 794 + +void pulse_funcs (struct hw_funcs *funcs); 795 + + 796 + +#ifdef __cplusplus 797 + +} 798 + +#endif 799 + + 800 + +#endif