mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 15:15:57 -05:00
Merge branch 'pulseaudio1'
This commit is contained in:
commit
7af9c25964
12
README.md
12
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)
|
||||
|
41
configure.ac
41
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
|
||||
|
@ -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
|
||||
|
@ -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) \
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
875
src/outputs/pulse.c
Normal file
875
src/outputs/pulse.c
Normal 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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user