X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=modules%2Faudio_output%2Fpulse.c;h=cd15f84f57ed53b8540de5fe35013b1e8773d133;hb=2b1a7345d574af9dd993c410aaa56575c447ac62;hp=a20db4ff5997792c16221e98548a072debc4358d;hpb=6c9479be6c55fb23fb7bbfce02543e2bd8bce50b;p=vlc diff --git a/modules/audio_output/pulse.c b/modules/audio_output/pulse.c index a20db4ff59..cd15f84f57 100644 --- a/modules/audio_output/pulse.c +++ b/modules/audio_output/pulse.c @@ -2,6 +2,7 @@ * pulse.c : Pulseaudio output plugin for vlc ***************************************************************************** * Copyright (C) 2008 the VideoLAN team + * Copyright (C) 2009-2011 Rémi Denis-Courmont * * Authors: Martin Hamrle * @@ -20,492 +21,974 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ -/***************************************************************************** - * Preamble - *****************************************************************************/ #ifdef HAVE_CONFIG_H # include "config.h" #endif +#include #include #include - #include #include #include - -#include - -/***************************************************************************** - * aout_sys_t: Pulseaudio output method descriptor - ***************************************************************************** - * This structure is part of the audio output thread descriptor. - * It describes the specific properties of an audio device. - *****************************************************************************/ -struct aout_sys_t -{ - /** PulseAudio playback stream object */ - struct pa_stream *stream; - - /** PulseAudio connection context */ - struct pa_context *context; - - /** Main event loop object */ - struct pa_threaded_mainloop *mainloop; - - int started; - size_t buffer_size; - mtime_t start_date; -}; - -#define PULSE_CLIENT_NAME N_("VLC media player") - -#if 0 -#define PULSE_DEBUG( ...) \ - msg_Dbg( p_aout, __VA_ARGS__ ) -#else -#define PULSE_DEBUG( ...) \ - (void) 0 +#include +#if !PA_CHECK_VERSION(0,9,22) +# include #endif - -#define CHECK_DEAD_GOTO(label) do { \ -if (!p_sys->context || pa_context_get_state(p_sys->context) != PA_CONTEXT_READY || \ - !p_sys->stream || pa_stream_get_state(p_sys->stream) != PA_STREAM_READY) { \ - msg_Err(p_aout, "Connection died: %s", p_sys->context ? pa_strerror(pa_context_errno(p_sys->context)) : "NULL"); \ - goto label; \ - } \ -} while(0); - -/***************************************************************************** - * Local prototypes - *****************************************************************************/ static int Open ( vlc_object_t * ); static void Close ( vlc_object_t * ); -static void Play ( aout_instance_t * ); - -static void context_state_cb(pa_context *c, void *userdata); -static void stream_state_cb(pa_stream *s, void * userdata); -static void stream_request_cb(pa_stream *s, size_t length, void *userdata); -static void stream_latency_update_cb(pa_stream *s, void *userdata); -static void success_cb(pa_stream *s, int sucess, void *userdata); -static void uninit(aout_instance_t *p_aout); -/***************************************************************************** - * Module descriptor - *****************************************************************************/ vlc_module_begin () - set_shortname( "Pulse Audio" ) + set_shortname( "PulseAudio" ) set_description( N_("Pulseaudio audio output") ) set_capability( "audio output", 160 ) set_category( CAT_AUDIO ) set_subcategory( SUBCAT_AUDIO_AOUT ) - add_shortcut( "pulseaudio" ) - add_shortcut( "pa" ) + add_shortcut( "pulseaudio", "pa" ) set_callbacks( Open, Close ) - linked_with_a_crap_library_which_uses_atexit() vlc_module_end () -/***************************************************************************** - * Open: open the audio device - *****************************************************************************/ -static int Open ( vlc_object_t *p_this ) -{ - aout_instance_t *p_aout = (aout_instance_t *)p_this; - struct aout_sys_t * p_sys; - struct pa_sample_spec ss; - const struct pa_buffer_attr *buffer_attr; - struct pa_buffer_attr a; - struct pa_channel_map map; +/* TODO: + * - pause input on policy event + * - resample to compensate for long term drift + * - select music or video stream property correctly (?) + * - set further appropriate stream properties + * - update output devices list dynamically + */ - /* Allocate structures */ - p_aout->output.p_sys = p_sys = calloc( 1, sizeof( aout_sys_t ) ); - if( p_sys == NULL ) - return VLC_ENOMEM; +/* NOTE: + * Be careful what you do when the PulseAudio mainloop is held, which is to say + * within PulseAudio callbacks, or after vlc_pa_lock(). + * In particular, a VLC variable callback cannot be triggered nor deleted with + * the PulseAudio mainloop lock held, if the callback acquires the lock. */ - PULSE_DEBUG( "Pulse start initialization"); +struct aout_sys_t +{ + pa_stream *stream; /**< PulseAudio playback stream object */ + pa_context *context; /**< PulseAudio connection context */ + pa_time_event *trigger; /**< Deferred stream trigger */ + pa_volume_t base_volume; /**< 0dB reference volume */ + pa_cvolume cvolume; /**< actual sink input volume */ + mtime_t paused; /**< Time when (last) paused */ + mtime_t pts; /**< Play time of buffer write offset */ + mtime_t desync; /**< Measured desynchronization */ + unsigned rate; /**< Current stream sample rate */ +}; + +static void sink_list_cb(pa_context *, const pa_sink_info *, int, void *); +static void sink_input_info_cb(pa_context *, const pa_sink_input_info *, + int, void *); - ss.channels = aout_FormatNbChannels( &p_aout->output.output ); /* Get the input stream channel count */ +/*** Context ***/ +static void context_cb(pa_context *ctx, pa_subscription_event_type_t type, + uint32_t idx, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + pa_operation *op; - /* Setup the pulse audio stream based on the input stream count */ - switch(ss.channels) + switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { - case 8: - p_aout->output.output.i_physical_channels - = AOUT_CHAN_LEFT | AOUT_CHAN_RIGHT | AOUT_CHAN_CENTER - | AOUT_CHAN_MIDDLELEFT | AOUT_CHAN_MIDDLERIGHT - | AOUT_CHAN_REARLEFT | AOUT_CHAN_REARRIGHT - | AOUT_CHAN_LFE; - break; - case 6: - p_aout->output.output.i_physical_channels - = AOUT_CHAN_LEFT | AOUT_CHAN_RIGHT | AOUT_CHAN_CENTER - | AOUT_CHAN_REARLEFT | AOUT_CHAN_REARRIGHT - | AOUT_CHAN_LFE; + case PA_SUBSCRIPTION_EVENT_SINK: + switch (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) + { + case PA_SUBSCRIPTION_EVENT_NEW: + case PA_SUBSCRIPTION_EVENT_CHANGE: + op = pa_context_get_sink_info_by_index(ctx, idx, sink_list_cb, aout); + if (likely(op != NULL)) + pa_operation_unref(op); break; - case 4: - p_aout->output.output.i_physical_channels - = AOUT_CHAN_LEFT | AOUT_CHAN_RIGHT - | AOUT_CHAN_REARLEFT | AOUT_CHAN_REARRIGHT; + case PA_SUBSCRIPTION_EVENT_REMOVE: + var_Change(aout, "audio-device", VLC_VAR_DELCHOICE, + &(vlc_value_t){ .i_int = idx }, NULL); break; + } + break; - case 2: - p_aout->output.output.i_physical_channels - = AOUT_CHAN_LEFT | AOUT_CHAN_RIGHT; + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + if (idx != pa_stream_get_index(sys->stream)) + break; /* only interested in our sink input */ + + /* Gee... PA will not provide the infos directly in the event. */ + switch (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) + { + case PA_SUBSCRIPTION_EVENT_REMOVE: + msg_Err(aout, "sink input killed!"); break; - case 1: - p_aout->output.output.i_physical_channels = AOUT_CHAN_CENTER; + default: + op = pa_context_get_sink_input_info(ctx, idx, sink_input_info_cb, + aout); + if (likely(op != NULL)) + pa_operation_unref(op); break; + } + break; - default: - msg_Err(p_aout,"Invalid number of channels"); - goto fail; + default: /* unsubscribed facility?! */ + assert(0); } +} - /* Add a quick command line info message */ - msg_Dbg(p_aout, "%d audio channels", ss.channels); - ss.rate = p_aout->output.output.i_rate; - if (vlc_CPU() & CPU_CAPABILITY_FPU) - { - ss.format = PA_SAMPLE_FLOAT32NE; - p_aout->output.output.i_format = VLC_CODEC_FL32; - } - else - { - ss.format = PA_SAMPLE_S16NE; - p_aout->output.output.i_format = VLC_CODEC_S16N; - } - - if (!pa_sample_spec_valid(&ss)) { - msg_Err(p_aout,"Invalid sample spec"); - goto fail; - } - - /* Reduce overall latency to 200mS to reduce audible clicks - * Also pulse minreq and internal buffers are now 20mS which reduces resampling - */ - a.tlength = pa_bytes_per_second(&ss)/5; - a.maxlength = a.tlength * 2; - a.prebuf = a.tlength / 2; - a.minreq = a.tlength / 10; +/*** Sink ***/ +static void sink_list_cb(pa_context *c, const pa_sink_info *i, int eol, + void *userdata) +{ + audio_output_t *aout = userdata; + vlc_value_t val, text; + + if (eol) + return; + (void) c; + + msg_Dbg(aout, "listing sink %s (%"PRIu32"): %s", i->name, i->index, + i->description); + val.i_int = i->index; + text.psz_string = (char *)i->description; + var_Change(aout, "audio-device", VLC_VAR_DELCHOICE, &val, NULL); + var_Change(aout, "audio-device", VLC_VAR_ADDCHOICE, &val, &text); +} - /* Buffer size is 20mS */ - p_sys->buffer_size = a.minreq; +static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, + void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + + if (eol) + return; + (void) c; + + /* PulseAudio flat volume NORM / 100% / 0dB corresponds to no software + * amplification and maximum hardware amplification. + * VLC maps DEFAULT / 100% to no gain at all (software/hardware). + * Thus we need to use the sink base_volume as a multiplier, + * if and only if flat volume is active for our current sink. */ + if (i->flags & PA_SINK_FLAT_VOLUME) + sys->base_volume = i->base_volume; + else + sys->base_volume = PA_VOLUME_NORM; + msg_Dbg(aout, "base volume: %"PRIu32, sys->base_volume); +} - /* Initialise the speaker map setup above */ - pa_channel_map_init_auto(&map, ss.channels, PA_CHANNEL_MAP_ALSA); - if (!(p_sys->mainloop = pa_threaded_mainloop_new())) { - msg_Err(p_aout, "Failed to allocate main loop"); - goto fail; - } +/*** Latency management and lip synchronization ***/ +static mtime_t vlc_pa_get_latency(audio_output_t *aout, + pa_context *ctx, pa_stream *s) +{ + pa_usec_t latency; + int negative; - if (!(p_sys->context = pa_context_new(pa_threaded_mainloop_get_api(p_sys->mainloop), _( PULSE_CLIENT_NAME )))) { - msg_Err(p_aout, "Failed to allocate context"); - goto fail; + if (pa_stream_get_latency(s, &latency, &negative)) { + if (pa_context_errno (ctx) != PA_ERR_NODATA) + vlc_pa_error(aout, "unknown latency", ctx); + return VLC_TS_INVALID; } + return negative ? -latency : +latency; +} - pa_context_set_state_callback(p_sys->context, context_state_cb, p_aout); +static void stream_reset_sync(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + const unsigned rate = aout->format.i_rate; + + sys->pts = VLC_TS_INVALID; + sys->desync = 0; + pa_operation *op = pa_stream_update_sample_rate(s, rate, NULL, NULL); + if (unlikely(op == NULL)) + return; + pa_operation_unref(op); + sys->rate = rate; +} - PULSE_DEBUG( "Pulse before context connect"); +static void stream_start(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + pa_operation *op; - if (pa_context_connect(p_sys->context, NULL, 0, NULL) < 0) { - msg_Err(p_aout, "Failed to connect to server: %s", pa_strerror(pa_context_errno(p_sys->context))); - goto fail; + if (sys->trigger != NULL) { + vlc_pa_rttime_free(sys->trigger); + sys->trigger = NULL; } - PULSE_DEBUG( "Pulse after context connect"); + op = pa_stream_cork(s, 0, NULL, NULL); + if (op != NULL) + pa_operation_unref(op); + op = pa_stream_trigger(s, NULL, NULL); + if (likely(op != NULL)) + pa_operation_unref(op); +} - pa_threaded_mainloop_lock(p_sys->mainloop); +static void stream_stop(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + pa_operation *op; - if (pa_threaded_mainloop_start(p_sys->mainloop) < 0) { - msg_Err(p_aout, "Failed to start main loop"); - goto unlock_and_fail; + if (sys->trigger != NULL) { + vlc_pa_rttime_free(sys->trigger); + sys->trigger = NULL; } - msg_Dbg(p_aout, "Pulse mainloop started"); + op = pa_stream_cork(s, 1, NULL, NULL); + if (op != NULL) + pa_operation_unref(op); +} - /* Wait until the context is ready */ - pa_threaded_mainloop_wait(p_sys->mainloop); +static void stream_trigger_cb(pa_mainloop_api *api, pa_time_event *e, + const struct timeval *tv, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; - if (pa_context_get_state(p_sys->context) != PA_CONTEXT_READY) { - msg_Err(p_aout, "Failed to connect to server: %s", pa_strerror(pa_context_errno(p_sys->context))); - goto unlock_and_fail; - } + msg_Dbg(aout, "starting deferred"); + assert (sys->trigger == e); + stream_start(sys->stream, aout); + (void) api; (void) e; (void) tv; +} - if (!(p_sys->stream = pa_stream_new(p_sys->context, "audio stream", &ss, &map))) { - msg_Err(p_aout, "Failed to create stream: %s", pa_strerror(pa_context_errno(p_sys->context))); - goto unlock_and_fail; +/** + * Starts or resumes the playback stream. + * Tries start playing back audio samples at the most accurate time + * in order to minimize desync and resampling during early playback. + * @note PulseAudio lock required. + */ +static void stream_resync(audio_output_t *aout, pa_stream *s) +{ + aout_sys_t *sys = aout->sys; + mtime_t delta; + + assert (pa_stream_is_corked(s) > 0); + assert (sys->pts != VLC_TS_INVALID); + + delta = vlc_pa_get_latency(aout, sys->context, s); + if (unlikely(delta == VLC_TS_INVALID)) + delta = 0; /* screwed */ + + delta = (sys->pts - mdate()) - delta; + if (delta > 0) { + if (sys->trigger == NULL) { + msg_Dbg(aout, "deferring start (%"PRId64" us)", delta); + delta += pa_rtclock_now(); + sys->trigger = pa_context_rttime_new(sys->context, delta, + stream_trigger_cb, aout); + } + } else { + msg_Warn(aout, "starting late (%"PRId64" us)", delta); + stream_start(s, aout); } +} - PULSE_DEBUG( "Pulse after new stream"); - - pa_stream_set_state_callback(p_sys->stream, stream_state_cb, p_aout); - pa_stream_set_write_callback(p_sys->stream, stream_request_cb, p_aout); - pa_stream_set_latency_update_callback(p_sys->stream, stream_latency_update_cb, p_aout); +static void stream_latency_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + mtime_t delta, change; - if (pa_stream_connect_playback(p_sys->stream, NULL, &a, PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_ADJUST_LATENCY, NULL, NULL) < 0) { - msg_Err(p_aout, "Failed to connect stream: %s", pa_strerror(pa_context_errno(p_sys->context))); - goto unlock_and_fail; + if (pa_stream_is_corked(s)) + return; + if (sys->pts == VLC_TS_INVALID) + { + msg_Dbg(aout, "missing latency from input"); + return; } - PULSE_DEBUG("Pulse stream connect"); - - /* Wait until the stream is ready */ - pa_threaded_mainloop_wait(p_sys->mainloop); + /* Compute lip desynchronization */ + delta = vlc_pa_get_latency(aout, sys->context, s); + if (delta == VLC_TS_INVALID) + return; + + delta = (sys->pts - mdate()) - delta; + change = delta - sys->desync; + sys->desync = delta; + //msg_Dbg(aout, "desync: %+"PRId64" us (variation: %+"PRId64" us)", + // delta, change); + + const unsigned inrate = aout->format.i_rate; + unsigned outrate = sys->rate; + bool sync = false; + + if (delta < -AOUT_MAX_PTS_DELAY) + msg_Warn(aout, "too late by %"PRId64" us", -delta); + else if (delta > +AOUT_MAX_PTS_ADVANCE) + msg_Warn(aout, "too early by %"PRId64" us", delta); + else if (outrate == inrate) + return; /* In sync, do not add unnecessary disturbance! */ + else + sync = true; + + /* Compute playback sample rate */ + /* This is empirical (especially the shift values). + * Feel free to define something smarter. */ + int adj = sync ? (outrate - inrate) + : outrate * ((delta >> 4) + change) / (CLOCK_FREQ << 2); + /* This avoids too quick rate variation. It sounds really bad and + * causes unstability (e.g. oscillation around the correct rate). */ + int limit = inrate >> 10; + /* However, to improve stability and try to converge, closing to the + * nominal rate is favored over drifting from it. */ + if ((adj > 0) == (sys->rate > inrate)) + limit *= 2; + if (adj > +limit) + adj = +limit; + if (adj < -limit) + adj = -limit; + outrate -= adj; + + /* This keeps the effective rate within specified range + * (+/-AOUT_MAX_RESAMPLING% - see ) of the nominal rate. */ + limit = inrate * AOUT_MAX_RESAMPLING / 100; + if (outrate > inrate + limit) + outrate = inrate + limit; + if (outrate < inrate - limit) + outrate = inrate - limit; + + /* Apply adjusted sample rate */ + if (outrate == sys->rate) + return; + pa_operation *op = pa_stream_update_sample_rate(s, outrate, NULL, NULL); + if (unlikely(op == NULL)) { + vlc_pa_error(aout, "cannot change sample rate", sys->context); + return; + } + pa_operation_unref(op); + msg_Dbg(aout, "changed sample rate to %u Hz",outrate); + sys->rate = outrate; +} - msg_Dbg(p_aout,"Pulse stream connected"); - if (pa_stream_get_state(p_sys->stream) != PA_STREAM_READY) { - msg_Err(p_aout, "Failed to connect to server: %s", pa_strerror(pa_context_errno(p_sys->context))); - goto unlock_and_fail; +/*** Stream helpers ***/ +static void stream_state_cb(pa_stream *s, void *userdata) +{ + switch (pa_stream_get_state(s)) { + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + vlc_pa_signal(0); + default: + break; } + (void) userdata; +} +static void stream_event_cb(pa_stream *s, const char *name, pa_proplist *pl, + void *userdata) +{ + audio_output_t *aout = userdata; - PULSE_DEBUG("Pulse after stream get status"); +#if PA_CHECK_VERSION(1,0,0) + /* FIXME: expose aout_Restart() directly */ + if (!strcmp(name, PA_STREAM_EVENT_FORMAT_LOST)) { + vlc_value_t dummy = { .i_int = 0 }; - pa_threaded_mainloop_unlock(p_sys->mainloop); + msg_Dbg (aout, "format lost"); + aout_ChannelsRestart (VLC_OBJECT(aout), "audio-device", + dummy, dummy, NULL); + } else +#endif + msg_Warn (aout, "unhandled event %s", name); + (void) s; + (void) pl; +} - buffer_attr = pa_stream_get_buffer_attr(p_sys->stream); - p_aout->output.i_nb_samples = buffer_attr->minreq / pa_frame_size(&ss); - p_aout->output.pf_play = Play; - aout_VolumeSoftInit(p_aout); - msg_Dbg(p_aout, "Pulse initialized successfully"); +static void stream_moved_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + pa_operation *op; + uint32_t idx = pa_stream_get_device_index(s); + + msg_Dbg(aout, "connected to sink %"PRIu32": %s", idx, + pa_stream_get_device_name(s)); + op = pa_context_get_sink_info_by_index(sys->context, idx, + sink_info_cb, aout); + if (likely(op != NULL)) + pa_operation_unref(op); + + /* Update the variable if someone else moved our stream */ + var_Change(aout, "audio-device", VLC_VAR_SETVALUE, + &(vlc_value_t){ .i_int = idx }, NULL); + + /* Sink unknown as yet, create stub choice for it */ + if (var_GetInteger(aout, "audio-device") != idx) { - char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX]; - - msg_Dbg(p_aout, "Buffer metrics: maxlength=%u, tlength=%u, prebuf=%u, minreq=%u", buffer_attr->maxlength, buffer_attr->tlength, buffer_attr->prebuf, buffer_attr->minreq); - msg_Dbg(p_aout, "Using sample spec '%s', channel map '%s'.", - pa_sample_spec_snprint(sst, sizeof(sst), pa_stream_get_sample_spec(p_sys->stream)), - pa_channel_map_snprint(cmt, sizeof(cmt), pa_stream_get_channel_map(p_sys->stream))); - - msg_Dbg(p_aout, "Connected to device %s (%u, %ssuspended).", - pa_stream_get_device_name(p_sys->stream), - pa_stream_get_device_index(p_sys->stream), - pa_stream_is_suspended(p_sys->stream) ? "" : "not "); + var_Change(aout, "audio-device", VLC_VAR_ADDCHOICE, + &(vlc_value_t){ .i_int = idx }, + &(vlc_value_t){ .psz_string = (char *)"?" }); + var_Change(aout, "audio-device", VLC_VAR_SETVALUE, + &(vlc_value_t){ .i_int = idx }, NULL); } +} - return VLC_SUCCESS; +static void stream_overflow_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; + + msg_Err(aout, "overflow"); + (void) s; +} -unlock_and_fail: - msg_Dbg(p_aout, "Pulse initialization unlock and fail"); +static void stream_started_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; - if (p_sys->mainloop) - pa_threaded_mainloop_unlock(p_sys->mainloop); -fail: - msg_Err(p_aout, "Pulse initialization failed"); - uninit(p_aout); - return VLC_EGENERIC; + msg_Dbg(aout, "started"); + (void) s; } -/***************************************************************************** - * Play: play a sound samples buffer - *****************************************************************************/ -static void Play( aout_instance_t * p_aout ) +static void stream_suspended_cb(pa_stream *s, void *userdata) { - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; + audio_output_t *aout = userdata; - pa_operation *o; + msg_Dbg(aout, "suspended"); + stream_reset_sync(s, aout); +} - if(!p_sys->started){ - msg_Dbg(p_aout, "Pulse stream started"); - p_sys->start_date = - aout_FifoFirstDate( p_aout, &p_aout->output.fifo ); - p_sys->started = 1; +static void stream_underflow_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; - pa_threaded_mainloop_lock(p_sys->mainloop); - if((o = pa_stream_flush(p_sys->stream, success_cb, p_aout))){ - pa_operation_unref(o); - } - pa_threaded_mainloop_unlock(p_sys->mainloop); + msg_Warn(aout, "underflow"); + stream_stop(s, aout); + stream_reset_sync(s, aout); +} - pa_threaded_mainloop_signal(p_sys->mainloop, 0); +static int stream_wait(pa_stream *stream) +{ + pa_stream_state_t state; + + while ((state = pa_stream_get_state(stream)) != PA_STREAM_READY) { + if (state == PA_STREAM_FAILED || state == PA_STREAM_TERMINATED) + return -1; + vlc_pa_wait(); } + return 0; } -/***************************************************************************** - * Close: close the audio device - *****************************************************************************/ -static void Close ( vlc_object_t *p_this ) + +/*** Sink input ***/ +static void sink_input_info_cb(pa_context *ctx, const pa_sink_input_info *i, + int eol, void *userdata) { - aout_instance_t *p_aout = (aout_instance_t *)p_this; - struct aout_sys_t * p_sys = p_aout->output.p_sys; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + float volume; - msg_Dbg(p_aout, "Pulse Close"); + if (eol) + return; + (void) ctx; - if(p_sys->stream){ - pa_operation *o; - pa_threaded_mainloop_lock(p_sys->mainloop); - pa_stream_set_write_callback(p_sys->stream, NULL, NULL); + sys->cvolume = i->volume; + volume = pa_cvolume_max(&i->volume) / (float)PA_VOLUME_NORM; + aout_VolumeHardSet(aout, volume, i->mute); +} - if((o = pa_stream_drain(p_sys->stream, success_cb, p_aout))){ - while (pa_operation_get_state(o) != PA_OPERATION_DONE) { - CHECK_DEAD_GOTO(fail); - pa_threaded_mainloop_wait(p_sys->mainloop); - } - fail: +/*** VLC audio output callbacks ***/ - pa_operation_unref(o); - } +/* Memory free callback. The block_t address is in front of the data. */ +static void data_free(void *data) +{ + block_t **pp = data, *block; - pa_threaded_mainloop_unlock(p_sys->mainloop); - } - uninit(p_aout); + memcpy(&block, pp - 1, sizeof (block)); + block_Release(block); } -static void uninit(aout_instance_t *p_aout){ - struct aout_sys_t * p_sys = p_aout->output.p_sys; - - if (p_sys->mainloop) - pa_threaded_mainloop_stop(p_sys->mainloop); +static void *data_convert(block_t **pp) +{ + block_t *block = *pp; + /* In most cases, there is enough head room, and this is really cheap: */ + block = block_Realloc(block, sizeof (block), block->i_buffer); + *pp = block; + if (unlikely(block == NULL)) + return NULL; + + memcpy(block->p_buffer, &block, sizeof (block)); + block->p_buffer += sizeof (block); + block->i_buffer -= sizeof (block); + return block->p_buffer; +} - if (p_sys->stream) { - pa_stream_disconnect(p_sys->stream); - pa_stream_unref(p_sys->stream); - p_sys->stream = NULL; +/** + * Queue one audio frame to the playabck stream + */ +static void Play(audio_output_t *aout, block_t *block) +{ + aout_sys_t *sys = aout->sys; + pa_stream *s = sys->stream; + + const void *ptr = data_convert(&block); + if (unlikely(ptr == NULL)) + return; + + size_t len = block->i_buffer; + mtime_t pts = block->i_pts + block->i_length; + + /* Note: The core already holds the output FIFO lock at this point. + * Therefore we must not under any circumstances (try to) acquire the + * output FIFO lock while the PulseAudio threaded main loop lock is held + * (including from PulseAudio stream callbacks). Otherwise lock inversion + * will take place, and sooner or later a deadlock. */ + vlc_pa_lock(); + + sys->pts = pts; + if (pa_stream_is_corked(s) > 0) + stream_resync(aout, s); + +#if 0 /* Fault injector to test underrun recovery */ + static volatile unsigned u = 0; + if ((++u % 1000) == 0) { + msg_Err(aout, "fault injection"); + pa_operation_unref(pa_stream_flush(s, NULL, NULL)); } +#endif - if (p_sys->context) { - pa_context_disconnect(p_sys->context); - pa_context_unref(p_sys->context); - p_sys->context = NULL; + if (pa_stream_write(s, ptr, len, data_free, 0, PA_SEEK_RELATIVE) < 0) { + vlc_pa_error(aout, "cannot write", sys->context); + block_Release(block); } - if (p_sys->mainloop) { - pa_threaded_mainloop_free(p_sys->mainloop); - p_sys->mainloop = NULL; + vlc_pa_unlock(); +} + +/** + * Cork or uncork the playback stream + */ +static void Pause(audio_output_t *aout, bool paused, mtime_t date) +{ + aout_sys_t *sys = aout->sys; + pa_stream *s = sys->stream; + + vlc_pa_lock(); + + if (paused) { + sys->paused = date; + stream_stop(s, aout); + } else { + assert (sys->paused != VLC_TS_INVALID); + date -= sys->paused; + msg_Dbg(aout, "resuming after %"PRId64" us", date); + sys->paused = VLC_TS_INVALID; + sys->pts += date; + stream_resync(aout, s); } - free(p_sys); - p_aout->output.p_sys = NULL; + vlc_pa_unlock(); } -static void context_state_cb(pa_context *c, void *userdata) { - aout_instance_t *p_aout = (aout_instance_t *)userdata; - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; +/** + * Flush or drain the playback stream + */ +static void Flush(audio_output_t *aout, bool wait) +{ + aout_sys_t *sys = aout->sys; + pa_stream *s = sys->stream; + pa_operation *op; - assert(c); + vlc_pa_lock(); - PULSE_DEBUG( "Pulse context state changed"); + if (wait) + op = pa_stream_drain(s, NULL, NULL); + /* TODO: wait for drain completion*/ + else + op = pa_stream_flush(s, NULL, NULL); + if (op != NULL) + pa_operation_unref(op); + vlc_pa_unlock(); +} - switch (pa_context_get_state(c)) { - case PA_CONTEXT_READY: - case PA_CONTEXT_TERMINATED: - case PA_CONTEXT_FAILED: - PULSE_DEBUG( "Pulse context state changed signal"); - pa_threaded_mainloop_signal(p_sys->mainloop, 0); - break; +static int VolumeSet(audio_output_t *aout, float vol, bool mute) +{ + aout_sys_t *sys = aout->sys; + pa_operation *op; + uint32_t idx = pa_stream_get_index(sys->stream); + + pa_cvolume cvolume = sys->cvolume; + pa_volume_t volume = sys->base_volume; + + pa_cvolume_scale(&cvolume, PA_VOLUME_NORM); /* preserve balance */ + + /* VLC provides the software volume so convert directly to PulseAudio + * software volume, pa_volume_t. This is not a linear amplification factor + * so do not use PulseAudio linear amplification! */ + vol *= PA_VOLUME_NORM; + if (unlikely(vol >= PA_VOLUME_MAX)) + vol = PA_VOLUME_MAX; + volume = pa_sw_volume_multiply(volume, lround(vol)); + pa_sw_cvolume_multiply_scalar(&cvolume, &cvolume, volume); + + assert(pa_cvolume_valid(&cvolume)); + + vlc_pa_lock(); + op = pa_context_set_sink_input_volume(sys->context, idx, &cvolume, NULL, NULL); + if (likely(op != NULL)) + pa_operation_unref(op); + op = pa_context_set_sink_input_mute(sys->context, idx, mute, NULL, NULL); + if (likely(op != NULL)) + pa_operation_unref(op); + vlc_pa_unlock(); + + return 0; +} - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - PULSE_DEBUG( "Pulse context state changed no signal"); - break; - } +static int StreamMove(vlc_object_t *obj, const char *varname, vlc_value_t old, + vlc_value_t val, void *userdata) +{ + audio_output_t *aout = (audio_output_t *)obj; + aout_sys_t *sys = aout->sys; + pa_stream *s = userdata; + pa_operation *op; + uint32_t idx = pa_stream_get_index(s); + uint32_t sink_idx = val.i_int; + + (void) varname; (void) old; + + vlc_pa_lock(); + op = pa_context_move_sink_input_by_index(sys->context, idx, sink_idx, + NULL, NULL); + if (likely(op != NULL)) { + pa_operation_unref(op); + msg_Dbg(aout, "moving to sink %"PRIu32, sink_idx); + } else + vlc_pa_error(obj, "cannot move sink", sys->context); + vlc_pa_unlock(); + + return (op != NULL) ? VLC_SUCCESS : VLC_EGENERIC; } -static void stream_state_cb(pa_stream *s, void * userdata) { - aout_instance_t *p_aout = (aout_instance_t *)userdata; - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; - assert(s); +/** + * Create a PulseAudio playback stream, a.k.a. a sink input. + */ +static int Open(vlc_object_t *obj) +{ +#if !PA_CHECK_VERSION(0,9,22) + if (!vlc_xlib_init(obj)) + return VLC_EGENERIC; +#endif - PULSE_DEBUG( "Pulse stream state changed"); + audio_output_t *aout = (audio_output_t *)obj; + pa_operation *op; - switch (pa_stream_get_state(s)) { + /* Sample format specification */ + struct pa_sample_spec ss; + vlc_fourcc_t format = aout->format.i_format; +#if PA_CHECK_VERSION(1,0,0) + pa_encoding_t encoding = PA_ENCODING_INVALID; +#endif - case PA_STREAM_READY: - case PA_STREAM_FAILED: - case PA_STREAM_TERMINATED: - pa_threaded_mainloop_signal(p_sys->mainloop, 0); + switch(format) + { + case VLC_CODEC_F64B: + format = VLC_CODEC_F32B; + case VLC_CODEC_F32B: + ss.format = PA_SAMPLE_FLOAT32BE; break; - - case PA_STREAM_UNCONNECTED: - case PA_STREAM_CREATING: + case VLC_CODEC_F64L: + format = VLC_CODEC_F32L; + case VLC_CODEC_F32L: + ss.format = PA_SAMPLE_FLOAT32LE; + break; + case VLC_CODEC_FI32: + format = VLC_CODEC_FL32; + ss.format = PA_SAMPLE_FLOAT32NE; + break; + case VLC_CODEC_S32B: + ss.format = PA_SAMPLE_S32BE; + break; + case VLC_CODEC_S32L: + ss.format = PA_SAMPLE_S32LE; + break; + case VLC_CODEC_S24B: + ss.format = PA_SAMPLE_S24BE; + break; + case VLC_CODEC_S24L: + ss.format = PA_SAMPLE_S24LE; + break; + case VLC_CODEC_S16B: + ss.format = PA_SAMPLE_S16BE; + break; + case VLC_CODEC_S16L: + ss.format = PA_SAMPLE_S16LE; + break; + case VLC_CODEC_S8: + format = VLC_CODEC_U8; + case VLC_CODEC_U8: + ss.format = PA_SAMPLE_U8; + break; +#if PA_CHECK_VERSION(1,0,0) + case VLC_CODEC_A52: + format = VLC_CODEC_SPDIFL; + encoding = PA_ENCODING_AC3_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; + break; + /*case VLC_CODEC_EAC3: + format = VLC_CODEC_SPDIFL FIXME; + encoding = PA_ENCODING_EAC3_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; + break; + case VLC_CODEC_MPGA: + format = VLC_CODEC_SPDIFL FIXME; + encoding = PA_ENCODING_MPEG_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; + break;*/ + case VLC_CODEC_DTS: + format = VLC_CODEC_SPDIFL; + encoding = PA_ENCODING_DTS_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; + break; +#endif + default: + if (HAVE_FPU) + { + format = VLC_CODEC_FL32; + ss.format = PA_SAMPLE_FLOAT32NE; + } + else + { + format = VLC_CODEC_S16N; + ss.format = PA_SAMPLE_S16NE; + } break; } -} -static void stream_request_cb(pa_stream *s, size_t length, void *userdata) { - aout_instance_t *p_aout = (aout_instance_t *)userdata; - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; - mtime_t next_date; + ss.rate = aout->format.i_rate; + ss.channels = aout_FormatNbChannels(&aout->format); + if (!pa_sample_spec_valid(&ss)) { + msg_Err(aout, "unsupported sample specification"); + return VLC_EGENERIC; + } - assert(s); - assert(p_sys); + /* Channel mapping (order defined in vlc_aout.h) */ + struct pa_channel_map map; + map.channels = 0; + + if (aout->format.i_physical_channels & AOUT_CHAN_LEFT) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_LEFT; + if (aout->format.i_physical_channels & AOUT_CHAN_RIGHT) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_RIGHT; + if (aout->format.i_physical_channels & AOUT_CHAN_MIDDLELEFT) + map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_LEFT; + if (aout->format.i_physical_channels & AOUT_CHAN_MIDDLERIGHT) + map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_RIGHT; + if (aout->format.i_physical_channels & AOUT_CHAN_REARLEFT) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_LEFT; + if (aout->format.i_physical_channels & AOUT_CHAN_REARRIGHT) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_RIGHT; + if (aout->format.i_physical_channels & AOUT_CHAN_REARCENTER) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_CENTER; + if (aout->format.i_physical_channels & AOUT_CHAN_CENTER) + { + if (ss.channels == 1) + map.map[map.channels++] = PA_CHANNEL_POSITION_MONO; + else + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_CENTER; + } + if (aout->format.i_physical_channels & AOUT_CHAN_LFE) + map.map[map.channels++] = PA_CHANNEL_POSITION_LFE; - size_t buffer_size = p_sys->buffer_size; + for (unsigned i = 0; map.channels < ss.channels; i++) { + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX0 + i; + msg_Warn(aout, "mapping channel %"PRIu8" to AUX%u", map.channels, i); + } - PULSE_DEBUG( "Pulse stream request %d", length); + if (!pa_channel_map_valid(&map)) { + msg_Err(aout, "unsupported channel map"); + return VLC_EGENERIC; + } else { + const char *name = pa_channel_map_to_name(&map); + msg_Dbg(aout, "using %s channel map", (name != NULL) ? name : "?"); + } - do{ - aout_buffer_t * p_buffer = NULL; - if(p_sys->started){ - pa_usec_t latency; - int negative; - if(pa_stream_get_latency(p_sys->stream, &latency, &negative)<0){ - if (pa_context_errno(p_sys->context) != PA_ERR_NODATA) { - msg_Err(p_aout, "pa_stream_get_latency() failed: %s", pa_strerror(pa_context_errno(p_sys->context))); - } - latency = 0; + /* Stream parameters */ + const pa_stream_flags_t flags = PA_STREAM_START_CORKED + //| PA_STREAM_INTERPOLATE_TIMING + | PA_STREAM_AUTO_TIMING_UPDATE + | PA_STREAM_VARIABLE_RATE; + + struct pa_buffer_attr attr; + attr.maxlength = -1; + /* PulseAudio assumes that tlength bytes are available in the buffer. Thus + * we need to be conservative and set the minimum value that the VLC + * audio decoder thread warrants. Otherwise, PulseAudio buffers will + * underrun on hardware with large buffers. VLC keeps at least + * AOUT_MIN_PREPARE and at most AOUT_MAX_PREPARE worth of audio buffers. + * TODO? tlength could be adaptively increased to reduce wakeups. */ + attr.tlength = pa_usec_to_bytes(AOUT_MIN_PREPARE_TIME, &ss); + attr.prebuf = 0; /* trigger manually */ + attr.minreq = -1; + attr.fragsize = 0; /* not used for output */ - } + /* Allocate structures */ + aout_sys_t *sys = malloc(sizeof(*sys)); + if (unlikely(sys == NULL)) + return VLC_ENOMEM; - PULSE_DEBUG( "Pulse stream request latency=%"PRId64"", latency); - next_date = mdate() + latency; + pa_context *ctx = vlc_pa_connect (obj); + if (ctx == NULL) + { + free (sys); + return VLC_EGENERIC; + } - if(p_sys->start_date < next_date + AOUT_PTS_TOLERANCE ){ - p_buffer = aout_OutputNextBuffer( p_aout, next_date, 0); - } - } + aout->sys = sys; + sys->stream = NULL; + sys->context = ctx; + sys->trigger = NULL; + sys->paused = VLC_TS_INVALID; + sys->pts = VLC_TS_INVALID; + sys->desync = 0; + sys->rate = ss.rate; + + /* Context events */ + const pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SINK + | PA_SUBSCRIPTION_MASK_SINK_INPUT; + + pa_context_set_subscribe_callback(ctx, context_cb, aout); + op = pa_context_subscribe(ctx, mask, NULL, NULL); + if (likely(op != NULL)) + pa_operation_unref(op); + + /* Channel volume */ + sys->base_volume = PA_VOLUME_NORM; + pa_cvolume_set(&sys->cvolume, ss.channels, PA_VOLUME_NORM); + +#if PA_CHECK_VERSION(1,0,0) + pa_format_info *formatv[2]; + unsigned formatc = 0; + + /* Favor digital pass-through if available*/ + if (encoding != PA_ENCODING_INVALID) { + formatv[formatc] = pa_format_info_new(); + formatv[formatc]->encoding = encoding; + pa_format_info_set_rate(formatv[formatc], ss.rate); + pa_format_info_set_channels(formatv[formatc], ss.channels); + formatc++; + } - if ( p_buffer != NULL ) - { - PULSE_DEBUG( "Pulse stream request write buffer %d", p_buffer->i_buffer); - pa_stream_write(p_sys->stream, p_buffer->p_buffer, p_buffer->i_buffer, NULL, 0, PA_SEEK_RELATIVE); - length -= p_buffer->i_buffer; - aout_BufferFree( p_buffer ); - } - else - { - PULSE_DEBUG( "Pulse stream request write zeroes"); - void *data = pa_xmalloc(buffer_size); - bzero(data, buffer_size); - pa_stream_write(p_sys->stream, data, buffer_size, pa_xfree, 0, PA_SEEK_RELATIVE); - length -= buffer_size; - } - }while(length > buffer_size); + /* Fallback to PCM */ + formatv[formatc] = pa_format_info_new(); + formatv[formatc]->encoding = PA_ENCODING_PCM; + pa_format_info_set_sample_format(formatv[formatc], ss.format); + pa_format_info_set_rate(formatv[formatc], ss.rate); + pa_format_info_set_channels(formatv[formatc], ss.channels); + formatc++; - pa_threaded_mainloop_signal(p_sys->mainloop, 0); -} + /* Create a playback stream */ + pa_stream *s; -static void stream_latency_update_cb(pa_stream *s, void *userdata) { - aout_instance_t *p_aout = (aout_instance_t *)userdata; - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; + vlc_pa_lock(); + s = pa_stream_new_extended(ctx, "audio stream", formatv, formatc, NULL); - assert(s); + for (unsigned i = 0; i < formatc; i++) + pa_format_info_free(formatv[i]); +#else + vlc_pa_lock(); + pa_stream *s = pa_stream_new(ctx, "audio stream", &ss, &map); +#endif + if (s == NULL) { + vlc_pa_error(obj, "stream creation failure", ctx); + goto fail; + } + sys->stream = s; + pa_stream_set_state_callback(s, stream_state_cb, NULL); + pa_stream_set_event_callback(s, stream_event_cb, aout); + pa_stream_set_latency_update_callback(s, stream_latency_cb, aout); + pa_stream_set_moved_callback(s, stream_moved_cb, aout); + pa_stream_set_overflow_callback(s, stream_overflow_cb, aout); + pa_stream_set_started_callback(s, stream_started_cb, aout); + pa_stream_set_suspended_callback(s, stream_suspended_cb, aout); + pa_stream_set_underflow_callback(s, stream_underflow_cb, aout); + + if (pa_stream_connect_playback(s, NULL, &attr, flags, NULL, NULL) < 0 + || stream_wait(s)) { + vlc_pa_error(obj, "stream connection failure", ctx); + goto fail; + } + +#if PA_CHECK_VERSION(1,0,0) + if (encoding != PA_ENCODING_INVALID) { + const pa_format_info *info = pa_stream_get_format_info(s); + + assert (info != NULL); + if (pa_format_info_is_pcm (info)) { + msg_Dbg(aout, "digital pass-through not available"); + format = HAVE_FPU ? VLC_CODEC_FL32 : VLC_CODEC_S16N; + } else { + msg_Dbg(aout, "digital pass-through enabled"); + pa_stream_set_latency_update_callback(s, NULL, NULL); + } + } +#endif - PULSE_DEBUG( "Pulse stream latency update"); + const struct pa_buffer_attr *pba = pa_stream_get_buffer_attr(s); + msg_Dbg(aout, "using buffer metrics: maxlength=%u, tlength=%u, " + "prebuf=%u, minreq=%u", + pba->maxlength, pba->tlength, pba->prebuf, pba->minreq); + + var_Create(aout, "audio-device", VLC_VAR_INTEGER|VLC_VAR_HASCHOICE); + var_Change(aout, "audio-device", VLC_VAR_SETTEXT, + &(vlc_value_t){ .psz_string = (char *)_("Audio device") }, + NULL); + var_AddCallback (aout, "audio-device", StreamMove, s); + op = pa_context_get_sink_info_list(ctx, sink_list_cb, aout); + /* We may need to wait for completion... once LibVLC supports this */ + if (op != NULL) + pa_operation_unref(op); + stream_moved_cb(s, aout); + vlc_pa_unlock(); + + aout->format.i_format = format; + aout->pf_play = Play; + aout->pf_pause = Pause; + aout->pf_flush = Flush; + aout_VolumeHardInit (aout, VolumeSet); + return VLC_SUCCESS; - pa_threaded_mainloop_signal(p_sys->mainloop, 0); +fail: + vlc_pa_unlock(); + Close(obj); + return VLC_EGENERIC; } -static void success_cb(pa_stream *s, int sucess, void *userdata) +/** + * Removes a PulseAudio playback stream + */ +static void Close (vlc_object_t *obj) { - aout_instance_t *p_aout = (aout_instance_t *)userdata; - struct aout_sys_t * p_sys = (struct aout_sys_t *) p_aout->output.p_sys; - - VLC_UNUSED(sucess); - - assert(s); + audio_output_t *aout = (audio_output_t *)obj; + aout_sys_t *sys = aout->sys; + pa_context *ctx = sys->context; + pa_stream *s = sys->stream; + + if (s != NULL) { + /* The callback takes mainloop lock, so it CANNOT be held here! */ + var_DelCallback (aout, "audio-device", StreamMove, s); + var_Destroy (aout, "audio-device"); + + vlc_pa_lock (); + if (unlikely(sys->trigger != NULL)) + vlc_pa_rttime_free(sys->trigger); + pa_stream_disconnect(s); + + /* Clear all callbacks */ + pa_stream_set_state_callback(s, NULL, NULL); + pa_stream_set_event_callback(s, NULL, NULL); + pa_stream_set_latency_update_callback(s, NULL, NULL); + pa_stream_set_moved_callback(s, NULL, NULL); + pa_stream_set_overflow_callback(s, NULL, NULL); + pa_stream_set_started_callback(s, NULL, NULL); + pa_stream_set_suspended_callback(s, NULL, NULL); + pa_stream_set_underflow_callback(s, NULL, NULL); + + pa_stream_unref(s); + vlc_pa_unlock (); + } - pa_threaded_mainloop_signal(p_sys->mainloop, 0); + vlc_pa_disconnect(obj, ctx); + free(sys); } - -#undef PULSE_DEBUG