From 5b67f84855ec22ac5ff99ab2adbb78f3bd84a780 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 21 Jul 2016 22:13:32 +0200 Subject: [PATCH 01/13] [pulseaudio] First draft --- configure.ac | 32 +- src/Makefile.am | 16 +- src/outputs.c | 6 + src/outputs.h | 3 + src/outputs/pulse.c | 732 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 764 insertions(+), 25 deletions(-) create mode 100644 src/outputs/pulse.c diff --git a/configure.ac b/configure.ac index 2aa02f92..3849bd58 100644 --- a/configure.ac +++ b/configure.ac @@ -214,30 +214,24 @@ AS_IF([test "x$enable_mpd" != "xno"], [ CPPFLAGS="${CPPFLAGS} -DMPD" ]) AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"]) -dnl --- End options --- -dnl Selection of local audio sound system -dnl TODO exchange oss4 with Pulseaudio -case "$host" in - *-*-linux-*) - use_alsa=true - use_oss4=false - ;; - *-*-kfreebsd*-*|*-*-freebsd*) - use_alsa=true - use_oss4=false - ;; -esac - -AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa], [use ALSA (default yes)]), [ - AS_IF([test "x$with_alsa" = "xyes"], [use_alsa=true], [use_alsa=false]) -]) -if test x$use_alsa = xtrue; then +dnl ALSA +AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa], [use ALSA (default yes)])) +AS_IF([test "x$with_alsa" = "xyes"], [ CPPFLAGS="${CPPFLAGS} -DALSA" PKG_CHECK_MODULES(ALSA, [ alsa ]) -fi +]) AM_CONDITIONAL(COND_ALSA, test x$use_alsa = xtrue) +dnl PULSEAUDIO +AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--with-pulseaudio], [use Pulseaudio (default no)])) +AS_IF([test "x$with_pulseaudio" = "xyes"], [ + CPPFLAGS="${CPPFLAGS} -DPULSEAUDIO" + PKG_CHECK_MODULES(LIBPULSE, [ libpulse ]) +]) +AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" = "xyes"]) +dnl --- End options --- + dnl Checks for header files. AC_HEADER_STDC AC_HEADER_SYS_WAIT diff --git a/src/Makefile.am b/src/Makefile.am index 3b40dfe8..2663261b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -29,6 +29,10 @@ if COND_ALSA ALSA_SRC=outputs/alsa.c endif +if COND_PULSEAUDIO +PULSEAUDIO_SRC=outputs/pulse.c +endif + GPERF_FILES = \ daap_query.gperf \ rsp_query.gperf \ @@ -62,15 +66,15 @@ forked_daapd_CPPFLAGS = -D_GNU_SOURCE \ forked_daapd_CFLAGS = \ @ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \ - @CONFUSE_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \ - @LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@ \ + @CONFUSE_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ @SPOTIFY_CFLAGS@ \ + @LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @LIBPULSE_CFLAGS@ \ @LIBCURL_CFLAGS@ @LIBPROTOBUF_C_CFLAGS@ @GNUTLS_CFLAGS@ @JSON_C_CFLAGS@ forked_daapd_LDADD = -lrt \ @ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \ - @CONFUSE_LIBS@ @LIBEVENT_LIBS@ \ - @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \ - @LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ \ + @CONFUSE_LIBS@ @LIBEVENT_LIBS@ @LIBUNISTRING@ \ + @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ @SPOTIFY_LIBS@ \ + @LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBPULSE_LIBS@ \ @LIBCURL_LIBS@ @LIBPROTOBUF_C_LIBS@ @GNUTLS_LIBS@ @JSON_C_LIBS@ forked_daapd_SOURCES = main.c \ @@ -104,7 +108,7 @@ forked_daapd_SOURCES = main.c \ worker.c worker.h \ outputs.h outputs.c \ outputs/raop.c outputs/streaming.c outputs/dummy.c \ - $(ALSA_SRC) $(CHROMECAST_SRC) \ + $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ evrtsp/rtsp.c evrtp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \ $(SPOTIFY_SRC) \ $(LASTFM_SRC) \ diff --git a/src/outputs.c b/src/outputs.c index 0ca17fbc..2a96a65c 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -37,6 +37,9 @@ extern struct output_definition output_dummy; #ifdef ALSA extern struct output_definition output_alsa; #endif +#ifdef PULSEAUDIO +extern struct output_definition output_pulse; +#endif #ifdef CHROMECAST extern struct output_definition output_cast; #endif @@ -49,6 +52,9 @@ static struct output_definition *outputs[] = { #ifdef ALSA &output_alsa, #endif +#ifdef PULSEAUDIO + &output_pulse, +#endif #ifdef CHROMECAST &output_cast, #endif diff --git a/src/outputs.h b/src/outputs.h index cec7b9d7..25c77c80 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -55,6 +55,9 @@ enum output_types #ifdef ALSA OUTPUT_TYPE_ALSA, #endif +#ifdef PULSEAUDIO + OUTPUT_TYPE_PULSE, +#endif #ifdef CHROMECAST OUTPUT_TYPE_CAST, #endif diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c new file mode 100644 index 00000000..5abea4b6 --- /dev/null +++ b/src/outputs/pulse.c @@ -0,0 +1,732 @@ +/* + * Copyright (C) 2016 Espen Jürgensen + * + * Adapted from pulseaudio's simple.c + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "misc.h" +#include "conffile.h" +#include "logger.h" +#include "player.h" +#include "outputs.h" + +struct pulse +{ + pa_threaded_mainloop *mainloop; + pa_context *context; + + int operation_success; +}; + +struct pulse_session +{ + pa_stream_state_t state; + pa_stream *stream; + + char *devname; + int volume; + + struct event *deferredev; + output_status_cb defer_cb; + + /* Do not dereference - only passed to the status cb */ + struct output_device *device; + struct output_session *output_session; + output_status_cb status_cb; + + struct pulse_session *next; +}; + +// From player.c +extern struct event_base *evbase_player; + +// Globals +static struct pulse pulse; +static struct pulse_session *sessions; + +/* Forwards */ +static void +defer_cb(int fd, short what, void *arg); + +/* ---------------------------- SESSION HANDLING ---------------------------- */ + +static void +pulse_session_free(struct pulse_session *ps) +{ + event_free(ps->deferredev); + + if (ps->stream) + { + pa_stream_disconnect(ps->stream); + pa_stream_unref(ps->stream); + } + + if (ps->devname) + free(ps->devname); + + free(ps->output_session); + + free(ps); +} + +static void +pulse_session_cleanup(struct pulse_session *ps) +{ + struct pulse_session *p; + + if (ps == sessions) + sessions = sessions->next; + else + { + for (p = sessions; p && (p->next != ps); p = p->next) + ; /* EMPTY */ + + if (!p) + DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct pulse_session not found in list; BUG!\n"); + else + p->next = ps->next; + } + + pulse_session_free(ps); +} + +static struct pulse_session * +pulse_session_make(struct output_device *device, output_status_cb cb) +{ + struct output_session *os; + struct pulse_session *ps; + + os = calloc(1, sizeof(struct output_session)); + ps = calloc(1, sizeof(struct pulse_session)); + if (!os || !ps) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for Pulseaudio session\n"); + return NULL; + } + + ps->deferredev = evtimer_new(evbase_player, defer_cb, ps); + if (!ps->deferredev) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for Pulseaudio deferred event\n"); + free(os); + free(ps); + return NULL; + } + + os->session = ps; + os->type = device->type; + + ps->output_session = os; + ps->state = PA_STREAM_UNCONNECTED; + ps->device = device; + ps->status_cb = cb; + ps->volume = device->volume; + ps->devname = strdup(device->name); + + ps->next = sessions; + sessions = ps; + + return ps; +} + +/* ---------------------------- STATUS HANDLERS ----------------------------- */ + +// Maps our internal state to the generic output state and then makes a callback +// to the player to tell that state. Note: Will free the session if the state is +// stopped or failed. +static void +defer_cb(int fd, short what, void *arg) +{ + struct pulse_session *ps = arg; + enum output_device_state state; + + switch (ps->state) + { + case PA_STREAM_FAILED: + state = OUTPUT_STATE_FAILED; + break; + case PA_STREAM_UNCONNECTED: + case PA_STREAM_TERMINATED: + state = OUTPUT_STATE_STOPPED; + break; + case PA_STREAM_READY: + state = OUTPUT_STATE_CONNECTED; + break; + case PA_STREAM_CREATING: + state = OUTPUT_STATE_STARTUP; + break; + default: + DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in pulse_status()\n"); + state = OUTPUT_STATE_FAILED; + } + + if (ps->defer_cb) + ps->defer_cb(ps->device, ps->output_session, state); + + if (!(state > OUTPUT_STATE_STOPPED)) + pulse_session_cleanup(ps); +} + +static void +pulse_status(struct pulse_session *ps) +{ + ps->defer_cb = ps->status_cb; + event_active(ps->deferredev, 0, 0); + ps->status_cb = NULL; +} + + +/* --------------------- CALLBACKS FROM PULSEAUDIO THREAD ------------------- */ + +static void +stream_state_cb(pa_stream *s, void * userdata) +{ + struct pulse *p = &pulse; + struct pulse_session *ps = userdata; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stream state CB\n"); + + ps->state = pa_stream_get_state(s); + + switch (ps->state) + { + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: +// pulse_status(ps); // TODO Not thread safe + pa_threaded_mainloop_signal(p->mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + break; + } +} + +static void +stream_request_cb(pa_stream *s, size_t length, void *userdata) +{ + struct pulse *p = &pulse; + + pa_threaded_mainloop_signal(p->mainloop, 0); +} + +static void +stream_latency_update_cb(pa_stream *s, void *userdata) +{ + struct pulse *p = &pulse; + + pa_threaded_mainloop_signal(p->mainloop, 0); +} + +/*static void +success_cb(pa_stream *s, int success, void *userdata) +{ + struct pulse *p = userdata; + + p->operation_success = success; + pa_threaded_mainloop_signal(p->mainloop, 0); +} +*/ + +// TODO Remove devices? +// TODO Will this get called multiple times? +static void +sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) +{ + struct output_device *device; + uint32_t id; + + if (eol > 0) + return; + + id = djb_hash(i->name, strlen(i->name)); + if (!id) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not hash Pulseaudio sink name (%s)\n", i->name); + return; + } + + DPRINTF(E_DBG, L_LAUDIO, "Event for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, id); + + device = calloc(1, sizeof(struct output_device)); + if (!device) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for new Pulseaudio sink\n"); + return; + } + + device->id = id; + device->name = strdup(i->name); + device->type = OUTPUT_TYPE_PULSE; + device->type_name = outputs_name(device->type); + device->advertised = 1; + + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", i->description, i->name); + + player_device_add(device); +} + +static void +context_state_cb(pa_context *c, void *userdata) +{ + struct pulse *p = userdata; + pa_operation *o; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio context state CB\n"); + + switch (pa_context_get_state(c)) + { + case PA_CONTEXT_READY: + DPRINTF(E_DBG, L_LAUDIO, "CTX READY\n"); + o = pa_context_get_sink_info_list(c, sinklist_cb, p); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to Pulseaudio sink info\n"); + return; + } + pa_operation_unref(o); + pa_threaded_mainloop_signal(p->mainloop, 0); + break; + + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + DPRINTF(E_DBG, L_LAUDIO, "CTX FAIL\n"); + pa_threaded_mainloop_signal(p->mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + DPRINTF(E_DBG, L_LAUDIO, "CTX START\n"); + break; + } +} + + +/* ------------------------------- MISC HELPERS ----------------------------- */ + +// Used by init and deinit to stop main thread +static void +pulse_free(struct pulse *p) +{ + if (p->mainloop) + pa_threaded_mainloop_stop(p->mainloop); + + if (p->context) + { + pa_context_disconnect(p->context); + pa_context_unref(p->context); + } + + if (p->mainloop) + pa_threaded_mainloop_free(p->mainloop); +} + +static int +stream_open(struct pulse_session *ps) +{ + struct pulse *p = &pulse; + pa_stream_flags_t flags; + pa_sample_spec ss; + int ret; + + DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream\n"); + + ss.format = PA_SAMPLE_S16LE; + ss.channels = 2; + ss.rate = 44100; + + pa_threaded_mainloop_lock(p->mainloop); + + if (!(ps->stream = pa_stream_new(p->context, "forked-daapd audio", &ss, NULL))) + goto unlock_and_fail; + + pa_stream_set_state_callback(ps->stream, stream_state_cb, ps); + pa_stream_set_write_callback(ps->stream, stream_request_cb, ps); + pa_stream_set_latency_update_callback(ps->stream, stream_latency_update_cb, ps); + + flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE; + + // TODO sync stream + ret = pa_stream_connect_playback(ps->stream, ps->devname, NULL, flags, NULL, NULL); + if (ret < 0) + goto unlock_and_fail; + + for (;;) + { + ps->state = pa_stream_get_state(ps->stream); + + if (ps->state == PA_STREAM_READY) + break; + + if (!PA_STREAM_IS_GOOD(ps->state)) + goto unlock_and_fail; + + /* Wait until the stream is ready */ + pa_threaded_mainloop_wait(p->mainloop); + } + + pa_threaded_mainloop_unlock(p->mainloop); + + return 0; + + unlock_and_fail: + ret = pa_context_errno(p->context); + + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s': %s\n", ps->devname, pa_strerror(ret)); + + pa_threaded_mainloop_unlock(p->mainloop); + + return -1; +} + +static void +stream_close(struct pulse_session *ps) +{ + struct pulse *p = &pulse; + + pa_threaded_mainloop_lock(p->mainloop); + + pa_stream_disconnect(ps->stream); + + for (;;) + { + ps->state = pa_stream_get_state(ps->stream); + + if (ps->state != PA_STREAM_READY) + break; + + /* Wait until the stream is closed */ + pa_threaded_mainloop_wait(p->mainloop); + } + + pa_threaded_mainloop_unlock(p->mainloop); +} + +static int +check_state_context(struct pulse *p) +{ + pa_context_state_t state; + int errno; + + state = pa_context_get_state(p->context); + if (!PA_CONTEXT_IS_GOOD(state)) + { + if (state == PA_CONTEXT_FAILED) + { + errno = pa_context_errno(p->context); + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context failed with error: %s\n", pa_strerror(errno)); + } + else + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context invalid state\n"); + + return -1; + } + + return 0; +} + +static int +check_state_stream(struct pulse *p, struct pulse_session *ps) +{ + pa_stream_state_t state; + int errno; + + state = pa_stream_get_state(ps->stream); + if (!PA_STREAM_IS_GOOD(state)) + { + if (state == PA_STREAM_FAILED) + { + errno = pa_context_errno(p->context); + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio stream failed with error: %s\n", pa_strerror(errno)); + } + else + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio stream invalid state\n"); + + return -1; + } + + return 0; +} + + +/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ + +static int +pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +{ + struct pulse_session *ps; + int ret; + + ps = pulse_session_make(device, cb); + if (!ps) + return -1; + + ret = stream_open(ps); + if (ret < 0) + return -1; + + pulse_status(ps); + + return 0; +} + +static void +pulse_device_stop(struct output_session *session) +{ + struct pulse_session *ps = session->session; + + stream_close(ps); + + pulse_status(ps); +} + +static int +pulse_device_probe(struct output_device *device, output_status_cb cb) +{ + struct pulse_session *ps; + int ret; + + ps = pulse_session_make(device, cb); + if (!ps) + return -1; + + ret = stream_open(ps); + if (ret < 0) + { + pulse_session_cleanup(ps); + return -1; + } + + stream_close(ps); + + pulse_status(ps); + + return 0; +} + +static int +pulse_volume_set(struct output_device *device, output_status_cb cb) +{ + return 1; +} + +static void +pulse_playback_start(uint64_t next_pkt, struct timespec *ts) +{ + if (!sessions) + return; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio playback start called\n"); +} + +static void +pulse_playback_stop(void) +{ +} + +static void +pulse_write(uint8_t *buf, uint64_t rtptime) +{ + struct pulse *p = &pulse; + struct pulse_session *ps; + struct pulse_session *next; + size_t length; + int ret; + + if (!sessions) + return; + + length = STOB(AIRTUNES_V2_PACKET_SAMPLES); + + pa_threaded_mainloop_lock(p->mainloop); + + if (check_state_context(p) < 0) + { + // TODO Not a good situation... we should kill all sessions + pa_threaded_mainloop_unlock(p->mainloop); + return; + } + + for (ps = sessions; ps; ps = next) + { + next = ps->next; + + if (check_state_stream(p, ps) < 0) + { + pulse_status(ps); // Note: This will nuke the session (deferred) + continue; + } + + ret = pa_stream_writable_size(ps->stream); + if (ret < 0) + { + ret = pa_context_errno(p->context); + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio error determining writable size: %s\n", pa_strerror(ret)); + continue; + } + else if (ret < length) + { + DPRINTF(E_WARN, L_LAUDIO, "Pulseaudio buffer overrun detected, skipping packet\n"); + continue; + } + + ret = pa_stream_write(ps->stream, buf, length, NULL, 0LL, PA_SEEK_RELATIVE); + if (ret < 0) + { + ret = pa_context_errno(p->context); + DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data: %s\n", pa_strerror(ret)); + continue; + } + } + + pa_threaded_mainloop_unlock(p->mainloop); + return; +} + +static int +pulse_flush(output_status_cb cb, uint64_t rtptime) +{ + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush called\n"); + + return 0; +} + +static void +pulse_set_status_cb(struct output_session *session, output_status_cb cb) +{ + struct pulse_session *ps = session->session; + + ps->status_cb = cb; +} + +static int +pulse_init(void) +{ + struct pulse *p; + cfg_t *cfg_audio; + char *type; + int state; + int ret; + + cfg_audio = cfg_getsec(cfg, "audio"); + type = cfg_getstr(cfg_audio, "type"); + + if (type && (strcasecmp(type, "pulseaudio") != 0)) + return -1; + + p = &pulse; + ret = 0; + + if (!(p->mainloop = pa_threaded_mainloop_new())) + goto fail; + +// TODO +#ifdef HAVE_PULSE_MAINLOOP_SET_NAME + pa_threaded_mainloop_set_name(p->mainloop, "pulseaudio"); +#endif + + if (!(p->context = pa_context_new(pa_threaded_mainloop_get_api(p->mainloop), "forked-daapd"))) + goto fail; + + pa_context_set_state_callback(p->context, context_state_cb, p); + + if (pa_context_connect(p->context, NULL, 0, NULL) < 0) + { + ret = pa_context_errno(p->context); + goto fail; + } + + pa_threaded_mainloop_lock(p->mainloop); + + if (pa_threaded_mainloop_start(p->mainloop) < 0) + goto unlock_and_fail; + + for (;;) + { + state = pa_context_get_state(p->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) + { + ret = pa_context_errno(p->context); + goto unlock_and_fail; + } + + /* Wait until the context is ready */ + pa_threaded_mainloop_wait(p->mainloop); + } + + pa_threaded_mainloop_unlock(p->mainloop); + + return 0; + + unlock_and_fail: + pa_threaded_mainloop_unlock(p->mainloop); + + fail: + if (ret) + DPRINTF(E_LOG, L_LAUDIO, "Error initializing Pulseaudio: %s\n", pa_strerror(ret)); + + pulse_free(p); + return -1; +} + +static void +pulse_deinit(void) +{ + pulse_free(&pulse); +} + +struct output_definition output_pulse = +{ + .name = "Pulseaudio", + .type = OUTPUT_TYPE_PULSE, + .priority = 3, + .disabled = 0, + .init = pulse_init, + .deinit = pulse_deinit, + .device_start = pulse_device_start, + .device_stop = pulse_device_stop, + .device_probe = pulse_device_probe, + .device_volume_set = pulse_volume_set, + .playback_start = pulse_playback_start, + .playback_stop = pulse_playback_stop, + .write = pulse_write, + .flush = pulse_flush, + .status_cb = pulse_set_status_cb, +}; + From eb40415348194e781b4feee43a007f0d50659eda Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 21 Jul 2016 22:31:39 +0200 Subject: [PATCH 02/13] [pulseaudio] A bit of renaming --- src/outputs/pulse.c | 70 +++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 5abea4b6..bcd4e8e6 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -354,9 +354,31 @@ pulse_free(struct pulse *p) } static int -stream_open(struct pulse_session *ps) +context_check(pa_context *context) +{ + pa_context_state_t state; + int errno; + + state = pa_context_get_state(context); + if (!PA_CONTEXT_IS_GOOD(state)) + { + if (state == PA_CONTEXT_FAILED) + { + errno = pa_context_errno(context); + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context failed with error: %s\n", pa_strerror(errno)); + } + else + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context invalid state\n"); + + return -1; + } + + return 0; +} + +static int +stream_open(struct pulse *p, struct pulse_session *ps) { - struct pulse *p = &pulse; pa_stream_flags_t flags; pa_sample_spec ss; int ret; @@ -412,10 +434,8 @@ stream_open(struct pulse_session *ps) } static void -stream_close(struct pulse_session *ps) +stream_close(struct pulse *p, struct pulse_session *ps) { - struct pulse *p = &pulse; - pa_threaded_mainloop_lock(p->mainloop); pa_stream_disconnect(ps->stream); @@ -435,30 +455,7 @@ stream_close(struct pulse_session *ps) } static int -check_state_context(struct pulse *p) -{ - pa_context_state_t state; - int errno; - - state = pa_context_get_state(p->context); - if (!PA_CONTEXT_IS_GOOD(state)) - { - if (state == PA_CONTEXT_FAILED) - { - errno = pa_context_errno(p->context); - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context failed with error: %s\n", pa_strerror(errno)); - } - else - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio context invalid state\n"); - - return -1; - } - - return 0; -} - -static int -check_state_stream(struct pulse *p, struct pulse_session *ps) +stream_check(struct pulse *p, struct pulse_session *ps) { pa_stream_state_t state; int errno; @@ -493,7 +490,7 @@ pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t r if (!ps) return -1; - ret = stream_open(ps); + ret = stream_open(&pulse, ps); if (ret < 0) return -1; @@ -507,7 +504,7 @@ pulse_device_stop(struct output_session *session) { struct pulse_session *ps = session->session; - stream_close(ps); + stream_close(&pulse, ps); pulse_status(ps); } @@ -522,14 +519,14 @@ pulse_device_probe(struct output_device *device, output_status_cb cb) if (!ps) return -1; - ret = stream_open(ps); + ret = stream_open(&pulse, ps); if (ret < 0) { pulse_session_cleanup(ps); return -1; } - stream_close(ps); + stream_close(&pulse, ps); pulse_status(ps); @@ -572,7 +569,7 @@ pulse_write(uint8_t *buf, uint64_t rtptime) pa_threaded_mainloop_lock(p->mainloop); - if (check_state_context(p) < 0) + if (context_check(p->context) < 0) { // TODO Not a good situation... we should kill all sessions pa_threaded_mainloop_unlock(p->mainloop); @@ -583,7 +580,7 @@ pulse_write(uint8_t *buf, uint64_t rtptime) { next = ps->next; - if (check_state_stream(p, ps) < 0) + if (stream_check(p, ps) < 0) { pulse_status(ps); // Note: This will nuke the session (deferred) continue; @@ -634,7 +631,7 @@ pulse_set_status_cb(struct output_session *session, output_status_cb cb) static int pulse_init(void) { - struct pulse *p; + struct pulse *p = &pulse; cfg_t *cfg_audio; char *type; int state; @@ -646,7 +643,6 @@ pulse_init(void) if (type && (strcasecmp(type, "pulseaudio") != 0)) return -1; - p = &pulse; ret = 0; if (!(p->mainloop = pa_threaded_mainloop_new())) From 4399466f21df5bb5e7cb9d2c8264e291c250d9e8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 21 Jul 2016 23:15:06 +0200 Subject: [PATCH 03/13] [pulseaudio] Friendly naming of sinks in speaker list --- src/outputs/alsa.c | 2 +- src/outputs/pulse.c | 35 ++++++++++++++++++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index ecec35cc..1c10c043 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -1012,7 +1012,7 @@ alsa_init(void) device->advertised = 1; device->has_video = 0; - DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' using friendly name '%s'\n", card_name, nickname); + DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", card_name, nickname); player_device_add(device); diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index bcd4e8e6..9732003e 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -264,19 +264,12 @@ static void sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { struct output_device *device; - uint32_t id; + const char *name; if (eol > 0) return; - id = djb_hash(i->name, strlen(i->name)); - if (!id) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not hash Pulseaudio sink name (%s)\n", i->name); - return; - } - - DPRINTF(E_DBG, L_LAUDIO, "Event for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, id); + DPRINTF(E_DBG, L_LAUDIO, "Event for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, i->index); device = calloc(1, sizeof(struct output_device)); if (!device) @@ -285,14 +278,25 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) return; } - device->id = id; - device->name = strdup(i->name); + if (i->index == 0) + { + name = cfg_getstr(cfg_getsec(cfg, "audio"), "nickname"); + + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s) with name '%s'\n", i->description, i->name, name); + } + else + { + name = i->description; + + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", i->description, i->name); + } + + device->id = i->index; + device->name = strdup(name); device->type = OUTPUT_TYPE_PULSE; device->type_name = outputs_name(device->type); device->advertised = 1; - DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", i->description, i->name); - player_device_add(device); } @@ -632,14 +636,11 @@ static int pulse_init(void) { struct pulse *p = &pulse; - cfg_t *cfg_audio; char *type; int state; int ret; - cfg_audio = cfg_getsec(cfg, "audio"); - type = cfg_getstr(cfg_audio, "type"); - + type = cfg_getstr(cfg_getsec(cfg, "audio"), "type"); if (type && (strcasecmp(type, "pulseaudio") != 0)) return -1; From db6823d676ad57e158a968ef76489ac5a6e1e906 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 30 Jul 2016 00:05:32 +0200 Subject: [PATCH 04/13] [pulseaudio] Support for adding/removing sinks --- src/outputs/pulse.c | 101 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 9732003e..9089ce67 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -38,12 +38,15 @@ #include "logger.h" #include "player.h" #include "outputs.h" +#include "commands.h" struct pulse { pa_threaded_mainloop *mainloop; pa_context *context; + struct commands_base *cmdbase; + int operation_success; }; @@ -66,6 +69,11 @@ struct pulse_session struct pulse_session *next; }; +union pulse_arg +{ + struct pulse_session *ps; +}; + // From player.c extern struct event_base *evbase_player; @@ -150,7 +158,7 @@ pulse_session_make(struct output_device *device, output_status_cb cb) ps->device = device; ps->status_cb = cb; ps->volume = device->volume; - ps->devname = strdup(device->name); + ps->devname = strdup(device->extra_device_info); ps->next = sessions; sessions = ps; @@ -204,6 +212,18 @@ pulse_status(struct pulse_session *ps) ps->status_cb = NULL; } +// Callbacks from the Pulseaudio thread will invoke commands_async_exec(), +// which will trigger a status update in the player thread +static enum command_state +pulse_status_cmd_wrapper(void *arg, int *ret) +{ + union pulse_arg *cmdarg = arg; + + pulse_status(cmdarg->ps); + + return COMMAND_END; +} + /* --------------------- CALLBACKS FROM PULSEAUDIO THREAD ------------------- */ @@ -212,6 +232,7 @@ stream_state_cb(pa_stream *s, void * userdata) { struct pulse *p = &pulse; struct pulse_session *ps = userdata; + union pulse_arg *cmdarg; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stream state CB\n"); @@ -222,7 +243,9 @@ stream_state_cb(pa_stream *s, void * userdata) case PA_STREAM_READY: case PA_STREAM_FAILED: case PA_STREAM_TERMINATED: -// pulse_status(ps); // TODO Not thread safe + cmdarg = calloc(1, sizeof(union pulse_arg)); + cmdarg->ps = ps; + commands_exec_async(p->cmdbase, pulse_status_cmd_wrapper, cmdarg); pa_threaded_mainloop_signal(p->mainloop, 0); break; @@ -258,8 +281,6 @@ success_cb(pa_stream *s, int success, void *userdata) } */ -// TODO Remove devices? -// TODO Will this get called multiple times? static void sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) { @@ -269,7 +290,7 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) if (eol > 0) return; - DPRINTF(E_DBG, L_LAUDIO, "Event for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, i->index); + DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, i->index); device = calloc(1, sizeof(struct output_device)); if (!device) @@ -296,10 +317,51 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) device->type = OUTPUT_TYPE_PULSE; device->type_name = outputs_name(device->type); device->advertised = 1; + device->extra_device_info = strdup(i->name); player_device_add(device); } +static void +subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) +{ + struct output_device *device; + pa_operation *o; + + DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio subscribe (id %" PRIu32 ", event %d)\n", index, t); + + if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) != PA_SUBSCRIPTION_EVENT_SINK) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio subscribe called back with unknown event\n"); + return; + } + + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) + { + device = calloc(1, sizeof(struct output_device)); + if (!device) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for temp Pulseaudio device\n"); + return; + } + + device->id = index; + + DPRINTF(E_LOG, L_LAUDIO, "Removing Pulseaudio sink with id %" PRIu32 "\n", index); + + player_device_remove(device); + return; + } + + o = pa_context_get_sink_info_by_index(c, index, sinklist_cb, NULL); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio error getting sink info for id %" PRIu32 "\n", index); + return; + } + pa_operation_unref(o); +} + static void context_state_cb(pa_context *c, void *userdata) { @@ -312,13 +374,23 @@ context_state_cb(pa_context *c, void *userdata) { case PA_CONTEXT_READY: DPRINTF(E_DBG, L_LAUDIO, "CTX READY\n"); - o = pa_context_get_sink_info_list(c, sinklist_cb, p); + o = pa_context_get_sink_info_list(c, sinklist_cb, NULL); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not list Pulseaudio sink info\n"); + return; + } + pa_operation_unref(o); + + pa_context_set_subscribe_callback(c, subscribe_cb, NULL); + o = pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL); if (!o) { DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to Pulseaudio sink info\n"); return; } pa_operation_unref(o); + pa_threaded_mainloop_signal(p->mainloop, 0); break; @@ -353,6 +425,9 @@ pulse_free(struct pulse *p) pa_context_unref(p->context); } + if (p->cmdbase) + commands_base_free(p->cmdbase); + if (p->mainloop) pa_threaded_mainloop_free(p->mainloop); } @@ -498,7 +573,7 @@ pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t r if (ret < 0) return -1; - pulse_status(ps); +// pulse_status(ps); return 0; } @@ -510,7 +585,7 @@ pulse_device_stop(struct output_session *session) stream_close(&pulse, ps); - pulse_status(ps); +// pulse_status(ps); } static int @@ -537,6 +612,12 @@ pulse_device_probe(struct output_device *device, output_status_cb cb) return 0; } +static void +pulse_device_free_extra(struct output_device *device) +{ + free(device->extra_device_info); +} + static int pulse_volume_set(struct output_device *device, output_status_cb cb) { @@ -649,6 +730,9 @@ pulse_init(void) if (!(p->mainloop = pa_threaded_mainloop_new())) goto fail; + if (!(p->cmdbase = commands_base_new(evbase_player, NULL))) + goto fail; + // TODO #ifdef HAVE_PULSE_MAINLOOP_SET_NAME pa_threaded_mainloop_set_name(p->mainloop, "pulseaudio"); @@ -719,6 +803,7 @@ struct output_definition output_pulse = .device_start = pulse_device_start, .device_stop = pulse_device_stop, .device_probe = pulse_device_probe, + .device_free_extra = pulse_device_free_extra, .device_volume_set = pulse_volume_set, .playback_start = pulse_playback_start, .playback_stop = pulse_playback_stop, From 9b243f855a3bebe27f4112b6e02947857bf3167b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 24 Aug 2016 21:23:33 +0200 Subject: [PATCH 05/13] [pulseaudio] Support for setting volume --- src/outputs/pulse.c | 50 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 9089ce67..37e35fc6 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -238,14 +238,15 @@ stream_state_cb(pa_stream *s, void * userdata) ps->state = pa_stream_get_state(s); + cmdarg = calloc(1, sizeof(union pulse_arg)); + cmdarg->ps = ps; + commands_exec_async(p->cmdbase, pulse_status_cmd_wrapper, cmdarg); + switch (ps->state) { case PA_STREAM_READY: case PA_STREAM_FAILED: case PA_STREAM_TERMINATED: - cmdarg = calloc(1, sizeof(union pulse_arg)); - cmdarg->ps = ps; - commands_exec_async(p->cmdbase, pulse_status_cmd_wrapper, cmdarg); pa_threaded_mainloop_signal(p->mainloop, 0); break; @@ -573,7 +574,7 @@ pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t r if (ret < 0) return -1; -// pulse_status(ps); + pulse_status(ps); return 0; } @@ -585,7 +586,7 @@ pulse_device_stop(struct output_session *session) stream_close(&pulse, ps); -// pulse_status(ps); + pulse_status(ps); } static int @@ -619,8 +620,43 @@ pulse_device_free_extra(struct output_device *device) } static int -pulse_volume_set(struct output_device *device, output_status_cb cb) +pulse_device_volume_set(struct output_device *device, output_status_cb cb) { + struct pulse *p = &pulse; + struct pulse_session *ps; + uint32_t idx; + pa_operation* o; + pa_cvolume cvol; + pa_volume_t vol; + + if (!sessions || !device->session || !device->session->session) + return 0; + + ps = device->session->session; + + vol = PA_VOLUME_MUTED + (device->volume * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / 100; + pa_cvolume_set(&cvol, 2, vol); + + idx = pa_stream_get_index(ps->stream); + + DPRINTF(E_DBG, L_LAUDIO, "Setting Pulseaudio volume for stream %" PRIu32 " to %d (%d)\n", idx, (int)vol, device->volume); + + pa_threaded_mainloop_lock(p->mainloop); + + o = pa_context_set_sink_input_volume(p->context, idx, &cvol, NULL, NULL); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not set volume: %s\n", pa_strerror(pa_context_errno(p->context))); + pa_threaded_mainloop_unlock(p->mainloop); + return 0; + } + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(p->mainloop); + + ps->status_cb = cb; + pulse_status(ps); + return 1; } @@ -804,7 +840,7 @@ struct output_definition output_pulse = .device_stop = pulse_device_stop, .device_probe = pulse_device_probe, .device_free_extra = pulse_device_free_extra, - .device_volume_set = pulse_volume_set, + .device_volume_set = pulse_device_volume_set, .playback_start = pulse_playback_start, .playback_stop = pulse_playback_stop, .write = pulse_write, From 84a57724398f0f760c20336d7d2721a4634bdb02 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 24 Aug 2016 23:06:25 +0200 Subject: [PATCH 06/13] [Pulseaudio] Implement flush, avoid adding known sinks and misc fixing up --- src/outputs/pulse.c | 118 ++++++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 48 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 37e35fc6..0a5a67f6 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -40,6 +40,8 @@ #include "outputs.h" #include "commands.h" +#define PULSE_MAX_DEVICES 64 + struct pulse { pa_threaded_mainloop *mainloop; @@ -69,11 +71,6 @@ struct pulse_session struct pulse_session *next; }; -union pulse_arg -{ - struct pulse_session *ps; -}; - // From player.c extern struct event_base *evbase_player; @@ -81,6 +78,9 @@ extern struct event_base *evbase_player; static struct pulse pulse; static struct pulse_session *sessions; +// Internal list with indeces of the Pulseaudio devices (sinks) we have registered +static uint32_t pulse_known_devices[PULSE_MAX_DEVICES]; + /* Forwards */ static void defer_cb(int fd, short what, void *arg); @@ -212,18 +212,6 @@ pulse_status(struct pulse_session *ps) ps->status_cb = NULL; } -// Callbacks from the Pulseaudio thread will invoke commands_async_exec(), -// which will trigger a status update in the player thread -static enum command_state -pulse_status_cmd_wrapper(void *arg, int *ret) -{ - union pulse_arg *cmdarg = arg; - - pulse_status(cmdarg->ps); - - return COMMAND_END; -} - /* --------------------- CALLBACKS FROM PULSEAUDIO THREAD ------------------- */ @@ -232,16 +220,11 @@ stream_state_cb(pa_stream *s, void * userdata) { struct pulse *p = &pulse; struct pulse_session *ps = userdata; - union pulse_arg *cmdarg; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stream state CB\n"); ps->state = pa_stream_get_state(s); - cmdarg = calloc(1, sizeof(union pulse_arg)); - cmdarg->ps = ps; - commands_exec_async(p->cmdbase, pulse_status_cmd_wrapper, cmdarg); - switch (ps->state) { case PA_STREAM_READY: @@ -283,15 +266,33 @@ success_cb(pa_stream *s, int success, void *userdata) */ static void -sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) +sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, void *userdata) { struct output_device *device; const char *name; + int i; + int pos; if (eol > 0) return; - DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio sink '%s' (id %" PRIu32 ")\n", i->name, i->index); + DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio sink '%s' (id %" PRIu32 ")\n", info->name, info->index); + + pos = -1; + for (i = 0; i < PULSE_MAX_DEVICES; i++) + { + if (pulse_known_devices[i] == (info->index + 1)) + return; + + if (pulse_known_devices[i] == 0) + pos = i; + } + + if (pos == -1) + { + DPRINTF(E_LOG, L_LAUDIO, "Maximum number of Pulseaudio devices reached (%d), cannot add '%s'\n", PULSE_MAX_DEVICES, info->name); + return; + } device = calloc(1, sizeof(struct output_device)); if (!device) @@ -300,25 +301,27 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *i, int eol, void *userdata) return; } - if (i->index == 0) + if (info->index == 0) { name = cfg_getstr(cfg_getsec(cfg, "audio"), "nickname"); - DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s) with name '%s'\n", i->description, i->name, name); + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s) with name '%s'\n", info->description, info->name, name); } else { - name = i->description; + name = info->description; - DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", i->description, i->name); + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", info->description, info->name); } - device->id = i->index; + pulse_known_devices[pos] = info->index + 1; // Array values of 0 mean no device, so we add 1 to make sure the value is > 0 + + device->id = info->index; device->name = strdup(name); device->type = OUTPUT_TYPE_PULSE; device->type_name = outputs_name(device->type); device->advertised = 1; - device->extra_device_info = strdup(i->name); + device->extra_device_info = strdup(info->name); player_device_add(device); } @@ -328,6 +331,7 @@ subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void { struct output_device *device; pa_operation *o; + int i; DPRINTF(E_DBG, L_LAUDIO, "Callback for Pulseaudio subscribe (id %" PRIu32 ", event %d)\n", index, t); @@ -350,6 +354,12 @@ subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void DPRINTF(E_LOG, L_LAUDIO, "Removing Pulseaudio sink with id %" PRIu32 "\n", index); + for (i = 0; i < PULSE_MAX_DEVICES; i++) + { + if (pulse_known_devices[i] == index) + pulse_known_devices[i] = 0; + } + player_device_remove(device); return; } @@ -566,6 +576,8 @@ pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t r struct pulse_session *ps; int ret; + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio start\n"); + ps = pulse_session_make(device, cb); if (!ps) return -1; @@ -584,6 +596,8 @@ pulse_device_stop(struct output_session *session) { struct pulse_session *ps = session->session; + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stop\n"); + stream_close(&pulse, ps); pulse_status(ps); @@ -595,6 +609,8 @@ pulse_device_probe(struct output_device *device, output_status_cb cb) struct pulse_session *ps; int ret; + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio probe\n"); + ps = pulse_session_make(device, cb); if (!ps) return -1; @@ -660,20 +676,6 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb) return 1; } -static void -pulse_playback_start(uint64_t next_pkt, struct timespec *ts) -{ - if (!sessions) - return; - - DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio playback start called\n"); -} - -static void -pulse_playback_stop(void) -{ -} - static void pulse_write(uint8_t *buf, uint64_t rtptime) { @@ -736,9 +738,31 @@ pulse_write(uint8_t *buf, uint64_t rtptime) static int pulse_flush(output_status_cb cb, uint64_t rtptime) { - DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush called\n"); + struct pulse *p = &pulse; + struct pulse_session *ps; + pa_operation* o; + int i; - return 0; + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n"); + + pa_threaded_mainloop_lock(p->mainloop); + + i = 0; + for (ps = sessions; ps; ps = ps->next) + { + i++; + + o = pa_stream_flush(ps->stream, NULL, NULL); + if (o) + { + ps->status_cb = cb; + pulse_status(ps); + } + } + + pa_threaded_mainloop_unlock(p->mainloop); + + return i; } static void @@ -841,8 +865,6 @@ struct output_definition output_pulse = .device_probe = pulse_device_probe, .device_free_extra = pulse_device_free_extra, .device_volume_set = pulse_device_volume_set, - .playback_start = pulse_playback_start, - .playback_stop = pulse_playback_stop, .write = pulse_write, .flush = pulse_flush, .status_cb = pulse_set_status_cb, From fa6c6a1832a78651604d970d316bcdc429155e4e Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 25 Aug 2016 22:31:25 +0200 Subject: [PATCH 07/13] [pulseaudio] Kill sessions if context becomes invalid --- src/outputs/pulse.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 0a5a67f6..1686c09d 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -42,6 +42,11 @@ #define PULSE_MAX_DEVICES 64 +/* TODO for Pulseaudio + - Get volume from Pulseaudio on startup and on callbacks + - Add sync with AirPlay with pa_buffer_attr +*/ + struct pulse { pa_threaded_mainloop *mainloop; @@ -488,9 +493,9 @@ stream_open(struct pulse *p, struct pulse_session *ps) pa_stream_set_write_callback(ps->stream, stream_request_cb, ps); pa_stream_set_latency_update_callback(ps->stream, stream_latency_update_cb, ps); + // TODO should we use PA_STREAM_ADJUST_LATENCY? flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE; - // TODO sync stream ret = pa_stream_connect_playback(ps->stream, ps->devname, NULL, flags, NULL, NULL); if (ret < 0) goto unlock_and_fail; @@ -683,6 +688,7 @@ pulse_write(uint8_t *buf, uint64_t rtptime) struct pulse_session *ps; struct pulse_session *next; size_t length; + int invalid_context; int ret; if (!sessions) @@ -692,18 +698,13 @@ pulse_write(uint8_t *buf, uint64_t rtptime) pa_threaded_mainloop_lock(p->mainloop); - if (context_check(p->context) < 0) - { - // TODO Not a good situation... we should kill all sessions - pa_threaded_mainloop_unlock(p->mainloop); - return; - } + invalid_context = (context_check(p->context) < 0); for (ps = sessions; ps; ps = next) { next = ps->next; - if (stream_check(p, ps) < 0) + if (invalid_context || (stream_check(p, ps) < 0)) { pulse_status(ps); // Note: This will nuke the session (deferred) continue; @@ -793,7 +794,6 @@ pulse_init(void) if (!(p->cmdbase = commands_base_new(evbase_player, NULL))) goto fail; -// TODO #ifdef HAVE_PULSE_MAINLOOP_SET_NAME pa_threaded_mainloop_set_name(p->mainloop, "pulseaudio"); #endif From f8d3ccddb2554a5eb0dcc1f14232b742070cbac2 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 25 Aug 2016 22:32:07 +0200 Subject: [PATCH 08/13] [pulseaudio] Let configure check if Pulseaudio supports setting its thread name --- configure.ac | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configure.ac b/configure.ac index 3849bd58..6c35376a 100644 --- a/configure.ac +++ b/configure.ac @@ -228,6 +228,9 @@ AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--with-pulseaudio], [use Pulseaudio (def AS_IF([test "x$with_pulseaudio" = "xyes"], [ CPPFLAGS="${CPPFLAGS} -DPULSEAUDIO" PKG_CHECK_MODULES(LIBPULSE, [ libpulse ]) + AC_SEARCH_LIBS([pa_threaded_mainloop_set_name], [pulse], + AC_DEFINE(HAVE_PULSE_MAINLOOP_SET_NAME, 1, [Define to 1 if you have Pulseaudio with pa_threaded_mainloop_set_name]) + ) ]) AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" = "xyes"]) dnl --- End options --- From 9de783f2bcb0423e0c3cd8d89304128f4c8e4624 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 25 Aug 2016 22:44:28 +0200 Subject: [PATCH 09/13] [pulseaudio] Update README and conf with Pulseaudio --- README.md | 12 +++++++++--- forked-daapd.conf | 7 ++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f9fb780..16ffd8a5 100644 --- a/README.md +++ b/README.md @@ -219,9 +219,15 @@ by your ffmpeg/libav. See [MP3 network streaming](#MP3-network-streaming-(stream ## Local audio output -forked-daapd supports local audio output through ALSA. The server will try to -syncronize playback with AirPlay. You can adjust the syncronization in the -config file. +forked-daapd supports local audio output through ALSA or Pulseaudio. You can +set your preference in the config file. + +If you select ALSA, the server will try to syncronize playback with AirPlay. +You can adjust the syncronization in the config file. + +If you select Pulseaudio, the "card" setting in the config file has no effect. +Instead all soundcards detected by Pulseaudio will be listed as speakers by +forked-daapd. ## MP3 network streaming (streaming to iOS) diff --git a/forked-daapd.conf b/forked-daapd.conf index 06c4ec33..5a2b7250 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -156,16 +156,17 @@ audio { # Name - used in the speaker list in Remote nickname = "Computer" - # Type of the output (alsa or dummy) + # Type of the output (alsa, pulseaudio or dummy) # type = "alsa" - # Audio device name for local audio output + # Audio device name for local audio output - ALSA only # card = "default" - # Mixer channel to use for volume control - ALSA/Linux only + # Mixer channel to use for volume control - ALSA only # If not set, PCM will be used if available, otherwise Master. # mixer = "" + # Syncronization - ALSA only # If your local audio is out of sync with AirPlay, you can adjust this # value. Positive values correspond to moving local audio ahead, # negative correspond to delaying it. The unit is samples, where is From 42d4e150d86fb5a1b8ae214c63e415c3cc57fb37 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 25 Aug 2016 23:06:51 +0200 Subject: [PATCH 10/13] [pulseaudio] Make both ALSA and Pulseaudio default in configure.ac --- configure.ac | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/configure.ac b/configure.ac index 6c35376a..04c87667 100644 --- a/configure.ac +++ b/configure.ac @@ -159,7 +159,7 @@ AC_CHECK_HEADERS(stdint.h,,) dnl --- Begin configuring the options --- dnl iTunes playlists with libplist -AC_ARG_ENABLE(itunes, AS_HELP_STRING([--enable-itunes], [enable iTunes library support (default=no)])) +AC_ARG_ENABLE(itunes, AS_HELP_STRING([--enable-itunes], [enable iTunes Music Library XML support (default=no)])) AS_IF([test "x$enable_itunes" = "xyes"], [ CPPFLAGS="${CPPFLAGS} -DITUNES" PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ]) @@ -167,7 +167,7 @@ AS_IF([test "x$enable_itunes" = "xyes"], [ AM_CONDITIONAL(COND_ITUNES, [test "x$enable_itunes" = "xyes"]) dnl Spotify with dynamic linking to libspotify -AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify library support (default=no)])) +AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify support (default=no)])) AS_IF([test "x$enable_spotify" = "xyes"], [ CPPFLAGS="${CPPFLAGS} -DSPOTIFY" AC_CHECK_HEADER(libspotify/api.h, , AC_MSG_ERROR([libspotify/api.h not found])) @@ -216,23 +216,23 @@ AS_IF([test "x$enable_mpd" != "xno"], [ AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"]) dnl ALSA -AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa], [use ALSA (default yes)])) -AS_IF([test "x$with_alsa" = "xyes"], [ +AC_ARG_WITH(alsa, AS_HELP_STRING([--without-alsa], [without ALSA support (default=no)])) +AS_IF([test "x$with_alsa" != "xno"], [ CPPFLAGS="${CPPFLAGS} -DALSA" PKG_CHECK_MODULES(ALSA, [ alsa ]) ]) -AM_CONDITIONAL(COND_ALSA, test x$use_alsa = xtrue) +AM_CONDITIONAL(COND_ALSA, [test "x$with_alsa" != "xno"]) dnl PULSEAUDIO -AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--with-pulseaudio], [use Pulseaudio (default no)])) -AS_IF([test "x$with_pulseaudio" = "xyes"], [ +AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--without-pulseaudio], [without Pulseaudio support (default=no)])) +AS_IF([test "x$with_pulseaudio" != "xno"], [ CPPFLAGS="${CPPFLAGS} -DPULSEAUDIO" PKG_CHECK_MODULES(LIBPULSE, [ libpulse ]) AC_SEARCH_LIBS([pa_threaded_mainloop_set_name], [pulse], AC_DEFINE(HAVE_PULSE_MAINLOOP_SET_NAME, 1, [Define to 1 if you have Pulseaudio with pa_threaded_mainloop_set_name]) ) ]) -AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" = "xyes"]) +AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" != "xno"]) dnl --- End options --- dnl Checks for header files. From 4318aa19b5bb11c2eb49e60ab7650c6cac22db66 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 25 Aug 2016 23:07:43 +0200 Subject: [PATCH 11/13] [commands] Update comment --- src/commands.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.c b/src/commands.c index 9f510623..d2143c99 100644 --- a/src/commands.c +++ b/src/commands.c @@ -171,7 +171,7 @@ send_command(struct commands_base *cmdbase, struct command *cmd) * Creates a new command base, needs to be freed by commands_base_destroy or commands_base_free. * * @param evbase The libevent base to use for command handling - * @param exit_cb Callback function to be called during commands_base_destroy + * @param exit_cb Optional callback function to be called during commands_base_destroy */ struct commands_base * commands_base_new(struct event_base *evbase, command_exit_cb exit_cb) From 1d862dd70177ef26549dc606cc9b638f4d539502 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 28 Aug 2016 22:33:58 +0200 Subject: [PATCH 12/13] [pulseaudio] Extra state check when changing volume --- src/outputs/pulse.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 1686c09d..0bb23888 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -655,6 +655,9 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb) ps = device->session->session; + if ((context_check(p->context) < 0) || (stream_check(p, ps) < 0)) + return 0; + vol = PA_VOLUME_MUTED + (device->volume * (PA_VOLUME_NORM - PA_VOLUME_MUTED)) / 100; pa_cvolume_set(&cvol, 2, vol); From 57227728178cd756d3fee401b7bb5f35eed3669d Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 28 Aug 2016 22:36:06 +0200 Subject: [PATCH 13/13] [pulseaudio] Don't try to build by default - yet --- configure.ac | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 04c87667..ad97191f 100644 --- a/configure.ac +++ b/configure.ac @@ -224,15 +224,15 @@ AS_IF([test "x$with_alsa" != "xno"], [ AM_CONDITIONAL(COND_ALSA, [test "x$with_alsa" != "xno"]) dnl PULSEAUDIO -AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--without-pulseaudio], [without Pulseaudio support (default=no)])) -AS_IF([test "x$with_pulseaudio" != "xno"], [ +AC_ARG_WITH(pulseaudio, AS_HELP_STRING([--with-pulseaudio], [with Pulseaudio support (default=no)])) +AS_IF([test "x$with_pulseaudio" = "xyes"], [ CPPFLAGS="${CPPFLAGS} -DPULSEAUDIO" PKG_CHECK_MODULES(LIBPULSE, [ libpulse ]) AC_SEARCH_LIBS([pa_threaded_mainloop_set_name], [pulse], AC_DEFINE(HAVE_PULSE_MAINLOOP_SET_NAME, 1, [Define to 1 if you have Pulseaudio with pa_threaded_mainloop_set_name]) ) ]) -AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" != "xno"]) +AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" = "xyes"]) dnl --- End options --- dnl Checks for header files.