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.
This commit is contained in:
wm4 2014-03-09 00:04:37 +01:00
parent 5ffd6a9e9b
commit a477481aab
7 changed files with 593 additions and 31 deletions

View File

@ -244,7 +244,7 @@ static void probe_softvol(struct mixer *mixer)
if (mixer->opts->softvol == SOFTVOL_AUTO) {
// No system-wide volume => fine with AO volume control.
mixer->softvol =
ao_control(mixer->ao, AOCONTROL_HAS_TEMP_VOLUME, 0) != 1 &&
ao_control(mixer->ao, AOCONTROL_HAS_TEMP_VOLUME, 0) != 1 ||
ao_control(mixer->ao, AOCONTROL_HAS_PER_APP_VOLUME, 0) != 1;
} else {
mixer->softvol = mixer->opts->softvol == SOFTVOL_YES;
@ -278,8 +278,8 @@ static void restore_volume(struct mixer *mixer)
const char *prev_driver = mixer->driver;
mixer->driver = mixer->softvol ? "softvol" : ao_get_name(ao);
bool restore
= mixer->softvol || ao_control(ao, AOCONTROL_HAS_TEMP_VOLUME, 0) == 1;
bool restore =
mixer->softvol || ao_control(ao, AOCONTROL_HAS_TEMP_VOLUME, 0) == 1;
// Restore old parameters if volume won't survive reinitialization.
// But not if volume scale is possibly different.

View File

@ -31,9 +31,14 @@
#include "options/options.h"
#include "options/m_config.h"
#include "osdep/timer.h"
#include "common/msg.h"
#include "common/common.h"
#include "common/global.h"
// Minimum buffer size in seconds.
#define MIN_BUFFER 0.2
extern const struct ao_driver audio_out_oss;
extern const struct ao_driver audio_out_coreaudio;
extern const struct ao_driver audio_out_rsound;
@ -156,16 +161,36 @@ static struct ao *ao_create(bool probing, struct mpv_global *global,
if (m_config_set_obj_params(config, args) < 0)
goto error;
ao->priv = config->optstruct;
char *chmap = mp_chmap_to_str(&ao->channels);
MP_VERBOSE(ao, "requested format: %d Hz, %s channels, %s\n",
ao->samplerate, chmap, af_fmt_to_str(ao->format));
talloc_free(chmap);
ao->api = ao->driver->play ? &ao_api_push : &ao_api_pull;
ao->api_priv = talloc_zero_size(ao, ao->api->priv_size);
assert(!ao->api->priv_defaults && !ao->api->options);
if (ao->driver->init(ao) < 0)
goto error;
ao->sstride = af_fmt2bits(ao->format) / 8;
if (!af_fmt_is_planar(ao->format))
ao->num_planes = 1;
if (af_fmt_is_planar(ao->format)) {
ao->num_planes = ao->channels.num;
} else {
ao->sstride *= ao->channels.num;
}
ao->bps = ao->samplerate * ao->sstride;
if (!ao->device_buffer && ao->driver->get_space)
ao->device_buffer = ao->driver->get_space(ao);
ao->buffer = MPMAX(ao->device_buffer, MIN_BUFFER * ao->samplerate);
MP_VERBOSE(ao, "using soft-buffer of %d samples.\n", ao->buffer);
if (ao->api->init(ao) < 0)
goto error;
return ao;
error:
talloc_free(ao);
@ -214,7 +239,7 @@ done:
// cut_audio: if false, block until all remaining audio was played.
void ao_uninit(struct ao *ao, bool cut_audio)
{
ao->driver->uninit(ao, cut_audio);
ao->api->uninit(ao, cut_audio);
talloc_free(ao);
}
@ -227,7 +252,7 @@ void ao_uninit(struct ao *ao, bool cut_audio)
// flags: currently AOPLAY_FINAL_CHUNK can be set
int ao_play(struct ao *ao, void **data, int samples, int flags)
{
return ao->driver->play(ao, data, samples, flags);
return ao->api->play(ao, data, samples, flags);
}
int ao_control(struct ao *ao, enum aocontrol cmd, void *arg)
@ -238,8 +263,8 @@ int ao_control(struct ao *ao, enum aocontrol cmd, void *arg)
case AOCONTROL_HAS_PER_APP_VOLUME:
return !!ao->per_application_mixer;
default:
if (ao->driver->control)
return ao->driver->control(ao, cmd, arg);
if (ao->api->control)
return ao->api->control(ao, cmd, arg);
}
return CONTROL_UNKNOWN;
}
@ -251,34 +276,34 @@ int ao_control(struct ao *ao, enum aocontrol cmd, void *arg)
// this correctly.
double ao_get_delay(struct ao *ao)
{
if (!ao->driver->get_delay) {
if (!ao->api->get_delay) {
assert(ao->untimed);
return 0;
}
return ao->driver->get_delay(ao);
return ao->api->get_delay(ao);
}
// Return free size of the internal audio buffer. This controls how much audio
// the core should decode and try to queue with ao_play().
int ao_get_space(struct ao *ao)
{
return ao->driver->get_space(ao);
return ao->api->get_space(ao);
}
// Stop playback and empty buffers. Essentially go back to the state after
// ao->init().
void ao_reset(struct ao *ao)
{
if (ao->driver->reset)
ao->driver->reset(ao);
if (ao->api->reset)
ao->api->reset(ao);
}
// Pause playback. Keep the current buffer. ao_get_delay() must return the
// same value as before pausing.
void ao_pause(struct ao *ao)
{
if (ao->driver->pause)
ao->driver->pause(ao);
if (ao->api->pause)
ao->api->pause(ao);
}
// Resume playback. Play the remaining buffer. If the driver doesn't support
@ -286,22 +311,17 @@ void ao_pause(struct ao *ao)
// the lost audio.
void ao_resume(struct ao *ao)
{
if (ao->driver->resume)
ao->driver->resume(ao);
if (ao->api->resume)
ao->api->resume(ao);
}
int ao_play_silence(struct ao *ao, int samples)
// Wait until the audio buffer is drained. This can be used to emulate draining
// if native functionality is not available.
// Only call this on uninit (otherwise, deadlock trouble ahead).
void ao_wait_drain(struct ao *ao)
{
if (samples <= 0 || AF_FORMAT_IS_SPECIAL(ao->format))
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_play(ao, tmp, samples, 0);
talloc_free(p);
return r;
// This is probably not entirely accurate, but good enough.
mp_sleep_us(ao_get_delay(ao) * 1000000);
}
bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s,

View File

@ -19,25 +19,74 @@
#ifndef MP_AO_INTERNAL_H_
#define MP_AO_INTERNAL_H_
#include <stdbool.h>
#include "audio/chmap.h"
#include "audio/chmap_sel.h"
/* global data used by ao.c and ao drivers */
struct ao {
int samplerate;
struct mp_chmap channels;
int format; // one of AF_FORMAT_...
int bps; // bytes per second
int bps; // bytes per second (per plane)
int sstride; // size of a sample on each plane
// (format_size*num_channels/num_planes)
int num_planes;
bool probing; // if true, don't fail loudly on init
bool untimed; // don't assume realtime playback
bool no_persistent_volume; // the AO does the equivalent of af_volume
bool per_application_mixer; // like above, but volume persists (per app)
int device_buffer; // device buffer in samples (guessed by
// common init code if not set by driver)
const struct ao_driver *api; // entrypoints to the wrapper (push.c/pull.c)
const struct ao_driver *driver;
void *priv;
struct encode_lavc_context *encode_lavc_ctx;
struct input_ctx *input_ctx;
struct mp_log *log; // Using e.g. "[ao/coreaudio]" as prefix
int buffer;
void *api_priv;
};
extern const struct ao_driver ao_api_push;
extern const struct ao_driver ao_api_pull;
/* Note:
*
* In general, there are two types of audio drivers:
* a) push based (the user queues data that should be played)
* b) pull callback based (the audio API calls a callback to get audio)
*
* The ao.c code can handle both. It basically implements two audio paths
* and provides a uniform API for them. If ao_driver->play is NULL, it assumes
* that the driver uses a callback based audio API, otherwise push based.
*
* Requirements:
* a) Most functions (except ->control) must be provided. ->play is called to
* queue audio. ao.c creates a thread to regularly refill audio device
* buffers with ->play, but all driver functions are always called under
* an exclusive lock.
* Mandatory:
* init
* uninit
* reset
* get_space
* play
* get_delay
* pause
* resume
* b) ->play must be NULL. The driver can start the audio API in init(). The
* audio API in turn will start a thread and call a callback provided by the
* driver. That callback calls ao_read_data() to get audio data. Most
* functions are optional and will be emulated if missing (e.g. pausing
* is emulated as silence). ->get_delay and ->get_space are never called.
* Mandatory:
* init
* uninit
*/
struct ao_driver {
// If true, use with encoding only.
bool encode;
@ -49,7 +98,7 @@ struct ao_driver {
// device doesn't accept these parameters, you can attempt to negotiate
// fallback parameters, and set the ao format fields accordingly.
int (*init)(struct ao *ao);
// See ao_control() etc. in ao.c
// Optional. See ao_control() etc. in ao.c
int (*control)(struct ao *ao, enum aocontrol cmd, void *arg);
void (*uninit)(struct ao *ao, bool cut_audio);
void (*reset)(struct ao*ao);
@ -65,8 +114,12 @@ struct ao_driver {
const struct m_option *options;
};
// These functions can be called by AOs. They don't lock the AO.
// These functions can be called by AOs.
int ao_play_silence(struct ao *ao, int samples);
void ao_wait_drain(struct ao *ao);
int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_us);
bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s,
struct mp_chmap *map);
bool ao_chmap_sel_get_def(struct ao *ao, const struct mp_chmap_sel *s,

219
audio/out/pull.c Normal file
View File

@ -0,0 +1,219 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <stddef.h>
#include <inttypes.h>
#include <assert.h>
#include "ao.h"
#include "internal.h"
#include "audio/format.h"
#include "common/msg.h"
#include "common/common.h"
#include "osdep/timer.h"
#include "osdep/threads.h"
#include "compat/atomics.h"
#include "misc/ring.h"
enum {
AO_STATE_NONE, // idle (e.g. before playback started, or after playback
// finished, but device is open)
AO_STATE_PLAY, // play the buffer
AO_STATE_PAUSE, // pause playback
};
struct ao_pull_state {
// Be very careful with the order when accessing planes.
struct mp_ring *buffers[MP_NUM_CHANNELS];
// AO_STATE_*
int state;
// Whether buffers[] can be accessed.
int ready;
// Device delay of the last written sample, in realtime.
int64_t end_time_us;
};
static int get_space(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
// Since the reader will read the last plane last, its free space is the
// minimum free space across all planes.
return mp_ring_available(p->buffers[ao->num_planes - 1]) / ao->sstride;
}
static int play(struct ao *ao, void **data, int samples, int flags)
{
struct ao_pull_state *p = ao->api_priv;
int write_samples = get_space(ao);
write_samples = MPMIN(write_samples, samples);
// Write starting from the last plane - this way, the first plane will
// always contain the minimum amount of data readable across all planes
// (assumes the reader starts with the first plane).
int write_bytes = write_samples * ao->sstride;
for (int n = ao->num_planes - 1; n >= 0; n--) {
int r = mp_ring_write(p->buffers[n], data[n], write_bytes);
assert(r == write_bytes);
}
if (p->state != AO_STATE_PLAY) {
p->end_time_us = 0;
p->state = AO_STATE_PLAY;
mp_memory_barrier();
if (ao->driver->resume)
ao->driver->resume(ao);
}
return write_samples;
}
// Read the given amount of samples in the user-provided data buffer. Returns
// the number of samples copied. If there is not enough data (buffer underrun
// or EOF), return the number of samples that could be copied, and fill the
// rest of the user-provided buffer with silence.
// This basically assumes that the audio device doesn't care about underruns.
// If this is called in paused mode, it will always return 0.
// The caller should set out_time_us to the expected delay the last sample
// reaches the speakers, in microseconds, using mp_time_us() as reference.
int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_us)
{
assert(ao->api == &ao_api_pull);
struct ao_pull_state *p = ao->api_priv;
int full_bytes = samples * ao->sstride;
mp_memory_barrier();
if (!p->ready) {
for (int n = 0; n < ao->num_planes; n++)
af_fill_silence(data[n], full_bytes, ao->format);
return 0;
}
// Since the writer will write the first plane last, its buffered amount
// of data is the minimum amount across all planes.
int bytes = mp_ring_buffered(p->buffers[0]);
bytes = MPMIN(bytes, full_bytes);
if (bytes > 0)
p->end_time_us = out_time_us;
mp_memory_barrier();
if (p->state == AO_STATE_PAUSE)
bytes = 0;
for (int n = 0; n < ao->num_planes; n++) {
int r = mp_ring_read(p->buffers[n], data[n], bytes);
assert(r == bytes);
// pad with silence (underflow/paused/eof)
int silence = full_bytes - bytes;
if (silence)
af_fill_silence((char *)data[n] + bytes, silence, ao->format);
}
return bytes / ao->sstride;
}
static int control(struct ao *ao, enum aocontrol cmd, void *arg)
{
if (ao->driver->control)
return ao->driver->control(ao, cmd, arg);
return CONTROL_UNKNOWN;
}
// Return size of the buffered data in seconds. Can include the device latency.
// Basically, this returns how much data there is still to play, and how long
// it takes until the last sample in the buffer reaches the speakers. This is
// used for audio/video synchronization, so it's very important to implement
// this correctly.
static float get_delay(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
mp_memory_barrier();
int64_t end = p->end_time_us;
int64_t now = mp_time_us();
double driver_delay = MPMAX(0, (end - now) / (1000.0 * 1000.0));
return mp_ring_buffered(p->buffers[0]) / (double)ao->bps + driver_delay;
}
static void reset(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
if (ao->driver->reset)
ao->driver->reset(ao);
// Not like this is race-condition free. It will work if ->reset
// stops the audio callback, though.
p->ready = 0;
p->state = AO_STATE_NONE;
mp_memory_barrier();
for (int n = 0; n < ao->num_planes; n++)
mp_ring_reset(p->buffers[n]);
p->end_time_us = 0;
mp_memory_barrier();
p->ready = 1;
mp_memory_barrier();
}
static void pause(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
if (ao->driver->pause)
ao->driver->pause(ao);
p->state = AO_STATE_PAUSE;
mp_memory_barrier();
}
static void resume(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
p->state = AO_STATE_PLAY;
mp_memory_barrier();
if (ao->driver->resume)
ao->driver->resume(ao);
}
static void uninit(struct ao *ao, bool cut_audio)
{
ao->driver->uninit(ao, cut_audio);
}
static int init(struct ao *ao)
{
struct ao_pull_state *p = ao->api_priv;
for (int n = 0; n < ao->num_planes; n++)
p->buffers[n] = mp_ring_new(ao, ao->buffer * ao->sstride);
p->ready = 1;
mp_memory_barrier();
return 0;
}
const struct ao_driver ao_api_pull = {
.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_pull_state),
};

266
audio/out/push.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include <stddef.h>
#include <pthread.h>
#include <inttypes.h>
#include <limits.h>
#include <assert.h>
#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;
}

View File

@ -181,6 +181,8 @@ SOURCES = audio/audio.c \
audio/out/ao.c \
audio/out/ao_null.c \
audio/out/ao_pcm.c \
audio/out/pull.c \
audio/out/push.c \
bstr/bstr.c \
common/asxparser.c \
common/av_common.c \

View File

@ -145,6 +145,8 @@ def build(ctx):
( "audio/out/ao_sdl.c", "sdl2" ),
( "audio/out/ao_sndio.c", "sndio" ),
( "audio/out/ao_wasapi.c", "wasapi" ),
( "audio/out/pull.c" ),
( "audio/out/push.c" ),
## Bstr
( "bstr/bstr.c" ),