From a477481aabaa16f1ed15af456125160bc8face5a Mon Sep 17 00:00:00 2001 From: wm4 Date: Sun, 9 Mar 2014 00:04:37 +0100 Subject: audio/out: feed AOs from a separate thread This has 2 goals: - Ensure that AOs have always enough data, even if the device buffers are very small. - Reduce complexity in some AOs, which do their own buffering. One disadvantage is that performance is slightly reduced due to more copying. Implementation-wise, we don't change ao.c much, and instead "redirect" the driver's callback to an API wrapper in push.c. Additionally, we add code for dealing with AOs that have a pull API. These AOs usually do their own buffering (jack, coreaudio, portaudio), and adding a thread is basically a waste. The code in pull.c manages a ringbuffer, and allows callback-based AOs to read data directly. --- audio/out/push.c | 266 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 audio/out/push.c (limited to 'audio/out/push.c') diff --git a/audio/out/push.c b/audio/out/push.c new file mode 100644 index 0000000000..de093988bd --- /dev/null +++ b/audio/out/push.c @@ -0,0 +1,266 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * mpv 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 mpv. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" + +#include "common/msg.h" +#include "common/common.h" + +#include "osdep/threads.h" +#include "compat/atomics.h" + +#include "audio/audio.h" +#include "audio/audio_buffer.h" + +struct ao_push_state { + pthread_t thread; + pthread_mutex_t lock; + pthread_cond_t wakeup; + + struct mp_audio_buffer *buffer; + + bool terminate; + bool playing; + + // Whether the current buffer contains the complete audio. + bool final_chunk; +}; + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + int r = CONTROL_UNKNOWN; + if (ao->driver->control) { + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + r = ao->driver->control(ao, cmd, arg); + pthread_mutex_unlock(&p->lock); + } + return r; +} + +static float get_delay(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + double driver_delay = 0; + if (ao->driver->get_delay) + driver_delay = ao->driver->get_delay(ao); + double delay = driver_delay + mp_audio_buffer_seconds(p->buffer); + pthread_mutex_unlock(&p->lock); + return delay; +} + +static void reset(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + if (ao->driver->reset) + ao->driver->reset(ao); + mp_audio_buffer_clear(p->buffer); + p->playing = false; + pthread_cond_signal(&p->wakeup); + pthread_mutex_unlock(&p->lock); +} + +static void pause(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + if (ao->driver->pause) + ao->driver->pause(ao); + p->playing = false; + pthread_cond_signal(&p->wakeup); + pthread_mutex_unlock(&p->lock); +} + +static void resume(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + if (ao->driver->resume) + ao->driver->resume(ao); + p->playing = true; // tentatively + pthread_cond_signal(&p->wakeup); + pthread_mutex_unlock(&p->lock); +} + + +static int get_space(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + int s = mp_audio_buffer_get_write_available(p->buffer); + pthread_mutex_unlock(&p->lock); + return s; +} + +static int play(struct ao *ao, void **data, int samples, int flags) +{ + struct ao_push_state *p = ao->api_priv; + + pthread_mutex_lock(&p->lock); + + int write_samples = mp_audio_buffer_get_write_available(p->buffer); + write_samples = MPMIN(write_samples, samples); + + struct mp_audio audio; + mp_audio_buffer_get_format(p->buffer, &audio); + for (int n = 0; n < ao->num_planes; n++) + audio.planes[n] = data[n]; + audio.samples = write_samples; + mp_audio_buffer_append(p->buffer, &audio); + + p->final_chunk = !!(flags & AOPLAY_FINAL_CHUNK); + p->playing = true; + + pthread_cond_signal(&p->wakeup); + pthread_mutex_unlock(&p->lock); + return write_samples; +} + +// called locked +static int ao_play_data(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + struct mp_audio data; + mp_audio_buffer_peek(p->buffer, &data); + int max = data.samples; + int space = ao->driver->get_space ? ao->driver->get_space(ao) : INT_MAX; + if (data.samples > space) + data.samples = space; + if (data.samples <= 0) + return 0; + int flags = 0; + if (p->final_chunk && data.samples == max) + flags |= AOPLAY_FINAL_CHUNK; + int r = ao->driver->play(ao, data.planes, data.samples, flags); + if (r > data.samples) { + MP_WARN(ao, "Audio device returned non-sense value."); + r = data.samples; + } + if (r > 0) + mp_audio_buffer_skip(p->buffer, r); + if (p->final_chunk && mp_audio_buffer_samples(p->buffer) == 0) + p->playing = false; + return r; +} + +static void *playthread(void *arg) +{ + struct ao *ao = arg; + struct ao_push_state *p = ao->api_priv; + pthread_mutex_lock(&p->lock); + while (!p->terminate) { + double timeout = 2.0; + if (p->playing) { + double min_wait = ao->device_buffer / (double)ao->samplerate; + min_wait *= 0.75; + int r = ao_play_data(ao); + // The device buffers are not necessarily full, but writing to the + // AO buffer will wake up this thread anyway. + bool buffers_full = r <= 0; + // We have to estimate when the AO needs data again. + if (buffers_full && ao->driver->get_delay) { + float buffered_audio = ao->driver->get_delay(ao); + timeout = buffered_audio - 0.050; + if (timeout > 0.100) { + // Keep extra safety margin if the buffers are large + timeout = MPMAX(timeout - 0.200, 0.100); + } else { + timeout = MPMAX(timeout, min_wait); + } + } else { + timeout = min_wait; + } + } + struct timespec deadline = mpthread_get_deadline(timeout); + pthread_cond_timedwait(&p->wakeup, &p->lock, &deadline); + } + pthread_mutex_unlock(&p->lock); + return NULL; +} + +static void uninit(struct ao *ao, bool cut_audio) +{ + struct ao_push_state *p = ao->api_priv; + + pthread_mutex_lock(&p->lock); + p->terminate = true; + pthread_cond_signal(&p->wakeup); + pthread_mutex_unlock(&p->lock); + + pthread_join(p->thread, NULL); + + ao->driver->uninit(ao, cut_audio); + + pthread_cond_destroy(&p->wakeup); + pthread_mutex_destroy(&p->lock); +} + +static int init(struct ao *ao) +{ + struct ao_push_state *p = ao->api_priv; + + pthread_mutex_init(&p->lock, NULL); + pthread_cond_init(&p->wakeup, NULL); + + p->buffer = mp_audio_buffer_create(ao); + mp_audio_buffer_reinit_fmt(p->buffer, ao->format, + &ao->channels, ao->samplerate); + mp_audio_buffer_preallocate_min(p->buffer, ao->buffer); + if (pthread_create(&p->thread, NULL, playthread, ao)) { + ao->driver->uninit(ao, true); + return -1; + } + return 0; +} + +const struct ao_driver ao_api_push = { + .init = init, + .control = control, + .uninit = uninit, + .reset = reset, + .get_space = get_space, + .play = play, + .get_delay = get_delay, + .pause = pause, + .resume = resume, + .priv_size = sizeof(struct ao_push_state), +}; + +int ao_play_silence(struct ao *ao, int samples) +{ + assert(ao->api == &ao_api_push); + if (samples <= 0 || AF_FORMAT_IS_SPECIAL(ao->format) || !ao->driver->play) + return 0; + char *p = talloc_size(NULL, samples * ao->sstride); + af_fill_silence(p, samples * ao->sstride, ao->format); + void *tmp[MP_NUM_CHANNELS]; + for (int n = 0; n < MP_NUM_CHANNELS; n++) + tmp[n] = p; + int r = ao->driver->play(ao, tmp, samples, 0); + talloc_free(p); + return r; +} -- cgit v1.2.3