X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=modules%2Faudio_output%2Fpulse.c;h=8ca3d4d8efef6fd3ef6eaa436e3331dabfbbbe61;hb=8b47d5a797a7e8dda7c40db634cf29ff040e3d73;hp=bad3e1d55d39724189ffcdeb8b331bda62731129;hpb=0462f44e2a133be0b07f365570b91c5f3cd4c38e;p=vlc diff --git a/modules/audio_output/pulse.c b/modules/audio_output/pulse.c index bad3e1d55d..8ca3d4d8ef 100644 --- a/modules/audio_output/pulse.c +++ b/modules/audio_output/pulse.c @@ -1,36 +1,41 @@ /***************************************************************************** * pulse.c : Pulseaudio output plugin for vlc ***************************************************************************** - * Copyright (C) 2008 the VideoLAN team + * Copyright (C) 2008 VLC authors and VideoLAN * Copyright (C) 2009-2011 Rémi Denis-Courmont * * Authors: Martin Hamrle * - * 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 + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 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. + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser 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., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #ifdef HAVE_CONFIG_H # include "config.h" #endif +#include #include #include #include #include #include +#include "vlcpulse.h" +#if !PA_CHECK_VERSION(0,9,22) +# include +#endif static int Open ( vlc_object_t * ); static void Close ( vlc_object_t * ); @@ -45,56 +50,126 @@ vlc_module_begin () set_callbacks( Open, Close ) vlc_module_end () -/* TODO: single static mainloop */ +/* NOTE: + * Be careful what you do when the PulseAudio mainloop is held, which is to say + * within PulseAudio callbacks, or after pa_threaded_mainloop_lock(). + * In particular, a VLC variable callback cannot be triggered nor deleted with + * the PulseAudio mainloop lock held, if the callback acquires the lock. */ + +struct sink +{ + struct sink *next; + char *description; + uint32_t index; + char name[1]; +}; struct aout_sys_t { pa_stream *stream; /**< PulseAudio playback stream object */ pa_context *context; /**< PulseAudio connection context */ - pa_threaded_mainloop *mainloop; /**< PulseAudio event loop */ + pa_threaded_mainloop *mainloop; /**< PulseAudio thread */ + pa_time_event *trigger; /**< Deferred stream trigger */ pa_volume_t base_volume; /**< 0dB reference volume */ pa_cvolume cvolume; /**< actual sink input volume */ - //uint32_t byterate; /**< bytes per second */ + mtime_t first_pts; /**< Play time of buffer start */ + mtime_t paused; /**< Time when (last) paused */ + + pa_stream_flags_t flags_force; /**< Forced flags (stream must be NULL) */ + char *sink_force; /**< Forced sink name (stream must be NULL) */ + + struct sink *sinks; /**< Locally-cached list of sinks */ }; -/* Context helpers */ -static void context_state_cb(pa_context *c, void *userdata) + +/*** Sink ***/ +static void sink_add_cb(pa_context *ctx, const pa_sink_info *i, int eol, + void *userdata) { - pa_threaded_mainloop *mainloop = userdata; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; - switch (pa_context_get_state(c)) { - case PA_CONTEXT_READY: - case PA_CONTEXT_FAILED: - case PA_CONTEXT_TERMINATED: - pa_threaded_mainloop_signal(mainloop, 0); - default: - break; - } + if (eol) + return; + (void) ctx; + + msg_Dbg(aout, "listing sink %s (%"PRIu32"): %s", i->name, i->index, + i->description); + + size_t namelen = strlen(i->name); + struct sink *sink = malloc(sizeof (*sink) + namelen); + if (unlikely(sink == NULL)) + return; + + sink->next = sys->sinks; + sink->index = i->index; + sink->description = strdup(i->description); + memcpy(sink->name, i->name, namelen + 1); + sys->sinks = sink; } -static bool context_wait(pa_threaded_mainloop *mainloop, pa_context *context) +static void sink_mod_cb(pa_context *ctx, const pa_sink_info *i, int eol, + void *userdata) { - pa_context_state_t state; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; - while ((state = pa_context_get_state(context)) != PA_CONTEXT_READY) { - if (state == PA_CONTEXT_FAILED || state == PA_CONTEXT_TERMINATED) - return -1; - pa_threaded_mainloop_wait(mainloop); - } - return 0; + if (eol) + return; + (void) ctx; + + for (struct sink *sink = sys->sinks; sink != NULL; sink = sink->next) + if (sink->index == i->index) + { + free(sink->description); + sink->description = strdup(i->description); + } } -static void error(aout_instance_t *aout, const char *msg, pa_context *context) +static void sink_del(uint32_t index, audio_output_t *aout) { - msg_Err(aout, "%s: %s", msg, pa_strerror(pa_context_errno(context))); + aout_sys_t *sys = aout->sys; + struct sink **pp = &sys->sinks, *sink; + + while ((sink = *pp) != NULL) + if (sink->index == index) + { + *pp = sink->next; + free(sink->description); + free(sink); + } + else + pp = &sink->next; +} + +static void sink_event(pa_context *ctx, unsigned type, uint32_t idx, + audio_output_t *aout) +{ + pa_operation *op = NULL; + + switch (type) + { + case PA_SUBSCRIPTION_EVENT_NEW: + op = pa_context_get_sink_info_by_index(ctx, idx, sink_add_cb, + aout); + break; + case PA_SUBSCRIPTION_EVENT_CHANGE: + op = pa_context_get_sink_info_by_index(ctx, idx, sink_mod_cb, + aout); + break; + case PA_SUBSCRIPTION_EVENT_REMOVE: + sink_del(idx, aout); + break; + } + if (op != NULL) + pa_operation_unref(op); } -/* Sink */ static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata) { - aout_instance_t *aout = userdata; - aout_sys_t *sys = aout->output.p_sys; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; if (eol) return; @@ -109,10 +184,107 @@ static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, sys->base_volume = i->base_volume; else sys->base_volume = PA_VOLUME_NORM; - msg_Dbg(aout, "base volume: %f", pa_sw_volume_to_linear(sys->base_volume)); + msg_Dbg(aout, "base volume: %"PRIu32, sys->base_volume); +} + + +/*** Latency management and lip synchronization ***/ +static void stream_start_now(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + pa_operation *op; + + assert (sys->trigger == NULL); + + 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); +} + +static void stream_stop(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + pa_operation *op; + + if (sys->trigger != NULL) { + vlc_pa_rttime_free(sys->mainloop, sys->trigger); + sys->trigger = NULL; + } + + op = pa_stream_cork(s, 1, NULL, NULL); + if (op != NULL) + pa_operation_unref(op); +} + +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; + + assert (sys->trigger == e); + + msg_Dbg(aout, "starting deferred"); + vlc_pa_rttime_free(sys->mainloop, sys->trigger); + sys->trigger = NULL; + stream_start_now(sys->stream, aout); + (void) api; (void) e; (void) tv; +} + +/** + * 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_start(pa_stream *s, audio_output_t *aout) +{ + aout_sys_t *sys = aout->sys; + mtime_t delta; + + assert (sys->first_pts != VLC_TS_INVALID); + + if (sys->trigger != NULL) { + vlc_pa_rttime_free(sys->mainloop, sys->trigger); + sys->trigger = NULL; + } + + delta = vlc_pa_get_latency(aout, sys->context, s); + if (unlikely(delta == VLC_TS_INVALID)) { + msg_Dbg(aout, "cannot synchronize start"); + delta = 0; /* screwed */ + } + + delta = (sys->first_pts - mdate()) - delta; + if (delta > 0) { + 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_now(s, aout); + } } -/* Stream helpers */ +static void stream_latency_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + + if (sys->paused != VLC_TS_INVALID) + return; /* nothing to do while paused */ + if (sys->first_pts == VLC_TS_INVALID) + return; /* nothing to do if buffers are (still) empty */ + if (pa_stream_is_corked(s) > 0) + stream_start(s, aout); +} + + +/*** Stream helpers ***/ static void stream_state_cb(pa_stream *s, void *userdata) { pa_threaded_mainloop *mainloop = userdata; @@ -127,32 +299,72 @@ static void stream_state_cb(pa_stream *s, void *userdata) } } +static void stream_buffer_attr_cb(pa_stream *s, void *userdata) +{ + audio_output_t *aout = userdata; + const pa_buffer_attr *pba = pa_stream_get_buffer_attr(s); + + msg_Dbg(aout, "changed buffer metrics: maxlength=%u, tlength=%u, " + "prebuf=%u, minreq=%u", + pba->maxlength, pba->tlength, pba->prebuf, pba->minreq); +} + +static void stream_event_cb(pa_stream *s, const char *name, pa_proplist *pl, + void *userdata) +{ + audio_output_t *aout = userdata; + + if (!strcmp(name, PA_STREAM_EVENT_REQUEST_CORK)) + aout_PolicyReport(aout, true); + else + if (!strcmp(name, PA_STREAM_EVENT_REQUEST_UNCORK)) + aout_PolicyReport(aout, false); + else +#if PA_CHECK_VERSION(1,0,0) + /* FIXME: expose aout_Restart() directly */ + if (!strcmp(name, PA_STREAM_EVENT_FORMAT_LOST)) { + msg_Dbg (aout, "format lost"); + aout_RestartRequest (aout, AOUT_RESTART_OUTPUT); + } else +#endif + msg_Warn (aout, "unhandled stream event \"%s\"", name); + (void) s; + (void) pl; +} + static void stream_moved_cb(pa_stream *s, void *userdata) { - aout_instance_t *aout = userdata; - aout_sys_t *sys = aout->output.p_sys; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + const char *name = pa_stream_get_device_name(s); 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); + msg_Dbg(aout, "connected to sink %s", name); + aout_DeviceReport(aout, name); + + op = pa_context_get_sink_info_by_name(sys->context, name, sink_info_cb, + aout); if (likely(op != NULL)) pa_operation_unref(op); } static void stream_overflow_cb(pa_stream *s, void *userdata) { - aout_instance_t *aout = userdata; + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + pa_operation *op; - msg_Err(aout, "overflow"); - (void) s; + msg_Err(aout, "overflow, flushing"); + op = pa_stream_flush(s, NULL, NULL); + if (unlikely(op == NULL)) + return; + pa_operation_unref(op); + sys->first_pts = VLC_TS_INVALID; } static void stream_started_cb(pa_stream *s, void *userdata) { - aout_instance_t *aout = userdata; + audio_output_t *aout = userdata; msg_Dbg(aout, "started"); (void) s; @@ -160,7 +372,7 @@ static void stream_started_cb(pa_stream *s, void *userdata) static void stream_suspended_cb(pa_stream *s, void *userdata) { - aout_instance_t *aout = userdata; + audio_output_t *aout = userdata; msg_Dbg(aout, "suspended"); (void) s; @@ -168,13 +380,13 @@ static void stream_suspended_cb(pa_stream *s, void *userdata) static void stream_underflow_cb(pa_stream *s, void *userdata) { - aout_instance_t *aout = userdata; + audio_output_t *aout = userdata; msg_Dbg(aout, "underflow"); (void) s; } -static int stream_wait(pa_threaded_mainloop *mainloop, pa_stream *stream) +static int stream_wait(pa_stream *stream, pa_threaded_mainloop *mainloop) { pa_stream_state_t state; @@ -186,6 +398,94 @@ static int stream_wait(pa_threaded_mainloop *mainloop, pa_stream *stream) return 0; } + +/*** Sink input ***/ +static void sink_input_info_cb(pa_context *ctx, const pa_sink_input_info *i, + int eol, void *userdata) +{ + audio_output_t *aout = userdata; + aout_sys_t *sys = aout->sys; + + if (eol) + return; + (void) ctx; + + sys->cvolume = i->volume; /* cache volume for balance preservation */ + + pa_volume_t volume = pa_cvolume_max(&i->volume); + volume = pa_sw_volume_divide(volume, sys->base_volume); + aout_VolumeReport(aout, (float)volume / PA_VOLUME_NORM); + aout_MuteReport(aout, i->mute); +} + +static void sink_input_event(pa_context *ctx, + pa_subscription_event_type_t type, + uint32_t idx, audio_output_t *aout) +{ + pa_operation *op; + + /* Gee... PA will not provide the infos directly in the event. */ + switch (type) + { + case PA_SUBSCRIPTION_EVENT_REMOVE: + msg_Err(aout, "sink input killed!"); + break; + + default: + op = pa_context_get_sink_input_info(ctx, idx, sink_input_info_cb, + aout); + if (likely(op != NULL)) + pa_operation_unref(op); + break; + } +} + + +/*** 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; + unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + + type &= PA_SUBSCRIPTION_EVENT_TYPE_MASK; + switch (facility) + { + case PA_SUBSCRIPTION_EVENT_SINK: + sink_event(ctx, type, idx, userdata); + break; + + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + /* only interested in our sink input */ + if (sys->stream != NULL && idx == pa_stream_get_index(sys->stream)) + sink_input_event(ctx, type, idx, userdata); + break; + + default: /* unsubscribed facility?! */ + assert(0); + } +} + + +/*** VLC audio output callbacks ***/ + +static int TimeGet(audio_output_t *aout, mtime_t *restrict delay) +{ + aout_sys_t *sys = aout->sys; + pa_stream *s = sys->stream; + + if (pa_stream_is_corked(s) > 0) + return -1; /* latency is irrelevant if corked */ + + mtime_t delta = vlc_pa_get_latency(aout, sys->context, s); + if (delta == VLC_TS_INVALID) + return -1; + + *delay = delta; + return 0; +} + /* Memory free callback. The block_t address is in front of the data. */ static void data_free(void *data) { @@ -210,14 +510,22 @@ static void *data_convert(block_t **pp) return block->p_buffer; } -/***************************************************************************** - * Play: play a sound samples buffer - *****************************************************************************/ -static void Play(aout_instance_t *aout) +/** + * Queue one audio frame to the playback stream + */ +static void Play(audio_output_t *aout, block_t *block) { - aout_sys_t *sys = aout->output.p_sys; + aout_sys_t *sys = aout->sys; pa_stream *s = sys->stream; + assert (sys->paused == VLC_TS_INVALID); + + const void *ptr = data_convert(&block); + if (unlikely(ptr == NULL)) + return; + + size_t len = block->i_buffer; + /* 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 @@ -225,163 +533,254 @@ static void Play(aout_instance_t *aout) * will take place, and sooner or later a deadlock. */ pa_threaded_mainloop_lock(sys->mainloop); - if (pa_stream_is_corked(sys->stream) > 0) { - pa_operation *op = pa_stream_cork(s, 0, NULL, NULL); - if (op != NULL) - pa_operation_unref(op); - msg_Dbg(aout, "uncorking"); - } + if (sys->first_pts == VLC_TS_INVALID) + sys->first_pts = block->i_pts; -#if 0 - /* This function should be called by the LibVLC core a header of time, - * but not more than AOUT_MAX_PREPARE. The PulseAudio latency should be - * shorter than that (though it might not be the case with some evil piece - * of audio output hardware). So we may need to trigger playback early, - * (that is to say, short cut the PulseAudio prebuffering). Otherwise, - * audio and video may be out of synchronization. */ - pa_usec_t latency; - int negative; - if (pa_stream_get_latency(s, &latency, &negative) < 0) { - /* Especially at start of stream, latency may not be known (yet). */ - if (pa_context_errno(sys->context) != PA_ERR_NODATA) - error(aout, "cannot determine latency", sys->context); - } else { - mtime_t gap = aout_FifoFirstDate(aout, &aout->output.fifo) - mdate() - - latency; + if (pa_stream_is_corked(s) > 0) + stream_start(s, aout); - if (gap > AOUT_PTS_TOLERANCE) - msg_Dbg(aout, "buffer too early (%"PRId64" us)", gap); - else if (gap < -AOUT_PTS_TOLERANCE) - msg_Err(aout, "buffer too late (%"PRId64" us)", -gap); - } -#endif #if 0 /* Fault injector to test underrun recovery */ - static unsigned u = 0; - if ((++u % 500) == 0) { + static volatile unsigned u = 0; + if ((++u % 1000) == 0) { msg_Err(aout, "fault injection"); - msleep(CLOCK_FREQ*2); + pa_operation_unref(pa_stream_flush(s, NULL, NULL)); } #endif - /* This function is called exactly once per block in the output FIFO, so - * this for-loop is not necessary. - * If this function is changed to not always dequeue blocks, be sure to - * limit the queue size to a reasonable limit to avoid huge leaks. */ - for (;;) { - block_t *block = aout_FifoPop(aout, &aout->output.fifo); - if (block == NULL) - break; + 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); + } - const void *ptr = data_convert(&block); - if (unlikely(ptr == NULL)) - break; + pa_threaded_mainloop_unlock(sys->mainloop); +} + +/** + * 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; - size_t len = block->i_buffer; - //mtime_t pts = block->i_pts, duration = block->i_length; + pa_threaded_mainloop_lock(sys->mainloop); - if (pa_stream_write(s, ptr, len, data_free, 0, PA_SEEK_RELATIVE) < 0) - { - error(aout, "cannot write", sys->context); - block_Release(block); + 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; + + if (sys->first_pts != VLC_TS_INVALID) { + sys->first_pts += date; + stream_start(s, aout); } } pa_threaded_mainloop_unlock(sys->mainloop); } -static int VolumeSet(aout_instance_t *aout, audio_volume_t vol, bool mute) +/** + * Flush or drain the playback stream + */ +static void Flush(audio_output_t *aout, bool wait) { - aout_sys_t *sys = aout->output.p_sys; - pa_threaded_mainloop *mainloop = sys->mainloop; + aout_sys_t *sys = aout->sys; + pa_stream *s = sys->stream; pa_operation *op; - uint32_t idx = pa_stream_get_index(sys->stream); - pa_volume_t volume = pa_sw_volume_from_linear(vol / (float)AOUT_VOLUME_DEFAULT); - pa_cvolume cvolume; + pa_threaded_mainloop_lock(sys->mainloop); + + 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); + pa_threaded_mainloop_unlock(sys->mainloop); +} + +static int VolumeSet(audio_output_t *aout, float vol) +{ + aout_sys_t *sys = aout->sys; + if (sys->stream == NULL) + { + msg_Err (aout, "cannot change volume while not playing"); + return -1; + } - /* TODO: do not ruin the channel balance (if set outside VLC) */ - /* TODO: notify UI about volume changes by other PulseAudio clients */ - pa_cvolume_set(&sys->cvolume, sys->cvolume.channels, volume); - pa_sw_cvolume_multiply_scalar(&cvolume, &sys->cvolume, sys->base_volume); + /* 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; + pa_volume_t volume = pa_sw_volume_multiply(lround(vol), sys->base_volume); + + /* Preserve the balance (VLC does not support it). */ + pa_cvolume cvolume = sys->cvolume; + pa_cvolume_scale(&cvolume, PA_VOLUME_NORM); + pa_sw_cvolume_multiply_scalar(&cvolume, &cvolume, volume); assert(pa_cvolume_valid(&cvolume)); - pa_threaded_mainloop_lock(mainloop); - op = pa_context_set_sink_input_volume(sys->context, idx, &cvolume, NULL, NULL); + pa_operation *op; + uint32_t idx = pa_stream_get_index(sys->stream); + pa_threaded_mainloop_lock(sys->mainloop); + op = pa_context_set_sink_input_volume(sys->context, idx, &cvolume, + NULL, NULL); if (likely(op != NULL)) pa_operation_unref(op); + pa_threaded_mainloop_unlock(sys->mainloop); + + return 0; +} + +static int MuteSet(audio_output_t *aout, bool mute) +{ + aout_sys_t *sys = aout->sys; + + if (sys->stream == NULL) + { + sys->flags_force &= ~(PA_STREAM_START_MUTED|PA_STREAM_START_UNMUTED); + sys->flags_force |= + mute ? PA_STREAM_START_MUTED : PA_STREAM_START_UNMUTED; + return 0; + } + + pa_operation *op; + uint32_t idx = pa_stream_get_index(sys->stream); + pa_threaded_mainloop_lock(sys->mainloop); op = pa_context_set_sink_input_mute(sys->context, idx, mute, NULL, NULL); if (likely(op != NULL)) pa_operation_unref(op); - pa_threaded_mainloop_unlock(mainloop); + pa_threaded_mainloop_unlock(sys->mainloop); return 0; } -/***************************************************************************** - * Open: open the audio device - *****************************************************************************/ -static int Open(vlc_object_t *obj) +static int SinksList(audio_output_t *aout, char ***namesp, char ***descsp) { - aout_instance_t *aout = (aout_instance_t *)obj; + aout_sys_t *sys = aout->sys; + char **names, **descs; + unsigned n = 0; + + pa_threaded_mainloop_lock(sys->mainloop); + for (struct sink *sink = sys->sinks; sink != NULL; sink = sink->next) + n++; + + *namesp = names = xmalloc(sizeof(*names) * n); + *descsp = descs = xmalloc(sizeof(*descs) * n); + + for (struct sink *sink = sys->sinks; sink != NULL; sink = sink->next) + { + *(names++) = strdup(sink->name); + *(descs++) = strdup(sink->description); + } + pa_threaded_mainloop_unlock(sys->mainloop); + return n; +} + +static int StreamMove(audio_output_t *aout, const char *name) +{ + aout_sys_t *sys = aout->sys; + + if (sys->stream == NULL) + { + msg_Dbg(aout, "will connect to sink %s", name); + free(sys->sink_force); + sys->sink_force = strdup(name); + return 0; + } + + pa_operation *op; + uint32_t idx = pa_stream_get_index(sys->stream); + + pa_threaded_mainloop_lock(sys->mainloop); + op = pa_context_move_sink_input_by_name(sys->context, idx, name, + NULL, NULL); + if (likely(op != NULL)) { + pa_operation_unref(op); + msg_Dbg(aout, "moving to sink %s", name); + } else + vlc_pa_error(aout, "cannot move sink input", sys->context); + pa_threaded_mainloop_unlock(sys->mainloop); + + return (op != NULL) ? 0 : -1; +} + +static void Stop(audio_output_t *); + +/** + * Create a PulseAudio playback stream, a.k.a. a sink input. + */ +static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt) +{ + aout_sys_t *sys = aout->sys; /* Sample format specification */ struct pa_sample_spec ss; +#if PA_CHECK_VERSION(1,0,0) + pa_encoding_t encoding = PA_ENCODING_INVALID; +#endif - switch(aout->output.output.i_format) + switch (fmt->i_format) { - case VLC_CODEC_F64B: - aout->output.output.i_format = VLC_CODEC_F32B; - case VLC_CODEC_F32B: - ss.format = PA_SAMPLE_FLOAT32BE; - break; - case VLC_CODEC_F64L: - aout->output.output.i_format = VLC_CODEC_F32L; - case VLC_CODEC_F32L: - ss.format = PA_SAMPLE_FLOAT32LE; - break; - case VLC_CODEC_FI32: - aout->output.output.i_format = VLC_CODEC_FL32; + case VLC_CODEC_FL64: + fmt->i_format = VLC_CODEC_FL32; + case VLC_CODEC_FL32: ss.format = PA_SAMPLE_FLOAT32NE; break; - case VLC_CODEC_S32B: - ss.format = PA_SAMPLE_S32BE; + case VLC_CODEC_S32N: + ss.format = PA_SAMPLE_S32NE; break; - case VLC_CODEC_S32L: - ss.format = PA_SAMPLE_S32LE; + case VLC_CODEC_S16N: + ss.format = PA_SAMPLE_S16NE; break; - case VLC_CODEC_S24B: - ss.format = PA_SAMPLE_S24BE; + case VLC_CODEC_U8: + ss.format = PA_SAMPLE_U8; break; - case VLC_CODEC_S24L: - ss.format = PA_SAMPLE_S24LE; +#if PA_CHECK_VERSION(1,0,0) + case VLC_CODEC_A52: + fmt->i_format = VLC_CODEC_SPDIFL; + encoding = PA_ENCODING_AC3_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; break; - case VLC_CODEC_S16B: - ss.format = PA_SAMPLE_S16BE; + /*case VLC_CODEC_EAC3: + fmt->i_format = VLC_CODEC_SPDIFL FIXME; + encoding = PA_ENCODING_EAC3_IEC61937; + ss.format = HAVE_FPU ? PA_SAMPLE_FLOAT32NE : PA_SAMPLE_S16NE; break; - case VLC_CODEC_S16L: - ss.format = PA_SAMPLE_S16LE; - break; - case VLC_CODEC_S8: - aout->output.output.i_format = VLC_CODEC_U8; - case VLC_CODEC_U8: - ss.format = PA_SAMPLE_U8; + case VLC_CODEC_MPGA: + fmt->i_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: + fmt->i_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) { - aout->output.output.i_format = VLC_CODEC_FL32; + fmt->i_format = VLC_CODEC_FL32; ss.format = PA_SAMPLE_FLOAT32NE; } else { - aout->output.output.i_format = VLC_CODEC_S16N; + fmt->i_format = VLC_CODEC_S16N; ss.format = PA_SAMPLE_S16NE; } break; } - ss.rate = aout->output.output.i_rate; - ss.channels = aout_FormatNbChannels(&aout->output.output); + ss.rate = fmt->i_rate; + ss.channels = aout_FormatNbChannels(fmt); if (!pa_sample_spec_valid(&ss)) { msg_Err(aout, "unsupported sample specification"); return VLC_EGENERIC; @@ -391,28 +790,28 @@ static int Open(vlc_object_t *obj) struct pa_channel_map map; map.channels = 0; - if (aout->output.output.i_physical_channels & AOUT_CHAN_LEFT) + if (fmt->i_physical_channels & AOUT_CHAN_LEFT) map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_LEFT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_RIGHT) + if (fmt->i_physical_channels & AOUT_CHAN_RIGHT) map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_RIGHT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_MIDDLELEFT) + if (fmt->i_physical_channels & AOUT_CHAN_MIDDLELEFT) map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_LEFT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_MIDDLERIGHT) + if (fmt->i_physical_channels & AOUT_CHAN_MIDDLERIGHT) map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_RIGHT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_REARLEFT) + if (fmt->i_physical_channels & AOUT_CHAN_REARLEFT) map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_LEFT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_REARRIGHT) + if (fmt->i_physical_channels & AOUT_CHAN_REARRIGHT) map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_RIGHT; - if (aout->output.output.i_physical_channels & AOUT_CHAN_REARCENTER) + if (fmt->i_physical_channels & AOUT_CHAN_REARCENTER) map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_CENTER; - if (aout->output.output.i_physical_channels & AOUT_CHAN_CENTER) + if (fmt->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->output.output.i_physical_channels & AOUT_CHAN_LFE) + if (fmt->i_physical_channels & AOUT_CHAN_LFE) map.map[map.channels++] = PA_CHANNEL_POSITION_LFE; for (unsigned i = 0; map.channels < ss.channels; i++) { @@ -424,135 +823,237 @@ static int Open(vlc_object_t *obj) msg_Err(aout, "unsupported channel map"); return VLC_EGENERIC; } else { - const char *name = pa_channel_map_to_pretty_name(&map); + const char *name = pa_channel_map_to_name(&map); msg_Dbg(aout, "using %s channel map", (name != NULL) ? name : "?"); } /* Stream parameters */ - const pa_stream_flags_t flags = PA_STREAM_INTERPOLATE_TIMING + const pa_stream_flags_t flags = sys->flags_force + | PA_STREAM_START_CORKED + | PA_STREAM_INTERPOLATE_TIMING + | PA_STREAM_NOT_MONOTONIC | PA_STREAM_AUTO_TIMING_UPDATE - | PA_STREAM_ADJUST_LATENCY - | PA_STREAM_START_CORKED; + | PA_STREAM_FIX_RATE; - const uint32_t byterate = pa_bytes_per_second(&ss); struct pa_buffer_attr attr; - /* no point in larger buffers on PA side than VLC */ attr.maxlength = -1; - attr.tlength = byterate * AOUT_MAX_ADVANCE_TIME / CLOCK_FREQ; - attr.prebuf = byterate * AOUT_MAX_PREPARE_TIME / CLOCK_FREQ; - attr.minreq = -1; + /* PulseAudio goes berserk if the target length (tlength) is not + * significantly longer than 2 periods (minreq), or when the period length + * is unspecified and the target length is short. */ + attr.tlength = pa_usec_to_bytes(3 * AOUT_MIN_PREPARE_TIME, &ss); + attr.prebuf = 0; /* trigger manually */ + attr.minreq = pa_usec_to_bytes(AOUT_MIN_PREPARE_TIME, &ss); attr.fragsize = 0; /* not used for output */ - /* Allocate structures */ - aout_sys_t *sys = malloc(sizeof(*sys)); - if (unlikely(sys == NULL)) - return VLC_ENOMEM; - aout->output.p_sys = sys; - sys->context = NULL; sys->stream = NULL; - //sys->byterate = byterate; + sys->trigger = NULL; + sys->first_pts = VLC_TS_INVALID; + sys->paused = VLC_TS_INVALID; /* Channel volume */ sys->base_volume = PA_VOLUME_NORM; pa_cvolume_set(&sys->cvolume, ss.channels, PA_VOLUME_NORM); - /* Allocate threaded main loop */ - pa_threaded_mainloop *mainloop = pa_threaded_mainloop_new(); - if (unlikely(mainloop == NULL)) { - free(sys); - return VLC_ENOMEM; +#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); + pa_format_info_set_channel_map(formatv[formatc], &map); + formatc++; } - sys->mainloop = mainloop; - if (pa_threaded_mainloop_start(mainloop) < 0) { - pa_threaded_mainloop_free(mainloop); - free(sys); - return VLC_ENOMEM; - } - pa_threaded_mainloop_lock(mainloop); - - /* Connect to PulseAudio server */ - char *user_agent = var_InheritString(aout, "user-agent"); - pa_context *ctx = pa_context_new(pa_threaded_mainloop_get_api(mainloop), - user_agent); - free(user_agent); - if (unlikely(ctx == NULL)) - goto fail; - sys->context = ctx; - - pa_context_set_state_callback(ctx, context_state_cb, mainloop); - if (pa_context_connect(ctx, NULL, 0, NULL) < 0 - || context_wait(mainloop, ctx)) { - error(aout, "cannot connect to server", ctx); - goto fail; - } + /* 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); + pa_format_info_set_channel_map(formatv[formatc], &map); + formatc++; /* Create a playback stream */ - pa_stream *s = pa_stream_new(ctx, "audio stream", &ss, &map); + pa_stream *s; + pa_proplist *props = pa_proplist_new(); + if (likely(props != NULL)) + /* TODO: set other stream properties */ + pa_proplist_sets (props, PA_PROP_MEDIA_ROLE, "video"); + + pa_threaded_mainloop_lock(sys->mainloop); + s = pa_stream_new_extended(sys->context, "audio stream", formatv, formatc, + props); + if (likely(props != NULL)) + pa_proplist_free(props); + + for (unsigned i = 0; i < formatc; i++) + pa_format_info_free(formatv[i]); +#else + pa_threaded_mainloop_lock(sys->mainloop); + pa_stream *s = pa_stream_new(sys->context, "audio stream", &ss, &map); +#endif if (s == NULL) { - error(aout, "cannot create stream", ctx); - goto fail; + pa_threaded_mainloop_unlock(sys->mainloop); + vlc_pa_error(aout, "stream creation failure", sys->context); + return VLC_EGENERIC; } sys->stream = s; - pa_stream_set_state_callback(s, stream_state_cb, mainloop); + pa_stream_set_state_callback(s, stream_state_cb, sys->mainloop); + pa_stream_set_buffer_attr_callback(s, stream_buffer_attr_cb, aout); + 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(mainloop, s)) { - error(aout, "cannot connect stream", ctx); + if (pa_stream_connect_playback(s, sys->sink_force, &attr, flags, NULL, + NULL) < 0 + || stream_wait(s, sys->mainloop)) { + vlc_pa_error(aout, "stream connection failure", sys->context); goto fail; } - stream_moved_cb(s, aout); - - 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); + sys->flags_force = PA_STREAM_NOFLAGS; + free(sys->sink_force); + sys->sink_force = NULL; + + const struct pa_sample_spec *spec = pa_stream_get_sample_spec(s); +#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"); + fmt->i_format = HAVE_FPU ? VLC_CODEC_FL32 : VLC_CODEC_S16N; + } else { + msg_Dbg(aout, "digital pass-through enabled"); + spec = NULL; + } + } +#endif + if (spec != NULL) + fmt->i_rate = spec->rate; - aout->output.i_nb_samples = pba->minreq / pa_frame_size(&ss); - pa_threaded_mainloop_unlock(mainloop); + stream_buffer_attr_cb(s, aout); + stream_moved_cb(s, aout); + pa_threaded_mainloop_unlock(sys->mainloop); - aout->output.pf_play = Play; - aout->output.pf_volume_set = VolumeSet; return VLC_SUCCESS; fail: - pa_threaded_mainloop_unlock(mainloop); - Close(obj); + pa_threaded_mainloop_unlock(sys->mainloop); + Stop(aout); return VLC_EGENERIC; } -/***************************************************************************** - * Close: close the audio device - *****************************************************************************/ -static void Close (vlc_object_t *obj) +/** + * Removes a PulseAudio playback stream + */ +static void Stop(audio_output_t *aout) { - aout_instance_t *aout = (aout_instance_t *)obj; - aout_sys_t *sys = aout->output.p_sys; - pa_threaded_mainloop *mainloop = sys->mainloop; - pa_context *ctx = sys->context; + aout_sys_t *sys = aout->sys; pa_stream *s = sys->stream; - pa_threaded_mainloop_lock(mainloop); - if (s != NULL) { - pa_operation *op; + pa_threaded_mainloop_lock(sys->mainloop); + if (unlikely(sys->trigger != NULL)) + vlc_pa_rttime_free(sys->mainloop, sys->trigger); + pa_stream_disconnect(s); + + /* Clear all callbacks */ + pa_stream_set_state_callback(s, NULL, NULL); + pa_stream_set_buffer_attr_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); + sys->stream = NULL; + pa_threaded_mainloop_unlock(sys->mainloop); +} + +static int Open(vlc_object_t *obj) +{ + audio_output_t *aout = (audio_output_t *)obj; + aout_sys_t *sys = malloc(sizeof (*sys)); + pa_operation *op; - op = pa_stream_flush(s, NULL, NULL); - if (op != NULL) - pa_operation_unref(op); - op = pa_stream_drain(s, NULL, NULL); - if (op != NULL) - pa_operation_unref(op); - pa_stream_disconnect(s); - pa_stream_unref(s); +#if !PA_CHECK_VERSION(0,9,22) + if (!vlc_xlib_init(obj)) + return VLC_EGENERIC; +#endif + if (unlikely(sys == NULL)) + return VLC_ENOMEM; + + /* Allocate structures */ + pa_context *ctx = vlc_pa_connect(obj, &sys->mainloop); + if (ctx == NULL) + { + free(sys); + return VLC_EGENERIC; + } + sys->stream = NULL; + sys->context = ctx; + sys->flags_force = PA_STREAM_NOFLAGS; + sys->sink_force = NULL; + sys->sinks = NULL; + + aout->sys = sys; + aout->start = Start; + aout->stop = Stop; + aout->time_get = TimeGet; + aout->play = Play; + aout->pause = Pause; + aout->flush = Flush; + aout->volume_set = VolumeSet; + aout->mute_set = MuteSet; + aout->device_enum = SinksList; + aout->device_select = StreamMove; + + pa_threaded_mainloop_lock(sys->mainloop); + /* Sinks (output devices) list */ + op = pa_context_get_sink_info_list(sys->context, sink_add_cb, aout); + if (op != NULL) + pa_operation_unref(op); + + /* Context events */ + const pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SINK + | PA_SUBSCRIPTION_MASK_SINK_INPUT; + pa_context_set_subscribe_callback(sys->context, context_cb, aout); + op = pa_context_subscribe(sys->context, mask, NULL, NULL); + if (likely(op != NULL)) + pa_operation_unref(op); + pa_threaded_mainloop_unlock(sys->mainloop); + + return VLC_SUCCESS; +} + +static void Close(vlc_object_t *obj) +{ + audio_output_t *aout = (audio_output_t *)obj; + aout_sys_t *sys = aout->sys; + pa_context *ctx = sys->context; + + pa_threaded_mainloop_lock(sys->mainloop); + pa_context_set_subscribe_callback(sys->context, NULL, NULL); + pa_threaded_mainloop_unlock(sys->mainloop); + vlc_pa_disconnect(obj, ctx, sys->mainloop); + + for (struct sink *sink = sys->sinks, *next; sink != NULL; sink = next) + { + next = sink->next; + free(sink->description); + free(sink); } - if (ctx != NULL) - pa_context_unref(ctx); - pa_threaded_mainloop_unlock(mainloop); - pa_threaded_mainloop_free(mainloop); + free(sys->sink_force); free(sys); }