Mercurial > audlegacy
changeset 1655:fd657c404f0b trunk
[svn] PulseAudio output plugin. Ported from the XMMS plugin by Lennart Poettering.
author | chainsaw |
---|---|
date | Fri, 08 Sep 2006 16:00:10 -0700 |
parents | e1667bdf1272 |
children | d05836d18d42 |
files | ChangeLog Plugins/Output/pulse_audio/Makefile.in Plugins/Output/pulse_audio/pulse_audio.c |
diffstat | 3 files changed, 783 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/ChangeLog Fri Sep 08 10:55:02 2006 -0700 +++ b/ChangeLog Fri Sep 08 16:00:10 2006 -0700 @@ -1,3 +1,11 @@ +2006-09-08 17:55:02 +0000 William Pitcock <nenolod@nenolod.net> + revision [2245] + - remove this + + + Changes: Modified: + + 2006-09-08 05:26:54 +0000 William Pitcock <nenolod@nenolod.net> revision [2239] - revert back to r2216
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Output/pulse_audio/Makefile.in Fri Sep 08 16:00:10 2006 -0700 @@ -0,0 +1,15 @@ +include ../../../mk/rules.mk +include ../../../mk/init.mk + +OBJECTIVE_LIBS = libpulse_audio$(SHARED_SUFFIX) + +LIBDIR = $(plugindir)/$(OUTPUT_PLUGIN_DIR) + +LIBADD = $(GTK_LIBS) -lpulse +SOURCES = pulse_audio.c + +OBJECTS = ${SOURCES:.c=.o} + +CFLAGS += $(PICFLAGS) $(GTK_CFLAGS) -I../../../intl -I../../.. + +include ../../../mk/objective.mk
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Output/pulse_audio/pulse_audio.c Fri Sep 08 16:00:10 2006 -0700 @@ -0,0 +1,760 @@ +/*** + This file is part of xmms-pulse. + + xmms-pulse 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. + + xmms-pulse 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 xmms-pulse; 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 <assert.h> +/* #include <pthread.h> */ +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include <gtk/gtk.h> +#include "audacious/plugin.h" +#include "libaudacious/beepctrl.h" +#include "libaudacious/util.h" + +#include <pulse/pulseaudio.h> + +static pa_context *context = NULL; +static pa_stream *stream = NULL; +static pa_threaded_mainloop *mainloop = NULL; + +static pa_cvolume volume; +static int volume_valid = 0; + +static int do_trigger = 0; +static uint64_t written = 0; +static int time_offset_msec = 0; +static int just_flushed = 0; + +static int connected = 0; + +static pa_time_event *volume_time_event = NULL; + +#define CHECK_DEAD_GOTO(label, warn) do { \ +if (!mainloop || \ + !context || pa_context_get_state(context) != PA_CONTEXT_READY || \ + !stream || pa_stream_get_state(stream) != PA_STREAM_READY) { \ + if (warn) \ + g_warning("Connection died: %s", context ? pa_strerror(pa_context_errno(context)) : "NULL"); \ + goto label; \ + } \ +} while(0); + +#define CHECK_CONNECTED(retval) \ +do { \ + if (!connected) return retval; \ +} while (0); + +/* This function is from xmms' core */ +gint ctrlsocket_get_session_id(void); + +static const char* get_song_name(void) { + static char t[256]; + gint session, pos; + char *str, *u; + + session = ctrlsocket_get_session_id(); + pos = xmms_remote_get_playlist_pos(session); + if (!(str = xmms_remote_get_playlist_title(session, pos))) + return "Playback Stream"; + + snprintf(t, sizeof(t), "%s", u = pa_locale_to_utf8(str)); + pa_xfree(u); + + return t; +} + +static void info_cb(struct pa_context *c, const struct pa_sink_input_info *i, int is_last, void *userdata) { + assert(c); + + if (!i) + return; + + volume = i->volume; + volume_valid = 1; +} + +static void subscribe_cb(struct pa_context *c, enum pa_subscription_event_type t, uint32_t index, void *userdata) { + pa_operation *o; + + assert(c); + + if (!stream || + index != pa_stream_get_index(stream) || + (t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE) && + t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_NEW))) + return; + + if (!(o = pa_context_get_sink_input_info(c, index, info_cb, NULL))) { + g_warning("pa_context_get_sink_input_info() failed: %s", pa_strerror(pa_context_errno(c))); + return; + } + + pa_operation_unref(o); +} + +static void context_state_cb(pa_context *c, void *userdata) { + assert(c); + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_READY: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + pa_threaded_mainloop_signal(mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } +} + +static void stream_state_cb(pa_stream *s, void * userdata) { + assert(s); + + switch (pa_stream_get_state(s)) { + + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + break; + } +} + +static void stream_success_cb(pa_stream *s, int success, void *userdata) { + assert(s); + + if (userdata) + *(int*) userdata = success; + + pa_threaded_mainloop_signal(mainloop, 0); +} + +static void context_success_cb(pa_context *c, int success, void *userdata) { + assert(c); + + if (userdata) + *(int*) userdata = success; + + pa_threaded_mainloop_signal(mainloop, 0); +} + +static void stream_request_cb(pa_stream *s, size_t length, void *userdata) { + assert(s); + + pa_threaded_mainloop_signal(mainloop, 0); +} + +static void stream_latency_update_cb(pa_stream *s, void *userdata) { + assert(s); + + pa_threaded_mainloop_signal(mainloop, 0); +} + +static void pulse_get_volume(int *l, int *r) { + pa_cvolume v; + int b = 0; + +/* g_message("get_volume"); */ + + *l = *r = 100; + + if (connected) { + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + v = volume; + b = volume_valid; + + fail: + pa_threaded_mainloop_unlock(mainloop); + } else { + v = volume; + b = volume_valid; + } + + if (b) { + if (v.channels == 2) { + *l = (int) ((v.values[0]*100)/PA_VOLUME_NORM); + *r = (int) ((v.values[1]*100)/PA_VOLUME_NORM); + } else + *l = *r = (int) ((pa_cvolume_avg(&v)*100)/PA_VOLUME_NORM); + } +} + +static void volume_time_cb(pa_mainloop_api *api, pa_time_event *e, const struct timeval *tv, void *userdata) { + pa_operation *o; + + if (!(o = pa_context_set_sink_input_volume(context, pa_stream_get_index(stream), &volume, NULL, NULL))) + g_warning("pa_context_set_sink_input_volume() failed: %s", pa_strerror(pa_context_errno(context))); + else + pa_operation_unref(o); + + /* We don't wait for completion of this command */ + + api->time_free(volume_time_event); + volume_time_event = NULL; +} + +static void pulse_set_volume(int l, int r) { + +/* g_message("set_volume"); */ + + if (connected) { + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + } + + if (!volume_valid || volume.channels != 1) { + volume.values[0] = ((pa_volume_t) l * PA_VOLUME_NORM)/100; + volume.values[1] = ((pa_volume_t) r * PA_VOLUME_NORM)/100; + volume.channels = 2; + } else { + volume.values[0] = ((pa_volume_t) l * PA_VOLUME_NORM)/100; + volume.channels = 1; + } + + volume_valid = 1; + + if (connected && !volume_time_event) { + struct timeval tv; + pa_mainloop_api *api = pa_threaded_mainloop_get_api(mainloop); + volume_time_event = api->time_new(api, pa_timeval_add(pa_gettimeofday(&tv), 100000), volume_time_cb, NULL); + } + +fail: + if (connected) + pa_threaded_mainloop_unlock(mainloop); +} + +static void pulse_pause(short b) { + pa_operation *o = NULL; + int success = 0; + +/* g_message("pause"); */ + + CHECK_CONNECTED(); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + if (!(o = pa_stream_cork(stream, b, stream_success_cb, &success))) { + g_warning("pa_stream_cork() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!success) + g_warning("pa_stream_cork() failed: %s", pa_strerror(pa_context_errno(context))); + +fail: + + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(mainloop); +} + +static int pulse_free(void) { + size_t l = 0; + pa_operation *o = NULL; + +/* g_message("free"); */ + + CHECK_CONNECTED(0); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + if ((l = pa_stream_writable_size(stream)) == (size_t) -1) { + g_warning("pa_stream_writable_size() failed: %s", pa_strerror(pa_context_errno(context))); + l = 0; + goto fail; + } + + /* If this function is called twice with no pulse_write() call in + * between this means we should trigger the playback */ + if (do_trigger) { + int success = 0; + + if (!(o = pa_stream_trigger(stream, stream_success_cb, &success))) { + g_warning("pa_stream_trigger() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!success) + g_warning("pa_stream_trigger() failed: %s", pa_strerror(pa_context_errno(context))); + } + +fail: + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(mainloop); + + do_trigger = !!l; + return (int) l; +} + +static int pulse_get_written_time(void) { + int r = 0; + +/* g_message("get_written_time"); */ + + CHECK_CONNECTED(0); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + r = (int) (((double) written*1000) / pa_bytes_per_second(pa_stream_get_sample_spec(stream))); + +/* g_message("written_time = %i", r); */ + +fail: + pa_threaded_mainloop_unlock(mainloop); + + return r; +} + +static int pulse_get_output_time(void) { + int r = 0; + pa_usec_t t; + +/* g_message("get_output_time"); */ + + CHECK_CONNECTED(0); + + pa_threaded_mainloop_lock(mainloop); + + for (;;) { + CHECK_DEAD_GOTO(fail, 1); + + if (pa_stream_get_time(stream, &t) >= 0) + break; + + if (pa_context_errno(context) != PA_ERR_NODATA) { + g_warning("pa_stream_get_time() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + pa_threaded_mainloop_wait(mainloop); + } + + r = (int) (t / 1000); + + if (just_flushed) { + time_offset_msec -= r; + just_flushed = 0; + } + + r += time_offset_msec; + +/* g_message("output_time = %i", r); */ + +fail: + pa_threaded_mainloop_unlock(mainloop); + + return r; +} + +static int pulse_playing(void) { + int r = 0; + const pa_timing_info *i; + + CHECK_CONNECTED(0); + +/* g_message("playing"); */ + + pa_threaded_mainloop_lock(mainloop); + + for (;;) { + CHECK_DEAD_GOTO(fail, 1); + + if ((i = pa_stream_get_timing_info(stream))) + break; + + if (pa_context_errno(context) != PA_ERR_NODATA) { + g_warning("pa_stream_get_timing_info() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + pa_threaded_mainloop_wait(mainloop); + } + + r = i->playing; + +fail: + pa_threaded_mainloop_unlock(mainloop); + + return r; +} + +static void pulse_flush(int time) { + pa_operation *o = NULL; + int success = 0; + +/* g_message("flush"); */ + + CHECK_CONNECTED(); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + if (!(o = pa_stream_flush(stream, stream_success_cb, &success))) { + g_warning("pa_stream_flush() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!success) + g_warning("pa_stream_flush() failed: %s", pa_strerror(pa_context_errno(context))); + + written = (uint64_t) (((double) time * pa_bytes_per_second(pa_stream_get_sample_spec(stream))) / 1000); + just_flushed = 1; + time_offset_msec = time; + +fail: + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(mainloop); +} + +static void pulse_write(void* ptr, int length) { + +/* g_message("write"); */ + + CHECK_CONNECTED(); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 1); + + if (pa_stream_write(stream, ptr, length, NULL, PA_SEEK_RELATIVE, 0) < 0) { + g_warning("pa_stream_write() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + do_trigger = 0; + written += length; + +fail: + + pa_threaded_mainloop_unlock(mainloop); +} + +static void drain(void) { + pa_operation *o = NULL; + int success = 0; + + CHECK_CONNECTED(); + + pa_threaded_mainloop_lock(mainloop); + CHECK_DEAD_GOTO(fail, 0); + + if (!(o = pa_stream_drain(stream, stream_success_cb, &success))) { + g_warning("pa_stream_drain() failed: %s", pa_strerror(pa_context_errno(context))); + goto fail; + } + + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!success) + g_warning("pa_stream_drain() failed: %s", pa_strerror(pa_context_errno(context))); + +fail: + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(mainloop); +} + +static void pulse_close(void) { + +/* g_message("close"); */ + + drain(); + + connected = 0; + + if (mainloop) + pa_threaded_mainloop_stop(mainloop); + + if (stream) { + pa_stream_disconnect(stream); + pa_stream_unref(stream); + stream = NULL; + } + + if (context) { + pa_context_disconnect(context); + pa_context_unref(context); + context = NULL; + } + + if (mainloop) { + pa_threaded_mainloop_free(mainloop); + mainloop = NULL; + } + + volume_time_event = NULL; +} + +static int pulse_open(AFormat fmt, int rate, int nch) { + pa_sample_spec ss; + pa_operation *o = NULL; + int success; + +/* g_message("open"); */ + + g_assert(!mainloop); + g_assert(!context); + g_assert(!stream); + g_assert(!connected); + + if (fmt == FMT_U8) + ss.format = PA_SAMPLE_U8; + else if (fmt == FMT_S16_LE) + ss.format = PA_SAMPLE_S16LE; + else if (fmt == FMT_S16_BE) + ss.format = PA_SAMPLE_S16BE; + else if (fmt == FMT_S16_NE) + ss.format = PA_SAMPLE_S16NE; + else + return FALSE; + + ss.rate = rate; + ss.channels = nch; + + if (!pa_sample_spec_valid(&ss)) + return FALSE; + + if (!volume_valid) { + pa_cvolume_reset(&volume, ss.channels); + volume_valid = 1; + } else if (volume.channels != ss.channels) + pa_cvolume_set(&volume, ss.channels, pa_cvolume_avg(&volume)); + + if (!(mainloop = pa_threaded_mainloop_new())) { + g_warning("Failed to allocate main loop"); + goto fail; + } + + pa_threaded_mainloop_lock(mainloop); + + if (!(context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "XMMS"))) { + g_warning("Failed to allocate context"); + goto unlock_and_fail; + } + + pa_context_set_state_callback(context, context_state_cb, NULL); + pa_context_set_subscribe_callback(context, subscribe_cb, NULL); + + if (pa_context_connect(context, NULL, 0, NULL) < 0) { + g_warning("Failed to connect to server: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + if (pa_threaded_mainloop_start(mainloop) < 0) { + g_warning("Failed to start main loop"); + goto unlock_and_fail; + } + + /* Wait until the context is ready */ + pa_threaded_mainloop_wait(mainloop); + + if (pa_context_get_state(context) != PA_CONTEXT_READY) { + g_warning("Failed to connect to server: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + if (!(stream = pa_stream_new(context, get_song_name(), &ss, NULL))) { + g_warning("Failed to create stream: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + pa_stream_set_state_callback(stream, stream_state_cb, NULL); + pa_stream_set_write_callback(stream, stream_request_cb, NULL); + pa_stream_set_latency_update_callback(stream, stream_latency_update_cb, NULL); + + if (pa_stream_connect_playback(stream, NULL, NULL, PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_AUTO_TIMING_UPDATE, &volume, NULL) < 0) { + g_warning("Failed to connect stream: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + /* Wait until the stream is ready */ + pa_threaded_mainloop_wait(mainloop); + + if (pa_stream_get_state(stream) != PA_STREAM_READY) { + g_warning("Failed to connect stream: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + /* Now subscribe to events */ + if (!(o = pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK_INPUT, context_success_cb, &success))) { + g_warning("pa_context_subscribe() failed: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + success = 0; + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!success) { + g_warning("pa_context_subscribe() failed: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + pa_operation_unref(o); + + /* Now request the initial stream info */ + if (!(o = pa_context_get_sink_input_info(context, pa_stream_get_index(stream), info_cb, NULL))) { + g_warning("pa_context_get_sink_input_info() failed: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + while (pa_operation_get_state(o) != PA_OPERATION_DONE) { + CHECK_DEAD_GOTO(fail, 1); + pa_threaded_mainloop_wait(mainloop); + } + + if (!volume_valid) { + g_warning("pa_context_get_sink_input_info() failed: %s", pa_strerror(pa_context_errno(context))); + goto unlock_and_fail; + } + + do_trigger = 0; + written = 0; + time_offset_msec = 0; + just_flushed = 0; + connected = 1; + volume_time_event = NULL; + + pa_threaded_mainloop_unlock(mainloop); + + return TRUE; + +unlock_and_fail: + + if (o) + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(mainloop); + +fail: + + pulse_close(); + + return FALSE; +} + +static void pulse_init(void) { +} + +static void pulse_about(void) { + static GtkWidget *dialog; + + if (dialog != NULL) + return; + + dialog = xmms_show_message( + "About XMMS PulseAudio Output Plugin", + "XMMS PulseAudio Output Plugin\n\n " + "This program is free software; you can redistribute it and/or modify\n" + "it under the terms of the GNU General Public License as published by\n" + "the Free Software Foundation; either version 2 of the License, or\n" + "(at your option) any later version.\n" + "\n" + "This program is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + "GNU General Public License for more details.\n" + "\n" + "You should have received a copy of the GNU General Public License\n" + "along with this program; if not, write to the Free Software\n" + "Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,\n" + "USA.", + "OK", + FALSE, + NULL, + NULL); + + gtk_signal_connect( + GTK_OBJECT(dialog), + "destroy", + GTK_SIGNAL_FUNC(gtk_widget_destroyed), + &dialog); +} + + +OutputPlugin *get_oplugin_info(void) { + static OutputPlugin pulse_plugin = { + NULL, + NULL, + "PulseAudio Output Plugin", + pulse_init, + NULL, + pulse_about, + NULL, /* pulse_configure, */ + pulse_get_volume, + pulse_set_volume, + pulse_open, + pulse_write, + pulse_close, + pulse_flush, + pulse_pause, + pulse_free, + pulse_playing, + pulse_get_output_time, + pulse_get_written_time, + NULL, + }; + + return &pulse_plugin; +}