From 5b67f84855ec22ac5ff99ab2adbb78f3bd84a780 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 21 Jul 2016 22:13:32 +0200 Subject: [PATCH] [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, +}; +