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/configure.ac b/configure.ac index 2aa02f92..ad97191f 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])) @@ -214,29 +214,26 @@ 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([--without-alsa], [without ALSA support (default=no)])) +AS_IF([test "x$with_alsa" != "xno"], [ CPPFLAGS="${CPPFLAGS} -DALSA" PKG_CHECK_MODULES(ALSA, [ alsa ]) -fi -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], [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" = "xyes"]) +dnl --- End options --- dnl Checks for header files. AC_HEADER_STDC 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 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/commands.c b/src/commands.c index c96fa51a..57edcd95 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) 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/alsa.c b/src/outputs/alsa.c index 9e1ac1c7..5ad9e3e3 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -1011,7 +1011,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 new file mode 100644 index 00000000..0bb23888 --- /dev/null +++ b/src/outputs/pulse.c @@ -0,0 +1,875 @@ +/* + * 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" +#include "commands.h" + +#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; + pa_context *context; + + struct commands_base *cmdbase; + + 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; + +// 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); + +/* ---------------------------- 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->extra_device_info); + + 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: + 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); +} +*/ + +static void +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", 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) + { + DPRINTF(E_LOG, L_LAUDIO, "Out of memory for new Pulseaudio sink\n"); + return; + } + + 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", info->description, info->name, name); + } + else + { + name = info->description; + + DPRINTF(E_LOG, L_LAUDIO, "Adding Pulseaudio sink '%s' (%s)\n", info->description, info->name); + } + + 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(info->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; + int i; + + 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); + + for (i = 0; i < PULSE_MAX_DEVICES; i++) + { + if (pulse_known_devices[i] == index) + pulse_known_devices[i] = 0; + } + + 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) +{ + 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, 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; + + 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->cmdbase) + commands_base_free(p->cmdbase); + + if (p->mainloop) + pa_threaded_mainloop_free(p->mainloop); +} + +static int +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) +{ + 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); + + // TODO should we use PA_STREAM_ADJUST_LATENCY? + flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE; + + 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 *p, struct pulse_session *ps) +{ + 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 +stream_check(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; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio start\n"); + + ps = pulse_session_make(device, cb); + if (!ps) + return -1; + + ret = stream_open(&pulse, 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; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stop\n"); + + stream_close(&pulse, ps); + + pulse_status(ps); +} + +static int +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; + + ret = stream_open(&pulse, ps); + if (ret < 0) + { + pulse_session_cleanup(ps); + return -1; + } + + stream_close(&pulse, ps); + + pulse_status(ps); + + return 0; +} + +static void +pulse_device_free_extra(struct output_device *device) +{ + free(device->extra_device_info); +} + +static int +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; + + 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); + + 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; +} + +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 invalid_context; + int ret; + + if (!sessions) + return; + + length = STOB(AIRTUNES_V2_PACKET_SAMPLES); + + pa_threaded_mainloop_lock(p->mainloop); + + invalid_context = (context_check(p->context) < 0); + + for (ps = sessions; ps; ps = next) + { + next = ps->next; + + if (invalid_context || (stream_check(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) +{ + struct pulse *p = &pulse; + struct pulse_session *ps; + pa_operation* o; + int i; + + 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 +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 = &pulse; + char *type; + int state; + int ret; + + type = cfg_getstr(cfg_getsec(cfg, "audio"), "type"); + if (type && (strcasecmp(type, "pulseaudio") != 0)) + return -1; + + ret = 0; + + if (!(p->mainloop = pa_threaded_mainloop_new())) + goto fail; + + if (!(p->cmdbase = commands_base_new(evbase_player, NULL))) + goto fail; + +#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_free_extra = pulse_device_free_extra, + .device_volume_set = pulse_device_volume_set, + .write = pulse_write, + .flush = pulse_flush, + .status_cb = pulse_set_status_cb, +}; +