mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-28 18:03:04 +00:00
Merge pull request #60859 from aethelz/moc-refactor
moc: refactor options, add patches
This commit is contained in:
commit
c6f7545209
@ -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;
|
||||
};
|
||||
}
|
||||
|
33
pkgs/applications/audio/moc/moc-ffmpeg4.patch
Normal file
33
pkgs/applications/audio/moc/moc-ffmpeg4.patch
Normal file
@ -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);
|
800
pkgs/applications/audio/moc/pulseaudio.patch
Normal file
800
pkgs/applications/audio/moc/pulseaudio.patch
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user