Merge branch 'pulseaudio1'

This commit is contained in:
ejurgensen 2016-08-28 22:37:41 +02:00
commit 7af9c25964
9 changed files with 928 additions and 36 deletions

View File

@ -219,9 +219,15 @@ by your ffmpeg/libav. See [MP3 network streaming](#MP3-network-streaming-(stream
## Local audio output ## Local audio output
forked-daapd supports local audio output through ALSA. The server will try to forked-daapd supports local audio output through ALSA or Pulseaudio. You can
syncronize playback with AirPlay. You can adjust the syncronization in the set your preference in the config file.
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) ## MP3 network streaming (streaming to iOS)

View File

@ -159,7 +159,7 @@ AC_CHECK_HEADERS(stdint.h,,)
dnl --- Begin configuring the options --- dnl --- Begin configuring the options ---
dnl iTunes playlists with libplist 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"], [ AS_IF([test "x$enable_itunes" = "xyes"], [
CPPFLAGS="${CPPFLAGS} -DITUNES" CPPFLAGS="${CPPFLAGS} -DITUNES"
PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ]) 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"]) AM_CONDITIONAL(COND_ITUNES, [test "x$enable_itunes" = "xyes"])
dnl Spotify with dynamic linking to libspotify 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"], [ AS_IF([test "x$enable_spotify" = "xyes"], [
CPPFLAGS="${CPPFLAGS} -DSPOTIFY" CPPFLAGS="${CPPFLAGS} -DSPOTIFY"
AC_CHECK_HEADER(libspotify/api.h, , AC_MSG_ERROR([libspotify/api.h not found])) 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" CPPFLAGS="${CPPFLAGS} -DMPD"
]) ])
AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"]) AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"])
dnl --- End options ---
dnl Selection of local audio sound system dnl ALSA
dnl TODO exchange oss4 with Pulseaudio AC_ARG_WITH(alsa, AS_HELP_STRING([--without-alsa], [without ALSA support (default=no)]))
case "$host" in AS_IF([test "x$with_alsa" != "xno"], [
*-*-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
CPPFLAGS="${CPPFLAGS} -DALSA" CPPFLAGS="${CPPFLAGS} -DALSA"
PKG_CHECK_MODULES(ALSA, [ alsa ]) 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. dnl Checks for header files.
AC_HEADER_STDC AC_HEADER_STDC

View File

@ -156,16 +156,17 @@ audio {
# Name - used in the speaker list in Remote # Name - used in the speaker list in Remote
nickname = "Computer" nickname = "Computer"
# Type of the output (alsa or dummy) # Type of the output (alsa, pulseaudio or dummy)
# type = "alsa" # type = "alsa"
# Audio device name for local audio output # Audio device name for local audio output - ALSA only
# card = "default" # 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. # If not set, PCM will be used if available, otherwise Master.
# mixer = "" # mixer = ""
# Syncronization - ALSA only
# If your local audio is out of sync with AirPlay, you can adjust this # If your local audio is out of sync with AirPlay, you can adjust this
# value. Positive values correspond to moving local audio ahead, # value. Positive values correspond to moving local audio ahead,
# negative correspond to delaying it. The unit is samples, where is # negative correspond to delaying it. The unit is samples, where is

View File

@ -29,6 +29,10 @@ if COND_ALSA
ALSA_SRC=outputs/alsa.c ALSA_SRC=outputs/alsa.c
endif endif
if COND_PULSEAUDIO
PULSEAUDIO_SRC=outputs/pulse.c
endif
GPERF_FILES = \ GPERF_FILES = \
daap_query.gperf \ daap_query.gperf \
rsp_query.gperf \ rsp_query.gperf \
@ -62,15 +66,15 @@ forked_daapd_CPPFLAGS = -D_GNU_SOURCE \
forked_daapd_CFLAGS = \ forked_daapd_CFLAGS = \
@ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \ @ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \
@CONFUSE_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \ @CONFUSE_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ @SPOTIFY_CFLAGS@ \
@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@ \ @LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @LIBPULSE_CFLAGS@ \
@LIBCURL_CFLAGS@ @LIBPROTOBUF_C_CFLAGS@ @GNUTLS_CFLAGS@ @JSON_C_CFLAGS@ @LIBCURL_CFLAGS@ @LIBPROTOBUF_C_CFLAGS@ @GNUTLS_CFLAGS@ @JSON_C_CFLAGS@
forked_daapd_LDADD = -lrt \ forked_daapd_LDADD = -lrt \
@ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \ @ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \
@CONFUSE_LIBS@ @LIBEVENT_LIBS@ \ @CONFUSE_LIBS@ @LIBEVENT_LIBS@ @LIBUNISTRING@ \
@MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \ @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ @SPOTIFY_LIBS@ \
@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ \ @LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBPULSE_LIBS@ \
@LIBCURL_LIBS@ @LIBPROTOBUF_C_LIBS@ @GNUTLS_LIBS@ @JSON_C_LIBS@ @LIBCURL_LIBS@ @LIBPROTOBUF_C_LIBS@ @GNUTLS_LIBS@ @JSON_C_LIBS@
forked_daapd_SOURCES = main.c \ forked_daapd_SOURCES = main.c \
@ -104,7 +108,7 @@ forked_daapd_SOURCES = main.c \
worker.c worker.h \ worker.c worker.h \
outputs.h outputs.c \ outputs.h outputs.c \
outputs/raop.c outputs/streaming.c outputs/dummy.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 \ evrtsp/rtsp.c evrtp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
$(SPOTIFY_SRC) \ $(SPOTIFY_SRC) \
$(LASTFM_SRC) \ $(LASTFM_SRC) \

View File

@ -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. * 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 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 * struct commands_base *
commands_base_new(struct event_base *evbase, command_exit_cb exit_cb) commands_base_new(struct event_base *evbase, command_exit_cb exit_cb)

View File

@ -37,6 +37,9 @@ extern struct output_definition output_dummy;
#ifdef ALSA #ifdef ALSA
extern struct output_definition output_alsa; extern struct output_definition output_alsa;
#endif #endif
#ifdef PULSEAUDIO
extern struct output_definition output_pulse;
#endif
#ifdef CHROMECAST #ifdef CHROMECAST
extern struct output_definition output_cast; extern struct output_definition output_cast;
#endif #endif
@ -49,6 +52,9 @@ static struct output_definition *outputs[] = {
#ifdef ALSA #ifdef ALSA
&output_alsa, &output_alsa,
#endif #endif
#ifdef PULSEAUDIO
&output_pulse,
#endif
#ifdef CHROMECAST #ifdef CHROMECAST
&output_cast, &output_cast,
#endif #endif

View File

@ -55,6 +55,9 @@ enum output_types
#ifdef ALSA #ifdef ALSA
OUTPUT_TYPE_ALSA, OUTPUT_TYPE_ALSA,
#endif #endif
#ifdef PULSEAUDIO
OUTPUT_TYPE_PULSE,
#endif
#ifdef CHROMECAST #ifdef CHROMECAST
OUTPUT_TYPE_CAST, OUTPUT_TYPE_CAST,
#endif #endif

View File

@ -1011,7 +1011,7 @@ alsa_init(void)
device->advertised = 1; device->advertised = 1;
device->has_video = 0; 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); player_device_add(device);

875
src/outputs/pulse.c Normal file
View File

@ -0,0 +1,875 @@
/*
* Copyright (C) 2016 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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 <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>
#include <event2/event.h>
#include <pulse/pulseaudio.h>
#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,
};