diff options
Diffstat (limited to '')
30 files changed, 12482 insertions, 0 deletions
diff --git a/audio/out/ao.c b/audio/out/ao.c new file mode 100644 index 0000000..a5aa3a9 --- /dev/null +++ b/audio/out/ao.c @@ -0,0 +1,719 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include <assert.h> + +#include "mpv_talloc.h" + +#include "config.h" +#include "ao.h" +#include "internal.h" +#include "audio/format.h" + +#include "options/options.h" +#include "options/m_config_frontend.h" +#include "osdep/endian.h" +#include "common/msg.h" +#include "common/common.h" +#include "common/global.h" + +extern const struct ao_driver audio_out_oss; +extern const struct ao_driver audio_out_audiotrack; +extern const struct ao_driver audio_out_audiounit; +extern const struct ao_driver audio_out_coreaudio; +extern const struct ao_driver audio_out_coreaudio_exclusive; +extern const struct ao_driver audio_out_rsound; +extern const struct ao_driver audio_out_pipewire; +extern const struct ao_driver audio_out_sndio; +extern const struct ao_driver audio_out_pulse; +extern const struct ao_driver audio_out_jack; +extern const struct ao_driver audio_out_openal; +extern const struct ao_driver audio_out_opensles; +extern const struct ao_driver audio_out_null; +extern const struct ao_driver audio_out_alsa; +extern const struct ao_driver audio_out_wasapi; +extern const struct ao_driver audio_out_pcm; +extern const struct ao_driver audio_out_lavc; +extern const struct ao_driver audio_out_sdl; + +static const struct ao_driver * const audio_out_drivers[] = { +// native: +#if HAVE_ANDROID + &audio_out_audiotrack, +#endif +#if HAVE_AUDIOUNIT + &audio_out_audiounit, +#endif +#if HAVE_COREAUDIO + &audio_out_coreaudio, +#endif +#if HAVE_PIPEWIRE + &audio_out_pipewire, +#endif +#if HAVE_PULSE + &audio_out_pulse, +#endif +#if HAVE_ALSA + &audio_out_alsa, +#endif +#if HAVE_WASAPI + &audio_out_wasapi, +#endif +#if HAVE_OSS_AUDIO + &audio_out_oss, +#endif + // wrappers: +#if HAVE_JACK + &audio_out_jack, +#endif +#if HAVE_OPENAL + &audio_out_openal, +#endif +#if HAVE_OPENSLES + &audio_out_opensles, +#endif +#if HAVE_SDL2_AUDIO + &audio_out_sdl, +#endif +#if HAVE_SNDIO + &audio_out_sndio, +#endif + &audio_out_null, +#if HAVE_COREAUDIO + &audio_out_coreaudio_exclusive, +#endif + &audio_out_pcm, + &audio_out_lavc, +}; + +static bool get_desc(struct m_obj_desc *dst, int index) +{ + if (index >= MP_ARRAY_SIZE(audio_out_drivers)) + return false; + const struct ao_driver *ao = audio_out_drivers[index]; + *dst = (struct m_obj_desc) { + .name = ao->name, + .description = ao->description, + .priv_size = ao->priv_size, + .priv_defaults = ao->priv_defaults, + .options = ao->options, + .options_prefix = ao->options_prefix, + .global_opts = ao->global_opts, + .hidden = ao->encode, + .p = ao, + }; + return true; +} + +// For the ao option +static const struct m_obj_list ao_obj_list = { + .get_desc = get_desc, + .description = "audio outputs", + .allow_trailer = true, + .disallow_positional_parameters = true, + .use_global_options = true, +}; + +#define OPT_BASE_STRUCT struct ao_opts +const struct m_sub_options ao_conf = { + .opts = (const struct m_option[]) { + {"ao", OPT_SETTINGSLIST(audio_driver_list, &ao_obj_list), + .flags = UPDATE_AUDIO}, + {"audio-device", OPT_STRING(audio_device), .flags = UPDATE_AUDIO}, + {"audio-client-name", OPT_STRING(audio_client_name), .flags = UPDATE_AUDIO}, + {"audio-buffer", OPT_DOUBLE(audio_buffer), + .flags = UPDATE_AUDIO, M_RANGE(0, 10)}, + {0} + }, + .size = sizeof(OPT_BASE_STRUCT), + .defaults = &(const OPT_BASE_STRUCT){ + .audio_buffer = 0.2, + .audio_device = "auto", + .audio_client_name = "mpv", + }, +}; + +static struct ao *ao_alloc(bool probing, struct mpv_global *global, + void (*wakeup_cb)(void *ctx), void *wakeup_ctx, + char *name) +{ + assert(wakeup_cb); + + struct mp_log *log = mp_log_new(NULL, global->log, "ao"); + struct m_obj_desc desc; + if (!m_obj_list_find(&desc, &ao_obj_list, bstr0(name))) { + mp_msg(log, MSGL_ERR, "Audio output %s not found!\n", name); + talloc_free(log); + return NULL; + }; + struct ao_opts *opts = mp_get_config_group(NULL, global, &ao_conf); + struct ao *ao = talloc_ptrtype(NULL, ao); + talloc_steal(ao, log); + *ao = (struct ao) { + .driver = desc.p, + .probing = probing, + .global = global, + .wakeup_cb = wakeup_cb, + .wakeup_ctx = wakeup_ctx, + .log = mp_log_new(ao, log, name), + .def_buffer = opts->audio_buffer, + .client_name = talloc_strdup(ao, opts->audio_client_name), + }; + talloc_free(opts); + ao->priv = m_config_group_from_desc(ao, ao->log, global, &desc, name); + if (!ao->priv) + goto error; + ao_set_gain(ao, 1.0f); + return ao; +error: + talloc_free(ao); + return NULL; +} + +static struct ao *ao_init(bool probing, struct mpv_global *global, + void (*wakeup_cb)(void *ctx), void *wakeup_ctx, + struct encode_lavc_context *encode_lavc_ctx, int flags, + int samplerate, int format, struct mp_chmap channels, + char *dev, char *name) +{ + struct ao *ao = ao_alloc(probing, global, wakeup_cb, wakeup_ctx, name); + if (!ao) + return NULL; + ao->samplerate = samplerate; + ao->channels = channels; + ao->format = format; + ao->encode_lavc_ctx = encode_lavc_ctx; + ao->init_flags = flags; + if (ao->driver->encode != !!ao->encode_lavc_ctx) + goto fail; + + MP_VERBOSE(ao, "requested format: %d Hz, %s channels, %s\n", + ao->samplerate, mp_chmap_to_str(&ao->channels), + af_fmt_to_str(ao->format)); + + ao->device = talloc_strdup(ao, dev); + ao->stream_silence = flags & AO_INIT_STREAM_SILENCE; + + init_buffer_pre(ao); + + int r = ao->driver->init(ao); + if (r < 0) { + // Silly exception for coreaudio spdif redirection + if (ao->redirect) { + char redirect[80], rdevice[80]; + snprintf(redirect, sizeof(redirect), "%s", ao->redirect); + snprintf(rdevice, sizeof(rdevice), "%s", ao->device ? ao->device : ""); + ao_uninit(ao); + return ao_init(probing, global, wakeup_cb, wakeup_ctx, + encode_lavc_ctx, flags, samplerate, format, channels, + rdevice, redirect); + } + goto fail; + } + ao->driver_initialized = true; + + ao->sstride = af_fmt_to_bytes(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 <= 0 && ao->driver->write) { + MP_ERR(ao, "Device buffer size not set.\n"); + goto fail; + } + if (ao->device_buffer) + MP_VERBOSE(ao, "device buffer: %d samples.\n", ao->device_buffer); + ao->buffer = MPMAX(ao->device_buffer, ao->def_buffer * ao->samplerate); + ao->buffer = MPMAX(ao->buffer, 1); + + int align = af_format_sample_alignment(ao->format); + ao->buffer = (ao->buffer + align - 1) / align * align; + MP_VERBOSE(ao, "using soft-buffer of %d samples.\n", ao->buffer); + + if (!init_buffer_post(ao)) + goto fail; + return ao; + +fail: + ao_uninit(ao); + return NULL; +} + +static void split_ao_device(void *tmp, char *opt, char **out_ao, char **out_dev) +{ + *out_ao = NULL; + *out_dev = NULL; + if (!opt) + return; + if (!opt[0] || strcmp(opt, "auto") == 0) + return; + // Split on "/". If "/" is the final character, or absent, out_dev is NULL. + bstr b_dev, b_ao; + bstr_split_tok(bstr0(opt), "/", &b_ao, &b_dev); + if (b_dev.len > 0) + *out_dev = bstrto0(tmp, b_dev); + *out_ao = bstrto0(tmp, b_ao); +} + +struct ao *ao_init_best(struct mpv_global *global, + int init_flags, + void (*wakeup_cb)(void *ctx), void *wakeup_ctx, + struct encode_lavc_context *encode_lavc_ctx, + int samplerate, int format, struct mp_chmap channels) +{ + void *tmp = talloc_new(NULL); + struct ao_opts *opts = mp_get_config_group(tmp, global, &ao_conf); + struct mp_log *log = mp_log_new(tmp, global->log, "ao"); + struct ao *ao = NULL; + struct m_obj_settings *ao_list = NULL; + int ao_num = 0; + + for (int n = 0; opts->audio_driver_list && opts->audio_driver_list[n].name; n++) + MP_TARRAY_APPEND(tmp, ao_list, ao_num, opts->audio_driver_list[n]); + + bool forced_dev = false; + char *pref_ao, *pref_dev; + split_ao_device(tmp, opts->audio_device, &pref_ao, &pref_dev); + if (!ao_num && pref_ao) { + // Reuse the autoselection code + MP_TARRAY_APPEND(tmp, ao_list, ao_num, + (struct m_obj_settings){.name = pref_ao}); + forced_dev = true; + } + + bool autoprobe = ao_num == 0; + + // Something like "--ao=a,b," means do autoprobing after a and b fail. + if (ao_num && strlen(ao_list[ao_num - 1].name) == 0) { + ao_num -= 1; + autoprobe = true; + } + + if (autoprobe) { + for (int n = 0; n < MP_ARRAY_SIZE(audio_out_drivers); n++) { + const struct ao_driver *driver = audio_out_drivers[n]; + if (driver == &audio_out_null) + break; + MP_TARRAY_APPEND(tmp, ao_list, ao_num, + (struct m_obj_settings){.name = (char *)driver->name}); + } + } + + if (init_flags & AO_INIT_NULL_FALLBACK) { + MP_TARRAY_APPEND(tmp, ao_list, ao_num, + (struct m_obj_settings){.name = "null"}); + } + + for (int n = 0; n < ao_num; n++) { + struct m_obj_settings *entry = &ao_list[n]; + bool probing = n + 1 != ao_num; + mp_verbose(log, "Trying audio driver '%s'\n", entry->name); + char *dev = NULL; + if (pref_ao && pref_dev && strcmp(entry->name, pref_ao) == 0) { + dev = pref_dev; + mp_verbose(log, "Using preferred device '%s'\n", dev); + } + ao = ao_init(probing, global, wakeup_cb, wakeup_ctx, encode_lavc_ctx, + init_flags, samplerate, format, channels, dev, entry->name); + if (ao) + break; + if (!probing) + mp_err(log, "Failed to initialize audio driver '%s'\n", entry->name); + if (dev && forced_dev) { + mp_err(log, "This audio driver/device was forced with the " + "--audio-device option.\nTry unsetting it.\n"); + } + } + + talloc_free(tmp); + return ao; +} + +// Query the AO_EVENT_*s as requested by the events parameter, and return them. +int ao_query_and_reset_events(struct ao *ao, int events) +{ + return atomic_fetch_and(&ao->events_, ~(unsigned)events) & events; +} + +// Returns events that were set by this calls. +int ao_add_events(struct ao *ao, int events) +{ + unsigned prev_events = atomic_fetch_or(&ao->events_, events); + unsigned new = events & ~prev_events; + if (new) + ao->wakeup_cb(ao->wakeup_ctx); + return new; +} + +// Request that the player core destroys and recreates the AO. Fully thread-safe. +void ao_request_reload(struct ao *ao) +{ + ao_add_events(ao, AO_EVENT_RELOAD); +} + +// Notify the player that the device list changed. Fully thread-safe. +void ao_hotplug_event(struct ao *ao) +{ + ao_add_events(ao, AO_EVENT_HOTPLUG); +} + +bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map) +{ + MP_VERBOSE(ao, "Channel layouts:\n"); + mp_chmal_sel_log(s, ao->log, MSGL_V); + bool r = mp_chmap_sel_adjust(s, map); + if (r) + MP_VERBOSE(ao, "result: %s\n", mp_chmap_to_str(map)); + return r; +} + +// safe_multichannel=true behaves like ao_chmap_sel_adjust. +// safe_multichannel=false is a helper for callers which do not support safe +// handling of arbitrary channel layouts. If the multichannel layouts are not +// considered "always safe" (e.g. HDMI), then allow only stereo or mono, if +// they are part of the list in *s. +bool ao_chmap_sel_adjust2(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map, bool safe_multichannel) +{ + if (!safe_multichannel && (ao->init_flags & AO_INIT_SAFE_MULTICHANNEL_ONLY)) { + struct mp_chmap res = *map; + if (mp_chmap_sel_adjust(s, &res)) { + if (!mp_chmap_equals(&res, &(struct mp_chmap)MP_CHMAP_INIT_MONO) && + !mp_chmap_equals(&res, &(struct mp_chmap)MP_CHMAP_INIT_STEREO)) + { + MP_VERBOSE(ao, "Disabling multichannel output.\n"); + *map = (struct mp_chmap)MP_CHMAP_INIT_STEREO; + } + } + } + + return ao_chmap_sel_adjust(ao, s, map); +} + +bool ao_chmap_sel_get_def(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map, int num) +{ + return mp_chmap_sel_get_def(s, map, num); +} + +// --- The following functions just return immutable information. + +void ao_get_format(struct ao *ao, + int *samplerate, int *format, struct mp_chmap *channels) +{ + *samplerate = ao->samplerate; + *format = ao->format; + *channels = ao->channels; +} + +const char *ao_get_name(struct ao *ao) +{ + return ao->driver->name; +} + +const char *ao_get_description(struct ao *ao) +{ + return ao->driver->description; +} + +bool ao_untimed(struct ao *ao) +{ + return ao->untimed; +} + +// --- + +struct ao_hotplug { + struct mpv_global *global; + void (*wakeup_cb)(void *ctx); + void *wakeup_ctx; + // A single AO instance is used to listen to hotplug events. It wouldn't + // make much sense to allow multiple AO drivers; all sane platforms have + // a single audio API providing all events. + // This is _not_ necessarily the same AO instance as used for playing + // audio. + struct ao *ao; + // cached + struct ao_device_list *list; + bool needs_update; +}; + +struct ao_hotplug *ao_hotplug_create(struct mpv_global *global, + void (*wakeup_cb)(void *ctx), + void *wakeup_ctx) +{ + struct ao_hotplug *hp = talloc_ptrtype(NULL, hp); + *hp = (struct ao_hotplug){ + .global = global, + .wakeup_cb = wakeup_cb, + .wakeup_ctx = wakeup_ctx, + .needs_update = true, + }; + return hp; +} + +static void get_devices(struct ao *ao, struct ao_device_list *list) +{ + if (ao->driver->list_devs) { + ao->driver->list_devs(ao, list); + } else { + ao_device_list_add(list, ao, &(struct ao_device_desc){"", ""}); + } +} + +bool ao_hotplug_check_update(struct ao_hotplug *hp) +{ + if (hp->ao && ao_query_and_reset_events(hp->ao, AO_EVENT_HOTPLUG)) { + hp->needs_update = true; + return true; + } + return false; +} + +// The return value is valid until the next call to this API. +struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp, + struct ao *playback_ao) +{ + if (hp->list && !hp->needs_update) + return hp->list; + + talloc_free(hp->list); + struct ao_device_list *list = talloc_zero(hp, struct ao_device_list); + hp->list = list; + + MP_TARRAY_APPEND(list, list->devices, list->num_devices, + (struct ao_device_desc){"auto", "Autoselect device"}); + + // Try to use the same AO for hotplug handling as for playback. + // Different AOs may not agree and the playback one is the only one the + // user knows about and may even have configured explicitly. + if (!hp->ao && playback_ao && playback_ao->driver->hotplug_init) { + struct ao *ao = ao_alloc(true, hp->global, hp->wakeup_cb, hp->wakeup_ctx, + (char *)playback_ao->driver->name); + if (playback_ao->driver->hotplug_init(ao) >= 0) { + hp->ao = ao; + } else { + talloc_free(ao); + } + } + + for (int n = 0; n < MP_ARRAY_SIZE(audio_out_drivers); n++) { + const struct ao_driver *d = audio_out_drivers[n]; + if (d == &audio_out_null) + break; // don't add unsafe/special entries + + struct ao *ao = ao_alloc(true, hp->global, hp->wakeup_cb, hp->wakeup_ctx, + (char *)d->name); + if (!ao) + continue; + + if (ao->driver->hotplug_init) { + if (ao->driver->hotplug_init(ao) >= 0) { + get_devices(ao, list); + if (hp->ao) + ao->driver->hotplug_uninit(ao); + else + hp->ao = ao; // keep this one + } + } else { + get_devices(ao, list); + } + if (ao != hp->ao) + talloc_free(ao); + } + hp->needs_update = false; + return list; +} + +void ao_device_list_add(struct ao_device_list *list, struct ao *ao, + struct ao_device_desc *e) +{ + struct ao_device_desc c = *e; + const char *dname = ao->driver->name; + char buf[80]; + if (!c.desc || !c.desc[0]) { + if (c.name && c.name[0]) { + c.desc = c.name; + } else if (list->num_devices) { + // Assume this is the default device. + snprintf(buf, sizeof(buf), "Default (%s)", dname); + c.desc = buf; + } else { + // First default device (and maybe the only one). + c.desc = "Default"; + } + } + c.name = (c.name && c.name[0]) ? talloc_asprintf(list, "%s/%s", dname, c.name) + : talloc_strdup(list, dname); + c.desc = talloc_strdup(list, c.desc); + MP_TARRAY_APPEND(list, list->devices, list->num_devices, c); +} + +void ao_hotplug_destroy(struct ao_hotplug *hp) +{ + if (!hp) + return; + if (hp->ao && hp->ao->driver->hotplug_uninit) + hp->ao->driver->hotplug_uninit(hp->ao); + talloc_free(hp->ao); + talloc_free(hp); +} + +static void dummy_wakeup(void *ctx) +{ +} + +void ao_print_devices(struct mpv_global *global, struct mp_log *log, + struct ao *playback_ao) +{ + struct ao_hotplug *hp = ao_hotplug_create(global, dummy_wakeup, NULL); + struct ao_device_list *list = ao_hotplug_get_device_list(hp, playback_ao); + mp_info(log, "List of detected audio devices:\n"); + for (int n = 0; n < list->num_devices; n++) { + struct ao_device_desc *desc = &list->devices[n]; + mp_info(log, " '%s' (%s)\n", desc->name, desc->desc); + } + ao_hotplug_destroy(hp); +} + +void ao_set_gain(struct ao *ao, float gain) +{ + atomic_store(&ao->gain, gain); +} + +#define MUL_GAIN_i(d, num_samples, gain, low, center, high) \ + for (int n = 0; n < (num_samples); n++) \ + (d)[n] = MPCLAMP( \ + ((((int64_t)((d)[n]) - (center)) * (gain) + 128) >> 8) + (center), \ + (low), (high)) + +#define MUL_GAIN_f(d, num_samples, gain) \ + for (int n = 0; n < (num_samples); n++) \ + (d)[n] = MPCLAMP(((d)[n]) * (gain), -1.0, 1.0) + +static void process_plane(struct ao *ao, void *data, int num_samples) +{ + float gain = atomic_load_explicit(&ao->gain, memory_order_relaxed); + int gi = lrint(256.0 * gain); + if (gi == 256) + return; + switch (af_fmt_from_planar(ao->format)) { + case AF_FORMAT_U8: + MUL_GAIN_i((uint8_t *)data, num_samples, gi, 0, 128, 255); + break; + case AF_FORMAT_S16: + MUL_GAIN_i((int16_t *)data, num_samples, gi, INT16_MIN, 0, INT16_MAX); + break; + case AF_FORMAT_S32: + MUL_GAIN_i((int32_t *)data, num_samples, gi, INT32_MIN, 0, INT32_MAX); + break; + case AF_FORMAT_FLOAT: + MUL_GAIN_f((float *)data, num_samples, gain); + break; + case AF_FORMAT_DOUBLE: + MUL_GAIN_f((double *)data, num_samples, gain); + break; + default:; + // all other sample formats are simply not supported + } +} + +void ao_post_process_data(struct ao *ao, void **data, int num_samples) +{ + bool planar = af_fmt_is_planar(ao->format); + int planes = planar ? ao->channels.num : 1; + int plane_samples = num_samples * (planar ? 1: ao->channels.num); + for (int n = 0; n < planes; n++) + process_plane(ao, data[n], plane_samples); +} + +static int get_conv_type(struct ao_convert_fmt *fmt) +{ + if (af_fmt_to_bytes(fmt->src_fmt) * 8 == fmt->dst_bits && !fmt->pad_msb) + return 0; // passthrough + if (fmt->src_fmt == AF_FORMAT_S32 && fmt->dst_bits == 24 && !fmt->pad_msb) + return 1; // simple 32->24 bit conversion + if (fmt->src_fmt == AF_FORMAT_S32 && fmt->dst_bits == 32 && fmt->pad_msb == 8) + return 2; // simple 32->24 bit conversion, with MSB padding + return -1; // unsupported +} + +// Check whether ao_convert_inplace() can be called. As an exception, the +// planar-ness of the sample format and the number of channels is ignored. +// All other parameters must be as passed to ao_convert_inplace(). +bool ao_can_convert_inplace(struct ao_convert_fmt *fmt) +{ + return get_conv_type(fmt) >= 0; +} + +bool ao_need_conversion(struct ao_convert_fmt *fmt) +{ + return get_conv_type(fmt) != 0; +} + +// The LSB is always ignored. +#if BYTE_ORDER == BIG_ENDIAN +#define SHIFT24(x) ((3-(x))*8) +#else +#define SHIFT24(x) (((x)+1)*8) +#endif + +static void convert_plane(int type, void *data, int num_samples) +{ + switch (type) { + case 0: + break; + case 1: /* fall through */ + case 2: { + int bytes = type == 1 ? 3 : 4; + for (int s = 0; s < num_samples; s++) { + uint32_t val = *((uint32_t *)data + s); + uint8_t *ptr = (uint8_t *)data + s * bytes; + ptr[0] = val >> SHIFT24(0); + ptr[1] = val >> SHIFT24(1); + ptr[2] = val >> SHIFT24(2); + if (type == 2) + ptr[3] = 0; + } + break; + } + default: + MP_ASSERT_UNREACHABLE(); + } +} + +// data[n] contains the pointer to the first sample of the n-th plane, in the +// format implied by fmt->src_fmt. src_fmt also controls whether the data is +// all in one plane, or if there is a plane per channel. +void ao_convert_inplace(struct ao_convert_fmt *fmt, void **data, int num_samples) +{ + int type = get_conv_type(fmt); + bool planar = af_fmt_is_planar(fmt->src_fmt); + int planes = planar ? fmt->channels : 1; + int plane_samples = num_samples * (planar ? 1: fmt->channels); + for (int n = 0; n < planes; n++) + convert_plane(type, data[n], plane_samples); +} diff --git a/audio/out/ao.h b/audio/out/ao.h new file mode 100644 index 0000000..18c7cdc --- /dev/null +++ b/audio/out/ao.h @@ -0,0 +1,122 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPLAYER_AUDIO_OUT_H +#define MPLAYER_AUDIO_OUT_H + +#include <stdbool.h> + +#include "misc/bstr.h" +#include "common/common.h" +#include "audio/chmap.h" +#include "audio/chmap_sel.h" + +enum aocontrol { + // _VOLUME commands take a pointer to float for input/output. + AOCONTROL_GET_VOLUME, + AOCONTROL_SET_VOLUME, + // _MUTE commands take a pointer to bool + AOCONTROL_GET_MUTE, + AOCONTROL_SET_MUTE, + // Has char* as argument, which contains the desired stream title. + AOCONTROL_UPDATE_STREAM_TITLE, +}; + +// If set, then the queued audio data is the last. Note that after a while, new +// data might be written again, instead of closing the AO. +#define PLAYER_FINAL_CHUNK 1 + +enum { + AO_EVENT_RELOAD = 1, + AO_EVENT_HOTPLUG = 2, + AO_EVENT_INITIAL_UNBLOCK = 4, +}; + +enum { + // Allow falling back to ao_null if nothing else works. + AO_INIT_NULL_FALLBACK = 1 << 0, + // Only accept multichannel configurations that are guaranteed to work + // (i.e. not sending arbitrary layouts over HDMI). + AO_INIT_SAFE_MULTICHANNEL_ONLY = 1 << 1, + // Stream silence as long as no audio is playing. + AO_INIT_STREAM_SILENCE = 1 << 2, + // Force exclusive mode, i.e. lock out the system mixer. + AO_INIT_EXCLUSIVE = 1 << 3, + // Initialize with music role. + AO_INIT_MEDIA_ROLE_MUSIC = 1 << 4, +}; + +struct ao_device_desc { + const char *name; // symbolic name; will be set on ao->device + const char *desc; // verbose human readable name +}; + +struct ao_device_list { + struct ao_device_desc *devices; + int num_devices; +}; + +struct ao; +struct mpv_global; +struct input_ctx; +struct encode_lavc_context; + +struct ao_opts { + struct m_obj_settings *audio_driver_list; + char *audio_device; + char *audio_client_name; + double audio_buffer; +}; + +struct ao *ao_init_best(struct mpv_global *global, + int init_flags, + void (*wakeup_cb)(void *ctx), void *wakeup_ctx, + struct encode_lavc_context *encode_lavc_ctx, + int samplerate, int format, struct mp_chmap channels); +void ao_uninit(struct ao *ao); +void ao_get_format(struct ao *ao, + int *samplerate, int *format, struct mp_chmap *channels); +const char *ao_get_name(struct ao *ao); +const char *ao_get_description(struct ao *ao); +bool ao_untimed(struct ao *ao); +int ao_control(struct ao *ao, enum aocontrol cmd, void *arg); +void ao_set_gain(struct ao *ao, float gain); +double ao_get_delay(struct ao *ao); +void ao_reset(struct ao *ao); +void ao_start(struct ao *ao); +void ao_set_paused(struct ao *ao, bool paused, bool eof); +void ao_drain(struct ao *ao); +bool ao_is_playing(struct ao *ao); +struct mp_async_queue; +struct mp_async_queue *ao_get_queue(struct ao *ao); +int ao_query_and_reset_events(struct ao *ao, int events); +int ao_add_events(struct ao *ao, int events); +void ao_unblock(struct ao *ao); +void ao_request_reload(struct ao *ao); +void ao_hotplug_event(struct ao *ao); + +struct ao_hotplug; +struct ao_hotplug *ao_hotplug_create(struct mpv_global *global, + void (*wakeup_cb)(void *ctx), + void *wakeup_ctx); +void ao_hotplug_destroy(struct ao_hotplug *hp); +bool ao_hotplug_check_update(struct ao_hotplug *hp); +struct ao_device_list *ao_hotplug_get_device_list(struct ao_hotplug *hp, struct ao *playback_ao); + +void ao_print_devices(struct mpv_global *global, struct mp_log *log, struct ao *playback_ao); + +#endif /* MPLAYER_AUDIO_OUT_H */ diff --git a/audio/out/ao_alsa.c b/audio/out/ao_alsa.c new file mode 100644 index 0000000..75eda3b --- /dev/null +++ b/audio/out/ao_alsa.c @@ -0,0 +1,1161 @@ +/* + * ALSA 0.9.x-1.x audio output driver + * + * Copyright (C) 2004 Alex Beregszaszi + * Zsolt Barat <joy@streamminister.de> + * + * modified for real ALSA 0.9.0 support by Zsolt Barat <joy@streamminister.de> + * additional AC-3 passthrough support by Andy Lo A Foe <andy@alsaplayer.org> + * 08/22/2002 iec958-init rewritten and merged with common init, zsolt + * 04/13/2004 merged with ao_alsa1.x, fixes provided by Jindrich Makovicka + * 04/25/2004 printfs converted to mp_msg, Zsolt. + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <errno.h> +#include <sys/time.h> +#include <stdlib.h> +#include <stdarg.h> +#include <limits.h> +#include <math.h> +#include <string.h> + +#include "options/options.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "common/msg.h" +#include "osdep/endian.h" + +#include <alsa/asoundlib.h> + +#if defined(SND_CHMAP_API_VERSION) && SND_CHMAP_API_VERSION >= (1 << 16) +#define HAVE_CHMAP_API 1 +#else +#define HAVE_CHMAP_API 0 +#endif + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" + +struct ao_alsa_opts { + char *mixer_device; + char *mixer_name; + int mixer_index; + bool resample; + bool ni; + bool ignore_chmap; + int buffer_time; + int frags; +}; + +#define OPT_BASE_STRUCT struct ao_alsa_opts +static const struct m_sub_options ao_alsa_conf = { + .opts = (const struct m_option[]) { + {"alsa-resample", OPT_BOOL(resample)}, + {"alsa-mixer-device", OPT_STRING(mixer_device)}, + {"alsa-mixer-name", OPT_STRING(mixer_name)}, + {"alsa-mixer-index", OPT_INT(mixer_index), M_RANGE(0, 99)}, + {"alsa-non-interleaved", OPT_BOOL(ni)}, + {"alsa-ignore-chmap", OPT_BOOL(ignore_chmap)}, + {"alsa-buffer-time", OPT_INT(buffer_time), M_RANGE(0, INT_MAX)}, + {"alsa-periods", OPT_INT(frags), M_RANGE(0, INT_MAX)}, + {0} + }, + .defaults = &(const struct ao_alsa_opts) { + .mixer_device = "default", + .mixer_name = "Master", + .buffer_time = 100000, + .frags = 4, + }, + .size = sizeof(struct ao_alsa_opts), +}; + +struct priv { + snd_pcm_t *alsa; + bool device_lost; + snd_pcm_format_t alsa_fmt; + bool can_pause; + snd_pcm_uframes_t buffersize; + snd_pcm_uframes_t outburst; + + snd_output_t *output; + + struct ao_convert_fmt convert; + + struct ao_alsa_opts *opts; +}; + +#define CHECK_ALSA_ERROR(message) \ + do { \ + if (err < 0) { \ + MP_ERR(ao, "%s: %s\n", (message), snd_strerror(err)); \ + goto alsa_error; \ + } \ + } while (0) + +#define CHECK_ALSA_WARN(message) \ + do { \ + if (err < 0) \ + MP_WARN(ao, "%s: %s\n", (message), snd_strerror(err)); \ + } while (0) + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *p = ao->priv; + snd_mixer_t *handle = NULL; + switch (cmd) { + case AOCONTROL_GET_MUTE: + case AOCONTROL_SET_MUTE: + case AOCONTROL_GET_VOLUME: + case AOCONTROL_SET_VOLUME: + { + int err; + snd_mixer_elem_t *elem; + snd_mixer_selem_id_t *sid; + + long pmin, pmax; + long get_vol, set_vol; + float f_multi; + + if (!af_fmt_is_pcm(ao->format)) + return CONTROL_FALSE; + + snd_mixer_selem_id_alloca(&sid); + + snd_mixer_selem_id_set_index(sid, p->opts->mixer_index); + snd_mixer_selem_id_set_name(sid, p->opts->mixer_name); + + err = snd_mixer_open(&handle, 0); + CHECK_ALSA_ERROR("Mixer open error"); + + err = snd_mixer_attach(handle, p->opts->mixer_device); + CHECK_ALSA_ERROR("Mixer attach error"); + + err = snd_mixer_selem_register(handle, NULL, NULL); + CHECK_ALSA_ERROR("Mixer register error"); + + err = snd_mixer_load(handle); + CHECK_ALSA_ERROR("Mixer load error"); + + elem = snd_mixer_find_selem(handle, sid); + if (!elem) { + MP_VERBOSE(ao, "Unable to find simple control '%s',%i.\n", + snd_mixer_selem_id_get_name(sid), + snd_mixer_selem_id_get_index(sid)); + goto alsa_error; + } + + snd_mixer_selem_get_playback_volume_range(elem, &pmin, &pmax); + f_multi = (100 / (float)(pmax - pmin)); + + switch (cmd) { + case AOCONTROL_SET_VOLUME: { + float *vol = arg; + set_vol = *vol / f_multi + pmin + 0.5; + + err = snd_mixer_selem_set_playback_volume(elem, 0, set_vol); + CHECK_ALSA_ERROR("Error setting left channel"); + MP_DBG(ao, "left=%li, ", set_vol); + + err = snd_mixer_selem_set_playback_volume(elem, 1, set_vol); + CHECK_ALSA_ERROR("Error setting right channel"); + MP_DBG(ao, "right=%li, pmin=%li, pmax=%li, mult=%f\n", + set_vol, pmin, pmax, f_multi); + break; + } + case AOCONTROL_GET_VOLUME: { + float *vol = arg; + float left, right; + snd_mixer_selem_get_playback_volume(elem, 0, &get_vol); + left = (get_vol - pmin) * f_multi; + snd_mixer_selem_get_playback_volume(elem, 1, &get_vol); + right = (get_vol - pmin) * f_multi; + *vol = (left + right) / 2.0; + MP_DBG(ao, "vol=%f\n", *vol); + break; + } + case AOCONTROL_SET_MUTE: { + bool *mute = arg; + if (!snd_mixer_selem_has_playback_switch(elem)) + goto alsa_error; + if (!snd_mixer_selem_has_playback_switch_joined(elem)) { + snd_mixer_selem_set_playback_switch(elem, 1, !*mute); + } + snd_mixer_selem_set_playback_switch(elem, 0, !*mute); + break; + } + case AOCONTROL_GET_MUTE: { + bool *mute = arg; + if (!snd_mixer_selem_has_playback_switch(elem)) + goto alsa_error; + int tmp = 1; + snd_mixer_selem_get_playback_switch(elem, 0, &tmp); + *mute = !tmp; + if (!snd_mixer_selem_has_playback_switch_joined(elem)) { + snd_mixer_selem_get_playback_switch(elem, 1, &tmp); + *mute &= !tmp; + } + break; + } + } + snd_mixer_close(handle); + return CONTROL_OK; + } + + } //end switch + return CONTROL_UNKNOWN; + +alsa_error: + if (handle) + snd_mixer_close(handle); + return CONTROL_ERROR; +} + +struct alsa_fmt { + int mp_format; + int alsa_format; + int bits; // alsa format full sample size (optional) + int pad_msb; // how many MSB bits are 0 (optional) +}; + +// Entries that have the same mp_format must be: +// 1. consecutive +// 2. sorted by preferred format (worst comes last) +static const struct alsa_fmt mp_alsa_formats[] = { + {AF_FORMAT_U8, SND_PCM_FORMAT_U8}, + {AF_FORMAT_S16, SND_PCM_FORMAT_S16}, + {AF_FORMAT_S32, SND_PCM_FORMAT_S32}, + {AF_FORMAT_S32, SND_PCM_FORMAT_S24, .bits = 32, .pad_msb = 8}, + {AF_FORMAT_S32, + MP_SELECT_LE_BE(SND_PCM_FORMAT_S24_3LE, SND_PCM_FORMAT_S24_3BE), + .bits = 24, .pad_msb = 0}, + {AF_FORMAT_FLOAT, SND_PCM_FORMAT_FLOAT}, + {AF_FORMAT_DOUBLE, SND_PCM_FORMAT_FLOAT64}, + {0}, +}; + +static const struct alsa_fmt *find_alsa_format(int mp_format) +{ + for (int n = 0; mp_alsa_formats[n].mp_format; n++) { + if (mp_alsa_formats[n].mp_format == mp_format) + return &mp_alsa_formats[n]; + } + return NULL; +} + +#if HAVE_CHMAP_API + +static const int alsa_to_mp_channels[][2] = { + {SND_CHMAP_FL, MP_SP(FL)}, + {SND_CHMAP_FR, MP_SP(FR)}, + {SND_CHMAP_RL, MP_SP(BL)}, + {SND_CHMAP_RR, MP_SP(BR)}, + {SND_CHMAP_FC, MP_SP(FC)}, + {SND_CHMAP_LFE, MP_SP(LFE)}, + {SND_CHMAP_SL, MP_SP(SL)}, + {SND_CHMAP_SR, MP_SP(SR)}, + {SND_CHMAP_RC, MP_SP(BC)}, + {SND_CHMAP_FLC, MP_SP(FLC)}, + {SND_CHMAP_FRC, MP_SP(FRC)}, + {SND_CHMAP_FLW, MP_SP(WL)}, + {SND_CHMAP_FRW, MP_SP(WR)}, + {SND_CHMAP_TC, MP_SP(TC)}, + {SND_CHMAP_TFL, MP_SP(TFL)}, + {SND_CHMAP_TFR, MP_SP(TFR)}, + {SND_CHMAP_TFC, MP_SP(TFC)}, + {SND_CHMAP_TRL, MP_SP(TBL)}, + {SND_CHMAP_TRR, MP_SP(TBR)}, + {SND_CHMAP_TRC, MP_SP(TBC)}, + {SND_CHMAP_RRC, MP_SP(SDR)}, + {SND_CHMAP_RLC, MP_SP(SDL)}, + {SND_CHMAP_MONO, MP_SP(FC)}, + {SND_CHMAP_NA, MP_SPEAKER_ID_NA}, + {SND_CHMAP_UNKNOWN, MP_SPEAKER_ID_NA}, + {SND_CHMAP_LAST, MP_SPEAKER_ID_COUNT} +}; + +static int find_mp_channel(int alsa_channel) +{ + for (int i = 0; alsa_to_mp_channels[i][1] != MP_SPEAKER_ID_COUNT; i++) { + if (alsa_to_mp_channels[i][0] == alsa_channel) + return alsa_to_mp_channels[i][1]; + } + + return MP_SPEAKER_ID_COUNT; +} + +#define CHMAP(n, ...) &(struct mp_chmap) MP_CONCAT(MP_CHMAP, n) (__VA_ARGS__) + +// Replace each channel in a with b (a->num == b->num) +static void replace_submap(struct mp_chmap *dst, struct mp_chmap *a, + struct mp_chmap *b) +{ + struct mp_chmap t = *dst; + if (!mp_chmap_is_valid(&t) || mp_chmap_diffn(a, &t) != 0) + return; + assert(a->num == b->num); + for (int n = 0; n < t.num; n++) { + for (int i = 0; i < a->num; i++) { + if (t.speaker[n] == a->speaker[i]) { + t.speaker[n] = b->speaker[i]; + break; + } + } + } + if (mp_chmap_is_valid(&t)) + *dst = t; +} + +static bool mp_chmap_from_alsa(struct mp_chmap *dst, snd_pcm_chmap_t *src) +{ + *dst = (struct mp_chmap) {0}; + + if (src->channels > MP_NUM_CHANNELS) + return false; + + dst->num = src->channels; + for (int c = 0; c < dst->num; c++) + dst->speaker[c] = find_mp_channel(src->pos[c]); + + // Assume anything with 1 channel is mono. + if (dst->num == 1) + dst->speaker[0] = MP_SP(FC); + + // Remap weird Intel HDA HDMI 7.1 layouts correctly. + replace_submap(dst, CHMAP(6, FL, FR, BL, BR, SDL, SDR), + CHMAP(6, FL, FR, SL, SR, BL, BR)); + + return mp_chmap_is_valid(dst); +} + +static bool query_chmaps(struct ao *ao, struct mp_chmap *chmap) +{ + struct priv *p = ao->priv; + struct mp_chmap_sel chmap_sel = {.tmp = p}; + + snd_pcm_chmap_query_t **maps = snd_pcm_query_chmaps(p->alsa); + if (!maps) { + MP_VERBOSE(ao, "snd_pcm_query_chmaps() returned NULL\n"); + return false; + } + + for (int i = 0; maps[i] != NULL; i++) { + char aname[128]; + if (snd_pcm_chmap_print(&maps[i]->map, sizeof(aname), aname) <= 0) + aname[0] = '\0'; + + struct mp_chmap entry; + if (mp_chmap_from_alsa(&entry, &maps[i]->map)) { + struct mp_chmap reorder = entry; + mp_chmap_reorder_norm(&reorder); + + MP_DBG(ao, "got ALSA chmap: %s (%s) -> %s", aname, + snd_pcm_chmap_type_name(maps[i]->type), + mp_chmap_to_str(&entry)); + if (!mp_chmap_equals(&entry, &reorder)) + MP_DBG(ao, " -> %s", mp_chmap_to_str(&reorder)); + MP_DBG(ao, "\n"); + + struct mp_chmap final = + maps[i]->type == SND_CHMAP_TYPE_VAR ? reorder : entry; + mp_chmap_sel_add_map(&chmap_sel, &final); + } else { + MP_VERBOSE(ao, "skipping unknown ALSA channel map: %s\n", aname); + } + } + + snd_pcm_free_chmaps(maps); + + return ao_chmap_sel_adjust2(ao, &chmap_sel, chmap, false); +} + +// Map back our selected channel layout to an ALSA one. This is done this way so +// that our ALSA->mp_chmap mapping function only has to go one way. +// The return value is to be freed with free(). +static snd_pcm_chmap_t *map_back_chmap(struct ao *ao, struct mp_chmap *chmap) +{ + struct priv *p = ao->priv; + if (!mp_chmap_is_valid(chmap)) + return NULL; + + snd_pcm_chmap_query_t **maps = snd_pcm_query_chmaps(p->alsa); + if (!maps) + return NULL; + + snd_pcm_chmap_t *alsa_chmap = NULL; + + for (int i = 0; maps[i] != NULL; i++) { + struct mp_chmap entry; + if (!mp_chmap_from_alsa(&entry, &maps[i]->map)) + continue; + + if (mp_chmap_equals(chmap, &entry) || + (mp_chmap_equals_reordered(chmap, &entry) && + maps[i]->type == SND_CHMAP_TYPE_VAR)) + { + alsa_chmap = calloc(1, sizeof(*alsa_chmap) + + sizeof(alsa_chmap->pos[0]) * entry.num); + if (!alsa_chmap) + break; + alsa_chmap->channels = entry.num; + + // Undo if mp_chmap_reorder() was called on the result. + int reorder[MP_NUM_CHANNELS]; + mp_chmap_get_reorder(reorder, chmap, &entry); + for (int n = 0; n < entry.num; n++) + alsa_chmap->pos[n] = maps[i]->map.pos[reorder[n]]; + break; + } + } + + snd_pcm_free_chmaps(maps); + return alsa_chmap; +} + + +static int set_chmap(struct ao *ao, struct mp_chmap *dev_chmap, int num_channels) +{ + struct priv *p = ao->priv; + int err; + + snd_pcm_chmap_t *alsa_chmap = map_back_chmap(ao, dev_chmap); + if (alsa_chmap) { + char tmp[128]; + if (snd_pcm_chmap_print(alsa_chmap, sizeof(tmp), tmp) > 0) + MP_VERBOSE(ao, "trying to set ALSA channel map: %s\n", tmp); + + err = snd_pcm_set_chmap(p->alsa, alsa_chmap); + if (err == -ENXIO) { + // A device my not be able to set any channel map, even channel maps + // that were reported as supported. This is either because the ALSA + // device is broken (dmix), or because the driver has only 1 + // channel map per channel count, and setting the map is not needed. + MP_VERBOSE(ao, "device returned ENXIO when setting channel map %s\n", + mp_chmap_to_str(dev_chmap)); + } else { + CHECK_ALSA_WARN("Channel map setup failed"); + } + + free(alsa_chmap); + } + + alsa_chmap = snd_pcm_get_chmap(p->alsa); + if (alsa_chmap) { + char tmp[128]; + if (snd_pcm_chmap_print(alsa_chmap, sizeof(tmp), tmp) > 0) + MP_VERBOSE(ao, "channel map reported by ALSA: %s\n", tmp); + + struct mp_chmap chmap; + mp_chmap_from_alsa(&chmap, alsa_chmap); + + MP_VERBOSE(ao, "which we understand as: %s\n", mp_chmap_to_str(&chmap)); + + if (p->opts->ignore_chmap) { + MP_VERBOSE(ao, "user set ignore-chmap; ignoring the channel map.\n"); + } else if (af_fmt_is_spdif(ao->format)) { + MP_VERBOSE(ao, "using spdif passthrough; ignoring the channel map.\n"); + } else if (!mp_chmap_is_valid(&chmap)) { + MP_WARN(ao, "Got unknown channel map from ALSA.\n"); + } else if (chmap.num != num_channels) { + MP_WARN(ao, "ALSA channel map conflicts with channel count!\n"); + } else { + if (mp_chmap_equals(&chmap, &ao->channels)) { + MP_VERBOSE(ao, "which is what we requested.\n"); + } else if (!mp_chmap_is_valid(dev_chmap)) { + MP_VERBOSE(ao, "ignoring the ALSA channel map.\n"); + } else { + MP_VERBOSE(ao, "using the ALSA channel map.\n"); + ao->channels = chmap; + } + } + + free(alsa_chmap); + } + + return 0; +} + +#else /* HAVE_CHMAP_API */ + +static bool query_chmaps(struct ao *ao, struct mp_chmap *chmap) +{ + return false; +} + +static int set_chmap(struct ao *ao, struct mp_chmap *dev_chmap, int num_channels) +{ + return 0; +} + +#endif /* else HAVE_CHMAP_API */ + +static void dump_hw_params(struct ao *ao, const char *msg, + snd_pcm_hw_params_t *hw_params) +{ + struct priv *p = ao->priv; + int err; + + err = snd_pcm_hw_params_dump(hw_params, p->output); + CHECK_ALSA_WARN("Dump hwparams error"); + + char *tmp = NULL; + size_t tmp_s = snd_output_buffer_string(p->output, &tmp); + if (tmp) + mp_msg(ao->log, MSGL_DEBUG, "%s---\n%.*s---\n", msg, (int)tmp_s, tmp); + snd_output_flush(p->output); +} + +static int map_iec958_srate(int srate) +{ + switch (srate) { + case 44100: return IEC958_AES3_CON_FS_44100; + case 48000: return IEC958_AES3_CON_FS_48000; + case 32000: return IEC958_AES3_CON_FS_32000; + case 22050: return IEC958_AES3_CON_FS_22050; + case 24000: return IEC958_AES3_CON_FS_24000; + case 88200: return IEC958_AES3_CON_FS_88200; + case 768000: return IEC958_AES3_CON_FS_768000; + case 96000: return IEC958_AES3_CON_FS_96000; + case 176400: return IEC958_AES3_CON_FS_176400; + case 192000: return IEC958_AES3_CON_FS_192000; + default: return IEC958_AES3_CON_FS_NOTID; + } +} + +// ALSA device strings can have parameters. They are usually appended to the +// device name. There can be various forms, and we (sometimes) want to append +// them to unknown device strings, which possibly already include params. +static char *append_params(void *ta_parent, const char *device, const char *p) +{ + if (!p || !p[0]) + return talloc_strdup(ta_parent, device); + + int len = strlen(device); + char *end = strchr(device, ':'); + if (!end) { + /* no existing parameters: add it behind device name */ + return talloc_asprintf(ta_parent, "%s:%s", device, p); + } else if (end[1] == '\0') { + /* ":" but no parameters */ + return talloc_asprintf(ta_parent, "%s%s", device, p); + } else if (end[1] == '{' && device[len - 1] == '}') { + /* parameters in config syntax: add it inside the { } block */ + return talloc_asprintf(ta_parent, "%.*s %s}", len - 1, device, p); + } else { + /* a simple list of parameters: add it at the end of the list */ + return talloc_asprintf(ta_parent, "%s,%s", device, p); + } + MP_ASSERT_UNREACHABLE(); +} + +static int try_open_device(struct ao *ao, const char *device, int mode) +{ + struct priv *p = ao->priv; + int err; + + if (af_fmt_is_spdif(ao->format)) { + void *tmp = talloc_new(NULL); + char *params = talloc_asprintf(tmp, + "AES0=%d,AES1=%d,AES2=0,AES3=%d", + IEC958_AES0_NONAUDIO | IEC958_AES0_PRO_EMPHASIS_NONE, + IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER, + map_iec958_srate(ao->samplerate)); + const char *ac3_device = append_params(tmp, device, params); + MP_VERBOSE(ao, "opening device '%s' => '%s'\n", device, ac3_device); + err = snd_pcm_open(&p->alsa, ac3_device, SND_PCM_STREAM_PLAYBACK, mode); + if (err < 0) { + // Some spdif-capable devices do not accept the AES0 parameter, + // and instead require the iec958 pseudo-device (they will play + // noise otherwise). Unfortunately, ALSA gives us no way to map + // these devices, so try it for the default device only. + bstr dev; + bstr_split_tok(bstr0(device), ":", &dev, &(bstr){0}); + if (bstr_equals0(dev, "default")) { + const char *const fallbacks[] = {"hdmi", "iec958", NULL}; + for (int n = 0; fallbacks[n]; n++) { + char *ndev = append_params(tmp, fallbacks[n], params); + MP_VERBOSE(ao, "got error '%s'; opening iec fallback " + "device '%s'\n", snd_strerror(err), ndev); + err = snd_pcm_open + (&p->alsa, ndev, SND_PCM_STREAM_PLAYBACK, mode); + if (err >= 0) + break; + } + } + } + talloc_free(tmp); + } else { + MP_VERBOSE(ao, "opening device '%s'\n", device); + err = snd_pcm_open(&p->alsa, device, SND_PCM_STREAM_PLAYBACK, mode); + } + + return err; +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + + if (p->output) + snd_output_close(p->output); + p->output = NULL; + + if (p->alsa) { + int err; + + err = snd_pcm_close(p->alsa); + p->alsa = NULL; + CHECK_ALSA_ERROR("pcm close error"); + } + +alsa_error: ; +} + +#define INIT_DEVICE_ERR_GENERIC -1 +#define INIT_DEVICE_ERR_HWPARAMS -2 +static int init_device(struct ao *ao, int mode) +{ + struct priv *p = ao->priv; + struct ao_alsa_opts *opts = p->opts; + int ret = INIT_DEVICE_ERR_GENERIC; + char *tmp; + size_t tmp_s; + int err; + + p->alsa_fmt = SND_PCM_FORMAT_UNKNOWN; + + err = snd_output_buffer_open(&p->output); + CHECK_ALSA_ERROR("Unable to create output buffer"); + + const char *device = "default"; + if (ao->device) + device = ao->device; + + err = try_open_device(ao, device, mode); + CHECK_ALSA_ERROR("Playback open error"); + + err = snd_pcm_dump(p->alsa, p->output); + CHECK_ALSA_WARN("Dump PCM error"); + tmp_s = snd_output_buffer_string(p->output, &tmp); + if (tmp) + MP_DBG(ao, "PCM setup:\n---\n%.*s---\n", (int)tmp_s, tmp); + snd_output_flush(p->output); + + err = snd_pcm_nonblock(p->alsa, 0); + CHECK_ALSA_WARN("Unable to set blocking mode"); + + snd_pcm_hw_params_t *alsa_hwparams; + snd_pcm_hw_params_alloca(&alsa_hwparams); + + err = snd_pcm_hw_params_any(p->alsa, alsa_hwparams); + CHECK_ALSA_ERROR("Unable to get initial parameters"); + + dump_hw_params(ao, "Start HW params:\n", alsa_hwparams); + + // Some ALSA drivers have broken delay reporting, so disable the ALSA + // resampling plugin by default. + if (!p->opts->resample) { + err = snd_pcm_hw_params_set_rate_resample(p->alsa, alsa_hwparams, 0); + CHECK_ALSA_ERROR("Unable to disable resampling"); + } + dump_hw_params(ao, "HW params after rate:\n", alsa_hwparams); + + snd_pcm_access_t access = af_fmt_is_planar(ao->format) + ? SND_PCM_ACCESS_RW_NONINTERLEAVED + : SND_PCM_ACCESS_RW_INTERLEAVED; + err = snd_pcm_hw_params_set_access(p->alsa, alsa_hwparams, access); + if (err < 0 && af_fmt_is_planar(ao->format)) { + ao->format = af_fmt_from_planar(ao->format); + access = SND_PCM_ACCESS_RW_INTERLEAVED; + err = snd_pcm_hw_params_set_access(p->alsa, alsa_hwparams, access); + } + CHECK_ALSA_ERROR("Unable to set access type"); + dump_hw_params(ao, "HW params after access:\n", alsa_hwparams); + + bool found_format = false; + int try_formats[AF_FORMAT_COUNT + 1]; + af_get_best_sample_formats(ao->format, try_formats); + for (int n = 0; try_formats[n] && !found_format; n++) { + int mp_format = try_formats[n]; + if (af_fmt_is_planar(ao->format) != af_fmt_is_planar(mp_format)) + continue; // implied SND_PCM_ACCESS mismatches + int mp_pformat = af_fmt_from_planar(mp_format); + if (af_fmt_is_spdif(mp_pformat)) + mp_pformat = AF_FORMAT_S16; + const struct alsa_fmt *fmt = find_alsa_format(mp_pformat); + if (!fmt) + continue; + for (; fmt->mp_format == mp_pformat; fmt++) { + p->alsa_fmt = fmt->alsa_format; + p->convert = (struct ao_convert_fmt){ + .src_fmt = mp_format, + .dst_bits = fmt->bits ? fmt->bits : af_fmt_to_bytes(mp_format) * 8, + .pad_msb = fmt->pad_msb, + }; + if (!ao_can_convert_inplace(&p->convert)) + continue; + MP_VERBOSE(ao, "trying format %s/%d\n", af_fmt_to_str(mp_pformat), + p->alsa_fmt); + if (snd_pcm_hw_params_test_format(p->alsa, alsa_hwparams, + p->alsa_fmt) >= 0) + { + ao->format = mp_format; + found_format = true; + break; + } + } + } + + if (!found_format) { + MP_ERR(ao, "Can't find appropriate sample format.\n"); + goto alsa_error; + } + + err = snd_pcm_hw_params_set_format(p->alsa, alsa_hwparams, p->alsa_fmt); + CHECK_ALSA_ERROR("Unable to set format"); + dump_hw_params(ao, "HW params after format:\n", alsa_hwparams); + + // Stereo, or mono if input is 1 channel. + struct mp_chmap reduced; + mp_chmap_from_channels(&reduced, MPMIN(2, ao->channels.num)); + + struct mp_chmap dev_chmap = {0}; + if (!af_fmt_is_spdif(ao->format) && !p->opts->ignore_chmap && + !mp_chmap_equals(&ao->channels, &reduced)) + { + struct mp_chmap res = ao->channels; + if (query_chmaps(ao, &res)) + dev_chmap = res; + + // Whatever it is, we dumb it down to mono or stereo. Some drivers may + // return things like bl-br, but the user (probably) still wants stereo. + // This also handles the failure case (dev_chmap.num==0). + if (dev_chmap.num <= 2) { + dev_chmap.num = 0; + ao->channels = reduced; + } else if (dev_chmap.num) { + ao->channels = dev_chmap; + } + } + + int num_channels = ao->channels.num; + err = snd_pcm_hw_params_set_channels_near + (p->alsa, alsa_hwparams, &num_channels); + CHECK_ALSA_ERROR("Unable to set channels"); + dump_hw_params(ao, "HW params after channels:\n", alsa_hwparams); + + if (num_channels > MP_NUM_CHANNELS) { + MP_FATAL(ao, "Too many audio channels (%d).\n", num_channels); + goto alsa_error; + } + + err = snd_pcm_hw_params_set_rate_near + (p->alsa, alsa_hwparams, &ao->samplerate, NULL); + CHECK_ALSA_ERROR("Unable to set samplerate-2"); + dump_hw_params(ao, "HW params after rate-2:\n", alsa_hwparams); + + snd_pcm_hw_params_t *hwparams_backup; + snd_pcm_hw_params_alloca(&hwparams_backup); + snd_pcm_hw_params_copy(hwparams_backup, alsa_hwparams); + + // Cargo-culted buffer settings; might still be useful for PulseAudio. + err = 0; + if (opts->buffer_time) { + err = snd_pcm_hw_params_set_buffer_time_near + (p->alsa, alsa_hwparams, &(unsigned int){opts->buffer_time}, NULL); + CHECK_ALSA_WARN("Unable to set buffer time near"); + } + if (err >= 0 && opts->frags) { + err = snd_pcm_hw_params_set_periods_near + (p->alsa, alsa_hwparams, &(unsigned int){opts->frags}, NULL); + CHECK_ALSA_WARN("Unable to set periods"); + } + if (err < 0) + snd_pcm_hw_params_copy(alsa_hwparams, hwparams_backup); + + dump_hw_params(ao, "Going to set final HW params:\n", alsa_hwparams); + + /* finally install hardware parameters */ + err = snd_pcm_hw_params(p->alsa, alsa_hwparams); + ret = INIT_DEVICE_ERR_HWPARAMS; + CHECK_ALSA_ERROR("Unable to set hw-parameters"); + ret = INIT_DEVICE_ERR_GENERIC; + dump_hw_params(ao, "Final HW params:\n", alsa_hwparams); + + if (set_chmap(ao, &dev_chmap, num_channels) < 0) + goto alsa_error; + + if (num_channels != ao->channels.num) { + int req = ao->channels.num; + mp_chmap_from_channels(&ao->channels, MPMIN(2, num_channels)); + mp_chmap_fill_na(&ao->channels, num_channels); + MP_ERR(ao, "Asked for %d channels, got %d - fallback to %s.\n", req, + num_channels, mp_chmap_to_str(&ao->channels)); + if (num_channels != ao->channels.num) { + MP_FATAL(ao, "mismatching channel counts.\n"); + goto alsa_error; + } + } + + err = snd_pcm_hw_params_get_buffer_size(alsa_hwparams, &p->buffersize); + CHECK_ALSA_ERROR("Unable to get buffersize"); + + err = snd_pcm_hw_params_get_period_size(alsa_hwparams, &p->outburst, NULL); + CHECK_ALSA_ERROR("Unable to get period size"); + + p->can_pause = snd_pcm_hw_params_can_pause(alsa_hwparams); + + snd_pcm_sw_params_t *alsa_swparams; + snd_pcm_sw_params_alloca(&alsa_swparams); + + err = snd_pcm_sw_params_current(p->alsa, alsa_swparams); + CHECK_ALSA_ERROR("Unable to get sw-parameters"); + + snd_pcm_uframes_t boundary; + err = snd_pcm_sw_params_get_boundary(alsa_swparams, &boundary); + CHECK_ALSA_ERROR("Unable to get boundary"); + + // Manual trigger; INT_MAX as suggested by ALSA doxygen (they call it MAXINT). + err = snd_pcm_sw_params_set_start_threshold(p->alsa, alsa_swparams, INT_MAX); + CHECK_ALSA_ERROR("Unable to set start threshold"); + + /* play silence when there is an underrun */ + err = snd_pcm_sw_params_set_silence_size + (p->alsa, alsa_swparams, boundary); + CHECK_ALSA_ERROR("Unable to set silence size"); + + err = snd_pcm_sw_params(p->alsa, alsa_swparams); + CHECK_ALSA_ERROR("Unable to set sw-parameters"); + + MP_VERBOSE(ao, "hw pausing supported: %s\n", p->can_pause ? "yes" : "no"); + MP_VERBOSE(ao, "buffersize: %d samples\n", (int)p->buffersize); + MP_VERBOSE(ao, "period size: %d samples\n", (int)p->outburst); + + ao->device_buffer = p->buffersize; + + p->convert.channels = ao->channels.num; + + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); + + return 0; + +alsa_error: + uninit(ao); + return ret; +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + p->opts = mp_get_config_group(ao, ao->global, &ao_alsa_conf); + + if (!p->opts->ni) + ao->format = af_fmt_from_planar(ao->format); + + MP_VERBOSE(ao, "using ALSA version: %s\n", snd_asoundlib_version()); + + int mode = 0; + int r = init_device(ao, mode); + if (r == INIT_DEVICE_ERR_HWPARAMS) { + // With some drivers, ALSA appears to be unable to set valid hwparams, + // but they work if at least SND_PCM_NO_AUTO_FORMAT is set. Also, it + // appears you can set this flag only on opening a device, thus there + // is the need to retry opening the device. + MP_WARN(ao, "Attempting to work around even more ALSA bugs...\n"); + mode |= SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_FORMAT | + SND_PCM_NO_AUTO_RESAMPLE; + r = init_device(ao, mode); + } + + // Sometimes, ALSA will advertise certain chmaps, but it's not possible to + // set them. This can happen with dmix: as of alsa 1.0.29, dmix can do + // stereo only, but advertises the surround chmaps of the underlying device. + // In this case, e.g. setting 6 channels will succeed, but requesting 5.1 + // afterwards will fail. Then it will return something like "FL FR NA NA NA NA" + // as channel map. This means we would have to pad stereo output to 6 + // channels with silence, which would require lots of extra processing. You + // can't change the number of channels to 2 either, because the hw params + // are already set! So just fuck it and reopen the device with the chmap + // "cleaned out" of NA entries. + if (r >= 0) { + struct mp_chmap without_na = ao->channels; + mp_chmap_remove_na(&without_na); + + if (mp_chmap_is_valid(&without_na) && without_na.num <= 2 && + ao->channels.num > 2) + { + MP_VERBOSE(ao, "Working around braindead dmix multichannel behavior.\n"); + uninit(ao); + ao->channels = without_na; + r = init_device(ao, mode); + } + } + + return r; +} + +// Function for dealing with playback state. This attempts to recover the ALSA +// state (bring it into SND_PCM_STATE_{PREPARED,RUNNING,PAUSED,UNDERRUN}). If +// state!=NULL, fill it after recovery is attempted. +// Returns true if PCM is in one the expected states. +static bool recover_and_get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *p = ao->priv; + int err; + + snd_pcm_status_t *st; + snd_pcm_status_alloca(&st); + + bool state_ok = false; + snd_pcm_state_t pcmst = SND_PCM_STATE_DISCONNECTED; + + // Give it a number of chances to recover. This tries to deal with the fact + // that the API is asynchronous, and to account for some past cargo-cult + // (where things were retried in a loop). + for (int n = 0; n < 10; n++) { + err = snd_pcm_status(p->alsa, st); + if (err == -EPIPE) { + // ALSA APIs can return -EPIPE when an XRUN happens, + // we skip right to handling it by setting pcmst + // manually. + pcmst = SND_PCM_STATE_XRUN; + } else { + // Otherwise do error checking and query the PCM state properly. + CHECK_ALSA_ERROR("snd_pcm_status"); + + pcmst = snd_pcm_status_get_state(st); + } + + if (pcmst == SND_PCM_STATE_PREPARED || + pcmst == SND_PCM_STATE_RUNNING || + pcmst == SND_PCM_STATE_PAUSED) + { + state_ok = true; + break; + } + + MP_VERBOSE(ao, "attempt %d to recover from state '%s'...\n", + n + 1, snd_pcm_state_name(pcmst)); + + switch (pcmst) { + // Underrun; recover. (We never use draining.) + case SND_PCM_STATE_XRUN: + case SND_PCM_STATE_DRAINING: + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); + continue; + // Hardware suspend. + case SND_PCM_STATE_SUSPENDED: + MP_INFO(ao, "PCM in suspend mode, trying to resume.\n"); + err = snd_pcm_resume(p->alsa); + if (err == -EAGAIN) { + // Cargo-cult from decades ago, with a cargo cult timeout. + MP_INFO(ao, "PCM resume EAGAIN - retrying.\n"); + sleep(1); + continue; + } + if (err == -ENOSYS) { + // As suggested by ALSA doxygen. + MP_VERBOSE(ao, "ENOSYS, retrying with snd_pcm_prepare().\n"); + err = snd_pcm_prepare(p->alsa); + } + if (err < 0) + MP_ERR(ao, "resuming from SUSPENDED: %s\n", snd_strerror(err)); + continue; + // Device lost. OPEN/SETUP are states we never enter after init, so + // treat them like DISCONNECTED. + case SND_PCM_STATE_DISCONNECTED: + case SND_PCM_STATE_OPEN: + case SND_PCM_STATE_SETUP: + default: + if (!p->device_lost) { + MP_WARN(ao, "Device lost, trying to recover...\n"); + ao_request_reload(ao); + p->device_lost = true; + } + goto alsa_error; + } + } + + if (!state_ok) { + MP_ERR(ao, "could not recover\n"); + } + +alsa_error: + + if (state) { + snd_pcm_sframes_t del = state_ok ? snd_pcm_status_get_delay(st) : 0; + state->delay = MPMAX(del, 0) / (double)ao->samplerate; + state->free_samples = state_ok ? snd_pcm_status_get_avail(st) : 0; + state->free_samples = MPCLAMP(state->free_samples, 0, ao->device_buffer); + // Align to period size. + state->free_samples = state->free_samples / p->outburst * p->outburst; + state->queued_samples = ao->device_buffer - state->free_samples; + state->playing = pcmst == SND_PCM_STATE_RUNNING || + pcmst == SND_PCM_STATE_PAUSED; + } + + return state_ok; +} + +static void audio_get_state(struct ao *ao, struct mp_pcm_state *state) +{ + recover_and_get_state(ao, state); +} + +static void audio_start(struct ao *ao) +{ + struct priv *p = ao->priv; + int err; + + recover_and_get_state(ao, NULL); + + err = snd_pcm_start(p->alsa); + CHECK_ALSA_ERROR("pcm start error"); + +alsa_error: ; +} + +static void audio_reset(struct ao *ao) +{ + struct priv *p = ao->priv; + int err; + + err = snd_pcm_drop(p->alsa); + CHECK_ALSA_ERROR("pcm drop error"); + err = snd_pcm_prepare(p->alsa); + CHECK_ALSA_ERROR("pcm prepare error"); + + recover_and_get_state(ao, NULL); + +alsa_error: ; +} + +static bool audio_set_paused(struct ao *ao, bool paused) +{ + struct priv *p = ao->priv; + int err; + + recover_and_get_state(ao, NULL); + + if (!p->can_pause) + return false; + + snd_pcm_state_t pcmst = snd_pcm_state(p->alsa); + if (pcmst == SND_PCM_STATE_RUNNING && paused) { + err = snd_pcm_pause(p->alsa, 1); + CHECK_ALSA_ERROR("pcm pause error"); + } else if (pcmst == SND_PCM_STATE_PAUSED && !paused) { + err = snd_pcm_pause(p->alsa, 0); + CHECK_ALSA_ERROR("pcm resume error"); + } + + return true; + +alsa_error: + return false; +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *p = ao->priv; + + ao_convert_inplace(&p->convert, data, samples); + + if (!recover_and_get_state(ao, NULL)) + return false; + + snd_pcm_sframes_t err = 0; + if (af_fmt_is_planar(ao->format)) { + err = snd_pcm_writen(p->alsa, data, samples); + } else { + err = snd_pcm_writei(p->alsa, data[0], samples); + } + + CHECK_ALSA_ERROR("pcm write error"); + if (err >= 0 && err != samples) { + MP_ERR(ao, "unexpected partial write (%d of %d frames), dropping audio\n", + (int)err, samples); + } + + return true; + +alsa_error: + return false; +} + +static bool is_useless_device(char *name) +{ + char *crap[] = {"rear", "center_lfe", "side", "pulse", "null", "dsnoop", "hw"}; + for (int i = 0; i < MP_ARRAY_SIZE(crap); i++) { + int l = strlen(crap[i]); + if (name && strncmp(name, crap[i], l) == 0 && + (!name[l] || name[l] == ':')) + return true; + } + // The standard default entry will achieve exactly the same. + if (name && strcmp(name, "default") == 0) + return true; + return false; +} + +static void list_devs(struct ao *ao, struct ao_device_list *list) +{ + void **hints; + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return; + + ao_device_list_add(list, ao, &(struct ao_device_desc){"", ""}); + + for (int n = 0; hints[n]; n++) { + char *name = snd_device_name_get_hint(hints[n], "NAME"); + char *desc = snd_device_name_get_hint(hints[n], "DESC"); + char *io = snd_device_name_get_hint(hints[n], "IOID"); + if (!is_useless_device(name) && (!io || strcmp(io, "Output") == 0)) { + char desc2[1024]; + snprintf(desc2, sizeof(desc2), "%s", desc ? desc : ""); + for (int i = 0; desc2[i]; i++) { + if (desc2[i] == '\n') + desc2[i] = '/'; + } + ao_device_list_add(list, ao, &(struct ao_device_desc){name, desc2}); + } + free(name); + free(desc); + free(io); + } + + snd_device_name_free_hint(hints); +} + +const struct ao_driver audio_out_alsa = { + .description = "ALSA audio output", + .name = "alsa", + .init = init, + .uninit = uninit, + .control = control, + .get_state = audio_get_state, + .write = audio_write, + .start = audio_start, + .set_pause = audio_set_paused, + .reset = audio_reset, + .list_devs = list_devs, + .priv_size = sizeof(struct priv), + .global_opts = &ao_alsa_conf, +}; diff --git a/audio/out/ao_audiotrack.c b/audio/out/ao_audiotrack.c new file mode 100644 index 0000000..1392699 --- /dev/null +++ b/audio/out/ao_audiotrack.c @@ -0,0 +1,852 @@ +/* + * Android AudioTrack audio output driver. + * Copyright (C) 2018 Aman Gupta <aman@tmm1.net> + * Copyright (C) 2012-2015 VLC authors and VideoLAN, VideoLabs + * Authors: Thomas Guillem <thomas@gllm.fr> + * Ming Hu <tewilove@gmail.com> + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "ao.h" +#include "internal.h" +#include "common/msg.h" +#include "audio/format.h" +#include "options/m_option.h" +#include "osdep/threads.h" +#include "osdep/timer.h" +#include "misc/jni.h" + +struct priv { + jobject audiotrack; + jint samplerate; + jint channel_config; + jint format; + jint size; + + jobject timestamp; + int64_t timestamp_fetched; + bool timestamp_set; + int timestamp_stable; + + uint32_t written_frames; /* requires uint32_t rollover semantics */ + uint32_t playhead_pos; + uint32_t playhead_offset; + bool reset_pending; + + void *chunk; + int chunksize; + jbyteArray bytearray; + jshortArray shortarray; + jfloatArray floatarray; + jobject bbuf; + + bool cfg_pcm_float; + int cfg_session_id; + + bool needs_timestamp_offset; + int64_t timestamp_offset; + + bool thread_terminate; + bool thread_created; + mp_thread thread; + mp_mutex lock; + mp_cond wakeup; +}; + +struct JNIByteBuffer { + jclass clazz; + jmethodID clear; + struct MPJniField mapping[]; +} ByteBuffer = {.mapping = { + #define OFFSET(member) offsetof(struct JNIByteBuffer, member) + {"java/nio/ByteBuffer", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1}, + {"java/nio/ByteBuffer", "clear", "()Ljava/nio/Buffer;", MP_JNI_METHOD, OFFSET(clear), 1}, + {0}, + #undef OFFSET +}}; + +struct JNIAudioTrack { + jclass clazz; + jmethodID ctor; + jmethodID ctorV21; + jmethodID release; + jmethodID getState; + jmethodID getPlayState; + jmethodID play; + jmethodID stop; + jmethodID flush; + jmethodID pause; + jmethodID write; + jmethodID writeFloat; + jmethodID writeShortV23; + jmethodID writeBufferV21; + jmethodID getBufferSizeInFramesV23; + jmethodID getPlaybackHeadPosition; + jmethodID getTimestamp; + jmethodID getLatency; + jmethodID getMinBufferSize; + jmethodID getNativeOutputSampleRate; + jint STATE_INITIALIZED; + jint PLAYSTATE_STOPPED; + jint PLAYSTATE_PAUSED; + jint PLAYSTATE_PLAYING; + jint MODE_STREAM; + jint ERROR; + jint ERROR_BAD_VALUE; + jint ERROR_INVALID_OPERATION; + jint WRITE_BLOCKING; + jint WRITE_NON_BLOCKING; + struct MPJniField mapping[]; +} AudioTrack = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioTrack, member) + {"android/media/AudioTrack", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1}, + {"android/media/AudioTrack", "<init>", "(IIIIIII)V", MP_JNI_METHOD, OFFSET(ctor), 1}, + {"android/media/AudioTrack", "<init>", "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;III)V", MP_JNI_METHOD, OFFSET(ctorV21), 0}, + {"android/media/AudioTrack", "release", "()V", MP_JNI_METHOD, OFFSET(release), 1}, + {"android/media/AudioTrack", "getState", "()I", MP_JNI_METHOD, OFFSET(getState), 1}, + {"android/media/AudioTrack", "getPlayState", "()I", MP_JNI_METHOD, OFFSET(getPlayState), 1}, + {"android/media/AudioTrack", "play", "()V", MP_JNI_METHOD, OFFSET(play), 1}, + {"android/media/AudioTrack", "stop", "()V", MP_JNI_METHOD, OFFSET(stop), 1}, + {"android/media/AudioTrack", "flush", "()V", MP_JNI_METHOD, OFFSET(flush), 1}, + {"android/media/AudioTrack", "pause", "()V", MP_JNI_METHOD, OFFSET(pause), 1}, + {"android/media/AudioTrack", "write", "([BII)I", MP_JNI_METHOD, OFFSET(write), 1}, + {"android/media/AudioTrack", "write", "([FIII)I", MP_JNI_METHOD, OFFSET(writeFloat), 1}, + {"android/media/AudioTrack", "write", "([SIII)I", MP_JNI_METHOD, OFFSET(writeShortV23), 0}, + {"android/media/AudioTrack", "write", "(Ljava/nio/ByteBuffer;II)I", MP_JNI_METHOD, OFFSET(writeBufferV21), 1}, + {"android/media/AudioTrack", "getBufferSizeInFrames", "()I", MP_JNI_METHOD, OFFSET(getBufferSizeInFramesV23), 0}, + {"android/media/AudioTrack", "getTimestamp", "(Landroid/media/AudioTimestamp;)Z", MP_JNI_METHOD, OFFSET(getTimestamp), 1}, + {"android/media/AudioTrack", "getPlaybackHeadPosition", "()I", MP_JNI_METHOD, OFFSET(getPlaybackHeadPosition), 1}, + {"android/media/AudioTrack", "getLatency", "()I", MP_JNI_METHOD, OFFSET(getLatency), 1}, + {"android/media/AudioTrack", "getMinBufferSize", "(III)I", MP_JNI_STATIC_METHOD, OFFSET(getMinBufferSize), 1}, + {"android/media/AudioTrack", "getNativeOutputSampleRate", "(I)I", MP_JNI_STATIC_METHOD, OFFSET(getNativeOutputSampleRate), 1}, + {"android/media/AudioTrack", "WRITE_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_BLOCKING), 0}, + {"android/media/AudioTrack", "WRITE_NON_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_NON_BLOCKING), 0}, + {"android/media/AudioTrack", "STATE_INITIALIZED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STATE_INITIALIZED), 1}, + {"android/media/AudioTrack", "PLAYSTATE_STOPPED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_STOPPED), 1}, + {"android/media/AudioTrack", "PLAYSTATE_PAUSED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PAUSED), 1}, + {"android/media/AudioTrack", "PLAYSTATE_PLAYING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PLAYING), 1}, + {"android/media/AudioTrack", "MODE_STREAM", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(MODE_STREAM), 1}, + {"android/media/AudioTrack", "ERROR", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR), 1}, + {"android/media/AudioTrack", "ERROR_BAD_VALUE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_BAD_VALUE), 1}, + {"android/media/AudioTrack", "ERROR_INVALID_OPERATION", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_INVALID_OPERATION), 1}, + {0} + #undef OFFSET +}}; + +struct JNIAudioAttributes { + jclass clazz; + jint CONTENT_TYPE_MOVIE; + jint CONTENT_TYPE_MUSIC; + jint USAGE_MEDIA; + struct MPJniField mapping[]; +} AudioAttributes = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioAttributes, member) + {"android/media/AudioAttributes", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0}, + {"android/media/AudioAttributes", "CONTENT_TYPE_MOVIE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MOVIE), 0}, + {"android/media/AudioAttributes", "CONTENT_TYPE_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MUSIC), 0}, + {"android/media/AudioAttributes", "USAGE_MEDIA", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(USAGE_MEDIA), 0}, + {0} + #undef OFFSET +}}; + +struct JNIAudioAttributesBuilder { + jclass clazz; + jmethodID ctor; + jmethodID setUsage; + jmethodID setContentType; + jmethodID build; + struct MPJniField mapping[]; +} AudioAttributesBuilder = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioAttributesBuilder, member) + {"android/media/AudioAttributes$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0}, + {"android/media/AudioAttributes$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0}, + {"android/media/AudioAttributes$Builder", "setUsage", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setUsage), 0}, + {"android/media/AudioAttributes$Builder", "setContentType", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setContentType), 0}, + {"android/media/AudioAttributes$Builder", "build", "()Landroid/media/AudioAttributes;", MP_JNI_METHOD, OFFSET(build), 0}, + {0} + #undef OFFSET +}}; + +struct JNIAudioFormat { + jclass clazz; + jint ENCODING_PCM_8BIT; + jint ENCODING_PCM_16BIT; + jint ENCODING_PCM_FLOAT; + jint ENCODING_IEC61937; + jint CHANNEL_OUT_MONO; + jint CHANNEL_OUT_STEREO; + jint CHANNEL_OUT_FRONT_CENTER; + jint CHANNEL_OUT_QUAD; + jint CHANNEL_OUT_5POINT1; + jint CHANNEL_OUT_BACK_CENTER; + jint CHANNEL_OUT_7POINT1_SURROUND; + struct MPJniField mapping[]; +} AudioFormat = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioFormat, member) + {"android/media/AudioFormat", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1}, + {"android/media/AudioFormat", "ENCODING_PCM_8BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_8BIT), 1}, + {"android/media/AudioFormat", "ENCODING_PCM_16BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_16BIT), 1}, + {"android/media/AudioFormat", "ENCODING_PCM_FLOAT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_FLOAT), 1}, + {"android/media/AudioFormat", "ENCODING_IEC61937", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_IEC61937), 0}, + {"android/media/AudioFormat", "CHANNEL_OUT_MONO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_MONO), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_STEREO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_STEREO), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_FRONT_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_FRONT_CENTER), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_QUAD", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_QUAD), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_5POINT1", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_5POINT1), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_BACK_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_BACK_CENTER), 1}, + {"android/media/AudioFormat", "CHANNEL_OUT_7POINT1_SURROUND", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_7POINT1_SURROUND), 0}, + {0} + #undef OFFSET +}}; + +struct JNIAudioFormatBuilder { + jclass clazz; + jmethodID ctor; + jmethodID setEncoding; + jmethodID setSampleRate; + jmethodID setChannelMask; + jmethodID build; + struct MPJniField mapping[]; +} AudioFormatBuilder = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioFormatBuilder, member) + {"android/media/AudioFormat$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0}, + {"android/media/AudioFormat$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0}, + {"android/media/AudioFormat$Builder", "setEncoding", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setEncoding), 0}, + {"android/media/AudioFormat$Builder", "setSampleRate", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setSampleRate), 0}, + {"android/media/AudioFormat$Builder", "setChannelMask", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setChannelMask), 0}, + {"android/media/AudioFormat$Builder", "build", "()Landroid/media/AudioFormat;", MP_JNI_METHOD, OFFSET(build), 0}, + {0} + #undef OFFSET +}}; + + +struct JNIAudioManager { + jclass clazz; + jint ERROR_DEAD_OBJECT; + jint STREAM_MUSIC; + struct MPJniField mapping[]; +} AudioManager = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioManager, member) + {"android/media/AudioManager", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1}, + {"android/media/AudioManager", "STREAM_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STREAM_MUSIC), 1}, + {"android/media/AudioManager", "ERROR_DEAD_OBJECT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_DEAD_OBJECT), 0}, + {0} + #undef OFFSET +}}; + +struct JNIAudioTimestamp { + jclass clazz; + jmethodID ctor; + jfieldID framePosition; + jfieldID nanoTime; + struct MPJniField mapping[]; +} AudioTimestamp = {.mapping = { + #define OFFSET(member) offsetof(struct JNIAudioTimestamp, member) + {"android/media/AudioTimestamp", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1}, + {"android/media/AudioTimestamp", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 1}, + {"android/media/AudioTimestamp", "framePosition", "J", MP_JNI_FIELD, OFFSET(framePosition), 1}, + {"android/media/AudioTimestamp", "nanoTime", "J", MP_JNI_FIELD, OFFSET(nanoTime), 1}, + {0} + #undef OFFSET +}}; + +#define MP_JNI_DELETELOCAL(o) (*env)->DeleteLocalRef(env, o) + +static int AudioTrack_New(struct ao *ao) +{ + struct priv *p = ao->priv; + JNIEnv *env = MP_JNI_GET_ENV(ao); + jobject audiotrack = NULL; + + if (AudioTrack.ctorV21) { + MP_VERBOSE(ao, "Using API21 initializer\n"); + jobject tmp = NULL; + + jobject format_builder = MP_JNI_NEW(AudioFormatBuilder.clazz, AudioFormatBuilder.ctor); + MP_JNI_EXCEPTION_LOG(ao); + tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setEncoding, p->format); + MP_JNI_DELETELOCAL(tmp); + tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setSampleRate, p->samplerate); + MP_JNI_DELETELOCAL(tmp); + tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setChannelMask, p->channel_config); + MP_JNI_DELETELOCAL(tmp); + jobject format = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.build); + MP_JNI_DELETELOCAL(format_builder); + + jobject attr_builder = MP_JNI_NEW(AudioAttributesBuilder.clazz, AudioAttributesBuilder.ctor); + MP_JNI_EXCEPTION_LOG(ao); + tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setUsage, AudioAttributes.USAGE_MEDIA); + MP_JNI_DELETELOCAL(tmp); + jint content_type = (ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC) ? + AudioAttributes.CONTENT_TYPE_MUSIC : AudioAttributes.CONTENT_TYPE_MOVIE; + tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setContentType, content_type); + MP_JNI_DELETELOCAL(tmp); + jobject attr = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.build); + MP_JNI_DELETELOCAL(attr_builder); + + audiotrack = MP_JNI_NEW( + AudioTrack.clazz, + AudioTrack.ctorV21, + attr, + format, + p->size, + AudioTrack.MODE_STREAM, + p->cfg_session_id + ); + + MP_JNI_DELETELOCAL(format); + MP_JNI_DELETELOCAL(attr); + } else { + MP_VERBOSE(ao, "Using legacy initializer\n"); + audiotrack = MP_JNI_NEW( + AudioTrack.clazz, + AudioTrack.ctor, + AudioManager.STREAM_MUSIC, + p->samplerate, + p->channel_config, + p->format, + p->size, + AudioTrack.MODE_STREAM, + p->cfg_session_id + ); + } + if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !audiotrack) { + MP_FATAL(ao, "AudioTrack Init failed\n"); + return -1; + } + + if (MP_JNI_CALL_INT(audiotrack, AudioTrack.getState) != AudioTrack.STATE_INITIALIZED) { + MP_JNI_CALL_VOID(audiotrack, AudioTrack.release); + MP_JNI_EXCEPTION_LOG(ao); + (*env)->DeleteLocalRef(env, audiotrack); + MP_ERR(ao, "AudioTrack.getState failed\n"); + return -1; + } + + if (AudioTrack.getBufferSizeInFramesV23) { + int bufferSize = MP_JNI_CALL_INT(audiotrack, AudioTrack.getBufferSizeInFramesV23); + if (bufferSize > 0) { + MP_VERBOSE(ao, "AudioTrack.getBufferSizeInFrames = %d\n", bufferSize); + ao->device_buffer = bufferSize; + } + } + + p->audiotrack = (*env)->NewGlobalRef(env, audiotrack); + (*env)->DeleteLocalRef(env, audiotrack); + if (!p->audiotrack) + return -1; + + return 0; +} + +static int AudioTrack_Recreate(struct ao *ao) +{ + struct priv *p = ao->priv; + JNIEnv *env = MP_JNI_GET_ENV(ao); + + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release); + MP_JNI_EXCEPTION_LOG(ao); + (*env)->DeleteGlobalRef(env, p->audiotrack); + p->audiotrack = NULL; + return AudioTrack_New(ao); +} + +static uint32_t AudioTrack_getPlaybackHeadPosition(struct ao *ao) +{ + struct priv *p = ao->priv; + if (!p->audiotrack) + return 0; + JNIEnv *env = MP_JNI_GET_ENV(ao); + uint32_t pos = 0; + int64_t now = mp_raw_time_ns(); + int state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState); + + int stable_count = 20; + int64_t wait = p->timestamp_stable < stable_count ? 50000000 : 3000000000; + + if (state == AudioTrack.PLAYSTATE_PLAYING && p->format != AudioFormat.ENCODING_IEC61937 && + (p->timestamp_fetched == 0 || now - p->timestamp_fetched >= wait)) { + if (!p->timestamp_fetched) + p->timestamp_stable = 0; + + int64_t time1 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime); + if (MP_JNI_CALL_BOOL(p->audiotrack, AudioTrack.getTimestamp, p->timestamp)) { + p->timestamp_set = true; + p->timestamp_fetched = now; + if (p->timestamp_stable < stable_count) { + uint32_t fpos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition); + int64_t time2 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime); + //MP_VERBOSE(ao, "getTimestamp: fpos= %u / time= %"PRId64" / now= %"PRId64" / stable= %d\n", fpos, time2, now, p->timestamp_stable); + if (time1 != time2 && time2 != 0 && fpos != 0) { + p->timestamp_stable++; + } + } + } + } + + /* AudioTrack's framePosition and playbackHeadPosition return a signed integer, + * but documentation states it should be interpreted as a 32-bit unsigned integer. + */ + if (p->timestamp_set) { + pos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition); + uint32_t fpos = pos; + int64_t time = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime); + if (time == 0) + fpos = pos = 0; + if (p->needs_timestamp_offset) { + if (time != 0 && !p->timestamp_offset) + p->timestamp_offset = now - time; + time += p->timestamp_offset; + } + if (fpos != 0 && time != 0 && state == AudioTrack.PLAYSTATE_PLAYING) { + double diff = (double)(now - time) / 1e9; + pos += diff * ao->samplerate; + } + //MP_VERBOSE(ao, "position = %u via getTimestamp (state = %d / fpos= %u / time= %"PRId64")\n", pos, state, fpos, time); + } else { + pos = 0xFFFFFFFFL & MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlaybackHeadPosition); + //MP_VERBOSE(ao, "playbackHeadPosition = %u (reset_pending=%d)\n", pos, p->reset_pending); + } + + + if (p->format == AudioFormat.ENCODING_IEC61937) { + if (p->reset_pending) { + // after a flush(), playbackHeadPosition will not reset to 0 right away. + // sometimes, it will never reset at all. + // save the initial offset after the reset, to subtract it going forward. + if (p->playhead_offset == 0) + p->playhead_offset = pos; + p->reset_pending = false; + MP_VERBOSE(ao, "IEC/playbackHead offset = %d\n", pos); + } + + // usually shortly after a flush(), playbackHeadPosition will reset to 0. + // clear out the position and offset to avoid regular "rollover" below + if (pos == 0 && p->playhead_offset != 0) { + MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (flush)\n", p->playhead_pos, pos); + p->playhead_offset = 0; + p->playhead_pos = 0; + } + + // sometimes on a new AudioTrack instance, playbackHeadPosition will reset + // to 0 shortly after playback starts for no reason. + if (pos == 0 && p->playhead_pos != 0) { + MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (reset)\n", p->playhead_pos, pos); + p->playhead_offset = 0; + p->playhead_pos = 0; + p->written_frames = 0; + } + } + + p->playhead_pos = pos; + return p->playhead_pos - p->playhead_offset; +} + +static double AudioTrack_getLatency(struct ao *ao) +{ + JNIEnv *env = MP_JNI_GET_ENV(ao); + struct priv *p = ao->priv; + if (!p->audiotrack) + return 0; + + uint32_t playhead = AudioTrack_getPlaybackHeadPosition(ao); + uint32_t diff = p->written_frames - playhead; + double delay = diff / (double)(ao->samplerate); + if (!p->timestamp_set && + p->format != AudioFormat.ENCODING_IEC61937) + delay += (double)MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getLatency)/1000.0; + if (delay > 2.0) { + //MP_WARN(ao, "getLatency: written=%u playhead=%u diff=%u delay=%f\n", p->written_frames, playhead, diff, delay); + p->timestamp_fetched = 0; + return 0; + } + return MPCLAMP(delay, 0.0, 2.0); +} + +static int AudioTrack_write(struct ao *ao, int len) +{ + struct priv *p = ao->priv; + if (!p->audiotrack) + return -1; + JNIEnv *env = MP_JNI_GET_ENV(ao); + void *buf = p->chunk; + + jint ret; + if (p->format == AudioFormat.ENCODING_IEC61937) { + (*env)->SetShortArrayRegion(env, p->shortarray, 0, len / 2, buf); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeShortV23, p->shortarray, 0, len / 2, AudioTrack.WRITE_BLOCKING); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + if (ret > 0) ret *= 2; + + } else if (AudioTrack.writeBufferV21) { + // reset positions for reading + jobject bbuf = MP_JNI_CALL_OBJECT(p->bbuf, ByteBuffer.clear); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + (*env)->DeleteLocalRef(env, bbuf); + ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeBufferV21, p->bbuf, len, AudioTrack.WRITE_BLOCKING); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + + } else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) { + (*env)->SetFloatArrayRegion(env, p->floatarray, 0, len / sizeof(float), buf); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeFloat, p->floatarray, 0, len / sizeof(float), AudioTrack.WRITE_BLOCKING); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + if (ret > 0) ret *= sizeof(float); + + } else { + (*env)->SetByteArrayRegion(env, p->bytearray, 0, len, buf); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.write, p->bytearray, 0, len); + if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1; + } + + return ret; +} + +static void uninit_jni(struct ao *ao) +{ + JNIEnv *env = MP_JNI_GET_ENV(ao); + mp_jni_reset_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log); + mp_jni_reset_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log); +} + +static int init_jni(struct ao *ao) +{ + JNIEnv *env = MP_JNI_GET_ENV(ao); + if (mp_jni_init_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log) < 0 || + mp_jni_init_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log) < 0) { + uninit_jni(ao); + return -1; + } + + return 0; +} + +static MP_THREAD_VOID playthread(void *arg) +{ + struct ao *ao = arg; + struct priv *p = ao->priv; + JNIEnv *env = MP_JNI_GET_ENV(ao); + mp_thread_set_name("ao/audiotrack"); + mp_mutex_lock(&p->lock); + while (!p->thread_terminate) { + int state = AudioTrack.PLAYSTATE_PAUSED; + if (p->audiotrack) { + state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState); + } + if (state == AudioTrack.PLAYSTATE_PLAYING) { + int read_samples = p->chunksize / ao->sstride; + int64_t ts = mp_time_ns(); + ts += MP_TIME_S_TO_NS(read_samples / (double)(ao->samplerate)); + ts += MP_TIME_S_TO_NS(AudioTrack_getLatency(ao)); + int samples = ao_read_data_nonblocking(ao, &p->chunk, read_samples, ts); + int ret = AudioTrack_write(ao, samples * ao->sstride); + if (ret >= 0) { + p->written_frames += ret / ao->sstride; + } else if (ret == AudioManager.ERROR_DEAD_OBJECT) { + MP_WARN(ao, "AudioTrack.write failed with ERROR_DEAD_OBJECT. Recreating AudioTrack...\n"); + if (AudioTrack_Recreate(ao) < 0) { + MP_ERR(ao, "AudioTrack_Recreate failed\n"); + } + } else { + MP_ERR(ao, "AudioTrack.write failed with %d\n", ret); + } + } else { + mp_cond_timedwait(&p->wakeup, &p->lock, MP_TIME_MS_TO_NS(300)); + } + } + mp_mutex_unlock(&p->lock); + MP_THREAD_RETURN(); +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + JNIEnv *env = MP_JNI_GET_ENV(ao); + if (p->audiotrack) { + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.stop); + MP_JNI_EXCEPTION_LOG(ao); + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush); + MP_JNI_EXCEPTION_LOG(ao); + } + + mp_mutex_lock(&p->lock); + p->thread_terminate = true; + mp_cond_signal(&p->wakeup); + mp_mutex_unlock(&p->lock); + + if (p->thread_created) + mp_thread_join(p->thread); + + if (p->audiotrack) { + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release); + MP_JNI_EXCEPTION_LOG(ao); + (*env)->DeleteGlobalRef(env, p->audiotrack); + p->audiotrack = NULL; + } + + if (p->bytearray) { + (*env)->DeleteGlobalRef(env, p->bytearray); + p->bytearray = NULL; + } + + if (p->shortarray) { + (*env)->DeleteGlobalRef(env, p->shortarray); + p->shortarray = NULL; + } + + if (p->floatarray) { + (*env)->DeleteGlobalRef(env, p->floatarray); + p->floatarray = NULL; + } + + if (p->bbuf) { + (*env)->DeleteGlobalRef(env, p->bbuf); + p->bbuf = NULL; + } + + if (p->timestamp) { + (*env)->DeleteGlobalRef(env, p->timestamp); + p->timestamp = NULL; + } + + mp_cond_destroy(&p->wakeup); + mp_mutex_destroy(&p->lock); + + uninit_jni(ao); +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + JNIEnv *env = MP_JNI_GET_ENV(ao); + if (!env) + return -1; + + mp_mutex_init(&p->lock); + mp_cond_init(&p->wakeup); + + if (init_jni(ao) < 0) + return -1; + + if (af_fmt_is_spdif(ao->format)) { + p->format = AudioFormat.ENCODING_IEC61937; + } else if (ao->format == AF_FORMAT_U8) { + p->format = AudioFormat.ENCODING_PCM_8BIT; + } else if (p->cfg_pcm_float && af_fmt_is_float(ao->format)) { + ao->format = AF_FORMAT_FLOAT; + p->format = AudioFormat.ENCODING_PCM_FLOAT; + } else { + ao->format = AF_FORMAT_S16; + p->format = AudioFormat.ENCODING_PCM_16BIT; + } + + if (AudioTrack.getNativeOutputSampleRate) { + jint samplerate = MP_JNI_CALL_STATIC_INT( + AudioTrack.clazz, + AudioTrack.getNativeOutputSampleRate, + AudioManager.STREAM_MUSIC + ); + if (MP_JNI_EXCEPTION_LOG(ao) == 0) { + MP_VERBOSE(ao, "AudioTrack.nativeOutputSampleRate = %d\n", samplerate); + ao->samplerate = MPMIN(samplerate, ao->samplerate); + } + } + p->samplerate = ao->samplerate; + + /* https://developer.android.com/reference/android/media/AudioFormat#channelPositionMask */ + static const struct mp_chmap layouts[] = { + {0}, // empty + MP_CHMAP_INIT_MONO, // mono + MP_CHMAP_INIT_STEREO, // stereo + MP_CHMAP3(FL, FR, FC), // 3.0 + MP_CHMAP4(FL, FR, BL, BR), // quad + MP_CHMAP5(FL, FR, FC, BL, BR), // 5.0 + MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1 + MP_CHMAP7(FL, FR, FC, LFE, BL, BR, BC), // 6.1 + MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1 + }; + const jint layout_map[] = { + 0, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER, + AudioFormat.CHANNEL_OUT_QUAD, + AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER, + AudioFormat.CHANNEL_OUT_5POINT1, + AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER, + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, + }; + static_assert(MP_ARRAY_SIZE(layout_map) == MP_ARRAY_SIZE(layouts), ""); + if (p->format == AudioFormat.ENCODING_IEC61937) { + p->channel_config = AudioFormat.CHANNEL_OUT_STEREO; + } else { + struct mp_chmap_sel sel = {0}; + for (int i = 0; i < MP_ARRAY_SIZE(layouts); i++) { + if (layout_map[i]) + mp_chmap_sel_add_map(&sel, &layouts[i]); + } + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) + goto error; + p->channel_config = layout_map[ao->channels.num]; + assert(p->channel_config); + } + + jint buffer_size = MP_JNI_CALL_STATIC_INT( + AudioTrack.clazz, + AudioTrack.getMinBufferSize, + p->samplerate, + p->channel_config, + p->format + ); + if (MP_JNI_EXCEPTION_LOG(ao) < 0 || buffer_size <= 0) { + MP_FATAL(ao, "AudioTrack.getMinBufferSize returned an invalid size: %d", buffer_size); + return -1; + } + + // Choose double of the minimum buffer size suggested by the driver, but not + // less than 75ms or more than 150ms. + const int bps = af_fmt_to_bytes(ao->format); + int min = 0.075 * p->samplerate * bps * ao->channels.num; + int max = min * 2; + min = MP_ALIGN_UP(min, bps); + max = MP_ALIGN_UP(max, bps); + p->size = MPCLAMP(buffer_size * 2, min, max); + MP_VERBOSE(ao, "Setting bufferSize = %d (driver=%d, min=%d, max=%d)\n", p->size, buffer_size, min, max); + assert(p->size % bps == 0); + ao->device_buffer = p->size / bps; + + p->chunksize = p->size; + p->chunk = talloc_size(ao, p->size); + + jobject timestamp = MP_JNI_NEW(AudioTimestamp.clazz, AudioTimestamp.ctor); + if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !timestamp) { + MP_FATAL(ao, "AudioTimestamp could not be created\n"); + return -1; + } + p->timestamp = (*env)->NewGlobalRef(env, timestamp); + (*env)->DeleteLocalRef(env, timestamp); + + // decide and create buffer of right type + if (p->format == AudioFormat.ENCODING_IEC61937) { + jshortArray shortarray = (*env)->NewShortArray(env, p->chunksize / 2); + p->shortarray = (*env)->NewGlobalRef(env, shortarray); + (*env)->DeleteLocalRef(env, shortarray); + } else if (AudioTrack.writeBufferV21) { + MP_VERBOSE(ao, "Using NIO ByteBuffer\n"); + jobject bbuf = (*env)->NewDirectByteBuffer(env, p->chunk, p->chunksize); + p->bbuf = (*env)->NewGlobalRef(env, bbuf); + (*env)->DeleteLocalRef(env, bbuf); + } else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) { + jfloatArray floatarray = (*env)->NewFloatArray(env, p->chunksize / sizeof(float)); + p->floatarray = (*env)->NewGlobalRef(env, floatarray); + (*env)->DeleteLocalRef(env, floatarray); + } else { + jbyteArray bytearray = (*env)->NewByteArray(env, p->chunksize); + p->bytearray = (*env)->NewGlobalRef(env, bytearray); + (*env)->DeleteLocalRef(env, bytearray); + } + + /* create AudioTrack object */ + if (AudioTrack_New(ao) != 0) { + MP_FATAL(ao, "Failed to create AudioTrack\n"); + goto error; + } + + if (mp_thread_create(&p->thread, playthread, ao)) { + MP_ERR(ao, "pthread creation failed\n"); + goto error; + } + p->thread_created = true; + + return 1; + +error: + uninit(ao); + return -1; +} + +static void stop(struct ao *ao) +{ + struct priv *p = ao->priv; + if (!p->audiotrack) { + MP_ERR(ao, "AudioTrack does not exist to stop!\n"); + return; + } + + JNIEnv *env = MP_JNI_GET_ENV(ao); + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.pause); + MP_JNI_EXCEPTION_LOG(ao); + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush); + MP_JNI_EXCEPTION_LOG(ao); + + p->playhead_offset = 0; + p->reset_pending = true; + p->written_frames = 0; + p->timestamp_fetched = 0; + p->timestamp_set = false; + p->timestamp_offset = 0; +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + if (!p->audiotrack) { + MP_ERR(ao, "AudioTrack does not exist to start!\n"); + return; + } + + JNIEnv *env = MP_JNI_GET_ENV(ao); + MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.play); + MP_JNI_EXCEPTION_LOG(ao); + + mp_cond_signal(&p->wakeup); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_audiotrack = { + .description = "Android AudioTrack audio output", + .name = "audiotrack", + .init = init, + .uninit = uninit, + .reset = stop, + .start = start, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const OPT_BASE_STRUCT) { + .cfg_pcm_float = 1, + }, + .options = (const struct m_option[]) { + {"pcm-float", OPT_BOOL(cfg_pcm_float)}, + {"session-id", OPT_INT(cfg_session_id)}, + {0} + }, + .options_prefix = "audiotrack", +}; diff --git a/audio/out/ao_audiounit.m b/audio/out/ao_audiounit.m new file mode 100644 index 0000000..85b1226 --- /dev/null +++ b/audio/out/ao_audiounit.m @@ -0,0 +1,260 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" +#include "osdep/timer.h" +#include "options/m_option.h" +#include "common/msg.h" +#include "ao_coreaudio_utils.h" +#include "ao_coreaudio_chmap.h" + +#import <AudioUnit/AudioUnit.h> +#import <CoreAudio/CoreAudioTypes.h> +#import <AudioToolbox/AudioToolbox.h> +#import <AVFoundation/AVFoundation.h> +#import <mach/mach_time.h> + +struct priv { + AudioUnit audio_unit; + double device_latency; +}; + +static OSStatus au_get_ary(AudioUnit unit, AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void **data, UInt32 *outDataSize) +{ + OSStatus err; + + err = AudioUnitGetPropertyInfo(unit, inID, inScope, inElement, outDataSize, NULL); + CHECK_CA_ERROR_SILENT_L(coreaudio_error); + + *data = talloc_zero_size(NULL, *outDataSize); + + err = AudioUnitGetProperty(unit, inID, inScope, inElement, *data, outDataSize); + CHECK_CA_ERROR_SILENT_L(coreaudio_error_free); + + return err; +coreaudio_error_free: + talloc_free(*data); +coreaudio_error: + return err; +} + +static AudioChannelLayout *convert_layout(AudioChannelLayout *layout, UInt32* size) +{ + AudioChannelLayoutTag tag = layout->mChannelLayoutTag; + AudioChannelLayout *new_layout; + if (tag == kAudioChannelLayoutTag_UseChannelDescriptions) + return layout; + else if (tag == kAudioChannelLayoutTag_UseChannelBitmap) + AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForBitmap, + sizeof(UInt32), &layout->mChannelBitmap, size); + else + AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForTag, + sizeof(AudioChannelLayoutTag), &tag, size); + new_layout = talloc_zero_size(NULL, *size); + if (!new_layout) { + talloc_free(layout); + return NULL; + } + if (tag == kAudioChannelLayoutTag_UseChannelBitmap) + AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap, + sizeof(UInt32), &layout->mChannelBitmap, size, new_layout); + else + AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag, + sizeof(AudioChannelLayoutTag), &tag, size, new_layout); + new_layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + talloc_free(layout); + return new_layout; +} + + +static OSStatus render_cb_lpcm(void *ctx, AudioUnitRenderActionFlags *aflags, + const AudioTimeStamp *ts, UInt32 bus, + UInt32 frames, AudioBufferList *buffer_list) +{ + struct ao *ao = ctx; + struct priv *p = ao->priv; + void *planes[MP_NUM_CHANNELS] = {0}; + + for (int n = 0; n < ao->num_planes; n++) + planes[n] = buffer_list->mBuffers[n].mData; + + int64_t end = mp_time_ns(); + end += MP_TIME_S_TO_NS(p->device_latency); + end += ca_get_latency(ts) + ca_frames_to_ns(ao, frames); + ao_read_data(ao, planes, frames, end); + return noErr; +} + +static bool init_audiounit(struct ao *ao) +{ + AudioStreamBasicDescription asbd; + OSStatus err; + uint32_t size; + AudioChannelLayout *layout = NULL; + struct priv *p = ao->priv; + AVAudioSession *instance = AVAudioSession.sharedInstance; + AVAudioSessionPortDescription *port = nil; + NSInteger maxChannels = instance.maximumOutputNumberOfChannels; + NSInteger prefChannels = MIN(maxChannels, ao->channels.num); + + MP_VERBOSE(ao, "max channels: %ld, requested: %d\n", maxChannels, (int)ao->channels.num); + + [instance setCategory:AVAudioSessionCategoryPlayback error:nil]; + [instance setMode:AVAudioSessionModeMoviePlayback error:nil]; + [instance setActive:YES error:nil]; + [instance setPreferredOutputNumberOfChannels:prefChannels error:nil]; + + AudioComponentDescription desc = (AudioComponentDescription) { + .componentType = kAudioUnitType_Output, + .componentSubType = kAudioUnitSubType_RemoteIO, + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0, + }; + + AudioComponent comp = AudioComponentFindNext(NULL, &desc); + if (comp == NULL) { + MP_ERR(ao, "unable to find audio component\n"); + goto coreaudio_error; + } + + err = AudioComponentInstanceNew(comp, &(p->audio_unit)); + CHECK_CA_ERROR("unable to open audio component"); + + err = AudioUnitInitialize(p->audio_unit); + CHECK_CA_ERROR_L(coreaudio_error_component, + "unable to initialize audio unit"); + + err = au_get_ary(p->audio_unit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, + 0, (void**)&layout, &size); + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "unable to retrieve audio unit channel layout"); + + MP_VERBOSE(ao, "AU channel layout tag: %x (%x)\n", layout->mChannelLayoutTag, layout->mChannelBitmap); + + layout = convert_layout(layout, &size); + if (!layout) { + MP_ERR(ao, "unable to convert channel layout to list format\n"); + goto coreaudio_error_audiounit; + } + + for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; i++) { + MP_VERBOSE(ao, "channel map: %i: %u\n", i, layout->mChannelDescriptions[i].mChannelLabel); + } + + if (af_fmt_is_spdif(ao->format) || instance.outputNumberOfChannels <= 2) { + ao->channels = (struct mp_chmap)MP_CHMAP_INIT_STEREO; + MP_VERBOSE(ao, "using stereo output\n"); + } else { + ao->channels.num = (uint8_t)layout->mNumberChannelDescriptions; + for (UInt32 i = 0; i < layout->mNumberChannelDescriptions; i++) { + ao->channels.speaker[i] = + ca_label_to_mp_speaker_id(layout->mChannelDescriptions[i].mChannelLabel); + } + MP_VERBOSE(ao, "using standard channel mapping\n"); + } + + ca_fill_asbd(ao, &asbd); + size = sizeof(AudioStreamBasicDescription); + err = AudioUnitSetProperty(p->audio_unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &asbd, size); + + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "unable to set the input format on the audio unit"); + + AURenderCallbackStruct render_cb = (AURenderCallbackStruct) { + .inputProc = render_cb_lpcm, + .inputProcRefCon = ao, + }; + + err = AudioUnitSetProperty(p->audio_unit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, &render_cb, + sizeof(AURenderCallbackStruct)); + + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "unable to set render callback on audio unit"); + + talloc_free(layout); + + return true; + +coreaudio_error_audiounit: + AudioUnitUninitialize(p->audio_unit); +coreaudio_error_component: + AudioComponentInstanceDispose(p->audio_unit); +coreaudio_error: + talloc_free(layout); + return false; +} + +static void stop(struct ao *ao) +{ + struct priv *p = ao->priv; + OSStatus err = AudioOutputUnitStop(p->audio_unit); + CHECK_CA_WARN("can't stop audio unit"); +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + AVAudioSession *instance = AVAudioSession.sharedInstance; + + p->device_latency = [instance outputLatency]; + + OSStatus err = AudioOutputUnitStart(p->audio_unit); + CHECK_CA_WARN("can't start audio unit"); +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + AudioOutputUnitStop(p->audio_unit); + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + + [AVAudioSession.sharedInstance + setActive:NO + withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation + error:nil]; +} + +static int init(struct ao *ao) +{ + if (!init_audiounit(ao)) + goto coreaudio_error; + + return CONTROL_OK; + +coreaudio_error: + return CONTROL_ERROR; +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_audiounit = { + .description = "AudioUnit (iOS)", + .name = "audiounit", + .uninit = uninit, + .init = init, + .reset = stop, + .start = start, + .priv_size = sizeof(struct priv), +}; diff --git a/audio/out/ao_coreaudio.c b/audio/out/ao_coreaudio.c new file mode 100644 index 0000000..37f1313 --- /dev/null +++ b/audio/out/ao_coreaudio.c @@ -0,0 +1,435 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <CoreAudio/HostTime.h> + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" +#include "osdep/timer.h" +#include "options/m_option.h" +#include "common/msg.h" +#include "ao_coreaudio_chmap.h" +#include "ao_coreaudio_properties.h" +#include "ao_coreaudio_utils.h" + +struct priv { + AudioDeviceID device; + AudioUnit audio_unit; + + uint64_t hw_latency_ns; + + AudioStreamBasicDescription original_asbd; + AudioStreamID original_asbd_stream; + + bool change_physical_format; +}; + +static int64_t ca_get_hardware_latency(struct ao *ao) { + struct priv *p = ao->priv; + + double audiounit_latency_sec = 0.0; + uint32_t size = sizeof(audiounit_latency_sec); + OSStatus err = AudioUnitGetProperty( + p->audio_unit, + kAudioUnitProperty_Latency, + kAudioUnitScope_Global, + 0, + &audiounit_latency_sec, + &size); + CHECK_CA_ERROR("cannot get audio unit latency"); + + uint64_t audiounit_latency_ns = MP_TIME_S_TO_NS(audiounit_latency_sec); + uint64_t device_latency_ns = ca_get_device_latency_ns(ao, p->device); + + MP_VERBOSE(ao, "audiounit latency [ns]: %lld\n", audiounit_latency_ns); + MP_VERBOSE(ao, "device latency [ns]: %lld\n", device_latency_ns); + + return audiounit_latency_ns + device_latency_ns; + +coreaudio_error: + return 0; +} + +static OSStatus render_cb_lpcm(void *ctx, AudioUnitRenderActionFlags *aflags, + const AudioTimeStamp *ts, UInt32 bus, + UInt32 frames, AudioBufferList *buffer_list) +{ + struct ao *ao = ctx; + struct priv *p = ao->priv; + void *planes[MP_NUM_CHANNELS] = {0}; + + for (int n = 0; n < ao->num_planes; n++) + planes[n] = buffer_list->mBuffers[n].mData; + + int64_t end = mp_time_ns(); + end += p->hw_latency_ns + ca_get_latency(ts) + ca_frames_to_ns(ao, frames); + int samples = ao_read_data_nonblocking(ao, planes, frames, end); + + if (samples == 0) + *aflags |= kAudioUnitRenderAction_OutputIsSilence; + + for (int n = 0; n < buffer_list->mNumberBuffers; n++) + buffer_list->mBuffers[n].mDataByteSize = samples * ao->sstride; + + return noErr; +} + +static int get_volume(struct ao *ao, float *vol) { + struct priv *p = ao->priv; + float auvol; + OSStatus err = + AudioUnitGetParameter(p->audio_unit, kHALOutputParam_Volume, + kAudioUnitScope_Global, 0, &auvol); + + CHECK_CA_ERROR("could not get HAL output volume"); + *vol = auvol * 100.0; + return CONTROL_TRUE; +coreaudio_error: + return CONTROL_ERROR; +} + +static int set_volume(struct ao *ao, float *vol) { + struct priv *p = ao->priv; + float auvol = *vol / 100.0; + OSStatus err = + AudioUnitSetParameter(p->audio_unit, kHALOutputParam_Volume, + kAudioUnitScope_Global, 0, auvol, 0); + CHECK_CA_ERROR("could not set HAL output volume"); + return CONTROL_TRUE; +coreaudio_error: + return CONTROL_ERROR; +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + switch (cmd) { + case AOCONTROL_GET_VOLUME: + return get_volume(ao, arg); + case AOCONTROL_SET_VOLUME: + return set_volume(ao, arg); + } + return CONTROL_UNKNOWN; +} + +static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd); +static void init_physical_format(struct ao *ao); + +static bool reinit_device(struct ao *ao) { + struct priv *p = ao->priv; + + OSStatus err = ca_select_device(ao, ao->device, &p->device); + CHECK_CA_ERROR("failed to select device"); + + return true; + +coreaudio_error: + return false; +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + + if (!af_fmt_is_pcm(ao->format) || (ao->init_flags & AO_INIT_EXCLUSIVE)) { + MP_VERBOSE(ao, "redirecting to coreaudio_exclusive\n"); + ao->redirect = "coreaudio_exclusive"; + return CONTROL_ERROR; + } + + if (!reinit_device(ao)) + goto coreaudio_error; + + if (p->change_physical_format) + init_physical_format(ao); + + if (!ca_init_chmap(ao, p->device)) + goto coreaudio_error; + + AudioStreamBasicDescription asbd; + ca_fill_asbd(ao, &asbd); + + if (!init_audiounit(ao, asbd)) + goto coreaudio_error; + + return CONTROL_OK; + +coreaudio_error: + return CONTROL_ERROR; +} + +static void init_physical_format(struct ao *ao) +{ + struct priv *p = ao->priv; + OSErr err; + + void *tmp = talloc_new(NULL); + + AudioStreamBasicDescription asbd; + ca_fill_asbd(ao, &asbd); + + AudioStreamID *streams; + size_t n_streams; + + err = CA_GET_ARY_O(p->device, kAudioDevicePropertyStreams, + &streams, &n_streams); + CHECK_CA_ERROR("could not get number of streams"); + + talloc_steal(tmp, streams); + + MP_VERBOSE(ao, "Found %zd substream(s).\n", n_streams); + + for (int i = 0; i < n_streams; i++) { + AudioStreamRangedDescription *formats; + size_t n_formats; + + MP_VERBOSE(ao, "Looking at formats in substream %d...\n", i); + + err = CA_GET_ARY(streams[i], kAudioStreamPropertyAvailablePhysicalFormats, + &formats, &n_formats); + + if (!CHECK_CA_WARN("could not get number of stream formats")) + continue; // try next one + + talloc_steal(tmp, formats); + + uint32_t direction; + err = CA_GET(streams[i], kAudioStreamPropertyDirection, &direction); + CHECK_CA_ERROR("could not get stream direction"); + if (direction != 0) { + MP_VERBOSE(ao, "Not an output stream.\n"); + continue; + } + + AudioStreamBasicDescription best_asbd = {0}; + + for (int j = 0; j < n_formats; j++) { + AudioStreamBasicDescription *stream_asbd = &formats[j].mFormat; + + ca_print_asbd(ao, "- ", stream_asbd); + + if (!best_asbd.mFormatID || ca_asbd_is_better(&asbd, &best_asbd, + stream_asbd)) + best_asbd = *stream_asbd; + } + + if (best_asbd.mFormatID) { + p->original_asbd_stream = streams[i]; + err = CA_GET(p->original_asbd_stream, + kAudioStreamPropertyPhysicalFormat, + &p->original_asbd); + CHECK_CA_WARN("could not get current physical stream format"); + + if (ca_asbd_equals(&p->original_asbd, &best_asbd)) { + MP_VERBOSE(ao, "Requested format already set, not changing.\n"); + p->original_asbd.mFormatID = 0; + break; + } + + if (!ca_change_physical_format_sync(ao, streams[i], best_asbd)) + p->original_asbd = (AudioStreamBasicDescription){0}; + break; + } + } + +coreaudio_error: + talloc_free(tmp); + return; +} + +static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd) +{ + OSStatus err; + uint32_t size; + struct priv *p = ao->priv; + + AudioComponentDescription desc = (AudioComponentDescription) { + .componentType = kAudioUnitType_Output, + .componentSubType = (ao->device) ? + kAudioUnitSubType_HALOutput : + kAudioUnitSubType_DefaultOutput, + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0, + }; + + AudioComponent comp = AudioComponentFindNext(NULL, &desc); + if (comp == NULL) { + MP_ERR(ao, "unable to find audio component\n"); + goto coreaudio_error; + } + + err = AudioComponentInstanceNew(comp, &(p->audio_unit)); + CHECK_CA_ERROR("unable to open audio component"); + + err = AudioUnitInitialize(p->audio_unit); + CHECK_CA_ERROR_L(coreaudio_error_component, + "unable to initialize audio unit"); + + size = sizeof(AudioStreamBasicDescription); + err = AudioUnitSetProperty(p->audio_unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &asbd, size); + + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "unable to set the input format on the audio unit"); + + err = AudioUnitSetProperty(p->audio_unit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, 0, &p->device, + sizeof(p->device)); + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "can't link audio unit to selected device"); + + p->hw_latency_ns = ca_get_hardware_latency(ao); + + AURenderCallbackStruct render_cb = (AURenderCallbackStruct) { + .inputProc = render_cb_lpcm, + .inputProcRefCon = ao, + }; + + err = AudioUnitSetProperty(p->audio_unit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, &render_cb, + sizeof(AURenderCallbackStruct)); + + CHECK_CA_ERROR_L(coreaudio_error_audiounit, + "unable to set render callback on audio unit"); + + return true; + +coreaudio_error_audiounit: + AudioUnitUninitialize(p->audio_unit); +coreaudio_error_component: + AudioComponentInstanceDispose(p->audio_unit); +coreaudio_error: + return false; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + OSStatus err = AudioUnitReset(p->audio_unit, kAudioUnitScope_Global, 0); + CHECK_CA_WARN("can't reset audio unit"); +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + OSStatus err = AudioOutputUnitStart(p->audio_unit); + CHECK_CA_WARN("can't start audio unit"); +} + + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + AudioOutputUnitStop(p->audio_unit); + AudioUnitUninitialize(p->audio_unit); + AudioComponentInstanceDispose(p->audio_unit); + + if (p->original_asbd.mFormatID) { + OSStatus err = CA_SET(p->original_asbd_stream, + kAudioStreamPropertyPhysicalFormat, + &p->original_asbd); + CHECK_CA_WARN("could not restore physical stream format"); + } +} + +static OSStatus hotplug_cb(AudioObjectID id, UInt32 naddr, + const AudioObjectPropertyAddress addr[], + void *ctx) +{ + struct ao *ao = ctx; + MP_VERBOSE(ao, "Handling potential hotplug event...\n"); + reinit_device(ao); + ao_hotplug_event(ao); + return noErr; +} + +static uint32_t hotplug_properties[] = { + kAudioHardwarePropertyDevices, + kAudioHardwarePropertyDefaultOutputDevice +}; + +static int hotplug_init(struct ao *ao) +{ + if (!reinit_device(ao)) + goto coreaudio_error; + + OSStatus err = noErr; + for (int i = 0; i < MP_ARRAY_SIZE(hotplug_properties); i++) { + AudioObjectPropertyAddress addr = { + hotplug_properties[i], + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + err = AudioObjectAddPropertyListener( + kAudioObjectSystemObject, &addr, hotplug_cb, (void *)ao); + if (err != noErr) { + char *c1 = mp_tag_str(hotplug_properties[i]); + char *c2 = mp_tag_str(err); + MP_ERR(ao, "failed to set device listener %s (%s)", c1, c2); + goto coreaudio_error; + } + } + + return 0; + +coreaudio_error: + return -1; +} + +static void hotplug_uninit(struct ao *ao) +{ + OSStatus err = noErr; + for (int i = 0; i < MP_ARRAY_SIZE(hotplug_properties); i++) { + AudioObjectPropertyAddress addr = { + hotplug_properties[i], + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + err = AudioObjectRemovePropertyListener( + kAudioObjectSystemObject, &addr, hotplug_cb, (void *)ao); + if (err != noErr) { + char *c1 = mp_tag_str(hotplug_properties[i]); + char *c2 = mp_tag_str(err); + MP_ERR(ao, "failed to set device listener %s (%s)", c1, c2); + } + } +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_coreaudio = { + .description = "CoreAudio AudioUnit", + .name = "coreaudio", + .uninit = uninit, + .init = init, + .control = control, + .reset = reset, + .start = start, + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .list_devs = ca_get_device_list, + .priv_size = sizeof(struct priv), + .options = (const struct m_option[]){ + {"change-physical-format", OPT_BOOL(change_physical_format)}, + {0} + }, + .options_prefix = "coreaudio", +}; diff --git a/audio/out/ao_coreaudio_chmap.c b/audio/out/ao_coreaudio_chmap.c new file mode 100644 index 0000000..3fd9550 --- /dev/null +++ b/audio/out/ao_coreaudio_chmap.c @@ -0,0 +1,340 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <Availability.h> + +#include "common/common.h" + +#include "ao_coreaudio_utils.h" + +#include "ao_coreaudio_chmap.h" + +static const int speaker_map[][2] = { + { kAudioChannelLabel_Left, MP_SPEAKER_ID_FL }, + { kAudioChannelLabel_Right, MP_SPEAKER_ID_FR }, + { kAudioChannelLabel_Center, MP_SPEAKER_ID_FC }, + { kAudioChannelLabel_LFEScreen, MP_SPEAKER_ID_LFE }, + { kAudioChannelLabel_LeftSurround, MP_SPEAKER_ID_BL }, + { kAudioChannelLabel_RightSurround, MP_SPEAKER_ID_BR }, + { kAudioChannelLabel_LeftCenter, MP_SPEAKER_ID_FLC }, + { kAudioChannelLabel_RightCenter, MP_SPEAKER_ID_FRC }, + { kAudioChannelLabel_CenterSurround, MP_SPEAKER_ID_BC }, + { kAudioChannelLabel_LeftSurroundDirect, MP_SPEAKER_ID_SL }, + { kAudioChannelLabel_RightSurroundDirect, MP_SPEAKER_ID_SR }, + { kAudioChannelLabel_TopCenterSurround, MP_SPEAKER_ID_TC }, + { kAudioChannelLabel_VerticalHeightLeft, MP_SPEAKER_ID_TFL }, + { kAudioChannelLabel_VerticalHeightCenter, MP_SPEAKER_ID_TFC }, + { kAudioChannelLabel_VerticalHeightRight, MP_SPEAKER_ID_TFR }, + { kAudioChannelLabel_TopBackLeft, MP_SPEAKER_ID_TBL }, + { kAudioChannelLabel_TopBackCenter, MP_SPEAKER_ID_TBC }, + { kAudioChannelLabel_TopBackRight, MP_SPEAKER_ID_TBR }, + + // unofficial extensions + { kAudioChannelLabel_RearSurroundLeft, MP_SPEAKER_ID_SDL }, + { kAudioChannelLabel_RearSurroundRight, MP_SPEAKER_ID_SDR }, + { kAudioChannelLabel_LeftWide, MP_SPEAKER_ID_WL }, + { kAudioChannelLabel_RightWide, MP_SPEAKER_ID_WR }, + { kAudioChannelLabel_LFE2, MP_SPEAKER_ID_LFE2 }, +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000 + { kAudioChannelLabel_LeftTopSurround, MP_SPEAKER_ID_TSL }, + { kAudioChannelLabel_RightTopSurround, MP_SPEAKER_ID_TSR }, + { kAudioChannelLabel_CenterBottom, MP_SPEAKER_ID_BFC }, + { kAudioChannelLabel_LeftBottom, MP_SPEAKER_ID_BFL }, + { kAudioChannelLabel_RightBottom, MP_SPEAKER_ID_BFR }, +#endif + + { kAudioChannelLabel_HeadphonesLeft, MP_SPEAKER_ID_DL }, + { kAudioChannelLabel_HeadphonesRight, MP_SPEAKER_ID_DR }, + + { kAudioChannelLabel_Unknown, MP_SPEAKER_ID_NA }, + + { 0, -1 }, +}; + +int ca_label_to_mp_speaker_id(AudioChannelLabel label) +{ + for (int i = 0; speaker_map[i][1] >= 0; i++) + if (speaker_map[i][0] == label) + return speaker_map[i][1]; + return -1; +} + +#if HAVE_COREAUDIO +static void ca_log_layout(struct ao *ao, int l, AudioChannelLayout *layout) +{ + if (!mp_msg_test(ao->log, l)) + return; + + AudioChannelDescription *descs = layout->mChannelDescriptions; + + mp_msg(ao->log, l, "layout: tag: <%u>, bitmap: <%u>, " + "descriptions <%u>\n", + (unsigned) layout->mChannelLayoutTag, + (unsigned) layout->mChannelBitmap, + (unsigned) layout->mNumberChannelDescriptions); + + for (int i = 0; i < layout->mNumberChannelDescriptions; i++) { + AudioChannelDescription d = descs[i]; + mp_msg(ao->log, l, " - description %d: label <%u, %u>, " + " flags: <%u>, coords: <%f, %f, %f>\n", i, + (unsigned) d.mChannelLabel, + (unsigned) ca_label_to_mp_speaker_id(d.mChannelLabel), + (unsigned) d.mChannelFlags, + d.mCoordinates[0], + d.mCoordinates[1], + d.mCoordinates[2]); + } +} + +static AudioChannelLayout *ca_layout_to_custom_layout(struct ao *ao, + void *talloc_ctx, + AudioChannelLayout *l) +{ + AudioChannelLayoutTag tag = l->mChannelLayoutTag; + AudioChannelLayout *r; + OSStatus err; + + if (tag == kAudioChannelLayoutTag_UseChannelDescriptions) + return l; + + if (tag == kAudioChannelLayoutTag_UseChannelBitmap) { + uint32_t psize; + err = AudioFormatGetPropertyInfo( + kAudioFormatProperty_ChannelLayoutForBitmap, + sizeof(uint32_t), &l->mChannelBitmap, &psize); + CHECK_CA_ERROR("failed to convert channel bitmap to descriptions (info)"); + r = talloc_size(NULL, psize); + err = AudioFormatGetProperty( + kAudioFormatProperty_ChannelLayoutForBitmap, + sizeof(uint32_t), &l->mChannelBitmap, &psize, r); + CHECK_CA_ERROR("failed to convert channel bitmap to descriptions (get)"); + } else { + uint32_t psize; + err = AudioFormatGetPropertyInfo( + kAudioFormatProperty_ChannelLayoutForTag, + sizeof(AudioChannelLayoutTag), &l->mChannelLayoutTag, &psize); + r = talloc_size(NULL, psize); + CHECK_CA_ERROR("failed to convert channel tag to descriptions (info)"); + err = AudioFormatGetProperty( + kAudioFormatProperty_ChannelLayoutForTag, + sizeof(AudioChannelLayoutTag), &l->mChannelLayoutTag, &psize, r); + CHECK_CA_ERROR("failed to convert channel tag to descriptions (get)"); + } + + MP_VERBOSE(ao, "converted input channel layout:\n"); + ca_log_layout(ao, MSGL_V, l); + + return r; +coreaudio_error: + return NULL; +} + + +#define CHMAP(n, ...) &(struct mp_chmap) MP_CONCAT(MP_CHMAP, n) (__VA_ARGS__) + +// Replace each channel in a with b (a->num == b->num) +static void replace_submap(struct mp_chmap *dst, struct mp_chmap *a, + struct mp_chmap *b) +{ + struct mp_chmap t = *dst; + if (!mp_chmap_is_valid(&t) || mp_chmap_diffn(a, &t) != 0) + return; + assert(a->num == b->num); + for (int n = 0; n < t.num; n++) { + for (int i = 0; i < a->num; i++) { + if (t.speaker[n] == a->speaker[i]) { + t.speaker[n] = b->speaker[i]; + break; + } + } + } + if (mp_chmap_is_valid(&t)) + *dst = t; +} + +static bool ca_layout_to_mp_chmap(struct ao *ao, AudioChannelLayout *layout, + struct mp_chmap *chmap) +{ + void *talloc_ctx = talloc_new(NULL); + + MP_VERBOSE(ao, "input channel layout:\n"); + ca_log_layout(ao, MSGL_V, layout); + + AudioChannelLayout *l = ca_layout_to_custom_layout(ao, talloc_ctx, layout); + if (!l) + goto coreaudio_error; + + if (l->mNumberChannelDescriptions > MP_NUM_CHANNELS) { + MP_VERBOSE(ao, "layout has too many descriptions (%u, max: %d)\n", + (unsigned) l->mNumberChannelDescriptions, MP_NUM_CHANNELS); + return false; + } + + chmap->num = l->mNumberChannelDescriptions; + for (int n = 0; n < l->mNumberChannelDescriptions; n++) { + AudioChannelLabel label = l->mChannelDescriptions[n].mChannelLabel; + int speaker = ca_label_to_mp_speaker_id(label); + if (speaker < 0) { + MP_VERBOSE(ao, "channel label=%u unusable to build channel " + "bitmap, skipping layout\n", (unsigned) label); + goto coreaudio_error; + } + chmap->speaker[n] = speaker; + } + + // Remap weird 7.1(rear) layouts correctly. + replace_submap(chmap, CHMAP(6, FL, FR, BL, BR, SDL, SDR), + CHMAP(6, FL, FR, SL, SR, BL, BR)); + + talloc_free(talloc_ctx); + MP_VERBOSE(ao, "mp chmap: %s\n", mp_chmap_to_str(chmap)); + return mp_chmap_is_valid(chmap) && !mp_chmap_is_unknown(chmap); +coreaudio_error: + MP_VERBOSE(ao, "converted input channel layout (failed):\n"); + ca_log_layout(ao, MSGL_V, layout); + talloc_free(talloc_ctx); + return false; +} + +static AudioChannelLayout* ca_query_layout(struct ao *ao, + AudioDeviceID device, + void *talloc_ctx) +{ + OSStatus err; + uint32_t psize; + AudioChannelLayout *r = NULL; + + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = kAudioDevicePropertyPreferredChannelLayout, + .mScope = kAudioDevicePropertyScopeOutput, + .mElement = kAudioObjectPropertyElementWildcard, + }; + + err = AudioObjectGetPropertyDataSize(device, &p_addr, 0, NULL, &psize); + CHECK_CA_ERROR("could not get device preferred layout (size)"); + + r = talloc_size(talloc_ctx, psize); + + err = AudioObjectGetPropertyData(device, &p_addr, 0, NULL, &psize, r); + CHECK_CA_ERROR("could not get device preferred layout (get)"); + +coreaudio_error: + return r; +} + +static AudioChannelLayout* ca_query_stereo_layout(struct ao *ao, + AudioDeviceID device, + void *talloc_ctx) +{ + OSStatus err; + const int nch = 2; + uint32_t channels[nch]; + AudioChannelLayout *r = NULL; + + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = kAudioDevicePropertyPreferredChannelsForStereo, + .mScope = kAudioDevicePropertyScopeOutput, + .mElement = kAudioObjectPropertyElementWildcard, + }; + + uint32_t psize = sizeof(channels); + err = AudioObjectGetPropertyData(device, &p_addr, 0, NULL, &psize, channels); + CHECK_CA_ERROR("could not get device preferred stereo layout"); + + psize = sizeof(AudioChannelLayout) + nch * sizeof(AudioChannelDescription); + r = talloc_zero_size(talloc_ctx, psize); + r->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions; + r->mNumberChannelDescriptions = nch; + + AudioChannelDescription desc = {0}; + desc.mChannelFlags = kAudioChannelFlags_AllOff; + + for(int i = 0; i < nch; i++) { + desc.mChannelLabel = channels[i]; + r->mChannelDescriptions[i] = desc; + } + +coreaudio_error: + return r; +} + +static void ca_retrieve_layouts(struct ao *ao, struct mp_chmap_sel *s, + AudioDeviceID device) +{ + void *ta_ctx = talloc_new(NULL); + struct mp_chmap chmap; + + AudioChannelLayout *ml = ca_query_layout(ao, device, ta_ctx); + if (ml && ca_layout_to_mp_chmap(ao, ml, &chmap)) + mp_chmap_sel_add_map(s, &chmap); + + AudioChannelLayout *sl = ca_query_stereo_layout(ao, device, ta_ctx); + if (sl && ca_layout_to_mp_chmap(ao, sl, &chmap)) + mp_chmap_sel_add_map(s, &chmap); + + talloc_free(ta_ctx); +} + +bool ca_init_chmap(struct ao *ao, AudioDeviceID device) +{ + struct mp_chmap_sel chmap_sel = {0}; + ca_retrieve_layouts(ao, &chmap_sel, device); + + if (!chmap_sel.num_chmaps) + mp_chmap_sel_add_map(&chmap_sel, &(struct mp_chmap)MP_CHMAP_INIT_STEREO); + + mp_chmap_sel_add_map(&chmap_sel, &(struct mp_chmap)MP_CHMAP_INIT_MONO); + + if (!ao_chmap_sel_adjust(ao, &chmap_sel, &ao->channels)) { + MP_ERR(ao, "could not select a suitable channel map among the " + "hardware supported ones. Make sure to configure your " + "output device correctly in 'Audio MIDI Setup.app'\n"); + return false; + } + return true; +} + +void ca_get_active_chmap(struct ao *ao, AudioDeviceID device, int channel_count, + struct mp_chmap *out_map) +{ + // Apparently, we have to guess by looking back at the supported layouts, + // and I haven't found a property that retrieves the actual currently + // active channel layout. + + struct mp_chmap_sel chmap_sel = {0}; + ca_retrieve_layouts(ao, &chmap_sel, device); + + // Use any exact match. + for (int n = 0; n < chmap_sel.num_chmaps; n++) { + if (chmap_sel.chmaps[n].num == channel_count) { + MP_VERBOSE(ao, "mismatching channels - fallback #%d\n", n); + *out_map = chmap_sel.chmaps[n]; + return; + } + } + + // Fall back to stereo or mono, and fill the rest with silence. (We don't + // know what the device expects. We could use a larger default layout here, + // but let's not.) + mp_chmap_from_channels(out_map, MPMIN(2, channel_count)); + out_map->num = channel_count; + for (int n = 2; n < out_map->num; n++) + out_map->speaker[n] = MP_SPEAKER_ID_NA; + MP_WARN(ao, "mismatching channels - falling back to %s\n", + mp_chmap_to_str(out_map)); +} +#endif diff --git a/audio/out/ao_coreaudio_chmap.h b/audio/out/ao_coreaudio_chmap.h new file mode 100644 index 0000000..b6d160c --- /dev/null +++ b/audio/out/ao_coreaudio_chmap.h @@ -0,0 +1,35 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_COREAUDIO_CHMAP_H +#define MPV_COREAUDIO_CHMAP_H + +#include <AudioToolbox/AudioToolbox.h> + +#include "config.h" + +struct mp_chmap; + +int ca_label_to_mp_speaker_id(AudioChannelLabel label); + +#if HAVE_COREAUDIO +bool ca_init_chmap(struct ao *ao, AudioDeviceID device); +void ca_get_active_chmap(struct ao *ao, AudioDeviceID device, int channel_count, + struct mp_chmap *out_map); +#endif + +#endif diff --git a/audio/out/ao_coreaudio_exclusive.c b/audio/out/ao_coreaudio_exclusive.c new file mode 100644 index 0000000..e24f791 --- /dev/null +++ b/audio/out/ao_coreaudio_exclusive.c @@ -0,0 +1,472 @@ +/* + * CoreAudio audio output driver for Mac OS X + * + * original copyright (C) Timothy J. Wood - Aug 2000 + * ported to MPlayer libao2 by Dan Christiansen + * + * Chris Roccati + * Stefano Pigozzi + * + * The S/PDIF part of the code is based on the auhal audio output + * module from VideoLAN: + * Copyright (c) 2006 Derk-Jan Hartman <hartman at videolan dot org> + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +/* + * The MacOS X CoreAudio framework doesn't mesh as simply as some + * simpler frameworks do. This is due to the fact that CoreAudio pulls + * audio samples rather than having them pushed at it (which is nice + * when you are wanting to do good buffering of audio). + */ + +#include <stdatomic.h> + +#include <CoreAudio/HostTime.h> + +#include <libavutil/intreadwrite.h> +#include <libavutil/intfloat.h> + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" +#include "osdep/timer.h" +#include "options/m_option.h" +#include "common/msg.h" +#include "audio/out/ao_coreaudio_chmap.h" +#include "audio/out/ao_coreaudio_properties.h" +#include "audio/out/ao_coreaudio_utils.h" + +struct priv { + AudioDeviceID device; // selected device + + bool paused; + + // audio render callback + AudioDeviceIOProcID render_cb; + + // pid set for hog mode, (-1) means that hog mode on the device was + // released. hog mode is exclusive access to a device + pid_t hog_pid; + + AudioStreamID stream; + + // stream index in an AudioBufferList + int stream_idx; + + // format we changed the stream to, and the original format to restore + AudioStreamBasicDescription stream_asbd; + AudioStreamBasicDescription original_asbd; + + // Output s16 physical format, float32 virtual format, ac3/dts mpv format + bool spdif_hack; + + bool changed_mixing; + + atomic_bool reload_requested; + + uint64_t hw_latency_ns; +}; + +static OSStatus property_listener_cb( + AudioObjectID object, uint32_t n_addresses, + const AudioObjectPropertyAddress addresses[], + void *data) +{ + struct ao *ao = data; + struct priv *p = ao->priv; + + // Check whether we need to reset the compressed output stream. + AudioStreamBasicDescription f; + OSErr err = CA_GET(p->stream, kAudioStreamPropertyVirtualFormat, &f); + CHECK_CA_WARN("could not get stream format"); + if (err != noErr || !ca_asbd_equals(&p->stream_asbd, &f)) { + if (atomic_compare_exchange_strong(&p->reload_requested, + &(bool){false}, true)) + { + ao_request_reload(ao); + MP_INFO(ao, "Stream format changed! Reloading.\n"); + } + } + + return noErr; +} + +static OSStatus enable_property_listener(struct ao *ao, bool enabled) +{ + struct priv *p = ao->priv; + + uint32_t selectors[] = {kAudioDevicePropertyDeviceHasChanged, + kAudioHardwarePropertyDevices}; + AudioDeviceID devs[] = {p->device, + kAudioObjectSystemObject}; + assert(MP_ARRAY_SIZE(selectors) == MP_ARRAY_SIZE(devs)); + + OSStatus status = noErr; + for (int n = 0; n < MP_ARRAY_SIZE(devs); n++) { + AudioObjectPropertyAddress addr = { + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster, + .mSelector = selectors[n], + }; + AudioDeviceID device = devs[n]; + + OSStatus status2; + if (enabled) { + status2 = AudioObjectAddPropertyListener( + device, &addr, property_listener_cb, ao); + } else { + status2 = AudioObjectRemovePropertyListener( + device, &addr, property_listener_cb, ao); + } + if (status == noErr) + status = status2; + } + + return status; +} + +// This is a hack for passing through AC3/DTS on drivers which don't support it. +// The goal is to have the driver output the AC3 data bitexact, so basically we +// feed it float data by converting the AC3 data to float in the reverse way we +// assume the driver outputs it. +// Input: data_as_int16[0..samples] +// Output: data_as_float[0..samples] +// The conversion is done in-place. +static void bad_hack_mygodwhy(char *data, int samples) +{ + // In reverse, so we can do it in-place. + for (int n = samples - 1; n >= 0; n--) { + int16_t val = AV_RN16(data + n * 2); + float fval = val / (float)(1 << 15); + uint32_t ival = av_float2int(fval); + AV_WN32(data + n * 4, ival); + } +} + +static OSStatus render_cb_compressed( + AudioDeviceID device, const AudioTimeStamp *ts, + const void *in_data, const AudioTimeStamp *in_ts, + AudioBufferList *out_data, const AudioTimeStamp *out_ts, void *ctx) +{ + struct ao *ao = ctx; + struct priv *p = ao->priv; + AudioBuffer buf = out_data->mBuffers[p->stream_idx]; + int requested = buf.mDataByteSize; + int sstride = p->spdif_hack ? 4 * ao->channels.num : ao->sstride; + + int pseudo_frames = requested / sstride; + + // we expect the callback to read full frames, which are aligned accordingly + if (pseudo_frames * sstride != requested) { + MP_ERR(ao, "Unsupported unaligned read of %d bytes.\n", requested); + return kAudioHardwareUnspecifiedError; + } + + int64_t end = mp_time_ns(); + end += p->hw_latency_ns + ca_get_latency(ts) + + ca_frames_to_ns(ao, pseudo_frames); + + ao_read_data(ao, &buf.mData, pseudo_frames, end); + + if (p->spdif_hack) + bad_hack_mygodwhy(buf.mData, pseudo_frames * ao->channels.num); + + return noErr; +} + +// Apparently, audio devices can have multiple sub-streams. It's not clear to +// me what devices with multiple streams actually do. So only select the first +// one that fulfills some minimum requirements. +// If this is not sufficient, we could duplicate the device list entries for +// each sub-stream, and make it explicit. +static int select_stream(struct ao *ao) +{ + struct priv *p = ao->priv; + + AudioStreamID *streams; + size_t n_streams; + OSStatus err; + + /* Get a list of all the streams on this device. */ + err = CA_GET_ARY_O(p->device, kAudioDevicePropertyStreams, + &streams, &n_streams); + CHECK_CA_ERROR("could not get number of streams"); + for (int i = 0; i < n_streams; i++) { + uint32_t direction; + err = CA_GET(streams[i], kAudioStreamPropertyDirection, &direction); + CHECK_CA_WARN("could not get stream direction"); + if (err == noErr && direction != 0) { + MP_VERBOSE(ao, "Substream %d is not an output stream.\n", i); + continue; + } + + if (af_fmt_is_pcm(ao->format) || p->spdif_hack || + ca_stream_supports_compressed(ao, streams[i])) + { + MP_VERBOSE(ao, "Using substream %d/%zd.\n", i, n_streams); + p->stream = streams[i]; + p->stream_idx = i; + break; + } + } + + talloc_free(streams); + + if (p->stream_idx < 0) { + MP_ERR(ao, "No useable substream found.\n"); + goto coreaudio_error; + } + + return 0; + +coreaudio_error: + return -1; +} + +static int find_best_format(struct ao *ao, AudioStreamBasicDescription *out_fmt) +{ + struct priv *p = ao->priv; + + // Build ASBD for the input format + AudioStreamBasicDescription asbd; + ca_fill_asbd(ao, &asbd); + ca_print_asbd(ao, "our format:", &asbd); + + *out_fmt = (AudioStreamBasicDescription){0}; + + AudioStreamRangedDescription *formats; + size_t n_formats; + OSStatus err; + + err = CA_GET_ARY(p->stream, kAudioStreamPropertyAvailablePhysicalFormats, + &formats, &n_formats); + CHECK_CA_ERROR("could not get number of stream formats"); + + for (int j = 0; j < n_formats; j++) { + AudioStreamBasicDescription *stream_asbd = &formats[j].mFormat; + + ca_print_asbd(ao, "- ", stream_asbd); + + if (!out_fmt->mFormatID || ca_asbd_is_better(&asbd, out_fmt, stream_asbd)) + *out_fmt = *stream_asbd; + } + + talloc_free(formats); + + if (!out_fmt->mFormatID) { + MP_ERR(ao, "no format found\n"); + return -1; + } + + return 0; +coreaudio_error: + return -1; +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + int original_format = ao->format; + + OSStatus err = ca_select_device(ao, ao->device, &p->device); + CHECK_CA_ERROR_L(coreaudio_error_nounlock, "failed to select device"); + + ao->format = af_fmt_from_planar(ao->format); + + if (!af_fmt_is_pcm(ao->format) && !af_fmt_is_spdif(ao->format)) { + MP_ERR(ao, "Unsupported format.\n"); + goto coreaudio_error_nounlock; + } + + if (af_fmt_is_pcm(ao->format)) + p->spdif_hack = false; + + if (p->spdif_hack) { + if (af_fmt_to_bytes(ao->format) != 2) { + MP_ERR(ao, "HD formats not supported with spdif hack.\n"); + goto coreaudio_error_nounlock; + } + // Let the pure evil begin! + ao->format = AF_FORMAT_S16; + } + + uint32_t is_alive = 1; + err = CA_GET(p->device, kAudioDevicePropertyDeviceIsAlive, &is_alive); + CHECK_CA_WARN("could not check whether device is alive"); + + if (!is_alive) + MP_WARN(ao, "device is not alive\n"); + + err = ca_lock_device(p->device, &p->hog_pid); + CHECK_CA_WARN("failed to set hogmode"); + + err = ca_disable_mixing(ao, p->device, &p->changed_mixing); + CHECK_CA_WARN("failed to disable mixing"); + + if (select_stream(ao) < 0) + goto coreaudio_error; + + AudioStreamBasicDescription hwfmt; + if (find_best_format(ao, &hwfmt) < 0) + goto coreaudio_error; + + err = CA_GET(p->stream, kAudioStreamPropertyPhysicalFormat, + &p->original_asbd); + CHECK_CA_ERROR("could not get stream's original physical format"); + + // Even if changing the physical format fails, we can try using the current + // virtual format. + ca_change_physical_format_sync(ao, p->stream, hwfmt); + + if (!ca_init_chmap(ao, p->device)) + goto coreaudio_error; + + err = CA_GET(p->stream, kAudioStreamPropertyVirtualFormat, &p->stream_asbd); + CHECK_CA_ERROR("could not get stream's virtual format"); + + ca_print_asbd(ao, "virtual format", &p->stream_asbd); + + if (p->stream_asbd.mChannelsPerFrame > MP_NUM_CHANNELS) { + MP_ERR(ao, "unsupported number of channels: %d > %d.\n", + p->stream_asbd.mChannelsPerFrame, MP_NUM_CHANNELS); + goto coreaudio_error; + } + + int new_format = ca_asbd_to_mp_format(&p->stream_asbd); + + // If both old and new formats are spdif, avoid changing it due to the + // imperfect mapping between mp and CA formats. + if (!(af_fmt_is_spdif(ao->format) && af_fmt_is_spdif(new_format))) + ao->format = new_format; + + if (!ao->format || af_fmt_is_planar(ao->format)) { + MP_ERR(ao, "hardware format not supported\n"); + goto coreaudio_error; + } + + ao->samplerate = p->stream_asbd.mSampleRate; + + if (ao->channels.num != p->stream_asbd.mChannelsPerFrame) { + ca_get_active_chmap(ao, p->device, p->stream_asbd.mChannelsPerFrame, + &ao->channels); + } + if (!ao->channels.num) { + MP_ERR(ao, "number of channels changed, and unknown channel layout!\n"); + goto coreaudio_error; + } + + if (p->spdif_hack) { + AudioStreamBasicDescription physical_format = {0}; + err = CA_GET(p->stream, kAudioStreamPropertyPhysicalFormat, + &physical_format); + CHECK_CA_ERROR("could not get stream's physical format"); + int ph_format = ca_asbd_to_mp_format(&physical_format); + if (ao->format != AF_FORMAT_FLOAT || ph_format != AF_FORMAT_S16) { + MP_ERR(ao, "Wrong parameters for spdif hack (%d / %d)\n", + ao->format, ph_format); + } + ao->format = original_format; // pretend AC3 or DTS *evil laughter* + MP_WARN(ao, "Using spdif passthrough hack. This could produce noise.\n"); + } + + p->hw_latency_ns = ca_get_device_latency_ns(ao, p->device); + MP_VERBOSE(ao, "base latency: %lld nanoseconds\n", p->hw_latency_ns); + + err = enable_property_listener(ao, true); + CHECK_CA_ERROR("cannot install format change listener during init"); + + err = AudioDeviceCreateIOProcID(p->device, + (AudioDeviceIOProc)render_cb_compressed, + (void *)ao, + &p->render_cb); + CHECK_CA_ERROR("failed to register audio render callback"); + + return CONTROL_TRUE; + +coreaudio_error: + err = enable_property_listener(ao, false); + CHECK_CA_WARN("can't remove format change listener"); + err = ca_unlock_device(p->device, &p->hog_pid); + CHECK_CA_WARN("can't release hog mode"); +coreaudio_error_nounlock: + return CONTROL_ERROR; +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + OSStatus err = noErr; + + err = enable_property_listener(ao, false); + CHECK_CA_WARN("can't remove device listener, this may cause a crash"); + + err = AudioDeviceStop(p->device, p->render_cb); + CHECK_CA_WARN("failed to stop audio device"); + + err = AudioDeviceDestroyIOProcID(p->device, p->render_cb); + CHECK_CA_WARN("failed to remove device render callback"); + + if (!ca_change_physical_format_sync(ao, p->stream, p->original_asbd)) + MP_WARN(ao, "can't revert to original device format\n"); + + err = ca_enable_mixing(ao, p->device, p->changed_mixing); + CHECK_CA_WARN("can't re-enable mixing"); + + err = ca_unlock_device(p->device, &p->hog_pid); + CHECK_CA_WARN("can't release hog mode"); +} + +static void audio_pause(struct ao *ao) +{ + struct priv *p = ao->priv; + + OSStatus err = AudioDeviceStop(p->device, p->render_cb); + CHECK_CA_WARN("can't stop audio device"); +} + +static void audio_resume(struct ao *ao) +{ + struct priv *p = ao->priv; + + OSStatus err = AudioDeviceStart(p->device, p->render_cb); + CHECK_CA_WARN("can't start audio device"); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_coreaudio_exclusive = { + .description = "CoreAudio Exclusive Mode", + .name = "coreaudio_exclusive", + .uninit = uninit, + .init = init, + .reset = audio_pause, + .start = audio_resume, + .list_devs = ca_get_device_list, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv){ + .hog_pid = -1, + .stream = 0, + .stream_idx = -1, + .changed_mixing = false, + }, + .options = (const struct m_option[]){ + {"spdif-hack", OPT_BOOL(spdif_hack)}, + {0} + }, + .options_prefix = "coreaudio", +}; diff --git a/audio/out/ao_coreaudio_properties.c b/audio/out/ao_coreaudio_properties.c new file mode 100644 index 0000000..e25170a --- /dev/null +++ b/audio/out/ao_coreaudio_properties.c @@ -0,0 +1,103 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +/* + * Abstractions on the CoreAudio API to make property setting/getting suck less +*/ + +#include "audio/out/ao_coreaudio_properties.h" +#include "audio/out/ao_coreaudio_utils.h" +#include "mpv_talloc.h" + +OSStatus ca_get(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t size, void *data) +{ + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = selector, + .mScope = scope, + .mElement = kAudioObjectPropertyElementMaster, + }; + + return AudioObjectGetPropertyData(id, &p_addr, 0, NULL, &size, data); +} + +OSStatus ca_set(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t size, void *data) +{ + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = selector, + .mScope = scope, + .mElement = kAudioObjectPropertyElementMaster, + }; + + return AudioObjectSetPropertyData(id, &p_addr, 0, NULL, size, data); +} + +OSStatus ca_get_ary(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t element_size, void **data, size_t *elements) +{ + OSStatus err; + uint32_t p_size; + + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = selector, + .mScope = scope, + .mElement = kAudioObjectPropertyElementMaster, + }; + + err = AudioObjectGetPropertyDataSize(id, &p_addr, 0, NULL, &p_size); + CHECK_CA_ERROR_SILENT_L(coreaudio_error); + + *data = talloc_zero_size(NULL, p_size); + *elements = p_size / element_size; + + err = ca_get(id, scope, selector, p_size, *data); + CHECK_CA_ERROR_SILENT_L(coreaudio_error_free); + + return err; +coreaudio_error_free: + talloc_free(*data); +coreaudio_error: + return err; +} + +OSStatus ca_get_str(AudioObjectID id, ca_scope scope, ca_sel selector, + char **data) +{ + CFStringRef string; + OSStatus err = + ca_get(id, scope, selector, sizeof(CFStringRef), (void **)&string); + CHECK_CA_ERROR_SILENT_L(coreaudio_error); + + *data = cfstr_get_cstr(string); + CFRelease(string); +coreaudio_error: + return err; +} + +Boolean ca_settable(AudioObjectID id, ca_scope scope, ca_sel selector, + Boolean *data) +{ + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = selector, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster, + }; + + return AudioObjectIsPropertySettable(id, &p_addr, data); +} + diff --git a/audio/out/ao_coreaudio_properties.h b/audio/out/ao_coreaudio_properties.h new file mode 100644 index 0000000..f293968 --- /dev/null +++ b/audio/out/ao_coreaudio_properties.h @@ -0,0 +1,61 @@ +/* + * This file is part of mpv. + * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_COREAUDIO_PROPERTIES_H +#define MPV_COREAUDIO_PROPERTIES_H + +#include <AudioToolbox/AudioToolbox.h> + +#include "internal.h" + +// CoreAudio names are way too verbose +#define ca_sel AudioObjectPropertySelector +#define ca_scope AudioObjectPropertyScope +#define CA_GLOBAL kAudioObjectPropertyScopeGlobal +#define CA_OUTPUT kAudioDevicePropertyScopeOutput + +OSStatus ca_get(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t size, void *data); + +OSStatus ca_set(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t size, void *data); + +#define CA_GET(id, sel, data) ca_get(id, CA_GLOBAL, sel, sizeof(*(data)), data) +#define CA_SET(id, sel, data) ca_set(id, CA_GLOBAL, sel, sizeof(*(data)), data) +#define CA_GET_O(id, sel, data) ca_get(id, CA_OUTPUT, sel, sizeof(*(data)), data) + +OSStatus ca_get_ary(AudioObjectID id, ca_scope scope, ca_sel selector, + uint32_t element_size, void **data, size_t *elements); + +#define CA_GET_ARY(id, sel, data, elements) \ + ca_get_ary(id, CA_GLOBAL, sel, sizeof(**(data)), (void **)data, elements) + +#define CA_GET_ARY_O(id, sel, data, elements) \ + ca_get_ary(id, CA_OUTPUT, sel, sizeof(**(data)), (void **)data, elements) + +OSStatus ca_get_str(AudioObjectID id, ca_scope scope,ca_sel selector, + char **data); + +#define CA_GET_STR(id, sel, data) ca_get_str(id, CA_GLOBAL, sel, data) + +Boolean ca_settable(AudioObjectID id, ca_scope scope, ca_sel selector, + Boolean *data); + +#define CA_SETTABLE(id, sel, data) ca_settable(id, CA_GLOBAL, sel, data) + +#endif /* MPV_COREAUDIO_PROPERTIES_H */ diff --git a/audio/out/ao_coreaudio_utils.c b/audio/out/ao_coreaudio_utils.c new file mode 100644 index 0000000..14db8e3 --- /dev/null +++ b/audio/out/ao_coreaudio_utils.c @@ -0,0 +1,539 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +/* + * This file contains functions interacting with the CoreAudio framework + * that are not specific to the AUHAL. These are split in a separate file for + * the sake of readability. In the future the could be used by other AOs based + * on CoreAudio but not the AUHAL (such as using AudioQueue services). + */ + +#include "audio/out/ao_coreaudio_utils.h" +#include "osdep/timer.h" +#include "osdep/endian.h" +#include "osdep/semaphore.h" +#include "audio/format.h" + +#if HAVE_COREAUDIO +#include "audio/out/ao_coreaudio_properties.h" +#include <CoreAudio/HostTime.h> +#else +#include <mach/mach_time.h> +#endif + +#if HAVE_COREAUDIO +static bool ca_is_output_device(struct ao *ao, AudioDeviceID dev) +{ + size_t n_buffers; + AudioBufferList *buffers; + const ca_scope scope = kAudioDevicePropertyStreamConfiguration; + OSStatus err = CA_GET_ARY_O(dev, scope, &buffers, &n_buffers); + if (err != noErr) + return false; + talloc_free(buffers); + return n_buffers > 0; +} + +void ca_get_device_list(struct ao *ao, struct ao_device_list *list) +{ + AudioDeviceID *devs; + size_t n_devs; + OSStatus err = + CA_GET_ARY(kAudioObjectSystemObject, kAudioHardwarePropertyDevices, + &devs, &n_devs); + CHECK_CA_ERROR("Failed to get list of output devices."); + for (int i = 0; i < n_devs; i++) { + if (!ca_is_output_device(ao, devs[i])) + continue; + void *ta_ctx = talloc_new(NULL); + char *name; + char *desc; + err = CA_GET_STR(devs[i], kAudioDevicePropertyDeviceUID, &name); + if (err != noErr) { + MP_VERBOSE(ao, "skipping device %d, which has no UID\n", i); + talloc_free(ta_ctx); + continue; + } + talloc_steal(ta_ctx, name); + err = CA_GET_STR(devs[i], kAudioObjectPropertyName, &desc); + if (err != noErr) + desc = talloc_strdup(NULL, "Unknown"); + talloc_steal(ta_ctx, desc); + ao_device_list_add(list, ao, &(struct ao_device_desc){name, desc}); + talloc_free(ta_ctx); + } + talloc_free(devs); +coreaudio_error: + return; +} + +OSStatus ca_select_device(struct ao *ao, char* name, AudioDeviceID *device) +{ + OSStatus err = noErr; + *device = kAudioObjectUnknown; + + if (name && name[0]) { + CFStringRef uid = cfstr_from_cstr(name); + AudioValueTranslation v = (AudioValueTranslation) { + .mInputData = &uid, + .mInputDataSize = sizeof(CFStringRef), + .mOutputData = device, + .mOutputDataSize = sizeof(*device), + }; + uint32_t size = sizeof(AudioValueTranslation); + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = kAudioHardwarePropertyDeviceForUID, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster, + }; + err = AudioObjectGetPropertyData( + kAudioObjectSystemObject, &p_addr, 0, 0, &size, &v); + CFRelease(uid); + CHECK_CA_ERROR("unable to query for device UID"); + + uint32_t is_alive = 1; + err = CA_GET(*device, kAudioDevicePropertyDeviceIsAlive, &is_alive); + CHECK_CA_ERROR("could not check whether device is alive (invalid device?)"); + + if (!is_alive) + MP_WARN(ao, "device is not alive!\n"); + } else { + // device not set by user, get the default one + err = CA_GET(kAudioObjectSystemObject, + kAudioHardwarePropertyDefaultOutputDevice, + device); + CHECK_CA_ERROR("could not get default audio device"); + } + + if (mp_msg_test(ao->log, MSGL_V)) { + char *desc; + OSStatus err2 = CA_GET_STR(*device, kAudioObjectPropertyName, &desc); + if (err2 == noErr) { + MP_VERBOSE(ao, "selected audio output device: %s (%" PRIu32 ")\n", + desc, *device); + talloc_free(desc); + } + } + +coreaudio_error: + return err; +} +#endif + +bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message) +{ + if (code == noErr) return true; + + mp_msg(ao->log, level, "%s (%s/%d)\n", message, mp_tag_str(code), (int)code); + + return false; +} + +static void ca_fill_asbd_raw(AudioStreamBasicDescription *asbd, int mp_format, + int samplerate, int num_channels) +{ + asbd->mSampleRate = samplerate; + // Set "AC3" for other spdif formats too - unknown if that works. + asbd->mFormatID = af_fmt_is_spdif(mp_format) ? + kAudioFormat60958AC3 : + kAudioFormatLinearPCM; + asbd->mChannelsPerFrame = num_channels; + asbd->mBitsPerChannel = af_fmt_to_bytes(mp_format) * 8; + asbd->mFormatFlags = kAudioFormatFlagIsPacked; + + int channels_per_buffer = num_channels; + if (af_fmt_is_planar(mp_format)) { + asbd->mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + channels_per_buffer = 1; + } + + if (af_fmt_is_float(mp_format)) { + asbd->mFormatFlags |= kAudioFormatFlagIsFloat; + } else if (!af_fmt_is_unsigned(mp_format)) { + asbd->mFormatFlags |= kAudioFormatFlagIsSignedInteger; + } + + if (BYTE_ORDER == BIG_ENDIAN) + asbd->mFormatFlags |= kAudioFormatFlagIsBigEndian; + + asbd->mFramesPerPacket = 1; + asbd->mBytesPerPacket = asbd->mBytesPerFrame = + asbd->mFramesPerPacket * channels_per_buffer * + (asbd->mBitsPerChannel / 8); +} + +void ca_fill_asbd(struct ao *ao, AudioStreamBasicDescription *asbd) +{ + ca_fill_asbd_raw(asbd, ao->format, ao->samplerate, ao->channels.num); +} + +bool ca_formatid_is_compressed(uint32_t formatid) +{ + switch (formatid) + case 'IAC3': + case 'iac3': + case kAudioFormat60958AC3: + case kAudioFormatAC3: + return true; + return false; +} + +// This might be wrong, but for now it's sufficient for us. +static uint32_t ca_normalize_formatid(uint32_t formatID) +{ + return ca_formatid_is_compressed(formatID) ? kAudioFormat60958AC3 : formatID; +} + +bool ca_asbd_equals(const AudioStreamBasicDescription *a, + const AudioStreamBasicDescription *b) +{ + int flags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsFloat | + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian; + bool spdif = ca_formatid_is_compressed(a->mFormatID) && + ca_formatid_is_compressed(b->mFormatID); + + return (a->mFormatFlags & flags) == (b->mFormatFlags & flags) && + a->mBitsPerChannel == b->mBitsPerChannel && + ca_normalize_formatid(a->mFormatID) == + ca_normalize_formatid(b->mFormatID) && + (spdif || a->mBytesPerPacket == b->mBytesPerPacket) && + (spdif || a->mChannelsPerFrame == b->mChannelsPerFrame) && + a->mSampleRate == b->mSampleRate; +} + +// Return the AF_FORMAT_* (AF_FORMAT_S16 etc.) corresponding to the asbd. +int ca_asbd_to_mp_format(const AudioStreamBasicDescription *asbd) +{ + for (int fmt = 1; fmt < AF_FORMAT_COUNT; fmt++) { + AudioStreamBasicDescription mp_asbd = {0}; + ca_fill_asbd_raw(&mp_asbd, fmt, asbd->mSampleRate, asbd->mChannelsPerFrame); + if (ca_asbd_equals(&mp_asbd, asbd)) + return af_fmt_is_spdif(fmt) ? AF_FORMAT_S_AC3 : fmt; + } + return 0; +} + +void ca_print_asbd(struct ao *ao, const char *description, + const AudioStreamBasicDescription *asbd) +{ + uint32_t flags = asbd->mFormatFlags; + char *format = mp_tag_str(asbd->mFormatID); + int mpfmt = ca_asbd_to_mp_format(asbd); + + MP_VERBOSE(ao, + "%s %7.1fHz %" PRIu32 "bit %s " + "[%" PRIu32 "][%" PRIu32 "bpp][%" PRIu32 "fbp]" + "[%" PRIu32 "bpf][%" PRIu32 "ch] " + "%s %s %s%s%s%s (%s)\n", + description, asbd->mSampleRate, asbd->mBitsPerChannel, format, + asbd->mFormatFlags, asbd->mBytesPerPacket, asbd->mFramesPerPacket, + asbd->mBytesPerFrame, asbd->mChannelsPerFrame, + (flags & kAudioFormatFlagIsFloat) ? "float" : "int", + (flags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE", + (flags & kAudioFormatFlagIsSignedInteger) ? "S" : "U", + (flags & kAudioFormatFlagIsPacked) ? " packed" : "", + (flags & kAudioFormatFlagIsAlignedHigh) ? " aligned" : "", + (flags & kAudioFormatFlagIsNonInterleaved) ? " P" : "", + mpfmt ? af_fmt_to_str(mpfmt) : "-"); +} + +// Return whether new is an improvement over old. Assume a higher value means +// better quality, and we always prefer the value closest to the requested one, +// which is still larger than the requested one. +// Equal values prefer the new one (so ca_asbd_is_better() checks other params). +static bool value_is_better(double req, double old, double new) +{ + if (new >= req) { + return old < req || new <= old; + } else { + return old < req && new >= old; + } +} + +// Return whether new is an improvement over old (req is the requested format). +bool ca_asbd_is_better(AudioStreamBasicDescription *req, + AudioStreamBasicDescription *old, + AudioStreamBasicDescription *new) +{ + if (new->mChannelsPerFrame > MP_NUM_CHANNELS) + return false; + if (old->mChannelsPerFrame > MP_NUM_CHANNELS) + return true; + if (req->mFormatID != new->mFormatID) + return false; + if (req->mFormatID != old->mFormatID) + return true; + + if (!value_is_better(req->mBitsPerChannel, old->mBitsPerChannel, + new->mBitsPerChannel)) + return false; + + if (!value_is_better(req->mSampleRate, old->mSampleRate, new->mSampleRate)) + return false; + + if (!value_is_better(req->mChannelsPerFrame, old->mChannelsPerFrame, + new->mChannelsPerFrame)) + return false; + + return true; +} + +int64_t ca_frames_to_ns(struct ao *ao, uint32_t frames) +{ + return MP_TIME_S_TO_NS(frames / (double)ao->samplerate); +} + +int64_t ca_get_latency(const AudioTimeStamp *ts) +{ +#if HAVE_COREAUDIO + uint64_t out = AudioConvertHostTimeToNanos(ts->mHostTime); + uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); + + if (now > out) + return 0; + + return out - now; +#else + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) + mach_timebase_info(&timebase); + + uint64_t out = ts->mHostTime; + uint64_t now = mach_absolute_time(); + + if (now > out) + return 0; + + return (out - now) * timebase.numer / timebase.denom; +#endif +} + +#if HAVE_COREAUDIO +bool ca_stream_supports_compressed(struct ao *ao, AudioStreamID stream) +{ + AudioStreamRangedDescription *formats = NULL; + size_t n_formats; + + OSStatus err = + CA_GET_ARY(stream, kAudioStreamPropertyAvailablePhysicalFormats, + &formats, &n_formats); + + CHECK_CA_ERROR("Could not get number of stream formats."); + + for (int i = 0; i < n_formats; i++) { + AudioStreamBasicDescription asbd = formats[i].mFormat; + + ca_print_asbd(ao, "- ", &asbd); + + if (ca_formatid_is_compressed(asbd.mFormatID)) { + talloc_free(formats); + return true; + } + } + + talloc_free(formats); +coreaudio_error: + return false; +} + +OSStatus ca_lock_device(AudioDeviceID device, pid_t *pid) +{ + *pid = getpid(); + OSStatus err = CA_SET(device, kAudioDevicePropertyHogMode, pid); + if (err != noErr) + *pid = -1; + + return err; +} + +OSStatus ca_unlock_device(AudioDeviceID device, pid_t *pid) +{ + if (*pid == getpid()) { + *pid = -1; + return CA_SET(device, kAudioDevicePropertyHogMode, &pid); + } + return noErr; +} + +static OSStatus ca_change_mixing(struct ao *ao, AudioDeviceID device, + uint32_t val, bool *changed) +{ + *changed = false; + + AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { + .mSelector = kAudioDevicePropertySupportsMixing, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster, + }; + + if (AudioObjectHasProperty(device, &p_addr)) { + OSStatus err; + Boolean writeable = 0; + err = CA_SETTABLE(device, kAudioDevicePropertySupportsMixing, + &writeable); + + if (!CHECK_CA_WARN("can't tell if mixing property is settable")) { + return err; + } + + if (!writeable) + return noErr; + + err = CA_SET(device, kAudioDevicePropertySupportsMixing, &val); + if (err != noErr) + return err; + + if (!CHECK_CA_WARN("can't set mix mode")) { + return err; + } + + *changed = true; + } + + return noErr; +} + +OSStatus ca_disable_mixing(struct ao *ao, AudioDeviceID device, bool *changed) +{ + return ca_change_mixing(ao, device, 0, changed); +} + +OSStatus ca_enable_mixing(struct ao *ao, AudioDeviceID device, bool changed) +{ + if (changed) { + bool dont_care = false; + return ca_change_mixing(ao, device, 1, &dont_care); + } + + return noErr; +} + +int64_t ca_get_device_latency_ns(struct ao *ao, AudioDeviceID device) +{ + uint32_t latency_frames = 0; + uint32_t latency_properties[] = { + kAudioDevicePropertyLatency, + kAudioDevicePropertyBufferFrameSize, + kAudioDevicePropertySafetyOffset, + }; + for (int n = 0; n < MP_ARRAY_SIZE(latency_properties); n++) { + uint32_t temp; + OSStatus err = CA_GET_O(device, latency_properties[n], &temp); + CHECK_CA_WARN("cannot get device latency"); + if (err == noErr) { + latency_frames += temp; + MP_VERBOSE(ao, "Latency property %s: %d frames\n", + mp_tag_str(latency_properties[n]), (int)temp); + } + } + + double sample_rate = ao->samplerate; + OSStatus err = CA_GET_O(device, kAudioDevicePropertyNominalSampleRate, + &sample_rate); + CHECK_CA_WARN("cannot get device sample rate, falling back to AO sample rate!"); + if (err == noErr) { + MP_VERBOSE(ao, "Device sample rate: %f\n", sample_rate); + } + + return MP_TIME_S_TO_NS(latency_frames / sample_rate); +} + +static OSStatus ca_change_format_listener( + AudioObjectID object, uint32_t n_addresses, + const AudioObjectPropertyAddress addresses[], + void *data) +{ + mp_sem_t *sem = data; + mp_sem_post(sem); + return noErr; +} + +bool ca_change_physical_format_sync(struct ao *ao, AudioStreamID stream, + AudioStreamBasicDescription change_format) +{ + OSStatus err = noErr; + bool format_set = false; + + ca_print_asbd(ao, "setting stream physical format:", &change_format); + + sem_t wakeup; + if (mp_sem_init(&wakeup, 0, 0)) { + MP_WARN(ao, "OOM\n"); + return false; + } + + AudioStreamBasicDescription prev_format; + err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format); + CHECK_CA_ERROR("can't get current physical format"); + + ca_print_asbd(ao, "format in use before switching:", &prev_format); + + /* Install the callback. */ + AudioObjectPropertyAddress p_addr = { + .mSelector = kAudioStreamPropertyPhysicalFormat, + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster, + }; + + err = AudioObjectAddPropertyListener(stream, &p_addr, + ca_change_format_listener, + &wakeup); + CHECK_CA_ERROR("can't add property listener during format change"); + + /* Change the format. */ + err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &change_format); + CHECK_CA_WARN("error changing physical format"); + + /* The AudioStreamSetProperty is not only asynchronous, + * it is also not Atomic, in its behaviour. */ + int64_t wait_until = mp_time_ns() + MP_TIME_S_TO_NS(2); + AudioStreamBasicDescription actual_format = {0}; + while (1) { + err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &actual_format); + if (!CHECK_CA_WARN("could not retrieve physical format")) + break; + + format_set = ca_asbd_equals(&change_format, &actual_format); + if (format_set) + break; + + if (mp_sem_timedwait(&wakeup, wait_until)) { + MP_VERBOSE(ao, "reached timeout\n"); + break; + } + } + + ca_print_asbd(ao, "actual format in use:", &actual_format); + + if (!format_set) { + MP_WARN(ao, "changing physical format failed\n"); + // Some drivers just fuck up and get into a broken state. Restore the + // old format in this case. + err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format); + CHECK_CA_WARN("error restoring physical format"); + } + + err = AudioObjectRemovePropertyListener(stream, &p_addr, + ca_change_format_listener, + &wakeup); + CHECK_CA_ERROR("can't remove property listener"); + +coreaudio_error: + mp_sem_destroy(&wakeup); + return format_set; +} +#endif diff --git a/audio/out/ao_coreaudio_utils.h b/audio/out/ao_coreaudio_utils.h new file mode 100644 index 0000000..0e2b8b1 --- /dev/null +++ b/audio/out/ao_coreaudio_utils.h @@ -0,0 +1,79 @@ +/* + * This file is part of mpv. + * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_COREAUDIO_UTILS_H +#define MPV_COREAUDIO_UTILS_H + +#include <AudioToolbox/AudioToolbox.h> +#include <inttypes.h> +#include <stdbool.h> + +#include "config.h" +#include "common/msg.h" +#include "audio/out/ao.h" +#include "internal.h" +#include "osdep/apple_utils.h" + +bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message); + +#define CHECK_CA_ERROR_L(label, message) \ + do { \ + if (!check_ca_st(ao, MSGL_ERR, err, message)) { \ + goto label; \ + } \ + } while (0) + +#define CHECK_CA_ERROR(message) CHECK_CA_ERROR_L(coreaudio_error, message) +#define CHECK_CA_WARN(message) check_ca_st(ao, MSGL_WARN, err, message) + +#define CHECK_CA_ERROR_SILENT_L(label) \ + do { \ + if (err != noErr) goto label; \ + } while (0) + +void ca_get_device_list(struct ao *ao, struct ao_device_list *list); +#if HAVE_COREAUDIO +OSStatus ca_select_device(struct ao *ao, char* name, AudioDeviceID *device); +#endif + +bool ca_formatid_is_compressed(uint32_t formatid); +void ca_fill_asbd(struct ao *ao, AudioStreamBasicDescription *asbd); +void ca_print_asbd(struct ao *ao, const char *description, + const AudioStreamBasicDescription *asbd); +bool ca_asbd_equals(const AudioStreamBasicDescription *a, + const AudioStreamBasicDescription *b); +int ca_asbd_to_mp_format(const AudioStreamBasicDescription *asbd); +bool ca_asbd_is_better(AudioStreamBasicDescription *req, + AudioStreamBasicDescription *old, + AudioStreamBasicDescription *new); + +int64_t ca_frames_to_ns(struct ao *ao, uint32_t frames); +int64_t ca_get_latency(const AudioTimeStamp *ts); + +#if HAVE_COREAUDIO +bool ca_stream_supports_compressed(struct ao *ao, AudioStreamID stream); +OSStatus ca_lock_device(AudioDeviceID device, pid_t *pid); +OSStatus ca_unlock_device(AudioDeviceID device, pid_t *pid); +OSStatus ca_disable_mixing(struct ao *ao, AudioDeviceID device, bool *changed); +OSStatus ca_enable_mixing(struct ao *ao, AudioDeviceID device, bool changed); +int64_t ca_get_device_latency_ns(struct ao *ao, AudioDeviceID device); +bool ca_change_physical_format_sync(struct ao *ao, AudioStreamID stream, + AudioStreamBasicDescription change_format); +#endif + +#endif /* MPV_COREAUDIO_UTILS_H */ diff --git a/audio/out/ao_jack.c b/audio/out/ao_jack.c new file mode 100644 index 0000000..412e91d --- /dev/null +++ b/audio/out/ao_jack.c @@ -0,0 +1,284 @@ +/* + * JACK audio output driver for MPlayer + * + * Copyleft 2001 by Felix Bünemann (atmosfear@users.sf.net) + * and Reimar Döffinger (Reimar.Doeffinger@stud.uni-karlsruhe.de) + * + * Copyleft 2013 by William Light <wrl@illest.net> for the mpv project + * + * 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 <stdatomic.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "config.h" +#include "common/msg.h" + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" +#include "osdep/timer.h" +#include "options/m_config.h" +#include "options/m_option.h" + +#include <jack/jack.h> + +#if !HAVE_GPL +#error GPL only +#endif + +struct jack_opts { + char *port; + char *client_name; + bool connect; + bool autostart; + int stdlayout; +}; + +#define OPT_BASE_STRUCT struct jack_opts +static const struct m_sub_options ao_jack_conf = { + .opts = (const struct m_option[]){ + {"jack-port", OPT_STRING(port)}, + {"jack-name", OPT_STRING(client_name)}, + {"jack-autostart", OPT_BOOL(autostart)}, + {"jack-connect", OPT_BOOL(connect)}, + {"jack-std-channel-layout", OPT_CHOICE(stdlayout, + {"waveext", 0}, {"any", 1})}, + {0} + }, + .defaults = &(const struct jack_opts) { + .client_name = "mpv", + .connect = true, + }, + .size = sizeof(struct jack_opts), +}; + +struct priv { + jack_client_t *client; + + atomic_uint graph_latency_max; + atomic_uint buffer_size; + + int last_chunk; + + int num_ports; + jack_port_t *ports[MP_NUM_CHANNELS]; + + int activated; + + struct jack_opts *opts; +}; + +static int graph_order_cb(void *arg) +{ + struct ao *ao = arg; + struct priv *p = ao->priv; + + jack_latency_range_t jack_latency_range; + jack_port_get_latency_range(p->ports[0], JackPlaybackLatency, + &jack_latency_range); + atomic_store(&p->graph_latency_max, jack_latency_range.max); + + return 0; +} + +static int buffer_size_cb(jack_nframes_t nframes, void *arg) +{ + struct ao *ao = arg; + struct priv *p = ao->priv; + + atomic_store(&p->buffer_size, nframes); + + return 0; +} + +static int process(jack_nframes_t nframes, void *arg) +{ + struct ao *ao = arg; + struct priv *p = ao->priv; + + void *buffers[MP_NUM_CHANNELS]; + + for (int i = 0; i < p->num_ports; i++) + buffers[i] = jack_port_get_buffer(p->ports[i], nframes); + + jack_nframes_t jack_latency = + atomic_load(&p->graph_latency_max) + atomic_load(&p->buffer_size); + + int64_t end_time = mp_time_ns(); + end_time += MP_TIME_S_TO_NS((jack_latency + nframes) / (double)ao->samplerate); + + ao_read_data(ao, buffers, nframes, end_time); + + return 0; +} + +static int +connect_to_outports(struct ao *ao) +{ + struct priv *p = ao->priv; + + char *port_name = (p->opts->port && p->opts->port[0]) ? p->opts->port : NULL; + const char **matching_ports = NULL; + int port_flags = JackPortIsInput; + int i; + + if (!port_name) + port_flags |= JackPortIsPhysical; + + const char *port_type = JACK_DEFAULT_AUDIO_TYPE; // exclude MIDI ports + matching_ports = jack_get_ports(p->client, port_name, port_type, port_flags); + + if (!matching_ports || !matching_ports[0]) { + MP_FATAL(ao, "no ports to connect to\n"); + goto err_get_ports; + } + + for (i = 0; i < p->num_ports && matching_ports[i]; i++) { + if (jack_connect(p->client, jack_port_name(p->ports[i]), + matching_ports[i])) + { + MP_FATAL(ao, "connecting failed\n"); + goto err_connect; + } + } + + free(matching_ports); + return 0; + +err_connect: + free(matching_ports); +err_get_ports: + return -1; +} + +static int +create_ports(struct ao *ao, int nports) +{ + struct priv *p = ao->priv; + char pname[30]; + int i; + + for (i = 0; i < nports; i++) { + snprintf(pname, sizeof(pname), "out_%d", i); + p->ports[i] = jack_port_register(p->client, pname, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + + if (!p->ports[i]) { + MP_FATAL(ao, "not enough ports available\n"); + goto err_port_register; + } + } + + p->num_ports = nports; + return 0; + +err_port_register: + return -1; +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + if (!p->activated) { + p->activated = true; + + if (jack_activate(p->client)) + MP_FATAL(ao, "activate failed\n"); + + if (p->opts->connect) + connect_to_outports(ao); + } +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + struct mp_chmap_sel sel = {0}; + jack_options_t open_options; + + p->opts = mp_get_config_group(ao, ao->global, &ao_jack_conf); + + ao->format = AF_FORMAT_FLOATP; + + switch (p->opts->stdlayout) { + case 0: + mp_chmap_sel_add_waveext(&sel); + break; + + default: + mp_chmap_sel_add_any(&sel); + } + + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) + goto err_chmap; + + open_options = JackNullOption; + if (!p->opts->autostart) + open_options |= JackNoStartServer; + + p->client = jack_client_open(p->opts->client_name, open_options, NULL); + if (!p->client) { + MP_FATAL(ao, "cannot open server\n"); + goto err_client_open; + } + + if (create_ports(ao, ao->channels.num)) + goto err_create_ports; + + jack_set_process_callback(p->client, process, ao); + + ao->samplerate = jack_get_sample_rate(p->client); + // The actual device buffer can change, but this is enough for pre-buffer + ao->device_buffer = jack_get_buffer_size(p->client); + + jack_set_buffer_size_callback(p->client, buffer_size_cb, ao); + jack_set_graph_order_callback(p->client, graph_order_cb, ao); + + if (!ao_chmap_sel_get_def(ao, &sel, &ao->channels, p->num_ports)) + goto err_chmap_sel_get_def; + + return 0; + +err_chmap_sel_get_def: +err_create_ports: + jack_client_close(p->client); +err_client_open: +err_chmap: + return -1; +} + +// close audio device +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + + jack_client_close(p->client); +} + +const struct ao_driver audio_out_jack = { + .description = "JACK audio output", + .name = "jack", + .init = init, + .uninit = uninit, + .start = start, + .priv_size = sizeof(struct priv), + .global_opts = &ao_jack_conf, +}; diff --git a/audio/out/ao_lavc.c b/audio/out/ao_lavc.c new file mode 100644 index 0000000..163fdca --- /dev/null +++ b/audio/out/ao_lavc.c @@ -0,0 +1,337 @@ +/* + * audio encoding using libavformat + * + * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org> + * NOTE: this file is partially based on ao_pcm.c by Atmosfear + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <assert.h> +#include <limits.h> + +#include <libavutil/common.h> + +#include "config.h" +#include "options/options.h" +#include "common/common.h" +#include "audio/aframe.h" +#include "audio/chmap_avchannel.h" +#include "audio/format.h" +#include "audio/fmt-conversion.h" +#include "filters/filter_internal.h" +#include "filters/f_utils.h" +#include "mpv_talloc.h" +#include "ao.h" +#include "internal.h" +#include "common/msg.h" + +#include "common/encode_lavc.h" + +struct priv { + struct encoder_context *enc; + + int pcmhack; + int aframesize; + int framecount; + int64_t lastpts; + int sample_size; + double expected_next_pts; + struct mp_filter *filter_root; + struct mp_filter *fix_frame_size; + + AVRational worst_time_base; + + bool shutdown; +}; + +static bool write_frame(struct ao *ao, struct mp_frame frame); + +static bool supports_format(const AVCodec *codec, int format) +{ + for (const enum AVSampleFormat *sampleformat = codec->sample_fmts; + sampleformat && *sampleformat != AV_SAMPLE_FMT_NONE; + sampleformat++) + { + if (af_from_avformat(*sampleformat) == format) + return true; + } + return false; +} + +static void select_format(struct ao *ao, const AVCodec *codec) +{ + int formats[AF_FORMAT_COUNT + 1]; + af_get_best_sample_formats(ao->format, formats); + + for (int n = 0; formats[n]; n++) { + if (supports_format(codec, formats[n])) { + ao->format = formats[n]; + break; + } + } +} + +static void on_ready(void *ptr) +{ + struct ao *ao = ptr; + struct priv *ac = ao->priv; + + ac->worst_time_base = encoder_get_mux_timebase_unlocked(ac->enc); + + ao_add_events(ao, AO_EVENT_INITIAL_UNBLOCK); +} + +// open & setup audio device +static int init(struct ao *ao) +{ + struct priv *ac = ao->priv; + + ac->enc = encoder_context_alloc(ao->encode_lavc_ctx, STREAM_AUDIO, ao->log); + if (!ac->enc) + return -1; + talloc_steal(ac, ac->enc); + + AVCodecContext *encoder = ac->enc->encoder; + const AVCodec *codec = encoder->codec; + + int samplerate = af_select_best_samplerate(ao->samplerate, + codec->supported_samplerates); + if (samplerate > 0) + ao->samplerate = samplerate; + + encoder->time_base.num = 1; + encoder->time_base.den = ao->samplerate; + + encoder->sample_rate = ao->samplerate; + + struct mp_chmap_sel sel = {0}; + mp_chmap_sel_add_any(&sel); + if (!ao_chmap_sel_adjust2(ao, &sel, &ao->channels, false)) + goto fail; + mp_chmap_reorder_to_lavc(&ao->channels); + +#if !HAVE_AV_CHANNEL_LAYOUT + encoder->channels = ao->channels.num; + encoder->channel_layout = mp_chmap_to_lavc(&ao->channels); +#else + mp_chmap_to_av_layout(&encoder->ch_layout, &ao->channels); +#endif + + encoder->sample_fmt = AV_SAMPLE_FMT_NONE; + + select_format(ao, codec); + + ac->sample_size = af_fmt_to_bytes(ao->format); + encoder->sample_fmt = af_to_avformat(ao->format); + encoder->bits_per_raw_sample = ac->sample_size * 8; + + if (!encoder_init_codec_and_muxer(ac->enc, on_ready, ao)) + goto fail; + + ac->pcmhack = 0; + if (encoder->frame_size <= 1) + ac->pcmhack = av_get_bits_per_sample(encoder->codec_id) / 8; + + if (ac->pcmhack) { + ac->aframesize = 16384; // "enough" + } else { + ac->aframesize = encoder->frame_size; + } + + // enough frames for at least 0.25 seconds + ac->framecount = ceil(ao->samplerate * 0.25 / ac->aframesize); + // but at least one! + ac->framecount = MPMAX(ac->framecount, 1); + + ac->lastpts = AV_NOPTS_VALUE; + + ao->untimed = true; + + ao->device_buffer = ac->aframesize * ac->framecount; + + ac->filter_root = mp_filter_create_root(ao->global); + ac->fix_frame_size = mp_fixed_aframe_size_create(ac->filter_root, + ac->aframesize, true); + MP_HANDLE_OOM(ac->fix_frame_size); + + return 0; + +fail: + mp_mutex_unlock(&ao->encode_lavc_ctx->lock); + ac->shutdown = true; + return -1; +} + +// close audio device +static void uninit(struct ao *ao) +{ + struct priv *ac = ao->priv; + + if (!ac->shutdown) { + if (!write_frame(ao, MP_EOF_FRAME)) + MP_WARN(ao, "could not flush last frame\n"); + encoder_encode(ac->enc, NULL); + } + + talloc_free(ac->filter_root); +} + +// must get exactly ac->aframesize amount of data +static void encode(struct ao *ao, struct mp_aframe *af) +{ + struct priv *ac = ao->priv; + AVCodecContext *encoder = ac->enc->encoder; + double outpts = mp_aframe_get_pts(af); + + AVFrame *frame = mp_aframe_to_avframe(af); + MP_HANDLE_OOM(frame); + + frame->pts = rint(outpts * av_q2d(av_inv_q(encoder->time_base))); + + int64_t frame_pts = av_rescale_q(frame->pts, encoder->time_base, + ac->worst_time_base); + if (ac->lastpts != AV_NOPTS_VALUE && frame_pts <= ac->lastpts) { + // whatever the fuck this code does? + MP_WARN(ao, "audio frame pts went backwards (%d <- %d), autofixed\n", + (int)frame->pts, (int)ac->lastpts); + frame_pts = ac->lastpts + 1; + ac->lastpts = frame_pts; + frame->pts = av_rescale_q(frame_pts, ac->worst_time_base, + encoder->time_base); + frame_pts = av_rescale_q(frame->pts, encoder->time_base, + ac->worst_time_base); + } + ac->lastpts = frame_pts; + + frame->quality = encoder->global_quality; + encoder_encode(ac->enc, frame); + av_frame_free(&frame); +} + +static bool write_frame(struct ao *ao, struct mp_frame frame) +{ + struct priv *ac = ao->priv; + + // Can't push in frame if it doesn't want it output one. + mp_pin_out_request_data(ac->fix_frame_size->pins[1]); + + if (!mp_pin_in_write(ac->fix_frame_size->pins[0], frame)) + return false; // shouldn't happenâ„¢ + + while (1) { + struct mp_frame fr = mp_pin_out_read(ac->fix_frame_size->pins[1]); + if (!fr.type) + break; + if (fr.type != MP_FRAME_AUDIO) + continue; + struct mp_aframe *af = fr.data; + encode(ao, af); + mp_frame_unref(&fr); + } + + return true; +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *ac = ao->priv; + struct encode_lavc_context *ectx = ao->encode_lavc_ctx; + + // See ao_driver.write_frames. + struct mp_aframe *af = mp_aframe_new_ref(*(struct mp_aframe **)data); + + double nextpts; + double pts = mp_aframe_get_pts(af); + double outpts = pts; + + // for ectx PTS fields + mp_mutex_lock(&ectx->lock); + + if (!ectx->options->rawts) { + // Fix and apply the discontinuity pts offset. + nextpts = pts; + if (ectx->discontinuity_pts_offset == MP_NOPTS_VALUE) { + ectx->discontinuity_pts_offset = ectx->next_in_pts - nextpts; + } else if (fabs(nextpts + ectx->discontinuity_pts_offset - + ectx->next_in_pts) > 30) + { + MP_WARN(ao, "detected an unexpected discontinuity (pts jumped by " + "%f seconds)\n", + nextpts + ectx->discontinuity_pts_offset - ectx->next_in_pts); + ectx->discontinuity_pts_offset = ectx->next_in_pts - nextpts; + } + + outpts = pts + ectx->discontinuity_pts_offset; + } + + // Calculate expected pts of next audio frame (input side). + ac->expected_next_pts = pts + mp_aframe_get_size(af) / (double) ao->samplerate; + + // Set next allowed input pts value (input side). + if (!ectx->options->rawts) { + nextpts = ac->expected_next_pts + ectx->discontinuity_pts_offset; + if (nextpts > ectx->next_in_pts) + ectx->next_in_pts = nextpts; + } + + mp_mutex_unlock(&ectx->lock); + + mp_aframe_set_pts(af, outpts); + + return write_frame(ao, MAKE_FRAME(MP_FRAME_AUDIO, af)); +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + state->free_samples = 1; + state->queued_samples = 0; + state->delay = 0; +} + +static bool set_pause(struct ao *ao, bool paused) +{ + return true; // signal support so common code doesn't write silence +} + +static void start(struct ao *ao) +{ + // we use data immediately +} + +static void reset(struct ao *ao) +{ +} + +const struct ao_driver audio_out_lavc = { + .encode = true, + .description = "audio encoding using libavcodec", + .name = "lavc", + .initially_blocked = true, + .write_frames = true, + .priv_size = sizeof(struct priv), + .init = init, + .uninit = uninit, + .get_state = get_state, + .set_pause = set_pause, + .write = audio_write, + .start = start, + .reset = reset, +}; + +// vim: sw=4 ts=4 et tw=80 diff --git a/audio/out/ao_null.c b/audio/out/ao_null.c new file mode 100644 index 0000000..fcb61d2 --- /dev/null +++ b/audio/out/ao_null.c @@ -0,0 +1,230 @@ +/* + * null audio output driver + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +/* + * Note: this does much more than just ignoring audio output. It simulates + * (to some degree) an ideal AO. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <math.h> + +#include "mpv_talloc.h" + +#include "osdep/timer.h" +#include "options/m_option.h" +#include "common/common.h" +#include "common/msg.h" +#include "audio/format.h" +#include "ao.h" +#include "internal.h" + +struct priv { + bool paused; + double last_time; + float buffered; // samples + int buffersize; // samples + bool playing; + + bool untimed; + float bufferlen; // seconds + float speed; // multiplier + float latency_sec; // seconds + float latency; // samples + bool broken_eof; + bool broken_delay; + + // Minimal unit of audio samples that can be written at once. If play() is + // called with sizes not aligned to this, a rounded size will be returned. + // (This is not needed by the AO API, but many AOs behave this way.) + int outburst; // samples + + struct m_channels channel_layouts; + int format; +}; + +static void drain(struct ao *ao) +{ + struct priv *priv = ao->priv; + + if (ao->untimed) { + priv->buffered = 0; + return; + } + + if (priv->paused) + return; + + double now = mp_time_sec(); + if (priv->buffered > 0) { + priv->buffered -= (now - priv->last_time) * ao->samplerate * priv->speed; + if (priv->buffered < 0) + priv->buffered = 0; + } + priv->last_time = now; +} + +static int init(struct ao *ao) +{ + struct priv *priv = ao->priv; + + if (priv->format) + ao->format = priv->format; + + ao->untimed = priv->untimed; + + struct mp_chmap_sel sel = {.tmp = ao}; + if (priv->channel_layouts.num_chmaps) { + for (int n = 0; n < priv->channel_layouts.num_chmaps; n++) + mp_chmap_sel_add_map(&sel, &priv->channel_layouts.chmaps[n]); + } else { + mp_chmap_sel_add_any(&sel); + } + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) + mp_chmap_from_channels(&ao->channels, 2); + + priv->latency = priv->latency_sec * ao->samplerate; + + // A "buffer" for this many seconds of audio + int bursts = (int)(ao->samplerate * priv->bufferlen + 1) / priv->outburst; + ao->device_buffer = priv->outburst * bursts + priv->latency; + + priv->last_time = mp_time_sec(); + + return 0; +} + +// close audio device +static void uninit(struct ao *ao) +{ +} + +// stop playing and empty buffers (for seeking/pause) +static void reset(struct ao *ao) +{ + struct priv *priv = ao->priv; + priv->buffered = 0; + priv->playing = false; +} + +static void start(struct ao *ao) +{ + struct priv *priv = ao->priv; + + if (priv->paused) + MP_ERR(ao, "illegal state: start() while paused\n"); + + drain(ao); + priv->paused = false; + priv->last_time = mp_time_sec(); + priv->playing = true; +} + +static bool set_pause(struct ao *ao, bool paused) +{ + struct priv *priv = ao->priv; + + if (!priv->playing) + MP_ERR(ao, "illegal state: set_pause() while not playing\n"); + + if (priv->paused != paused) { + + drain(ao); + priv->paused = paused; + if (!priv->paused) + priv->last_time = mp_time_sec(); + } + + return true; +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *priv = ao->priv; + + if (priv->buffered <= 0) + priv->buffered = priv->latency; // emulate fixed latency + + priv->buffered += samples; + return true; +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *priv = ao->priv; + + drain(ao); + + state->free_samples = ao->device_buffer - priv->latency - priv->buffered; + state->free_samples = state->free_samples / priv->outburst * priv->outburst; + state->queued_samples = priv->buffered; + + // Note how get_state returns the delay in audio device time (instead of + // adjusting for speed), since most AOs seem to also do that. + state->delay = priv->buffered; + + // Drivers with broken EOF handling usually always report the same device- + // level delay that is additional to the buffer time. + if (priv->broken_eof && priv->buffered < priv->latency) + state->delay = priv->latency; + + state->delay /= ao->samplerate; + + if (priv->broken_delay) { // Report only multiples of outburst + double q = priv->outburst / (double)ao->samplerate; + if (state->delay > 0) + state->delay = (int)(state->delay / q) * q; + } + + state->playing = priv->playing && priv->buffered > 0; +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_null = { + .description = "Null audio output", + .name = "null", + .init = init, + .uninit = uninit, + .reset = reset, + .get_state = get_state, + .set_pause = set_pause, + .write = audio_write, + .start = start, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .bufferlen = 0.2, + .outburst = 256, + .speed = 1, + }, + .options = (const struct m_option[]) { + {"untimed", OPT_BOOL(untimed)}, + {"buffer", OPT_FLOAT(bufferlen), M_RANGE(0, 100)}, + {"outburst", OPT_INT(outburst), M_RANGE(1, 100000)}, + {"speed", OPT_FLOAT(speed), M_RANGE(0, 10000)}, + {"latency", OPT_FLOAT(latency_sec), M_RANGE(0, 100)}, + {"broken-eof", OPT_BOOL(broken_eof)}, + {"broken-delay", OPT_BOOL(broken_delay)}, + {"channel-layouts", OPT_CHANNELS(channel_layouts)}, + {"format", OPT_AUDIOFORMAT(format)}, + {0} + }, + .options_prefix = "ao-null", +}; diff --git a/audio/out/ao_openal.c b/audio/out/ao_openal.c new file mode 100644 index 0000000..7172908 --- /dev/null +++ b/audio/out/ao_openal.c @@ -0,0 +1,401 @@ +/* + * OpenAL audio output driver for MPlayer + * + * Copyleft 2006 by Reimar Döffinger (Reimar.Doeffinger@stud.uni-karlsruhe.de) + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <stdio.h> +#include <inttypes.h> +#ifdef OPENAL_AL_H +#include <OpenAL/alc.h> +#include <OpenAL/al.h> +#include <OpenAL/alext.h> +#else +#include <AL/alc.h> +#include <AL/al.h> +#include <AL/alext.h> +#endif + +#include "common/msg.h" + +#include "ao.h" +#include "internal.h" +#include "audio/format.h" +#include "osdep/timer.h" +#include "options/m_option.h" + +#define MAX_CHANS MP_NUM_CHANNELS +#define MAX_BUF 128 +#define MAX_SAMPLES 32768 +static ALuint buffers[MAX_BUF]; +static ALuint buffer_size[MAX_BUF]; +static ALuint source; + +static int cur_buf; +static int unqueue_buf; + +static struct ao *ao_data; + +struct priv { + ALenum al_format; + int num_buffers; + int num_samples; + bool direct_channels; +}; + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + switch (cmd) { + case AOCONTROL_GET_VOLUME: + case AOCONTROL_SET_VOLUME: { + ALfloat volume; + float *vol = arg; + if (cmd == AOCONTROL_SET_VOLUME) { + volume = *vol / 100.0; + alListenerf(AL_GAIN, volume); + } + alGetListenerf(AL_GAIN, &volume); + *vol = volume * 100; + return CONTROL_TRUE; + } + case AOCONTROL_GET_MUTE: + case AOCONTROL_SET_MUTE: { + bool mute = *(bool *)arg; + + // openal has no mute control, only gain. + // Thus reverse the muted state to get required gain + ALfloat al_mute = (ALfloat)(!mute); + if (cmd == AOCONTROL_SET_MUTE) { + alSourcef(source, AL_GAIN, al_mute); + } + alGetSourcef(source, AL_GAIN, &al_mute); + *(bool *)arg = !((bool)al_mute); + return CONTROL_TRUE; + } + + } + return CONTROL_UNKNOWN; +} + +static enum af_format get_supported_format(int format) +{ + switch (format) { + case AF_FORMAT_U8: + if (alGetEnumValue((ALchar*)"AL_FORMAT_MONO8")) + return AF_FORMAT_U8; + break; + + case AF_FORMAT_S16: + if (alGetEnumValue((ALchar*)"AL_FORMAT_MONO16")) + return AF_FORMAT_S16; + break; + + case AF_FORMAT_S32: + if (strstr(alGetString(AL_RENDERER), "X-Fi") != NULL) + return AF_FORMAT_S32; + break; + + case AF_FORMAT_FLOAT: + if (alIsExtensionPresent((ALchar*)"AL_EXT_float32") == AL_TRUE) + return AF_FORMAT_FLOAT; + break; + } + return AL_FALSE; +} + +static ALenum get_supported_layout(int format, int channels) +{ + const char *channel_str[] = { + [1] = "MONO", + [2] = "STEREO", + [4] = "QUAD", + [6] = "51CHN", + [7] = "61CHN", + [8] = "71CHN", + }; + const char *format_str[] = { + [AF_FORMAT_U8] = "8", + [AF_FORMAT_S16] = "16", + [AF_FORMAT_S32] = "32", + [AF_FORMAT_FLOAT] = "_FLOAT32", + }; + if (channel_str[channels] == NULL || format_str[format] == NULL) + return AL_FALSE; + + char enum_name[32]; + // AF_FORMAT_FLOAT uses same enum name as AF_FORMAT_S32 for multichannel + // playback, while it is different for mono and stereo. + // OpenAL Soft does not support AF_FORMAT_S32 and seems to reuse the names. + if (channels > 2 && format == AF_FORMAT_FLOAT) + format = AF_FORMAT_S32; + snprintf(enum_name, sizeof(enum_name), "AL_FORMAT_%s%s", channel_str[channels], + format_str[format]); + + if (alGetEnumValue((ALchar*)enum_name)) { + return alGetEnumValue((ALchar*)enum_name); + } + return AL_FALSE; +} + +// close audio device +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + alSourceStop(source); + alSourcei(source, AL_BUFFER, 0); + + alDeleteBuffers(p->num_buffers, buffers); + alDeleteSources(1, &source); + + ALCcontext *ctx = alcGetCurrentContext(); + ALCdevice *dev = alcGetContextsDevice(ctx); + alcMakeContextCurrent(NULL); + alcDestroyContext(ctx); + alcCloseDevice(dev); + ao_data = NULL; +} + +static int init(struct ao *ao) +{ + float position[3] = {0, 0, 0}; + float direction[6] = {0, 0, -1, 0, 1, 0}; + ALCdevice *dev = NULL; + ALCcontext *ctx = NULL; + ALCint freq = 0; + ALCint attribs[] = {ALC_FREQUENCY, ao->samplerate, 0, 0}; + struct priv *p = ao->priv; + if (ao_data) { + MP_FATAL(ao, "Not reentrant!\n"); + return -1; + } + ao_data = ao; + char *dev_name = ao->device; + dev = alcOpenDevice(dev_name && dev_name[0] ? dev_name : NULL); + if (!dev) { + MP_FATAL(ao, "could not open device\n"); + goto err_out; + } + ctx = alcCreateContext(dev, attribs); + alcMakeContextCurrent(ctx); + alListenerfv(AL_POSITION, position); + alListenerfv(AL_ORIENTATION, direction); + + alGenSources(1, &source); + if (p->direct_channels) { + if (alIsExtensionPresent("AL_SOFT_direct_channels_remix")) { + alSourcei(source, + alGetEnumValue((ALchar*)"AL_DIRECT_CHANNELS_SOFT"), + alcGetEnumValue(dev, "AL_REMIX_UNMATCHED_SOFT")); + } else { + MP_WARN(ao, "Direct channels aren't supported by this version of OpenAL\n"); + } + } + + cur_buf = 0; + unqueue_buf = 0; + for (int i = 0; i < p->num_buffers; ++i) { + buffer_size[i] = 0; + } + + alGenBuffers(p->num_buffers, buffers); + + alcGetIntegerv(dev, ALC_FREQUENCY, 1, &freq); + if (alcGetError(dev) == ALC_NO_ERROR && freq) + ao->samplerate = freq; + + // Check sample format + int try_formats[AF_FORMAT_COUNT + 1]; + enum af_format sample_format = 0; + af_get_best_sample_formats(ao->format, try_formats); + for (int n = 0; try_formats[n]; n++) { + sample_format = get_supported_format(try_formats[n]); + if (sample_format != AF_FORMAT_UNKNOWN) { + ao->format = try_formats[n]; + break; + } + } + + if (sample_format == AF_FORMAT_UNKNOWN) { + MP_FATAL(ao, "Can't find appropriate sample format.\n"); + uninit(ao); + goto err_out; + } + + // Check if OpenAL driver supports the desired number of channels. + int num_channels = ao->channels.num; + do { + p->al_format = get_supported_layout(sample_format, num_channels); + if (p->al_format == AL_FALSE) { + num_channels = num_channels - 1; + } + } while (p->al_format == AL_FALSE && num_channels > 1); + + // Request number of speakers for output from ao. + const struct mp_chmap possible_layouts[] = { + {0}, // empty + MP_CHMAP_INIT_MONO, // mono + MP_CHMAP_INIT_STEREO, // stereo + {0}, // 2.1 + MP_CHMAP4(FL, FR, BL, BR), // 4.0 + {0}, // 5.0 + MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1 + MP_CHMAP7(FL, FR, FC, LFE, SL, SR, BC), // 6.1 + MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1 + }; + ao->channels = possible_layouts[num_channels]; + if (!ao->channels.num) + mp_chmap_set_unknown(&ao->channels, num_channels); + + if (p->al_format == AL_FALSE || !mp_chmap_is_valid(&ao->channels)) { + MP_FATAL(ao, "Can't find appropriate channel layout.\n"); + uninit(ao); + goto err_out; + } + + ao->device_buffer = p->num_buffers * p->num_samples; + return 0; + +err_out: + ao_data = NULL; + return -1; +} + +static void unqueue_buffers(struct ao *ao) +{ + struct priv *q = ao->priv; + ALint p; + int till_wrap = q->num_buffers - unqueue_buf; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &p); + if (p >= till_wrap) { + alSourceUnqueueBuffers(source, till_wrap, &buffers[unqueue_buf]); + unqueue_buf = 0; + p -= till_wrap; + } + if (p) { + alSourceUnqueueBuffers(source, p, &buffers[unqueue_buf]); + unqueue_buf += p; + } +} + +static void reset(struct ao *ao) +{ + alSourceStop(source); + unqueue_buffers(ao); +} + +static bool audio_set_pause(struct ao *ao, bool pause) +{ + if (pause) { + alSourcePause(source); + } else { + alSourcePlay(source); + } + return true; +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *p = ao->priv; + + int num = (samples + p->num_samples - 1) / p->num_samples; + + for (int i = 0; i < num; i++) { + char *d = *data; + buffer_size[cur_buf] = + MPMIN(samples - i * p->num_samples, p->num_samples); + d += i * buffer_size[cur_buf] * ao->sstride; + alBufferData(buffers[cur_buf], p->al_format, d, + buffer_size[cur_buf] * ao->sstride, ao->samplerate); + alSourceQueueBuffers(source, 1, &buffers[cur_buf]); + cur_buf = (cur_buf + 1) % p->num_buffers; + } + + return true; +} + +static void audio_start(struct ao *ao) +{ + alSourcePlay(source); +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *p = ao->priv; + + ALint queued; + unqueue_buffers(ao); + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + double source_offset = 0; + if(alIsExtensionPresent("AL_SOFT_source_latency")) { + ALdouble offsets[2]; + LPALGETSOURCEDVSOFT alGetSourcedvSOFT = alGetProcAddress("alGetSourcedvSOFT"); + alGetSourcedvSOFT(source, AL_SEC_OFFSET_LATENCY_SOFT, offsets); + // Additional latency to the play buffer, the remaining seconds to be + // played minus the offset (seconds already played) + source_offset = offsets[1] - offsets[0]; + } else { + float offset = 0; + alGetSourcef(source, AL_SEC_OFFSET, &offset); + source_offset = -offset; + } + + int queued_samples = 0; + for (int i = 0, index = cur_buf; i < queued; ++i) { + queued_samples += buffer_size[index]; + index = (index + 1) % p->num_buffers; + } + + state->delay = queued_samples / (double)ao->samplerate + source_offset; + + state->queued_samples = queued_samples; + state->free_samples = MPMAX(p->num_buffers - queued, 0) * p->num_samples; + + ALint source_state = 0; + alGetSourcei(source, AL_SOURCE_STATE, &source_state); + state->playing = source_state == AL_PLAYING; +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_openal = { + .description = "OpenAL audio output", + .name = "openal", + .init = init, + .uninit = uninit, + .control = control, + .get_state = get_state, + .write = audio_write, + .start = audio_start, + .set_pause = audio_set_pause, + .reset = reset, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .num_buffers = 4, + .num_samples = 8192, + .direct_channels = true, + }, + .options = (const struct m_option[]) { + {"num-buffers", OPT_INT(num_buffers), M_RANGE(2, MAX_BUF)}, + {"num-samples", OPT_INT(num_samples), M_RANGE(256, MAX_SAMPLES)}, + {"direct-channels", OPT_BOOL(direct_channels)}, + {0} + }, + .options_prefix = "openal", +}; diff --git a/audio/out/ao_opensles.c b/audio/out/ao_opensles.c new file mode 100644 index 0000000..ddcff19 --- /dev/null +++ b/audio/out/ao_opensles.c @@ -0,0 +1,265 @@ +/* + * OpenSL ES audio output driver. + * Copyright (C) 2016 Ilya Zhuravlev <whatever@xyz.is> + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "ao.h" +#include "internal.h" +#include "common/msg.h" +#include "audio/format.h" +#include "options/m_option.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +#include <SLES/OpenSLES.h> +#include <SLES/OpenSLES_Android.h> + +struct priv { + SLObjectItf sl, output_mix, player; + SLBufferQueueItf buffer_queue; + SLEngineItf engine; + SLPlayItf play; + void *buf; + int bytes_per_enqueue; + mp_mutex buffer_lock; + double audio_latency; + + int frames_per_enqueue; + int buffer_size_in_ms; +}; + +#define DESTROY(thing) \ + if (p->thing) { \ + (*p->thing)->Destroy(p->thing); \ + p->thing = NULL; \ + } + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + + DESTROY(player); + DESTROY(output_mix); + DESTROY(sl); + + p->buffer_queue = NULL; + p->engine = NULL; + p->play = NULL; + + mp_mutex_destroy(&p->buffer_lock); + + free(p->buf); + p->buf = NULL; +} + +#undef DESTROY + +static void buffer_callback(SLBufferQueueItf buffer_queue, void *context) +{ + struct ao *ao = context; + struct priv *p = ao->priv; + SLresult res; + double delay; + + mp_mutex_lock(&p->buffer_lock); + + delay = p->frames_per_enqueue / (double)ao->samplerate; + delay += p->audio_latency; + ao_read_data(ao, &p->buf, p->frames_per_enqueue, + mp_time_ns() + MP_TIME_S_TO_NS(delay)); + + res = (*buffer_queue)->Enqueue(buffer_queue, p->buf, p->bytes_per_enqueue); + if (res != SL_RESULT_SUCCESS) + MP_ERR(ao, "Failed to Enqueue: %d\n", res); + + mp_mutex_unlock(&p->buffer_lock); +} + +#define CHK(stmt) \ + { \ + SLresult res = stmt; \ + if (res != SL_RESULT_SUCCESS) { \ + MP_ERR(ao, "%s: %d\n", #stmt, res); \ + goto error; \ + } \ + } + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + SLDataLocator_BufferQueue locator_buffer_queue; + SLDataLocator_OutputMix locator_output_mix; + SLAndroidDataFormat_PCM_EX pcm; + SLDataSource audio_source; + SLDataSink audio_sink; + + // This AO only supports two channels at the moment + mp_chmap_from_channels(&ao->channels, 2); + // Upstream "Wilhelm" supports only 8000 <= rate <= 192000 + ao->samplerate = MPCLAMP(ao->samplerate, 8000, 192000); + + CHK(slCreateEngine(&p->sl, 0, NULL, 0, NULL, NULL)); + CHK((*p->sl)->Realize(p->sl, SL_BOOLEAN_FALSE)); + CHK((*p->sl)->GetInterface(p->sl, SL_IID_ENGINE, (void*)&p->engine)); + CHK((*p->engine)->CreateOutputMix(p->engine, &p->output_mix, 0, NULL, NULL)); + CHK((*p->output_mix)->Realize(p->output_mix, SL_BOOLEAN_FALSE)); + + locator_buffer_queue.locatorType = SL_DATALOCATOR_BUFFERQUEUE; + locator_buffer_queue.numBuffers = 8; + + if (af_fmt_is_int(ao->format)) { + // Be future-proof + if (af_fmt_to_bytes(ao->format) > 2) + ao->format = AF_FORMAT_S32; + else + ao->format = af_fmt_from_planar(ao->format); + pcm.formatType = SL_DATAFORMAT_PCM; + } else { + ao->format = AF_FORMAT_FLOAT; + pcm.formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + pcm.representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT; + } + pcm.numChannels = ao->channels.num; + pcm.containerSize = pcm.bitsPerSample = 8 * af_fmt_to_bytes(ao->format); + pcm.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + pcm.sampleRate = ao->samplerate * 1000; + + if (p->buffer_size_in_ms) { + ao->device_buffer = ao->samplerate * p->buffer_size_in_ms / 1000; + // As the purpose of buffer_size_in_ms is to request a specific + // soft buffer size: + ao->def_buffer = 0; + } + + // But it does not make sense if it is smaller than the enqueue size: + if (p->frames_per_enqueue) { + ao->device_buffer = MPMAX(ao->device_buffer, p->frames_per_enqueue); + } else { + if (ao->device_buffer) { + p->frames_per_enqueue = ao->device_buffer; + } else if (ao->def_buffer) { + p->frames_per_enqueue = ao->def_buffer * ao->samplerate; + } else { + MP_ERR(ao, "Enqueue size is not set and can neither be derived\n"); + goto error; + } + } + + p->bytes_per_enqueue = p->frames_per_enqueue * ao->channels.num * + af_fmt_to_bytes(ao->format); + p->buf = calloc(1, p->bytes_per_enqueue); + if (!p->buf) { + MP_ERR(ao, "Failed to allocate device buffer\n"); + goto error; + } + + int r = mp_mutex_init(&p->buffer_lock); + if (r) { + MP_ERR(ao, "Failed to initialize the mutex: %d\n", r); + goto error; + } + + audio_source.pFormat = (void*)&pcm; + audio_source.pLocator = (void*)&locator_buffer_queue; + + locator_output_mix.locatorType = SL_DATALOCATOR_OUTPUTMIX; + locator_output_mix.outputMix = p->output_mix; + + audio_sink.pLocator = (void*)&locator_output_mix; + audio_sink.pFormat = NULL; + + SLInterfaceID iid_array[] = { SL_IID_BUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION }; + SLboolean required[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE }; + CHK((*p->engine)->CreateAudioPlayer(p->engine, &p->player, &audio_source, + &audio_sink, 2, iid_array, required)); + + CHK((*p->player)->Realize(p->player, SL_BOOLEAN_FALSE)); + CHK((*p->player)->GetInterface(p->player, SL_IID_PLAY, (void*)&p->play)); + CHK((*p->player)->GetInterface(p->player, SL_IID_BUFFERQUEUE, + (void*)&p->buffer_queue)); + CHK((*p->buffer_queue)->RegisterCallback(p->buffer_queue, + buffer_callback, ao)); + CHK((*p->play)->SetPlayState(p->play, SL_PLAYSTATE_PLAYING)); + + SLAndroidConfigurationItf android_config; + SLuint32 audio_latency = 0, value_size = sizeof(SLuint32); + + SLint32 get_interface_result = (*p->player)->GetInterface( + p->player, + SL_IID_ANDROIDCONFIGURATION, + &android_config + ); + + if (get_interface_result == SL_RESULT_SUCCESS) { + SLint32 get_configuration_result = (*android_config)->GetConfiguration( + android_config, + (const SLchar *)"androidGetAudioLatency", + &value_size, + &audio_latency + ); + + if (get_configuration_result == SL_RESULT_SUCCESS) { + p->audio_latency = (double)audio_latency / 1000.0; + MP_INFO(ao, "Device latency is %f\n", p->audio_latency); + } + } + + return 1; +error: + uninit(ao); + return -1; +} + +#undef CHK + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + (*p->buffer_queue)->Clear(p->buffer_queue); +} + +static void resume(struct ao *ao) +{ + struct priv *p = ao->priv; + buffer_callback(p->buffer_queue, ao); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_opensles = { + .description = "OpenSL ES audio output", + .name = "opensles", + .init = init, + .uninit = uninit, + .reset = reset, + .start = resume, + + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .buffer_size_in_ms = 250, + }, + .options = (const struct m_option[]) { + {"frames-per-enqueue", OPT_INT(frames_per_enqueue), + M_RANGE(1, 96000)}, + {"buffer-size-in-ms", OPT_INT(buffer_size_in_ms), + M_RANGE(0, 500)}, + {0} + }, + .options_prefix = "opensles", +}; diff --git a/audio/out/ao_oss.c b/audio/out/ao_oss.c new file mode 100644 index 0000000..5c0b8c9 --- /dev/null +++ b/audio/out/ao_oss.c @@ -0,0 +1,400 @@ +/* + * OSS audio output driver + * + * Original author: A'rpi + * Support for >2 output channels added 2001-11-25 + * - Steve Davies <steve@daviesfam.org> + * Rozhuk Ivan <rozhuk.im@gmail.com> 2020-2023 + * + * 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 <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> + +#include <sys/ioctl.h> +#include <sys/soundcard.h> +#include <sys/stat.h> +#if defined(__DragonFly__) || defined(__FreeBSD__) +#include <sys/sysctl.h> +#endif +#include <sys/types.h> + +#include "audio/format.h" +#include "common/msg.h" +#include "options/options.h" +#include "osdep/endian.h" +#include "osdep/io.h" +#include "ao.h" +#include "internal.h" + +#ifndef AFMT_AC3 +#define AFMT_AC3 -1 +#endif + +#define PATH_DEV_DSP "/dev/dsp" +#define PATH_DEV_MIXER "/dev/mixer" + +struct priv { + int dsp_fd; + double bps; /* Bytes per second. */ +}; + +/* like alsa except for 6.1 and 7.1, from pcm/matrix_map.h */ +static const struct mp_chmap oss_layouts[MP_NUM_CHANNELS + 1] = { + {0}, /* empty */ + MP_CHMAP_INIT_MONO, /* mono */ + MP_CHMAP2(FL, FR), /* stereo */ + MP_CHMAP3(FL, FR, LFE), /* 2.1 */ + MP_CHMAP4(FL, FR, BL, BR), /* 4.0 */ + MP_CHMAP5(FL, FR, BL, BR, FC), /* 5.0 */ + MP_CHMAP6(FL, FR, BL, BR, FC, LFE), /* 5.1 */ + MP_CHMAP7(FL, FR, BL, BR, FC, LFE, BC), /* 6.1 */ + MP_CHMAP8(FL, FR, BL, BR, FC, LFE, SL, SR), /* 7.1 */ +}; + +#if !defined(AFMT_S32_NE) && defined(AFMT_S32_LE) && defined(AFMT_S32_BE) +#define AFMT_S32_NE AFMT_S32MP_SELECT_LE_BE(AFMT_S32_LE, AFMT_S32_BE) +#endif + +static const int format_table[][2] = { + {AFMT_U8, AF_FORMAT_U8}, + {AFMT_S16_NE, AF_FORMAT_S16}, +#ifdef AFMT_S32_NE + {AFMT_S32_NE, AF_FORMAT_S32}, +#endif +#ifdef AFMT_FLOAT + {AFMT_FLOAT, AF_FORMAT_FLOAT}, +#endif +#ifdef AFMT_MPEG + {AFMT_MPEG, AF_FORMAT_S_MP3}, +#endif + {-1, -1} +}; + +#define MP_WARN_IOCTL_ERR(__ao) \ + MP_WARN((__ao), "%s: ioctl() fail, err = %i: %s\n", \ + __FUNCTION__, errno, strerror(errno)) + + +static void uninit(struct ao *ao); + + +static void device_descr_get(size_t dev_idx, char *buf, size_t buf_size) +{ +#if defined(__DragonFly__) || defined(__FreeBSD__) + char dev_path[32]; + size_t tmp = (buf_size - 1); + + snprintf(dev_path, sizeof(dev_path), "dev.pcm.%zu.%%desc", dev_idx); + if (sysctlbyname(dev_path, buf, &tmp, NULL, 0) != 0) { + tmp = 0; + } + buf[tmp] = 0x00; +#elif defined(SOUND_MIXER_INFO) + size_t tmp = 0; + char dev_path[32]; + mixer_info mi; + + snprintf(dev_path, sizeof(dev_path), PATH_DEV_MIXER"%zu", dev_idx); + int fd = open(dev_path, O_RDONLY); + if (ioctl(fd, SOUND_MIXER_INFO, &mi) == 0) { + strncpy(buf, mi.name, buf_size - 1); + tmp = (buf_size - 1); + } + close(fd); + buf[tmp] = 0x00; +#else + buf[0] = 0x00; +#endif +} + +static int format2oss(int format) +{ + for (size_t i = 0; format_table[i][0] != -1; i++) { + if (format_table[i][1] == format) + return format_table[i][0]; + } + return -1; +} + +static bool try_format(struct ao *ao, int *format) +{ + struct priv *p = ao->priv; + int oss_format = format2oss(*format); + + if (oss_format == -1 && af_fmt_is_spdif(*format)) + oss_format = AFMT_AC3; + + if (oss_format == -1) { + MP_VERBOSE(ao, "Unknown/not supported internal format: %s\n", + af_fmt_to_str(*format)); + *format = 0; + return false; + } + + return (ioctl(p->dsp_fd, SNDCTL_DSP_SETFMT, &oss_format) != -1); +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + struct mp_chmap channels = ao->channels; + audio_buf_info info; + size_t i; + int format, samplerate, nchannels, reqchannels, trig = 0; + int best_sample_formats[AF_FORMAT_COUNT + 1]; + const char *device = ((ao->device) ? ao->device : PATH_DEV_DSP); + + /* Opening device. */ + MP_VERBOSE(ao, "Using '%s' audio device.\n", device); + p->dsp_fd = open(device, (O_WRONLY | O_CLOEXEC)); + if (p->dsp_fd < 0) { + MP_ERR(ao, "Can't open audio device %s: %s.\n", + device, mp_strerror(errno)); + goto err_out; + } + + /* Selecting sound format. */ + format = af_fmt_from_planar(ao->format); + af_get_best_sample_formats(format, best_sample_formats); + for (i = 0; best_sample_formats[i]; i++) { + format = best_sample_formats[i]; + if (try_format(ao, &format)) + break; + } + if (!format) { + MP_ERR(ao, "Can't set sample format.\n"); + goto err_out; + } + MP_VERBOSE(ao, "Sample format: %s\n", af_fmt_to_str(format)); + + /* Channels count. */ + if (af_fmt_is_spdif(format)) { + nchannels = reqchannels = channels.num; + if (ioctl(p->dsp_fd, SNDCTL_DSP_CHANNELS, &nchannels) == -1) { + MP_ERR(ao, "Failed to set audio device to %d channels.\n", + reqchannels); + goto err_out_ioctl; + } + } else { + struct mp_chmap_sel sel = {0}; + for (i = 0; i < MP_ARRAY_SIZE(oss_layouts); i++) { + mp_chmap_sel_add_map(&sel, &oss_layouts[i]); + } + if (!ao_chmap_sel_adjust(ao, &sel, &channels)) + goto err_out; + nchannels = reqchannels = channels.num; + if (ioctl(p->dsp_fd, SNDCTL_DSP_CHANNELS, &nchannels) == -1) { + MP_ERR(ao, "Failed to set audio device to %d channels.\n", + reqchannels); + goto err_out_ioctl; + } + if (nchannels != reqchannels) { + /* Update number of channels to OSS suggested value. */ + if (!ao_chmap_sel_get_def(ao, &sel, &channels, nchannels)) + goto err_out; + } + MP_VERBOSE(ao, "Using %d channels (requested: %d).\n", + channels.num, reqchannels); + } + + /* Sample rate. */ + samplerate = ao->samplerate; + if (ioctl(p->dsp_fd, SNDCTL_DSP_SPEED, &samplerate) == -1) + goto err_out_ioctl; + MP_VERBOSE(ao, "Using %d Hz samplerate.\n", samplerate); + + /* Get buffer size. */ + if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1) + goto err_out_ioctl; + /* See ao.c ao->sstride initializations and get_state(). */ + ao->device_buffer = ((info.fragstotal * info.fragsize) / + af_fmt_to_bytes(format)); + if (!af_fmt_is_planar(format)) { + ao->device_buffer /= channels.num; + } + + /* Do not start playback after data written. */ + if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) + goto err_out_ioctl; + + /* Update sound params. */ + ao->format = format; + ao->samplerate = samplerate; + ao->channels = channels; + p->bps = (channels.num * samplerate * af_fmt_to_bytes(format)); + + return 0; + +err_out_ioctl: + MP_WARN_IOCTL_ERR(ao); +err_out: + uninit(ao); + return -1; +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + + if (p->dsp_fd == -1) + return; + ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL); + close(p->dsp_fd); + p->dsp_fd = -1; +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *p = ao->priv; + float *vol = arg; + int v; + + if (p->dsp_fd < 0) + return CONTROL_ERROR; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: + if (ioctl(p->dsp_fd, SNDCTL_DSP_GETPLAYVOL, &v) == -1) { + MP_WARN_IOCTL_ERR(ao); + return CONTROL_ERROR; + } + *vol = ((v & 0x00ff) + ((v & 0xff00) >> 8)) / 2.0; + return CONTROL_OK; + case AOCONTROL_SET_VOLUME: + v = ((int)*vol << 8) | (int)*vol; + if (ioctl(p->dsp_fd, SNDCTL_DSP_SETPLAYVOL, &v) == -1) { + MP_WARN_IOCTL_ERR(ao); + return CONTROL_ERROR; + } + return CONTROL_OK; + } + + return CONTROL_UNKNOWN; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + int trig = 0; + + /* Clear buf and do not start playback after data written. */ + if (ioctl(p->dsp_fd, SNDCTL_DSP_HALT, NULL) == -1 || + ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) + { + MP_WARN_IOCTL_ERR(ao); + MP_WARN(ao, "Force reinitialize audio device.\n"); + uninit(ao); + init(ao); + } +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + int trig = PCM_ENABLE_OUTPUT; + + if (ioctl(p->dsp_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1) { + MP_WARN_IOCTL_ERR(ao); + return; + } +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *p = ao->priv; + ssize_t rc; + const size_t size = (samples * ao->sstride); + + if (size == 0) + return true; + + while ((rc = write(p->dsp_fd, data[0], size)) == -1) { + if (errno == EINTR) + continue; + MP_WARN(ao, "audio_write: write() fail, err = %i: %s.\n", + errno, strerror(errno)); + return false; + } + if ((size_t)rc != size) { + MP_WARN(ao, "audio_write: unexpected partial write: required: %zu, written: %zu.\n", + size, (size_t)rc); + return false; + } + + return true; +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *p = ao->priv; + audio_buf_info info; + int odelay; + + if (ioctl(p->dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1 || + ioctl(p->dsp_fd, SNDCTL_DSP_GETODELAY, &odelay) == -1) + { + MP_WARN_IOCTL_ERR(ao); + memset(state, 0x00, sizeof(struct mp_pcm_state)); + state->delay = 0.0; + return; + } + state->free_samples = (info.bytes / ao->sstride); + state->queued_samples = (ao->device_buffer - state->free_samples); + state->delay = (odelay / p->bps); + state->playing = (state->queued_samples != 0); +} + +static void list_devs(struct ao *ao, struct ao_device_list *list) +{ + struct stat st; + char dev_path[32] = PATH_DEV_DSP, dev_descr[256] = "Default"; + struct ao_device_desc dev = {.name = dev_path, .desc = dev_descr}; + + if (stat(PATH_DEV_DSP, &st) == 0) { + ao_device_list_add(list, ao, &dev); + } + + /* Auto detect. */ + for (size_t i = 0, fail_cnt = 0; fail_cnt < 8; i ++, fail_cnt ++) { + snprintf(dev_path, sizeof(dev_path), PATH_DEV_DSP"%zu", i); + if (stat(dev_path, &st) != 0) + continue; + device_descr_get(i, dev_descr, sizeof(dev_descr)); + ao_device_list_add(list, ao, &dev); + fail_cnt = 0; /* Reset fail counter. */ + } +} + +const struct ao_driver audio_out_oss = { + .name = "oss", + .description = "OSS/ioctl audio output", + .init = init, + .uninit = uninit, + .control = control, + .reset = reset, + .start = start, + .write = audio_write, + .get_state = get_state, + .list_devs = list_devs, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .dsp_fd = -1, + }, +}; diff --git a/audio/out/ao_pcm.c b/audio/out/ao_pcm.c new file mode 100644 index 0000000..4097aa3 --- /dev/null +++ b/audio/out/ao_pcm.c @@ -0,0 +1,248 @@ +/* + * PCM audio output driver + * + * Original author: Atmosfear + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <libavutil/common.h> + +#include "mpv_talloc.h" + +#include "options/m_option.h" +#include "audio/format.h" +#include "ao.h" +#include "internal.h" +#include "common/msg.h" +#include "osdep/endian.h" + +#ifdef __MINGW32__ +// for GetFileType to detect pipes +#include <windows.h> +#include <io.h> +#endif + +struct priv { + char *outputfilename; + bool waveheader; + bool append; + uint64_t data_length; + FILE *fp; +}; + +#define WAV_ID_RIFF 0x46464952 /* "RIFF" */ +#define WAV_ID_WAVE 0x45564157 /* "WAVE" */ +#define WAV_ID_FMT 0x20746d66 /* "fmt " */ +#define WAV_ID_DATA 0x61746164 /* "data" */ +#define WAV_ID_PCM 0x0001 +#define WAV_ID_FLOAT_PCM 0x0003 +#define WAV_ID_FORMAT_EXTENSIBLE 0xfffe + +static void fput16le(uint16_t val, FILE *fp) +{ + uint8_t bytes[2] = {val, val >> 8}; + fwrite(bytes, 1, 2, fp); +} + +static void fput32le(uint32_t val, FILE *fp) +{ + uint8_t bytes[4] = {val, val >> 8, val >> 16, val >> 24}; + fwrite(bytes, 1, 4, fp); +} + +static void write_wave_header(struct ao *ao, FILE *fp, uint64_t data_length) +{ + uint16_t fmt = ao->format == AF_FORMAT_FLOAT ? WAV_ID_FLOAT_PCM : WAV_ID_PCM; + int bits = af_fmt_to_bytes(ao->format) * 8; + + // Master RIFF chunk + fput32le(WAV_ID_RIFF, fp); + // RIFF chunk size: 'WAVE' + 'fmt ' + 4 + 40 + + // data chunk hdr (8) + data length + fput32le(12 + 40 + 8 + data_length, fp); + fput32le(WAV_ID_WAVE, fp); + + // Format chunk + fput32le(WAV_ID_FMT, fp); + fput32le(40, fp); + fput16le(WAV_ID_FORMAT_EXTENSIBLE, fp); + fput16le(ao->channels.num, fp); + fput32le(ao->samplerate, fp); + fput32le(ao->bps, fp); + fput16le(ao->channels.num * (bits / 8), fp); + fput16le(bits, fp); + + // Extension chunk + fput16le(22, fp); + fput16le(bits, fp); + fput32le(mp_chmap_to_waveext(&ao->channels), fp); + // 2 bytes format + 14 bytes guid + fput32le(fmt, fp); + fput32le(0x00100000, fp); + fput32le(0xAA000080, fp); + fput32le(0x719B3800, fp); + + // Data chunk + fput32le(WAV_ID_DATA, fp); + fput32le(data_length, fp); +} + +static int init(struct ao *ao) +{ + struct priv *priv = ao->priv; + + char *outputfilename = priv->outputfilename; + if (!outputfilename) { + outputfilename = talloc_strdup(priv, priv->waveheader ? "audiodump.wav" + : "audiodump.pcm"); + } + + ao->format = af_fmt_from_planar(ao->format); + + if (priv->waveheader) { + // WAV files must have one of the following formats + + // And they don't work in big endian; fixing it would be simple, but + // nobody cares. + if (BYTE_ORDER == BIG_ENDIAN) { + MP_FATAL(ao, "Not supported on big endian.\n"); + return -1; + } + + switch (ao->format) { + case AF_FORMAT_U8: + case AF_FORMAT_S16: + case AF_FORMAT_S32: + case AF_FORMAT_FLOAT: + break; + default: + if (!af_fmt_is_spdif(ao->format)) + ao->format = AF_FORMAT_S16; + break; + } + } + + struct mp_chmap_sel sel = {0}; + mp_chmap_sel_add_waveext(&sel); + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) + return -1; + + ao->bps = ao->channels.num * ao->samplerate * af_fmt_to_bytes(ao->format); + + MP_INFO(ao, "File: %s (%s)\nPCM: Samplerate: %d Hz Channels: %d Format: %s\n", + outputfilename, + priv->waveheader ? "WAVE" : "RAW PCM", ao->samplerate, + ao->channels.num, af_fmt_to_str(ao->format)); + + priv->fp = fopen(outputfilename, priv->append ? "ab" : "wb"); + if (!priv->fp) { + MP_ERR(ao, "Failed to open %s for writing!\n", outputfilename); + return -1; + } + if (priv->waveheader) // Reserve space for wave header + write_wave_header(ao, priv->fp, 0x7ffff000); + ao->untimed = true; + ao->device_buffer = 1 << 16; + + return 0; +} + +// close audio device +static void uninit(struct ao *ao) +{ + struct priv *priv = ao->priv; + + if (priv->waveheader) { // Rewrite wave header + bool broken_seek = false; +#ifdef __MINGW32__ + // Windows, in its usual idiocy "emulates" seeks on pipes so it always + // looks like they work. So we have to detect them brute-force. + broken_seek = FILE_TYPE_DISK != + GetFileType((HANDLE)_get_osfhandle(_fileno(priv->fp))); +#endif + if (broken_seek || fseek(priv->fp, 0, SEEK_SET) != 0) + MP_ERR(ao, "Could not seek to start, WAV size headers not updated!\n"); + else { + if (priv->data_length > 0xfffff000) { + MP_ERR(ao, "File larger than allowed for " + "WAV files, may play truncated!\n"); + priv->data_length = 0xfffff000; + } + write_wave_header(ao, priv->fp, priv->data_length); + } + } + fclose(priv->fp); +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *priv = ao->priv; + int len = samples * ao->sstride; + + fwrite(data[0], len, 1, priv->fp); + priv->data_length += len; + + return true; +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + state->free_samples = ao->device_buffer; + state->queued_samples = 0; + state->delay = 0; +} + +static bool set_pause(struct ao *ao, bool paused) +{ + return true; // signal support so common code doesn't write silence +} + +static void start(struct ao *ao) +{ + // we use data immediately +} + +static void reset(struct ao *ao) +{ +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_pcm = { + .description = "RAW PCM/WAVE file writer audio output", + .name = "pcm", + .init = init, + .uninit = uninit, + .get_state = get_state, + .set_pause = set_pause, + .write = audio_write, + .start = start, + .reset = reset, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { .waveheader = true }, + .options = (const struct m_option[]) { + {"file", OPT_STRING(outputfilename), .flags = M_OPT_FILE}, + {"waveheader", OPT_BOOL(waveheader)}, + {"append", OPT_BOOL(append)}, + {0} + }, + .options_prefix = "ao-pcm", +}; diff --git a/audio/out/ao_pipewire.c b/audio/out/ao_pipewire.c new file mode 100644 index 0000000..3fbcbf6 --- /dev/null +++ b/audio/out/ao_pipewire.c @@ -0,0 +1,883 @@ +/* + * PipeWire audio output driver. + * Copyright (C) 2021 Thomas Weißschuh <thomas@t-8ch.de> + * Copyright (C) 2021 Oschowa <oschowa@web.de> + * Copyright (C) 2020 Andreas Kempf <aakempf@gmail.com> + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <pipewire/pipewire.h> +#include <pipewire/global.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/props.h> +#include <spa/utils/result.h> +#include <math.h> + +#include "common/msg.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "ao.h" +#include "audio/format.h" +#include "internal.h" +#include "osdep/timer.h" + +#if !PW_CHECK_VERSION(0, 3, 50) +static inline int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size) { + return pw_stream_get_time(stream, time); +} +#endif + +#if !PW_CHECK_VERSION(0, 3, 57) +// Earlier versions segfault on zeroed hooks +#define spa_hook_remove(hook) if ((hook)->link.prev) spa_hook_remove(hook) +#endif + +enum init_state { + INIT_STATE_NONE, + INIT_STATE_SUCCESS, + INIT_STATE_ERROR, +}; + +enum { + VOLUME_MODE_CHANNEL, + VOLUME_MODE_GLOBAL, +}; + +struct priv { + struct pw_thread_loop *loop; + struct pw_stream *stream; + struct pw_core *core; + struct spa_hook stream_listener; + struct spa_hook core_listener; + enum init_state init_state; + + bool muted; + float volume; + + struct { + int buffer_msec; + char *remote; + int volume_mode; + } options; + + struct { + struct pw_registry *registry; + struct spa_hook registry_listener; + struct spa_list sinks; + } hotplug; +}; + +struct id_list { + uint32_t id; + struct spa_list node; +}; + +static enum spa_audio_format af_fmt_to_pw(struct ao *ao, enum af_format format) +{ + switch (format) { + case AF_FORMAT_U8: return SPA_AUDIO_FORMAT_U8; + case AF_FORMAT_S16: return SPA_AUDIO_FORMAT_S16; + case AF_FORMAT_S32: return SPA_AUDIO_FORMAT_S32; + case AF_FORMAT_FLOAT: return SPA_AUDIO_FORMAT_F32; + case AF_FORMAT_DOUBLE: return SPA_AUDIO_FORMAT_F64; + case AF_FORMAT_U8P: return SPA_AUDIO_FORMAT_U8P; + case AF_FORMAT_S16P: return SPA_AUDIO_FORMAT_S16P; + case AF_FORMAT_S32P: return SPA_AUDIO_FORMAT_S32P; + case AF_FORMAT_FLOATP: return SPA_AUDIO_FORMAT_F32P; + case AF_FORMAT_DOUBLEP: return SPA_AUDIO_FORMAT_F64P; + default: + MP_WARN(ao, "Unhandled format %d\n", format); + return SPA_AUDIO_FORMAT_UNKNOWN; + } +} + +static enum spa_audio_channel mp_speaker_id_to_spa(struct ao *ao, enum mp_speaker_id mp_speaker_id) +{ + switch (mp_speaker_id) { + case MP_SPEAKER_ID_FL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_FR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_FC: return SPA_AUDIO_CHANNEL_FC; + case MP_SPEAKER_ID_LFE: return SPA_AUDIO_CHANNEL_LFE; + case MP_SPEAKER_ID_BL: return SPA_AUDIO_CHANNEL_RL; + case MP_SPEAKER_ID_BR: return SPA_AUDIO_CHANNEL_RR; + case MP_SPEAKER_ID_FLC: return SPA_AUDIO_CHANNEL_FLC; + case MP_SPEAKER_ID_FRC: return SPA_AUDIO_CHANNEL_FRC; + case MP_SPEAKER_ID_BC: return SPA_AUDIO_CHANNEL_RC; + case MP_SPEAKER_ID_SL: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_SR: return SPA_AUDIO_CHANNEL_SR; + case MP_SPEAKER_ID_TC: return SPA_AUDIO_CHANNEL_TC; + case MP_SPEAKER_ID_TFL: return SPA_AUDIO_CHANNEL_TFL; + case MP_SPEAKER_ID_TFC: return SPA_AUDIO_CHANNEL_TFC; + case MP_SPEAKER_ID_TFR: return SPA_AUDIO_CHANNEL_TFR; + case MP_SPEAKER_ID_TBL: return SPA_AUDIO_CHANNEL_TRL; + case MP_SPEAKER_ID_TBC: return SPA_AUDIO_CHANNEL_TRC; + case MP_SPEAKER_ID_TBR: return SPA_AUDIO_CHANNEL_TRR; + case MP_SPEAKER_ID_DL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_DR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_WL: return SPA_AUDIO_CHANNEL_FL; + case MP_SPEAKER_ID_WR: return SPA_AUDIO_CHANNEL_FR; + case MP_SPEAKER_ID_SDL: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_SDR: return SPA_AUDIO_CHANNEL_SL; + case MP_SPEAKER_ID_LFE2: return SPA_AUDIO_CHANNEL_LFE2; + case MP_SPEAKER_ID_TSL: return SPA_AUDIO_CHANNEL_TSL; + case MP_SPEAKER_ID_TSR: return SPA_AUDIO_CHANNEL_TSR; + case MP_SPEAKER_ID_BFC: return SPA_AUDIO_CHANNEL_BC; + case MP_SPEAKER_ID_BFL: return SPA_AUDIO_CHANNEL_BLC; + case MP_SPEAKER_ID_BFR: return SPA_AUDIO_CHANNEL_BRC; + case MP_SPEAKER_ID_NA: return SPA_AUDIO_CHANNEL_NA; + default: + MP_WARN(ao, "Unhandled channel %d\n", mp_speaker_id); + return SPA_AUDIO_CHANNEL_UNKNOWN; + }; +} + +static void on_process(void *userdata) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + struct pw_time time; + struct pw_buffer *b; + void *data[MP_NUM_CHANNELS]; + + if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) { + MP_WARN(ao, "out of buffers: %s\n", strerror(errno)); + return; + } + + struct spa_buffer *buf = b->buffer; + + int bytes_per_channel = buf->datas[0].maxsize / ao->channels.num; + int nframes = bytes_per_channel / ao->sstride; +#if PW_CHECK_VERSION(0, 3, 49) + if (b->requested != 0) + nframes = MPMIN(b->requested, nframes); +#endif + + for (int i = 0; i < buf->n_datas; i++) + data[i] = buf->datas[i].data; + + pw_stream_get_time_n(p->stream, &time, sizeof(time)); + if (time.rate.denom == 0) + time.rate.denom = ao->samplerate; + if (time.rate.num == 0) + time.rate.num = 1; + + int64_t end_time = mp_time_ns(); + /* time.queued is always going to be 0, so we don't need to care */ + end_time += (nframes * 1e9 / ao->samplerate) + + ((double) time.delay * SPA_NSEC_PER_SEC * time.rate.num / time.rate.denom); + + int samples = ao_read_data_nonblocking(ao, data, nframes, end_time); + b->size = samples; + + for (int i = 0; i < buf->n_datas; i++) { + buf->datas[i].chunk->size = samples * ao->sstride; + buf->datas[i].chunk->offset = 0; + buf->datas[i].chunk->stride = ao->sstride; + } + + pw_stream_queue_buffer(p->stream, b); + + MP_TRACE(ao, "queued %d of %d samples\n", samples, nframes); +} + +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + /* We want to know when our node is linked. + * As there is no proper callback for this we use the Latency param for this + */ + if (id == SPA_PARAM_Latency) { + p->init_state = INIT_STATE_SUCCESS; + pw_thread_loop_signal(p->loop, false); + } + + if (param == NULL || id != SPA_PARAM_Format) + return; + + int buffer_size = ao->device_buffer * af_fmt_to_bytes(ao->format) * ao->channels.num; + + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(ao->num_planes), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + buffer_size, 0, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ao->sstride)); + if (!params[0]) { + MP_ERR(ao, "Could not build parameter pod\n"); + return; + } + + if (pw_stream_update_params(p->stream, params, 1) < 0) { + MP_ERR(ao, "Could not update stream parameters\n"); + return; + } +} + +static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + MP_DBG(ao, "Stream state changed: old_state=%s state=%s error=%s\n", + pw_stream_state_as_string(old), pw_stream_state_as_string(state), error); + + if (state == PW_STREAM_STATE_ERROR) { + MP_WARN(ao, "Stream in error state, trying to reload...\n"); + p->init_state = INIT_STATE_ERROR; + pw_thread_loop_signal(p->loop, false); + ao_request_reload(ao); + } + + if (state == PW_STREAM_STATE_UNCONNECTED && old != PW_STREAM_STATE_UNCONNECTED) { + MP_WARN(ao, "Stream disconnected, trying to reload...\n"); + ao_request_reload(ao); + } +} + +static float spa_volume_to_mp_volume(float vol) +{ + return vol * 100; +} + +static float mp_volume_to_spa_volume(float vol) +{ + return vol / 100; +} + +static float volume_avg(float* vols, uint32_t n) +{ + float sum = 0.0; + for (int i = 0; i < n; i++) + sum += vols[i]; + return sum / n; +} + +static void on_control_info(void *userdata, uint32_t id, + const struct pw_stream_control *control) +{ + struct ao *ao = userdata; + struct priv *p = ao->priv; + + switch (id) { + case SPA_PROP_mute: + if (control->n_values == 1) + p->muted = control->values[0] >= 0.5; + break; + case SPA_PROP_channelVolumes: + if (p->options.volume_mode != VOLUME_MODE_CHANNEL) + break; + if (control->n_values > 0) + p->volume = volume_avg(control->values, control->n_values); + break; + case SPA_PROP_volume: + if (p->options.volume_mode != VOLUME_MODE_GLOBAL) + break; + if (control->n_values > 0) + p->volume = control->values[0]; + break; + } +} + +static const struct pw_stream_events stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process, + .state_changed = on_state_changed, + .control_info = on_control_info, +}; + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + if (p->loop) + pw_thread_loop_stop(p->loop); + spa_hook_remove(&p->stream_listener); + spa_zero(p->stream_listener); + if (p->stream) + pw_stream_destroy(p->stream); + p->stream = NULL; + if (p->core) + pw_context_destroy(pw_core_get_context(p->core)); + p->core = NULL; + if (p->loop) + pw_thread_loop_destroy(p->loop); + p->loop = NULL; + pw_deinit(); +} + +struct registry_event_global_ctx { + struct ao *ao; + void (*sink_cb) (struct ao *ao, uint32_t id, const struct spa_dict *props, void *sink_cb_ctx); + void *sink_cb_ctx; +}; + +static bool is_sink_node(const char *type, const struct spa_dict *props) +{ + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) + return false; + + if (!props) + return false; + + const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!class || strcmp(class, "Audio/Sink") != 0) + return false; + + return true; +} + +static void for_each_sink_registry_event_global(void *data, uint32_t id, + uint32_t permissions, const + char *type, uint32_t version, + const struct spa_dict *props) +{ + struct registry_event_global_ctx *ctx = data; + + if (!is_sink_node(type, props)) + return; + + ctx->sink_cb(ctx->ao, id, props, ctx->sink_cb_ctx); +} + + +struct for_each_done_ctx { + struct pw_thread_loop *loop; + bool done; +}; + +static const struct pw_registry_events for_each_sink_registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = for_each_sink_registry_event_global, +}; + +static void for_each_sink_done(void *data, uint32_t it, int seq) +{ + struct for_each_done_ctx *ctx = data; + ctx->done = true; + pw_thread_loop_signal(ctx->loop, false); +} + +static const struct pw_core_events for_each_sink_core_events = { + .version = PW_VERSION_CORE_EVENTS, + .done = for_each_sink_done, +}; + +static int for_each_sink(struct ao *ao, void (cb) (struct ao *ao, uint32_t id, + const struct spa_dict *props, void *ctx), void *cb_ctx) +{ + struct priv *priv = ao->priv; + struct pw_registry *registry; + struct spa_hook core_listener; + struct for_each_done_ctx done_ctx = { + .loop = priv->loop, + .done = false, + }; + int ret = -1; + + pw_thread_loop_lock(priv->loop); + + spa_zero(core_listener); + if (pw_core_add_listener(priv->core, &core_listener, &for_each_sink_core_events, &done_ctx) < 0) + goto unlock_loop; + + registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); + if (!registry) + goto remove_core_listener; + + pw_core_sync(priv->core, 0, 0); + + struct spa_hook registry_listener; + struct registry_event_global_ctx revents_ctx = { + .ao = ao, + .sink_cb = cb, + .sink_cb_ctx = cb_ctx, + }; + spa_zero(registry_listener); + if (pw_registry_add_listener(registry, ®istry_listener, &for_each_sink_registry_events, &revents_ctx) < 0) + goto destroy_registry; + + while (!done_ctx.done) + pw_thread_loop_wait(priv->loop); + + spa_hook_remove(®istry_listener); + + ret = 0; + +destroy_registry: + pw_proxy_destroy((struct pw_proxy *)registry); + +remove_core_listener: + spa_hook_remove(&core_listener); + +unlock_loop: + pw_thread_loop_unlock(priv->loop); + + return ret; +} + +static void have_sink(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) +{ + bool *b = ctx; + *b = true; +} + +static bool session_has_sinks(struct ao *ao) +{ + bool b = false; + + if (for_each_sink(ao, have_sink, &b) < 0) + MP_WARN(ao, "Could not list devices, sink detection may be wrong\n"); + + return b; +} + +static void on_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct ao *ao = data; + + MP_WARN(ao, "Error during playback: %s, %s\n", spa_strerror(res), message); +} + +static void on_core_info(void *data, const struct pw_core_info *info) +{ + struct ao *ao = data; + + MP_VERBOSE(ao, "Core user: %s\n", info->user_name); + MP_VERBOSE(ao, "Core host: %s\n", info->host_name); + MP_VERBOSE(ao, "Core version: %s\n", info->version); + MP_VERBOSE(ao, "Core name: %s\n", info->name); +} + +static const struct pw_core_events core_events = { + .version = PW_VERSION_CORE_EVENTS, + .error = on_error, + .info = on_core_info, +}; + +static int pipewire_init_boilerplate(struct ao *ao) +{ + struct priv *p = ao->priv; + struct pw_context *context; + + pw_init(NULL, NULL); + + MP_VERBOSE(ao, "Headers version: %s\n", pw_get_headers_version()); + MP_VERBOSE(ao, "Library version: %s\n", pw_get_library_version()); + + p->loop = pw_thread_loop_new("mpv/ao/pipewire", NULL); + if (p->loop == NULL) + return -1; + + pw_thread_loop_lock(p->loop); + + if (pw_thread_loop_start(p->loop) < 0) + goto error; + + context = pw_context_new( + pw_thread_loop_get_loop(p->loop), + pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL), + 0); + if (!context) + goto error; + + p->core = pw_context_connect( + context, + pw_properties_new(PW_KEY_REMOTE_NAME, p->options.remote, NULL), + 0); + if (!p->core) { + MP_MSG(ao, ao->probing ? MSGL_V : MSGL_ERR, + "Could not connect to context '%s': %s\n", + p->options.remote, strerror(errno)); + pw_context_destroy(context); + goto error; + } + + if (pw_core_add_listener(p->core, &p->core_listener, &core_events, ao) < 0) + goto error; + + pw_thread_loop_unlock(p->loop); + + if (!session_has_sinks(ao)) { + MP_VERBOSE(ao, "PipeWire does not have any audio sinks, skipping\n"); + return -1; + } + + return 0; + +error: + pw_thread_loop_unlock(p->loop); + return -1; +} + +static void wait_for_init_done(struct ao *ao) +{ + struct priv *p = ao->priv; + struct timespec abstime; + int r; + + r = pw_thread_loop_get_time(p->loop, &abstime, 50 * SPA_NSEC_PER_MSEC); + if (r < 0) { + MP_WARN(ao, "Could not get timeout for initialization: %s\n", spa_strerror(r)); + return; + } + + while (p->init_state == INIT_STATE_NONE) { + r = pw_thread_loop_timed_wait_full(p->loop, &abstime); + if (r < 0) { + MP_WARN(ao, "Could not wait for initialization: %s\n", spa_strerror(r)); + return; + } + } +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + const struct spa_pod *params[1]; + struct pw_properties *props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC ? "Music" : "Movie", + PW_KEY_NODE_NAME, ao->client_name, + PW_KEY_NODE_DESCRIPTION, ao->client_name, + PW_KEY_APP_NAME, ao->client_name, + PW_KEY_APP_ID, ao->client_name, + PW_KEY_APP_ICON_NAME, ao->client_name, + PW_KEY_NODE_ALWAYS_PROCESS, "true", + PW_KEY_TARGET_OBJECT, ao->device, + NULL + ); + + if (pipewire_init_boilerplate(ao) < 0) + goto error_props; + + if (p->options.buffer_msec) { + ao->device_buffer = p->options.buffer_msec * ao->samplerate / 1000; + + pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d", ao->device_buffer, ao->samplerate); + } + + pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", ao->samplerate); + + enum spa_audio_format spa_format = af_fmt_to_pw(ao, ao->format); + if (spa_format == SPA_AUDIO_FORMAT_UNKNOWN) { + ao->format = AF_FORMAT_FLOATP; + spa_format = SPA_AUDIO_FORMAT_F32P; + } + + struct spa_audio_info_raw audio_info = { + .format = spa_format, + .rate = ao->samplerate, + .channels = ao->channels.num, + }; + + for (int i = 0; i < ao->channels.num; i++) + audio_info.position[i] = mp_speaker_id_to_spa(ao, ao->channels.speaker[i]); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audio_info); + if (!params[0]) + goto error_props; + + if (af_fmt_is_planar(ao->format)) { + ao->num_planes = ao->channels.num; + ao->sstride = af_fmt_to_bytes(ao->format); + } else { + ao->num_planes = 1; + ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format); + } + + pw_thread_loop_lock(p->loop); + + p->stream = pw_stream_new(p->core, "audio-src", props); + if (p->stream == NULL) { + pw_thread_loop_unlock(p->loop); + goto error; + } + + pw_stream_add_listener(p->stream, &p->stream_listener, &stream_events, ao); + + enum pw_stream_flags flags = PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_INACTIVE | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS; + + if (ao->init_flags & AO_INIT_EXCLUSIVE) + flags |= PW_STREAM_FLAG_EXCLUSIVE; + + if (pw_stream_connect(p->stream, + PW_DIRECTION_OUTPUT, PW_ID_ANY, flags, params, 1) < 0) { + pw_thread_loop_unlock(p->loop); + goto error; + } + + wait_for_init_done(ao); + + pw_thread_loop_unlock(p->loop); + + if (p->init_state == INIT_STATE_ERROR) + goto error; + + return 0; + +error_props: + pw_properties_free(props); +error: + uninit(ao); + return -1; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + pw_thread_loop_lock(p->loop); + pw_stream_set_active(p->stream, false); + pw_stream_flush(p->stream, false); + pw_thread_loop_unlock(p->loop); +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + pw_thread_loop_lock(p->loop); + pw_stream_set_active(p->stream, true); + pw_thread_loop_unlock(p->loop); +} + +#define CONTROL_RET(r) (!r ? CONTROL_OK : CONTROL_ERROR) + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *p = ao->priv; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: { + float *vol = arg; + *vol = spa_volume_to_mp_volume(p->volume); + return CONTROL_OK; + } + case AOCONTROL_GET_MUTE: { + bool *muted = arg; + *muted = p->muted; + return CONTROL_OK; + } + case AOCONTROL_SET_VOLUME: + case AOCONTROL_SET_MUTE: + case AOCONTROL_UPDATE_STREAM_TITLE: { + int ret; + + pw_thread_loop_lock(p->loop); + switch (cmd) { + case AOCONTROL_SET_VOLUME: { + float *vol = arg; + uint8_t n = ao->channels.num; + if (p->options.volume_mode == VOLUME_MODE_CHANNEL) { + float values[MP_NUM_CHANNELS] = {0}; + for (int i = 0; i < n; i++) + values[i] = mp_volume_to_spa_volume(*vol); + ret = CONTROL_RET(pw_stream_set_control( + p->stream, SPA_PROP_channelVolumes, n, values, 0)); + } else { + float value = mp_volume_to_spa_volume(*vol); + ret = CONTROL_RET(pw_stream_set_control( + p->stream, SPA_PROP_volume, 1, &value, 0)); + } + break; + } + case AOCONTROL_SET_MUTE: { + bool *muted = arg; + float value = *muted ? 1.f : 0.f; + ret = CONTROL_RET(pw_stream_set_control(p->stream, SPA_PROP_mute, 1, &value, 0)); + break; + } + case AOCONTROL_UPDATE_STREAM_TITLE: { + char *title = arg; + struct spa_dict_item items[1]; + items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, title); + ret = CONTROL_RET(pw_stream_update_properties(p->stream, &SPA_DICT_INIT(items, MP_ARRAY_SIZE(items)))); + break; + } + default: + ret = CONTROL_NA; + } + pw_thread_loop_unlock(p->loop); + return ret; + } + default: + return CONTROL_UNKNOWN; + } +} + +static void add_device_to_list(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) +{ + struct ao_device_list *list = ctx; + const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME); + + if (!name) + return; + + const char *description = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); + + ao_device_list_add(list, ao, &(struct ao_device_desc){name, description}); +} + +static void hotplug_registry_global_cb(void *data, uint32_t id, + uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) +{ + struct ao *ao = data; + struct priv *priv = ao->priv; + + if (!is_sink_node(type, props)) + return; + + pw_thread_loop_lock(priv->loop); + struct id_list *item = talloc(ao, struct id_list); + item->id = id; + spa_list_init(&item->node); + spa_list_append(&priv->hotplug.sinks, &item->node); + pw_thread_loop_unlock(priv->loop); + + ao_hotplug_event(ao); +} + +static void hotplug_registry_global_remove_cb(void *data, uint32_t id) +{ + struct ao *ao = data; + struct priv *priv = ao->priv; + bool removed_sink = false; + + struct id_list *e; + + pw_thread_loop_lock(priv->loop); + spa_list_for_each(e, &priv->hotplug.sinks, node) { + if (e->id == id) { + removed_sink = true; + spa_list_remove(&e->node); + talloc_free(e); + break; + } + } + + pw_thread_loop_unlock(priv->loop); + + if (removed_sink) + ao_hotplug_event(ao); +} + +static const struct pw_registry_events hotplug_registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = hotplug_registry_global_cb, + .global_remove = hotplug_registry_global_remove_cb, +}; + +static int hotplug_init(struct ao *ao) +{ + struct priv *priv = ao->priv; + + int res = pipewire_init_boilerplate(ao); + if (res) + goto error_no_unlock; + + pw_thread_loop_lock(priv->loop); + + spa_zero(priv->hotplug); + spa_list_init(&priv->hotplug.sinks); + + priv->hotplug.registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); + if (!priv->hotplug.registry) + goto error; + + if (pw_registry_add_listener(priv->hotplug.registry, &priv->hotplug.registry_listener, &hotplug_registry_events, ao) < 0) { + pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); + goto error; + } + + pw_thread_loop_unlock(priv->loop); + + return res; + +error: + pw_thread_loop_unlock(priv->loop); +error_no_unlock: + uninit(ao); + return -1; +} + +static void hotplug_uninit(struct ao *ao) +{ + struct priv *priv = ao->priv; + + pw_thread_loop_lock(priv->loop); + + spa_hook_remove(&priv->hotplug.registry_listener); + pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); + + pw_thread_loop_unlock(priv->loop); + uninit(ao); +} + +static void list_devs(struct ao *ao, struct ao_device_list *list) +{ + ao_device_list_add(list, ao, &(struct ao_device_desc){}); + + if (for_each_sink(ao, add_device_to_list, list) < 0) + MP_WARN(ao, "Could not list devices, list may be incomplete\n"); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_pipewire = { + .description = "PipeWire audio output", + .name = "pipewire", + + .init = init, + .uninit = uninit, + .reset = reset, + .start = start, + + .control = control, + + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .list_devs = list_devs, + + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) + { + .loop = NULL, + .stream = NULL, + .init_state = INIT_STATE_NONE, + .options.buffer_msec = 0, + .options.volume_mode = VOLUME_MODE_CHANNEL, + }, + .options_prefix = "pipewire", + .options = (const struct m_option[]) { + {"buffer", OPT_CHOICE(options.buffer_msec, {"native", 0}), + M_RANGE(1, 2000)}, + {"remote", OPT_STRING(options.remote) }, + {"volume-mode", OPT_CHOICE(options.volume_mode, + {"channel", VOLUME_MODE_CHANNEL}, {"global", VOLUME_MODE_GLOBAL})}, + {0} + }, +}; diff --git a/audio/out/ao_pulse.c b/audio/out/ao_pulse.c new file mode 100644 index 0000000..3b29b1a --- /dev/null +++ b/audio/out/ao_pulse.c @@ -0,0 +1,817 @@ +/* + * PulseAudio audio output driver. + * Copyright (C) 2006 Lennart Poettering + * Copyright (C) 2007 Reimar Doeffinger + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <stdint.h> +#include <math.h> + +#include <pulse/pulseaudio.h> + +#include "audio/format.h" +#include "common/msg.h" +#include "options/m_option.h" +#include "ao.h" +#include "internal.h" + +#define VOL_PA2MP(v) ((v) * 100.0 / PA_VOLUME_NORM) +#define VOL_MP2PA(v) lrint((v) * PA_VOLUME_NORM / 100) + +struct priv { + // PulseAudio playback stream object + struct pa_stream *stream; + + // PulseAudio connection context + struct pa_context *context; + + // Main event loop object + struct pa_threaded_mainloop *mainloop; + + // temporary during control() + struct pa_sink_input_info pi; + + int retval; + bool playing; + bool underrun_signalled; + + char *cfg_host; + int cfg_buffer; + bool cfg_latency_hacks; + bool cfg_allow_suspended; +}; + +#define GENERIC_ERR_MSG(str) \ + MP_ERR(ao, str": %s\n", \ + pa_strerror(pa_context_errno(((struct priv *)ao->priv)->context))) + +static void context_state_cb(pa_context *c, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + switch (pa_context_get_state(c)) { + case PA_CONTEXT_READY: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + pa_threaded_mainloop_signal(priv->mainloop, 0); + break; + } +} + +static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, + uint32_t idx, void *userdata) +{ + struct ao *ao = userdata; + int type = t & PA_SUBSCRIPTION_MASK_SINK; + int fac = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + if ((type == PA_SUBSCRIPTION_EVENT_NEW || type == PA_SUBSCRIPTION_EVENT_REMOVE) + && fac == PA_SUBSCRIPTION_EVENT_SINK) + { + ao_hotplug_event(ao); + } +} + +static void context_success_cb(pa_context *c, int success, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + priv->retval = success; + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +static void stream_state_cb(pa_stream *s, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + switch (pa_stream_get_state(s)) { + case PA_STREAM_FAILED: + MP_VERBOSE(ao, "Stream failed.\n"); + ao_request_reload(ao); + pa_threaded_mainloop_signal(priv->mainloop, 0); + break; + case PA_STREAM_READY: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(priv->mainloop, 0); + break; + } +} + +static void stream_request_cb(pa_stream *s, size_t length, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + ao_wakeup_playthread(ao); + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +static void stream_latency_update_cb(pa_stream *s, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +static void underflow_cb(pa_stream *s, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + priv->playing = false; + priv->underrun_signalled = true; + ao_wakeup_playthread(ao); + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +static void success_cb(pa_stream *s, int success, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + priv->retval = success; + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +// Like waitop(), but keep the lock (even if it may unlock temporarily). +static bool waitop_no_unlock(struct priv *priv, pa_operation *op) +{ + if (!op) + return false; + pa_operation_state_t state = pa_operation_get_state(op); + while (state == PA_OPERATION_RUNNING) { + pa_threaded_mainloop_wait(priv->mainloop); + state = pa_operation_get_state(op); + } + pa_operation_unref(op); + return state == PA_OPERATION_DONE; +} + +/** + * \brief waits for a pulseaudio operation to finish, frees it and + * unlocks the mainloop + * \param op operation to wait for + * \return 1 if operation has finished normally (DONE state), 0 otherwise + */ +static bool waitop(struct priv *priv, pa_operation *op) +{ + bool r = waitop_no_unlock(priv, op); + pa_threaded_mainloop_unlock(priv->mainloop); + return r; +} + +static const struct format_map { + int mp_format; + pa_sample_format_t pa_format; +} format_maps[] = { + {AF_FORMAT_FLOAT, PA_SAMPLE_FLOAT32NE}, + {AF_FORMAT_S32, PA_SAMPLE_S32NE}, + {AF_FORMAT_S16, PA_SAMPLE_S16NE}, + {AF_FORMAT_U8, PA_SAMPLE_U8}, + {AF_FORMAT_UNKNOWN, 0} +}; + +static pa_encoding_t map_digital_format(int format) +{ + switch (format) { + case AF_FORMAT_S_AC3: return PA_ENCODING_AC3_IEC61937; + case AF_FORMAT_S_EAC3: return PA_ENCODING_EAC3_IEC61937; + case AF_FORMAT_S_MP3: return PA_ENCODING_MPEG_IEC61937; + case AF_FORMAT_S_DTS: return PA_ENCODING_DTS_IEC61937; +#ifdef PA_ENCODING_DTSHD_IEC61937 + case AF_FORMAT_S_DTSHD: return PA_ENCODING_DTSHD_IEC61937; +#endif +#ifdef PA_ENCODING_MPEG2_AAC_IEC61937 + case AF_FORMAT_S_AAC: return PA_ENCODING_MPEG2_AAC_IEC61937; +#endif +#ifdef PA_ENCODING_TRUEHD_IEC61937 + case AF_FORMAT_S_TRUEHD: return PA_ENCODING_TRUEHD_IEC61937; +#endif + default: + if (af_fmt_is_spdif(format)) + return PA_ENCODING_ANY; + return PA_ENCODING_PCM; + } +} + +static const int speaker_map[][2] = { + {PA_CHANNEL_POSITION_FRONT_LEFT, MP_SPEAKER_ID_FL}, + {PA_CHANNEL_POSITION_FRONT_RIGHT, MP_SPEAKER_ID_FR}, + {PA_CHANNEL_POSITION_FRONT_CENTER, MP_SPEAKER_ID_FC}, + {PA_CHANNEL_POSITION_REAR_CENTER, MP_SPEAKER_ID_BC}, + {PA_CHANNEL_POSITION_REAR_LEFT, MP_SPEAKER_ID_BL}, + {PA_CHANNEL_POSITION_REAR_RIGHT, MP_SPEAKER_ID_BR}, + {PA_CHANNEL_POSITION_LFE, MP_SPEAKER_ID_LFE}, + {PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER, MP_SPEAKER_ID_FLC}, + {PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER, MP_SPEAKER_ID_FRC}, + {PA_CHANNEL_POSITION_SIDE_LEFT, MP_SPEAKER_ID_SL}, + {PA_CHANNEL_POSITION_SIDE_RIGHT, MP_SPEAKER_ID_SR}, + {PA_CHANNEL_POSITION_TOP_CENTER, MP_SPEAKER_ID_TC}, + {PA_CHANNEL_POSITION_TOP_FRONT_LEFT, MP_SPEAKER_ID_TFL}, + {PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, MP_SPEAKER_ID_TFR}, + {PA_CHANNEL_POSITION_TOP_FRONT_CENTER, MP_SPEAKER_ID_TFC}, + {PA_CHANNEL_POSITION_TOP_REAR_LEFT, MP_SPEAKER_ID_TBL}, + {PA_CHANNEL_POSITION_TOP_REAR_RIGHT, MP_SPEAKER_ID_TBR}, + {PA_CHANNEL_POSITION_TOP_REAR_CENTER, MP_SPEAKER_ID_TBC}, + {PA_CHANNEL_POSITION_INVALID, -1} +}; + +static bool chmap_pa_from_mp(pa_channel_map *dst, struct mp_chmap *src) +{ + if (src->num > PA_CHANNELS_MAX) + return false; + dst->channels = src->num; + if (mp_chmap_equals(src, &(const struct mp_chmap)MP_CHMAP_INIT_MONO)) { + dst->map[0] = PA_CHANNEL_POSITION_MONO; + return true; + } + for (int n = 0; n < src->num; n++) { + int mp_speaker = src->speaker[n]; + int pa_speaker = PA_CHANNEL_POSITION_INVALID; + for (int i = 0; speaker_map[i][1] != -1; i++) { + if (speaker_map[i][1] == mp_speaker) { + pa_speaker = speaker_map[i][0]; + break; + } + } + if (pa_speaker == PA_CHANNEL_POSITION_INVALID) + return false; + dst->map[n] = pa_speaker; + } + return true; +} + +static bool select_chmap(struct ao *ao, pa_channel_map *dst) +{ + struct mp_chmap_sel sel = {0}; + for (int n = 0; speaker_map[n][1] != -1; n++) + mp_chmap_sel_add_speaker(&sel, speaker_map[n][1]); + return ao_chmap_sel_adjust(ao, &sel, &ao->channels) && + chmap_pa_from_mp(dst, &ao->channels); +} + +static void uninit(struct ao *ao) +{ + struct priv *priv = ao->priv; + + if (priv->mainloop) + pa_threaded_mainloop_stop(priv->mainloop); + + if (priv->stream) { + pa_stream_disconnect(priv->stream); + pa_stream_unref(priv->stream); + priv->stream = NULL; + } + + if (priv->context) { + pa_context_disconnect(priv->context); + pa_context_unref(priv->context); + priv->context = NULL; + } + + if (priv->mainloop) { + pa_threaded_mainloop_free(priv->mainloop); + priv->mainloop = NULL; + } +} + +static int pa_init_boilerplate(struct ao *ao) +{ + struct priv *priv = ao->priv; + char *host = priv->cfg_host && priv->cfg_host[0] ? priv->cfg_host : NULL; + bool locked = false; + + if (!(priv->mainloop = pa_threaded_mainloop_new())) { + MP_ERR(ao, "Failed to allocate main loop\n"); + goto fail; + } + + if (pa_threaded_mainloop_start(priv->mainloop) < 0) + goto fail; + + pa_threaded_mainloop_lock(priv->mainloop); + locked = true; + + if (!(priv->context = pa_context_new(pa_threaded_mainloop_get_api( + priv->mainloop), ao->client_name))) + { + MP_ERR(ao, "Failed to allocate context\n"); + goto fail; + } + + MP_VERBOSE(ao, "Library version: %s\n", pa_get_library_version()); + MP_VERBOSE(ao, "Proto: %lu\n", + (long)pa_context_get_protocol_version(priv->context)); + MP_VERBOSE(ao, "Server proto: %lu\n", + (long)pa_context_get_server_protocol_version(priv->context)); + + pa_context_set_state_callback(priv->context, context_state_cb, ao); + pa_context_set_subscribe_callback(priv->context, subscribe_cb, ao); + + if (pa_context_connect(priv->context, host, 0, NULL) < 0) + goto fail; + + /* Wait until the context is ready */ + while (1) { + int state = pa_context_get_state(priv->context); + if (state == PA_CONTEXT_READY) + break; + if (!PA_CONTEXT_IS_GOOD(state)) + goto fail; + pa_threaded_mainloop_wait(priv->mainloop); + } + + pa_threaded_mainloop_unlock(priv->mainloop); + return 0; + +fail: + if (locked) + pa_threaded_mainloop_unlock(priv->mainloop); + + if (priv->context) { + pa_threaded_mainloop_lock(priv->mainloop); + if (!(pa_context_errno(priv->context) == PA_ERR_CONNECTIONREFUSED + && ao->probing)) + GENERIC_ERR_MSG("Init failed"); + pa_threaded_mainloop_unlock(priv->mainloop); + } + uninit(ao); + return -1; +} + +static bool set_format(struct ao *ao, pa_format_info *format) +{ + ao->format = af_fmt_from_planar(ao->format); + + format->encoding = map_digital_format(ao->format); + if (format->encoding == PA_ENCODING_PCM) { + const struct format_map *fmt_map = format_maps; + + while (fmt_map->mp_format != ao->format) { + if (fmt_map->mp_format == AF_FORMAT_UNKNOWN) { + MP_VERBOSE(ao, "Unsupported format, using default\n"); + fmt_map = format_maps; + break; + } + fmt_map++; + } + ao->format = fmt_map->mp_format; + + pa_format_info_set_sample_format(format, fmt_map->pa_format); + } + + struct pa_channel_map map; + + if (!select_chmap(ao, &map)) + return false; + + pa_format_info_set_rate(format, ao->samplerate); + pa_format_info_set_channels(format, ao->channels.num); + pa_format_info_set_channel_map(format, &map); + + return ao->samplerate < PA_RATE_MAX && pa_format_info_valid(format); +} + +static int init(struct ao *ao) +{ + pa_proplist *proplist = NULL; + pa_format_info *format = NULL; + struct priv *priv = ao->priv; + char *sink = ao->device; + + if (pa_init_boilerplate(ao) < 0) + return -1; + + pa_threaded_mainloop_lock(priv->mainloop); + + if (!(proplist = pa_proplist_new())) { + MP_ERR(ao, "Failed to allocate proplist\n"); + goto unlock_and_fail; + } + (void)pa_proplist_sets(proplist, PA_PROP_MEDIA_ICON_NAME, ao->client_name); + + if (!(format = pa_format_info_new())) + goto unlock_and_fail; + + if (!set_format(ao, format)) { + ao->channels = (struct mp_chmap) MP_CHMAP_INIT_STEREO; + ao->samplerate = 48000; + ao->format = AF_FORMAT_FLOAT; + if (!set_format(ao, format)) { + MP_ERR(ao, "Invalid audio format\n"); + goto unlock_and_fail; + } + } + + if (!(priv->stream = pa_stream_new_extended(priv->context, "audio stream", + &format, 1, proplist))) + goto unlock_and_fail; + + pa_format_info_free(format); + format = NULL; + + pa_proplist_free(proplist); + proplist = NULL; + + pa_stream_set_state_callback(priv->stream, stream_state_cb, ao); + pa_stream_set_write_callback(priv->stream, stream_request_cb, ao); + pa_stream_set_latency_update_callback(priv->stream, + stream_latency_update_cb, ao); + pa_stream_set_underflow_callback(priv->stream, underflow_cb, ao); + uint32_t buf_size = ao->samplerate * (priv->cfg_buffer / 1000.0) * + af_fmt_to_bytes(ao->format) * ao->channels.num; + pa_buffer_attr bufattr = { + .maxlength = -1, + .tlength = buf_size > 0 ? buf_size : -1, + .prebuf = 0, + .minreq = -1, + .fragsize = -1, + }; + + int flags = PA_STREAM_NOT_MONOTONIC | PA_STREAM_START_CORKED; + if (!priv->cfg_latency_hacks) + flags |= PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_AUTO_TIMING_UPDATE; + + if (pa_stream_connect_playback(priv->stream, sink, &bufattr, + flags, NULL, NULL) < 0) + goto unlock_and_fail; + + /* Wait until the stream is ready */ + while (1) { + int state = pa_stream_get_state(priv->stream); + if (state == PA_STREAM_READY) + break; + if (!PA_STREAM_IS_GOOD(state)) + goto unlock_and_fail; + pa_threaded_mainloop_wait(priv->mainloop); + } + + if (pa_stream_is_suspended(priv->stream) && !priv->cfg_allow_suspended) { + MP_ERR(ao, "The stream is suspended. Bailing out.\n"); + goto unlock_and_fail; + } + + const pa_buffer_attr* final_bufattr = pa_stream_get_buffer_attr(priv->stream); + if(!final_bufattr) { + MP_ERR(ao, "PulseAudio didn't tell us what buffer sizes it set. Bailing out.\n"); + goto unlock_and_fail; + } + ao->device_buffer = final_bufattr->tlength / + af_fmt_to_bytes(ao->format) / ao->channels.num; + + pa_threaded_mainloop_unlock(priv->mainloop); + return 0; + +unlock_and_fail: + pa_threaded_mainloop_unlock(priv->mainloop); + + if (format) + pa_format_info_free(format); + + if (proplist) + pa_proplist_free(proplist); + + uninit(ao); + return -1; +} + +static void cork(struct ao *ao, bool pause) +{ + struct priv *priv = ao->priv; + pa_threaded_mainloop_lock(priv->mainloop); + priv->retval = 0; + if (waitop_no_unlock(priv, pa_stream_cork(priv->stream, pause, success_cb, ao)) + && priv->retval) + { + if (!pause) + priv->playing = true; + } else { + GENERIC_ERR_MSG("pa_stream_cork() failed"); + priv->playing = false; + } + pa_threaded_mainloop_unlock(priv->mainloop); +} + +// Play the specified data to the pulseaudio server +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *priv = ao->priv; + bool res = true; + pa_threaded_mainloop_lock(priv->mainloop); + if (pa_stream_write(priv->stream, data[0], samples * ao->sstride, NULL, 0, + PA_SEEK_RELATIVE) < 0) { + GENERIC_ERR_MSG("pa_stream_write() failed"); + res = false; + } + pa_threaded_mainloop_unlock(priv->mainloop); + return res; +} + +static void start(struct ao *ao) +{ + cork(ao, false); +} + +// Reset the audio stream, i.e. flush the playback buffer on the server side +static void reset(struct ao *ao) +{ + // pa_stream_flush() works badly if not corked + cork(ao, true); + struct priv *priv = ao->priv; + pa_threaded_mainloop_lock(priv->mainloop); + priv->playing = false; + priv->retval = 0; + if (!waitop(priv, pa_stream_flush(priv->stream, success_cb, ao)) || + !priv->retval) + GENERIC_ERR_MSG("pa_stream_flush() failed"); +} + +static bool set_pause(struct ao *ao, bool paused) +{ + cork(ao, paused); + return true; +} + +static double get_delay_hackfixed(struct ao *ao) +{ + /* This code basically does what pa_stream_get_latency() _should_ + * do, but doesn't due to multiple known bugs in PulseAudio (at + * PulseAudio version 2.1). In particular, the timing interpolation + * mode (PA_STREAM_INTERPOLATE_TIMING) can return completely bogus + * values, and the non-interpolating code has a bug causing too + * large results at end of stream (so a stream never seems to finish). + * This code can still return wrong values in some cases due to known + * PulseAudio bugs that can not be worked around on the client side. + * + * We always query the server for latest timing info. This may take + * too long to work well with remote audio servers, but at least + * this should be enough to fix the normal local playback case. + */ + struct priv *priv = ao->priv; + if (!waitop_no_unlock(priv, pa_stream_update_timing_info(priv->stream, + NULL, NULL))) + { + GENERIC_ERR_MSG("pa_stream_update_timing_info() failed"); + return 0; + } + const pa_timing_info *ti = pa_stream_get_timing_info(priv->stream); + if (!ti) { + GENERIC_ERR_MSG("pa_stream_get_timing_info() failed"); + return 0; + } + const struct pa_sample_spec *ss = pa_stream_get_sample_spec(priv->stream); + if (!ss) { + GENERIC_ERR_MSG("pa_stream_get_sample_spec() failed"); + return 0; + } + // data left in PulseAudio's main buffers (not written to sink yet) + int64_t latency = pa_bytes_to_usec(ti->write_index - ti->read_index, ss); + // since this info may be from a while ago, playback has progressed since + latency -= ti->transport_usec; + // data already moved from buffers to sink, but not played yet + int64_t sink_latency = ti->sink_usec; + if (!ti->playing) + /* At the end of a stream, part of the data "left" in the sink may + * be padding silence after the end; that should be subtracted to + * get the amount of real audio from our stream. This adjustment + * is missing from Pulseaudio's own get_latency calculations + * (as of PulseAudio 2.1). */ + sink_latency -= pa_bytes_to_usec(ti->since_underrun, ss); + if (sink_latency > 0) + latency += sink_latency; + if (latency < 0) + latency = 0; + return latency / 1e6; +} + +static double get_delay_pulse(struct ao *ao) +{ + struct priv *priv = ao->priv; + pa_usec_t latency = (pa_usec_t) -1; + while (pa_stream_get_latency(priv->stream, &latency, NULL) < 0) { + if (pa_context_errno(priv->context) != PA_ERR_NODATA) { + GENERIC_ERR_MSG("pa_stream_get_latency() failed"); + break; + } + /* Wait until latency data is available again */ + pa_threaded_mainloop_wait(priv->mainloop); + } + return latency == (pa_usec_t) -1 ? 0 : latency / 1000000.0; +} + +static void audio_get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *priv = ao->priv; + + pa_threaded_mainloop_lock(priv->mainloop); + + size_t space = pa_stream_writable_size(priv->stream); + state->free_samples = space == (size_t)-1 ? 0 : space / ao->sstride; + + state->queued_samples = ao->device_buffer - state->free_samples; // dunno + + if (priv->cfg_latency_hacks) { + state->delay = get_delay_hackfixed(ao); + } else { + state->delay = get_delay_pulse(ao); + } + + state->playing = priv->playing; + + pa_threaded_mainloop_unlock(priv->mainloop); + + // Otherwise, PA will keep hammering us for underruns (which it does instead + // of stopping the stream automatically). + if (!state->playing && priv->underrun_signalled) { + reset(ao); + priv->underrun_signalled = false; + } +} + +/* A callback function that is called when the + * pa_context_get_sink_input_info() operation completes. Saves the + * volume field of the specified structure to the global variable volume. + */ +static void info_func(struct pa_context *c, const struct pa_sink_input_info *i, + int is_last, void *userdata) +{ + struct ao *ao = userdata; + struct priv *priv = ao->priv; + if (is_last < 0) { + GENERIC_ERR_MSG("Failed to get sink input info"); + return; + } + if (!i) + return; + priv->pi = *i; + pa_threaded_mainloop_signal(priv->mainloop, 0); +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *priv = ao->priv; + switch (cmd) { + case AOCONTROL_GET_MUTE: + case AOCONTROL_GET_VOLUME: { + uint32_t devidx = pa_stream_get_index(priv->stream); + pa_threaded_mainloop_lock(priv->mainloop); + if (!waitop(priv, pa_context_get_sink_input_info(priv->context, devidx, + info_func, ao))) { + GENERIC_ERR_MSG("pa_context_get_sink_input_info() failed"); + return CONTROL_ERROR; + } + // Warning: some information in pi might be unaccessible, because + // we naively copied the struct, without updating pointers etc. + // Pointers might point to invalid data, accessors might fail. + if (cmd == AOCONTROL_GET_VOLUME) { + float *vol = arg; + *vol = VOL_PA2MP(pa_cvolume_avg(&priv->pi.volume)); + } else if (cmd == AOCONTROL_GET_MUTE) { + bool *mute = arg; + *mute = priv->pi.mute; + } + return CONTROL_OK; + } + + case AOCONTROL_SET_MUTE: + case AOCONTROL_SET_VOLUME: { + pa_threaded_mainloop_lock(priv->mainloop); + priv->retval = 0; + uint32_t stream_index = pa_stream_get_index(priv->stream); + if (cmd == AOCONTROL_SET_VOLUME) { + const float *vol = arg; + struct pa_cvolume volume; + + pa_cvolume_reset(&volume, ao->channels.num); + pa_cvolume_set(&volume, volume.channels, VOL_MP2PA(*vol)); + if (!waitop(priv, pa_context_set_sink_input_volume(priv->context, + stream_index, + &volume, + context_success_cb, ao)) || + !priv->retval) { + GENERIC_ERR_MSG("pa_context_set_sink_input_volume() failed"); + return CONTROL_ERROR; + } + } else if (cmd == AOCONTROL_SET_MUTE) { + const bool *mute = arg; + if (!waitop(priv, pa_context_set_sink_input_mute(priv->context, + stream_index, + *mute, + context_success_cb, ao)) || + !priv->retval) { + GENERIC_ERR_MSG("pa_context_set_sink_input_mute() failed"); + return CONTROL_ERROR; + } + } else { + MP_ASSERT_UNREACHABLE(); + } + return CONTROL_OK; + } + + case AOCONTROL_UPDATE_STREAM_TITLE: { + char *title = (char *)arg; + pa_threaded_mainloop_lock(priv->mainloop); + if (!waitop(priv, pa_stream_set_name(priv->stream, title, + success_cb, ao))) + { + GENERIC_ERR_MSG("pa_stream_set_name() failed"); + return CONTROL_ERROR; + } + return CONTROL_OK; + } + + default: + return CONTROL_UNKNOWN; + } +} + +struct sink_cb_ctx { + struct ao *ao; + struct ao_device_list *list; +}; + +static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *ud) +{ + struct sink_cb_ctx *ctx = ud; + struct priv *priv = ctx->ao->priv; + + if (eol) { + pa_threaded_mainloop_signal(priv->mainloop, 0); // wakeup waitop() + return; + } + + struct ao_device_desc entry = {.name = i->name, .desc = i->description}; + ao_device_list_add(ctx->list, ctx->ao, &entry); +} + +static int hotplug_init(struct ao *ao) +{ + struct priv *priv = ao->priv; + if (pa_init_boilerplate(ao) < 0) + return -1; + + pa_threaded_mainloop_lock(priv->mainloop); + waitop(priv, pa_context_subscribe(priv->context, PA_SUBSCRIPTION_MASK_SINK, + context_success_cb, ao)); + + return 0; +} + +static void list_devs(struct ao *ao, struct ao_device_list *list) +{ + struct priv *priv = ao->priv; + struct sink_cb_ctx ctx = {ao, list}; + + pa_threaded_mainloop_lock(priv->mainloop); + waitop(priv, pa_context_get_sink_info_list(priv->context, sink_info_cb, &ctx)); +} + +static void hotplug_uninit(struct ao *ao) +{ + uninit(ao); +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_pulse = { + .description = "PulseAudio audio output", + .name = "pulse", + .control = control, + .init = init, + .uninit = uninit, + .reset = reset, + .get_state = audio_get_state, + .write = audio_write, + .start = start, + .set_pause = set_pause, + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .list_devs = list_devs, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .cfg_buffer = 100, + }, + .options = (const struct m_option[]) { + {"host", OPT_STRING(cfg_host)}, + {"buffer", OPT_CHOICE(cfg_buffer, {"native", 0}), + M_RANGE(1, 2000)}, + {"latency-hacks", OPT_BOOL(cfg_latency_hacks)}, + {"allow-suspended", OPT_BOOL(cfg_allow_suspended)}, + {0} + }, + .options_prefix = "pulse", +}; diff --git a/audio/out/ao_sdl.c b/audio/out/ao_sdl.c new file mode 100644 index 0000000..5a6a58b --- /dev/null +++ b/audio/out/ao_sdl.c @@ -0,0 +1,216 @@ +/* + * audio output driver for SDL 1.2+ + * Copyright (C) 2012 Rudolf Polzer <divVerent@xonotic.org> + * + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "audio/format.h" +#include "mpv_talloc.h" +#include "ao.h" +#include "internal.h" +#include "common/common.h" +#include "common/msg.h" +#include "options/m_option.h" +#include "osdep/timer.h" + +#include <SDL.h> + +struct priv +{ + bool paused; + + float buflen; +}; + +static const int fmtmap[][2] = { + {AF_FORMAT_U8, AUDIO_U8}, + {AF_FORMAT_S16, AUDIO_S16SYS}, +#ifdef AUDIO_S32SYS + {AF_FORMAT_S32, AUDIO_S32SYS}, +#endif +#ifdef AUDIO_F32SYS + {AF_FORMAT_FLOAT, AUDIO_F32SYS}, +#endif + {0} +}; + +static void audio_callback(void *userdata, Uint8 *stream, int len) +{ + struct ao *ao = userdata; + + void *data[1] = {stream}; + + if (len % ao->sstride) + MP_ERR(ao, "SDL audio callback not sample aligned"); + + // Time this buffer will take, plus assume 1 period (1 callback invocation) + // fixed latency. + double delay = 2 * len / (double)ao->bps; + + ao_read_data(ao, data, len / ao->sstride, mp_time_ns() + MP_TIME_S_TO_NS(delay)); +} + +static void uninit(struct ao *ao) +{ + struct priv *priv = ao->priv; + if (!priv) + return; + + if (SDL_WasInit(SDL_INIT_AUDIO)) { + // make sure the callback exits + SDL_LockAudio(); + + // close audio device + SDL_QuitSubSystem(SDL_INIT_AUDIO); + } +} + +static unsigned int ceil_power_of_two(unsigned int x) +{ + int y = 1; + while (y < x) + y *= 2; + return y; +} + +static int init(struct ao *ao) +{ + if (SDL_WasInit(SDL_INIT_AUDIO)) { + MP_ERR(ao, "already initialized\n"); + return -1; + } + + struct priv *priv = ao->priv; + + if (SDL_InitSubSystem(SDL_INIT_AUDIO)) { + if (!ao->probing) + MP_ERR(ao, "SDL_Init failed\n"); + uninit(ao); + return -1; + } + + struct mp_chmap_sel sel = {0}; + mp_chmap_sel_add_waveext_def(&sel); + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) { + uninit(ao); + return -1; + } + + ao->format = af_fmt_from_planar(ao->format); + + SDL_AudioSpec desired = {0}; + desired.format = AUDIO_S16SYS; + for (int n = 0; fmtmap[n][0]; n++) { + if (ao->format == fmtmap[n][0]) { + desired.format = fmtmap[n][1]; + break; + } + } + desired.freq = ao->samplerate; + desired.channels = ao->channels.num; + if (priv->buflen) { + desired.samples = MPMIN(32768, ceil_power_of_two(ao->samplerate * + priv->buflen)); + } + desired.callback = audio_callback; + desired.userdata = ao; + + MP_VERBOSE(ao, "requested format: %d Hz, %d channels, %x, " + "buffer size: %d samples\n", + (int) desired.freq, (int) desired.channels, + (int) desired.format, (int) desired.samples); + + SDL_AudioSpec obtained = desired; + if (SDL_OpenAudio(&desired, &obtained)) { + if (!ao->probing) + MP_ERR(ao, "could not open audio: %s\n", SDL_GetError()); + uninit(ao); + return -1; + } + + MP_VERBOSE(ao, "obtained format: %d Hz, %d channels, %x, " + "buffer size: %d samples\n", + (int) obtained.freq, (int) obtained.channels, + (int) obtained.format, (int) obtained.samples); + + // The sample count is usually the number of samples the callback requests, + // which we assume is the period size. Normally, ao.c will allocate a large + // enough buffer. But in case the period size should be pathologically + // large, this will help. + ao->device_buffer = 3 * obtained.samples; + + ao->format = 0; + for (int n = 0; fmtmap[n][0]; n++) { + if (obtained.format == fmtmap[n][1]) { + ao->format = fmtmap[n][0]; + break; + } + } + if (!ao->format) { + if (!ao->probing) + MP_ERR(ao, "could not find matching format\n"); + uninit(ao); + return -1; + } + + if (!ao_chmap_sel_get_def(ao, &sel, &ao->channels, obtained.channels)) { + uninit(ao); + return -1; + } + + ao->samplerate = obtained.freq; + + priv->paused = 1; + + return 1; +} + +static void reset(struct ao *ao) +{ + struct priv *priv = ao->priv; + if (!priv->paused) + SDL_PauseAudio(SDL_TRUE); + priv->paused = 1; +} + +static void start(struct ao *ao) +{ + struct priv *priv = ao->priv; + if (priv->paused) + SDL_PauseAudio(SDL_FALSE); + priv->paused = 0; +} + +#define OPT_BASE_STRUCT struct priv + +const struct ao_driver audio_out_sdl = { + .description = "SDL Audio", + .name = "sdl", + .init = init, + .uninit = uninit, + .reset = reset, + .start = start, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .buflen = 0, // use SDL default + }, + .options = (const struct m_option[]) { + {"buflen", OPT_FLOAT(buflen)}, + {0} + }, + .options_prefix = "sdl", +}; diff --git a/audio/out/ao_sndio.c b/audio/out/ao_sndio.c new file mode 100644 index 0000000..fce7139 --- /dev/null +++ b/audio/out/ao_sndio.c @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2008 Alexandre Ratchov <alex@caoua.org> + * Copyright (c) 2013 Christian Neukirchen <chneukirchen@gmail.com> + * Copyright (c) 2020 Rozhuk Ivan <rozhuk.im@gmail.com> + * Copyright (c) 2021 Andrew Krasavin <noiseless-ak@yandex.ru> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <poll.h> +#include <errno.h> +#include <sndio.h> + +#include "options/m_option.h" +#include "common/msg.h" + +#include "audio/format.h" +#include "ao.h" +#include "internal.h" + +struct priv { + struct sio_hdl *hdl; + struct sio_par par; + int delay; + bool playing; + int vol; + int havevol; + struct pollfd *pfd; +}; + + +static const struct mp_chmap sndio_layouts[] = { + {0}, /* empty */ + {1, {MP_SPEAKER_ID_FL}}, /* mono */ + MP_CHMAP2(FL, FR), /* stereo */ + {0}, /* 2.1 */ + MP_CHMAP4(FL, FR, BL, BR), /* 4.0 */ + {0}, /* 5.0 */ + MP_CHMAP6(FL, FR, BL, BR, FC, LFE), /* 5.1 */ + {0}, /* 6.1 */ + MP_CHMAP8(FL, FR, BL, BR, FC, LFE, SL, SR), /* 7.1 */ + /* Above is the fixed channel assignment for sndio, since we need to + * fill all channels and cannot insert silence, not all layouts are + * supported. + * NOTE: MP_SPEAKER_ID_NA could be used to add padding channels. */ +}; + +static void uninit(struct ao *ao); + + +/* Make libsndio call movecb(). */ +static void process_events(struct ao *ao) +{ + struct priv *p = ao->priv; + + int n = sio_pollfd(p->hdl, p->pfd, POLLOUT); + while (poll(p->pfd, n, 0) < 0 && errno == EINTR); + + sio_revents(p->hdl, p->pfd); +} + +/* Call-back invoked to notify of the hardware position. */ +static void movecb(void *addr, int delta) +{ + struct ao *ao = addr; + struct priv *p = ao->priv; + + p->delay -= delta; +} + +/* Call-back invoked to notify about volume changes. */ +static void volcb(void *addr, unsigned newvol) +{ + struct ao *ao = addr; + struct priv *p = ao->priv; + + p->vol = newvol; +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + struct mp_chmap_sel sel = {0}; + size_t i; + struct af_to_par { + int format, bits, sig; + }; + static const struct af_to_par af_to_par[] = { + {AF_FORMAT_U8, 8, 0}, + {AF_FORMAT_S16, 16, 1}, + {AF_FORMAT_S32, 32, 1}, + }; + const struct af_to_par *ap; + const char *device = ((ao->device) ? ao->device : SIO_DEVANY); + + /* Opening device. */ + MP_VERBOSE(ao, "Using '%s' audio device.\n", device); + p->hdl = sio_open(device, SIO_PLAY, 0); + if (p->hdl == NULL) { + MP_ERR(ao, "Can't open audio device %s.\n", device); + goto err_out; + } + + sio_initpar(&p->par); + + /* Selecting sound format. */ + ao->format = af_fmt_from_planar(ao->format); + + p->par.bits = 16; + p->par.sig = 1; + p->par.le = SIO_LE_NATIVE; + for (i = 0; i < MP_ARRAY_SIZE(af_to_par); i++) { + ap = &af_to_par[i]; + if (ap->format == ao->format) { + p->par.bits = ap->bits; + p->par.sig = ap->sig; + break; + } + } + + p->par.rate = ao->samplerate; + + /* Channels count. */ + for (i = 0; i < MP_ARRAY_SIZE(sndio_layouts); i++) { + mp_chmap_sel_add_map(&sel, &sndio_layouts[i]); + } + if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels)) + goto err_out; + + p->par.pchan = ao->channels.num; + p->par.appbufsz = p->par.rate * 250 / 1000; /* 250ms buffer */ + p->par.round = p->par.rate * 10 / 1000; /* 10ms block size */ + + if (!sio_setpar(p->hdl, &p->par)) { + MP_ERR(ao, "couldn't set params\n"); + goto err_out; + } + + /* Get current sound params. */ + if (!sio_getpar(p->hdl, &p->par)) { + MP_ERR(ao, "couldn't get params\n"); + goto err_out; + } + if (p->par.bps > 1 && p->par.le != SIO_LE_NATIVE) { + MP_ERR(ao, "swapped endian output not supported\n"); + goto err_out; + } + + /* Update sound params. */ + if (p->par.bits == 8 && p->par.bps == 1 && !p->par.sig) { + ao->format = AF_FORMAT_U8; + } else if (p->par.bits == 16 && p->par.bps == 2 && p->par.sig) { + ao->format = AF_FORMAT_S16; + } else if ((p->par.bits == 32 || p->par.msb) && p->par.bps == 4 && p->par.sig) { + ao->format = AF_FORMAT_S32; + } else { + MP_ERR(ao, "couldn't set format\n"); + goto err_out; + } + + p->havevol = sio_onvol(p->hdl, volcb, ao); + sio_onmove(p->hdl, movecb, ao); + + p->pfd = talloc_array_ptrtype(p, p->pfd, sio_nfds(p->hdl)); + if (!p->pfd) + goto err_out; + + ao->device_buffer = p->par.bufsz; + MP_VERBOSE(ao, "bufsz = %i, appbufsz = %i, round = %i\n", + p->par.bufsz, p->par.appbufsz, p->par.round); + + p->delay = 0; + p->playing = false; + if (!sio_start(p->hdl)) { + MP_ERR(ao, "start: sio_start() fail.\n"); + goto err_out; + } + + return 0; + +err_out: + uninit(ao); + return -1; +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + + if (p->hdl) { + sio_close(p->hdl); + p->hdl = NULL; + } + p->pfd = NULL; + p->playing = false; +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct priv *p = ao->priv; + float *vol = arg; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: + if (!p->havevol) + return CONTROL_FALSE; + *vol = p->vol * 100 / SIO_MAXVOL; + break; + case AOCONTROL_SET_VOLUME: + if (!p->havevol) + return CONTROL_FALSE; + sio_setvol(p->hdl, *vol * SIO_MAXVOL / 100); + break; + default: + return CONTROL_UNKNOWN; + } + return CONTROL_OK; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + + if (p->playing) { + p->playing = false; + +#if HAVE_SNDIO_1_9 + if (!sio_flush(p->hdl)) { + MP_ERR(ao, "reset: couldn't sio_flush()\n"); +#else + if (!sio_stop(p->hdl)) { + MP_ERR(ao, "reset: couldn't sio_stop()\n"); +#endif + } + p->delay = 0; + if (!sio_start(p->hdl)) { + MP_ERR(ao, "reset: sio_start() fail.\n"); + } + } +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + + p->playing = true; + process_events(ao); +} + +static bool audio_write(struct ao *ao, void **data, int samples) +{ + struct priv *p = ao->priv; + const size_t size = (samples * ao->sstride); + size_t rc; + + rc = sio_write(p->hdl, data[0], size); + if (rc != size) { + MP_WARN(ao, "audio_write: unexpected partial write: required: %zu, written: %zu.\n", + size, rc); + reset(ao); + p->playing = false; + return false; + } + p->delay += samples; + + return true; +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *p = ao->priv; + + process_events(ao); + + /* how many samples we can play without blocking */ + state->free_samples = ao->device_buffer - p->delay; + state->free_samples = state->free_samples / p->par.round * p->par.round; + /* how many samples are already in the buffer to be played */ + state->queued_samples = p->delay; + /* delay in seconds between first and last sample in buffer */ + state->delay = p->delay / (double)p->par.rate; + + /* report unexpected EOF / underrun */ + if ((state->queued_samples && state->queued_samples && + (state->queued_samples < state->free_samples) && + p->playing) || sio_eof(p->hdl)) + { + MP_VERBOSE(ao, "get_state: EOF/underrun detected.\n"); + MP_VERBOSE(ao, "get_state: free: %d, queued: %d, delay: %lf\n", \ + state->free_samples, state->queued_samples, state->delay); + p->playing = false; + state->playing = p->playing; + ao_wakeup_playthread(ao); + } else { + state->playing = p->playing; + } +} + +const struct ao_driver audio_out_sndio = { + .name = "sndio", + .description = "sndio audio output", + .init = init, + .uninit = uninit, + .control = control, + .reset = reset, + .start = start, + .write = audio_write, + .get_state = get_state, + .priv_size = sizeof(struct priv), +}; diff --git a/audio/out/ao_wasapi.c b/audio/out/ao_wasapi.c new file mode 100644 index 0000000..b201f26 --- /dev/null +++ b/audio/out/ao_wasapi.c @@ -0,0 +1,504 @@ +/* + * This file is part of mpv. + * + * Original author: Jonathan Yong <10walls@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> +#include <inttypes.h> +#include <libavutil/mathematics.h> + +#include "options/m_option.h" +#include "osdep/threads.h" +#include "osdep/timer.h" +#include "osdep/io.h" +#include "misc/dispatch.h" +#include "ao_wasapi.h" + +// naive av_rescale for unsigned +static UINT64 uint64_scale(UINT64 x, UINT64 num, UINT64 den) +{ + return (x / den) * num + + ((x % den) * (num / den)) + + ((x % den) * (num % den)) / den; +} + +static HRESULT get_device_delay(struct wasapi_state *state, double *delay_ns) +{ + UINT64 sample_count = atomic_load(&state->sample_count); + UINT64 position, qpc_position; + HRESULT hr; + + hr = IAudioClock_GetPosition(state->pAudioClock, &position, &qpc_position); + EXIT_ON_ERROR(hr); + // GetPosition succeeded, but the result may be + // inaccurate due to the length of the call + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd370889%28v=vs.85%29.aspx + if (hr == S_FALSE) + MP_VERBOSE(state, "Possibly inaccurate device position.\n"); + + // convert position to number of samples careful to avoid overflow + UINT64 sample_position = uint64_scale(position, + state->format.Format.nSamplesPerSec, + state->clock_frequency); + INT64 diff = sample_count - sample_position; + *delay_ns = diff * 1e9 / state->format.Format.nSamplesPerSec; + + // Correct for any delay in IAudioClock_GetPosition above. + // This should normally be very small (<1 us), but just in case. . . + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + INT64 qpc_diff = av_rescale(qpc.QuadPart, 10000000, state->qpc_frequency.QuadPart) + - qpc_position; + // ignore the above calculation if it yields more than 10 seconds (due to + // possible overflow inside IAudioClock_GetPosition) + if (qpc_diff < 10 * 10000000) { + *delay_ns -= qpc_diff * 100.0; // convert to ns + } else { + MP_VERBOSE(state, "Insane qpc delay correction of %g seconds. " + "Ignoring it.\n", qpc_diff / 10000000.0); + } + + if (sample_count > 0 && *delay_ns <= 0) { + MP_WARN(state, "Under-run: Device delay: %g ns\n", *delay_ns); + } else { + MP_TRACE(state, "Device delay: %g ns\n", *delay_ns); + } + + return S_OK; +exit_label: + MP_ERR(state, "Error getting device delay: %s\n", mp_HRESULT_to_str(hr)); + return hr; +} + +static bool thread_feed(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + HRESULT hr; + + UINT32 frame_count = state->bufferFrameCount; + UINT32 padding; + hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding); + EXIT_ON_ERROR(hr); + bool refill = false; + if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) { + // Return if there's nothing to do. + if (frame_count <= padding) + return false; + // In shared mode, there is only one buffer of size bufferFrameCount. + // We must therefore take care not to overwrite the samples that have + // yet to play. + frame_count -= padding; + } else if (padding >= 2 * frame_count) { + // In exclusive mode, we exchange entire buffers of size + // bufferFrameCount with the device. If there are already two such + // full buffers waiting to play, there is no work to do. + return false; + } else if (padding < frame_count) { + // If there is not at least one full buffer of audio queued to play in + // exclusive mode, call this function again immediately to try and catch + // up and avoid a cascade of under-runs. WASAPI doesn't seem to be smart + // enough to send more feed events when it gets behind. + refill = true; + } + MP_TRACE(ao, "Frame to fill: %"PRIu32". Padding: %"PRIu32"\n", + frame_count, padding); + + double delay_ns; + hr = get_device_delay(state, &delay_ns); + EXIT_ON_ERROR(hr); + // add the buffer delay + delay_ns += frame_count * 1e9 / state->format.Format.nSamplesPerSec; + + BYTE *pData; + hr = IAudioRenderClient_GetBuffer(state->pRenderClient, + frame_count, &pData); + EXIT_ON_ERROR(hr); + + BYTE *data[1] = {pData}; + + ao_read_data_converted(ao, &state->convert_format, + (void **)data, frame_count, + mp_time_ns() + (int64_t)llrint(delay_ns)); + + // note, we can't use ao_read_data return value here since we already + // committed to frame_count above in the GetBuffer call + hr = IAudioRenderClient_ReleaseBuffer(state->pRenderClient, + frame_count, 0); + EXIT_ON_ERROR(hr); + + atomic_fetch_add(&state->sample_count, frame_count); + + return refill; +exit_label: + MP_ERR(state, "Error feeding audio: %s\n", mp_HRESULT_to_str(hr)); + MP_VERBOSE(ao, "Requesting ao reload\n"); + ao_request_reload(ao); + return false; +} + +static void thread_reset(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + HRESULT hr; + MP_DBG(state, "Thread Reset\n"); + hr = IAudioClient_Stop(state->pAudioClient); + if (FAILED(hr)) + MP_ERR(state, "IAudioClient_Stop returned: %s\n", mp_HRESULT_to_str(hr)); + + hr = IAudioClient_Reset(state->pAudioClient); + if (FAILED(hr)) + MP_ERR(state, "IAudioClient_Reset returned: %s\n", mp_HRESULT_to_str(hr)); + + atomic_store(&state->sample_count, 0); +} + +static void thread_resume(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + MP_DBG(state, "Thread Resume\n"); + thread_reset(ao); + thread_feed(ao); + + HRESULT hr = IAudioClient_Start(state->pAudioClient); + if (FAILED(hr)) { + MP_ERR(state, "IAudioClient_Start returned %s\n", + mp_HRESULT_to_str(hr)); + } +} + +static void thread_wakeup(void *ptr) +{ + struct ao *ao = ptr; + struct wasapi_state *state = ao->priv; + SetEvent(state->hWake); +} + +static void set_thread_state(struct ao *ao, + enum wasapi_thread_state thread_state) +{ + struct wasapi_state *state = ao->priv; + atomic_store(&state->thread_state, thread_state); + thread_wakeup(ao); +} + +static DWORD __stdcall AudioThread(void *lpParameter) +{ + struct ao *ao = lpParameter; + struct wasapi_state *state = ao->priv; + mp_thread_set_name("ao/wasapi"); + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + state->init_ok = wasapi_thread_init(ao); + SetEvent(state->hInitDone); + if (!state->init_ok) + goto exit_label; + + MP_DBG(ao, "Entering dispatch loop\n"); + while (true) { + if (WaitForSingleObject(state->hWake, INFINITE) != WAIT_OBJECT_0) + MP_ERR(ao, "Unexpected return value from WaitForSingleObject\n"); + + mp_dispatch_queue_process(state->dispatch, 0); + + int thread_state = atomic_load(&state->thread_state); + switch (thread_state) { + case WASAPI_THREAD_FEED: + // fill twice on under-full buffer (see comment in thread_feed) + if (thread_feed(ao) && thread_feed(ao)) + MP_ERR(ao, "Unable to fill buffer fast enough\n"); + break; + case WASAPI_THREAD_RESET: + thread_reset(ao); + break; + case WASAPI_THREAD_RESUME: + thread_resume(ao); + break; + case WASAPI_THREAD_SHUTDOWN: + thread_reset(ao); + goto exit_label; + default: + MP_ERR(ao, "Unhandled thread state: %d\n", thread_state); + } + // the default is to feed unless something else is requested + atomic_compare_exchange_strong(&state->thread_state, &thread_state, + WASAPI_THREAD_FEED); + } +exit_label: + wasapi_thread_uninit(ao); + + CoUninitialize(); + MP_DBG(ao, "Thread return\n"); + return 0; +} + +static void uninit(struct ao *ao) +{ + MP_DBG(ao, "Uninit wasapi\n"); + struct wasapi_state *state = ao->priv; + if (state->hWake) + set_thread_state(ao, WASAPI_THREAD_SHUTDOWN); + + if (state->hAudioThread && + WaitForSingleObject(state->hAudioThread, INFINITE) != WAIT_OBJECT_0) + { + MP_ERR(ao, "Unexpected return value from WaitForSingleObject " + "while waiting for audio thread to terminate\n"); + } + + SAFE_DESTROY(state->hInitDone, CloseHandle(state->hInitDone)); + SAFE_DESTROY(state->hWake, CloseHandle(state->hWake)); + SAFE_DESTROY(state->hAudioThread,CloseHandle(state->hAudioThread)); + + wasapi_change_uninit(ao); + + talloc_free(state->deviceID); + + CoUninitialize(); + MP_DBG(ao, "Uninit wasapi done\n"); +} + +static int init(struct ao *ao) +{ + MP_DBG(ao, "Init wasapi\n"); + CoInitializeEx(NULL, COINIT_MULTITHREADED); + + struct wasapi_state *state = ao->priv; + state->log = ao->log; + + state->opt_exclusive |= ao->init_flags & AO_INIT_EXCLUSIVE; + +#if !HAVE_UWP + state->deviceID = wasapi_find_deviceID(ao); + if (!state->deviceID) { + uninit(ao); + return -1; + } +#endif + + if (state->deviceID) + wasapi_change_init(ao, false); + + state->hInitDone = CreateEventW(NULL, FALSE, FALSE, NULL); + state->hWake = CreateEventW(NULL, FALSE, FALSE, NULL); + if (!state->hInitDone || !state->hWake) { + MP_FATAL(ao, "Error creating events\n"); + uninit(ao); + return -1; + } + + state->dispatch = mp_dispatch_create(state); + mp_dispatch_set_wakeup_fn(state->dispatch, thread_wakeup, ao); + + state->init_ok = false; + state->hAudioThread = CreateThread(NULL, 0, &AudioThread, ao, 0, NULL); + if (!state->hAudioThread) { + MP_FATAL(ao, "Failed to create audio thread\n"); + uninit(ao); + return -1; + } + + WaitForSingleObject(state->hInitDone, INFINITE); // wait on init complete + SAFE_DESTROY(state->hInitDone,CloseHandle(state->hInitDone)); + if (!state->init_ok) { + if (!ao->probing) + MP_FATAL(ao, "Received failure from audio thread\n"); + uninit(ao); + return -1; + } + + MP_DBG(ao, "Init wasapi done\n"); + return 0; +} + +static int thread_control_exclusive(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + if (!state->pEndpointVolume) + return CONTROL_UNKNOWN; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: + case AOCONTROL_SET_VOLUME: + if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_VOLUME)) + return CONTROL_FALSE; + break; + case AOCONTROL_GET_MUTE: + case AOCONTROL_SET_MUTE: + if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_MUTE)) + return CONTROL_FALSE; + break; + } + + float volume; + BOOL mute; + switch (cmd) { + case AOCONTROL_GET_VOLUME: + IAudioEndpointVolume_GetMasterVolumeLevelScalar( + state->pEndpointVolume, &volume); + *(float *)arg = volume; + return CONTROL_OK; + case AOCONTROL_SET_VOLUME: + volume = (*(float *)arg) / 100.f; + IAudioEndpointVolume_SetMasterVolumeLevelScalar( + state->pEndpointVolume, volume, NULL); + return CONTROL_OK; + case AOCONTROL_GET_MUTE: + IAudioEndpointVolume_GetMute(state->pEndpointVolume, &mute); + *(bool *)arg = mute; + return CONTROL_OK; + case AOCONTROL_SET_MUTE: + mute = *(bool *)arg; + IAudioEndpointVolume_SetMute(state->pEndpointVolume, mute, NULL); + return CONTROL_OK; + } + return CONTROL_UNKNOWN; +} + +static int thread_control_shared(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + if (!state->pAudioVolume) + return CONTROL_UNKNOWN; + + float volume; + BOOL mute; + switch(cmd) { + case AOCONTROL_GET_VOLUME: + ISimpleAudioVolume_GetMasterVolume(state->pAudioVolume, &volume); + *(float *)arg = volume; + return CONTROL_OK; + case AOCONTROL_SET_VOLUME: + volume = (*(float *)arg) / 100.f; + ISimpleAudioVolume_SetMasterVolume(state->pAudioVolume, volume, NULL); + return CONTROL_OK; + case AOCONTROL_GET_MUTE: + ISimpleAudioVolume_GetMute(state->pAudioVolume, &mute); + *(bool *)arg = mute; + return CONTROL_OK; + case AOCONTROL_SET_MUTE: + mute = *(bool *)arg; + ISimpleAudioVolume_SetMute(state->pAudioVolume, mute, NULL); + return CONTROL_OK; + } + return CONTROL_UNKNOWN; +} + +static int thread_control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + + // common to exclusive and shared + switch (cmd) { + case AOCONTROL_UPDATE_STREAM_TITLE: + if (!state->pSessionControl) + return CONTROL_FALSE; + + wchar_t *title = mp_from_utf8(NULL, (const char *)arg); + HRESULT hr = IAudioSessionControl_SetDisplayName(state->pSessionControl, + title,NULL); + talloc_free(title); + + if (SUCCEEDED(hr)) + return CONTROL_OK; + + MP_WARN(ao, "Error setting audio session name: %s\n", + mp_HRESULT_to_str(hr)); + + assert(ao->client_name); + if (!ao->client_name) + return CONTROL_ERROR; + + // Fallback to client name + title = mp_from_utf8(NULL, ao->client_name); + IAudioSessionControl_SetDisplayName(state->pSessionControl, + title, NULL); + talloc_free(title); + + return CONTROL_ERROR; + } + + return state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ? + thread_control_exclusive(ao, cmd, arg) : + thread_control_shared(ao, cmd, arg); +} + +static void run_control(void *p) +{ + void **pp = p; + struct ao *ao = pp[0]; + enum aocontrol cmd = *(enum aocontrol *)pp[1]; + void *arg = pp[2]; + *(int *)pp[3] = thread_control(ao, cmd, arg); +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + int ret; + void *p[] = {ao, &cmd, arg, &ret}; + mp_dispatch_run(state->dispatch, run_control, p); + return ret; +} + +static void audio_reset(struct ao *ao) +{ + set_thread_state(ao, WASAPI_THREAD_RESET); +} + +static void audio_resume(struct ao *ao) +{ + set_thread_state(ao, WASAPI_THREAD_RESUME); +} + +static void hotplug_uninit(struct ao *ao) +{ + MP_DBG(ao, "Hotplug uninit\n"); + wasapi_change_uninit(ao); + CoUninitialize(); +} + +static int hotplug_init(struct ao *ao) +{ + MP_DBG(ao, "Hotplug init\n"); + struct wasapi_state *state = ao->priv; + state->log = ao->log; + CoInitializeEx(NULL, COINIT_MULTITHREADED); + HRESULT hr = wasapi_change_init(ao, true); + EXIT_ON_ERROR(hr); + + return 0; + exit_label: + MP_FATAL(state, "Error setting up audio hotplug: %s\n", mp_HRESULT_to_str(hr)); + hotplug_uninit(ao); + return -1; +} + +#define OPT_BASE_STRUCT struct wasapi_state + +const struct ao_driver audio_out_wasapi = { + .description = "Windows WASAPI audio output (event mode)", + .name = "wasapi", + .init = init, + .uninit = uninit, + .control = control, + .reset = audio_reset, + .start = audio_resume, + .list_devs = wasapi_list_devs, + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .priv_size = sizeof(wasapi_state), +}; diff --git a/audio/out/ao_wasapi.h b/audio/out/ao_wasapi.h new file mode 100644 index 0000000..17b8f7a --- /dev/null +++ b/audio/out/ao_wasapi.h @@ -0,0 +1,116 @@ +/* + * This file is part of mpv. + * + * Original author: Jonathan Yong <10walls@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_AO_WASAPI_H_ +#define MP_AO_WASAPI_H_ + +#include <stdatomic.h> +#include <stdlib.h> +#include <stdbool.h> + +#include <windows.h> +#include <mmdeviceapi.h> +#include <audioclient.h> +#include <audiopolicy.h> +#include <endpointvolume.h> + +#include "common/msg.h" +#include "osdep/windows_utils.h" +#include "internal.h" +#include "ao.h" + +typedef struct change_notify { + IMMNotificationClient client; // this must be first in the structure! + IMMDeviceEnumerator *pEnumerator; // object where client is registered + LPWSTR monitored; // Monitored device + bool is_hotplug; + struct ao *ao; +} change_notify; + +HRESULT wasapi_change_init(struct ao* ao, bool is_hotplug); +void wasapi_change_uninit(struct ao* ao); + +enum wasapi_thread_state { + WASAPI_THREAD_FEED = 0, + WASAPI_THREAD_RESUME, + WASAPI_THREAD_RESET, + WASAPI_THREAD_SHUTDOWN +}; + +typedef struct wasapi_state { + struct mp_log *log; + + bool init_ok; // status of init phase + // Thread handles + HANDLE hInitDone; // set when init is complete in audio thread + HANDLE hAudioThread; // the audio thread itself + HANDLE hWake; // thread wakeup event + atomic_int thread_state; // enum wasapi_thread_state (what to do on wakeup) + struct mp_dispatch_queue *dispatch; // for volume/mute/session display + + // for setting the audio thread priority + HANDLE hTask; + + // ID of the device to use + LPWSTR deviceID; + // WASAPI object handles owned and used by audio thread + IMMDevice *pDevice; + IAudioClient *pAudioClient; + IAudioRenderClient *pRenderClient; + + // WASAPI internal clock information, for estimating delay + IAudioClock *pAudioClock; + atomic_ullong sample_count; // samples per channel written by GetBuffer + UINT64 clock_frequency; // scale for position returned by GetPosition + LARGE_INTEGER qpc_frequency; // frequency of Windows' high resolution timer + + // WASAPI control + IAudioSessionControl *pSessionControl; // setting the stream title + IAudioEndpointVolume *pEndpointVolume; // exclusive mode volume/mute + ISimpleAudioVolume *pAudioVolume; // shared mode volume/mute + DWORD vol_hw_support; // is hardware volume supported for exclusive-mode? + + // ao options + int opt_exclusive; + + // format info + WAVEFORMATEXTENSIBLE format; + AUDCLNT_SHAREMODE share_mode; // AUDCLNT_SHAREMODE_EXCLUSIVE / SHARED + UINT32 bufferFrameCount; // number of frames in buffer + struct ao_convert_fmt convert_format; + + change_notify change; +} wasapi_state; + +char *mp_PKEY_to_str_buf(char *buf, size_t buf_size, const PROPERTYKEY *pkey); +#define mp_PKEY_to_str(pkey) mp_PKEY_to_str_buf((char[42]){0}, 42, (pkey)) + +void wasapi_list_devs(struct ao *ao, struct ao_device_list *list); +bstr wasapi_get_specified_device_string(struct ao *ao); +LPWSTR wasapi_find_deviceID(struct ao *ao); + +bool wasapi_thread_init(struct ao *ao); +void wasapi_thread_uninit(struct ao *ao); + +#define EXIT_ON_ERROR(hres) \ + do { if (FAILED(hres)) { goto exit_label; } } while(0) +#define SAFE_DESTROY(unk, release) \ + do { if ((unk) != NULL) { release; (unk) = NULL; } } while(0) + +#endif diff --git a/audio/out/ao_wasapi_changenotify.c b/audio/out/ao_wasapi_changenotify.c new file mode 100644 index 0000000..f0e1895 --- /dev/null +++ b/audio/out/ao_wasapi_changenotify.c @@ -0,0 +1,246 @@ +/* + * This file is part of mpv. + * + * Original author: Jonathan Yong <10walls@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <wchar.h> + +#include "ao_wasapi.h" + +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_QueryInterface( + IMMNotificationClient* This, REFIID riid, void **ppvObject) +{ + // Compatible with IMMNotificationClient and IUnknown + if (IsEqualGUID(&IID_IMMNotificationClient, riid) || + IsEqualGUID(&IID_IUnknown, riid)) + { + *ppvObject = (void *)This; + return S_OK; + } else { + *ppvObject = NULL; + return E_NOINTERFACE; + } +} + +// these are required, but not actually used +static ULONG STDMETHODCALLTYPE sIMMNotificationClient_AddRef( + IMMNotificationClient *This) +{ + return 1; +} + +// MSDN says it should free itself, but we're static +static ULONG STDMETHODCALLTYPE sIMMNotificationClient_Release( + IMMNotificationClient *This) +{ + return 1; +} + +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceStateChanged( + IMMNotificationClient *This, + LPCWSTR pwstrDeviceId, + DWORD dwNewState) +{ + change_notify *change = (change_notify *)This; + struct ao *ao = change->ao; + + if (change->is_hotplug) { + MP_VERBOSE(ao, + "OnDeviceStateChanged triggered: sending hotplug event\n"); + ao_hotplug_event(ao); + } else if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) { + switch (dwNewState) { + case DEVICE_STATE_DISABLED: + case DEVICE_STATE_NOTPRESENT: + case DEVICE_STATE_UNPLUGGED: + MP_VERBOSE(ao, "OnDeviceStateChanged triggered on device %ls: " + "requesting ao reload\n", pwstrDeviceId); + ao_request_reload(ao); + break; + case DEVICE_STATE_ACTIVE: + break; + } + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceAdded( + IMMNotificationClient *This, + LPCWSTR pwstrDeviceId) +{ + change_notify *change = (change_notify *)This; + struct ao *ao = change->ao; + + if (change->is_hotplug) { + MP_VERBOSE(ao, "OnDeviceAdded triggered: sending hotplug event\n"); + ao_hotplug_event(ao); + } + + return S_OK; +} + +// maybe MPV can go over to the preferred device once it is plugged in? +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDeviceRemoved( + IMMNotificationClient *This, + LPCWSTR pwstrDeviceId) +{ + change_notify *change = (change_notify *)This; + struct ao *ao = change->ao; + + if (change->is_hotplug) { + MP_VERBOSE(ao, "OnDeviceRemoved triggered: sending hotplug event\n"); + ao_hotplug_event(ao); + } else if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) { + MP_VERBOSE(ao, "OnDeviceRemoved triggered for device %ls: " + "requesting ao reload\n", pwstrDeviceId); + ao_request_reload(ao); + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnDefaultDeviceChanged( + IMMNotificationClient *This, + EDataFlow flow, + ERole role, + LPCWSTR pwstrDeviceId) +{ + change_notify *change = (change_notify *)This; + struct ao *ao = change->ao; + + // don't care about "eCapture" or non-"eMultimedia" roles + if (flow == eCapture || role != eMultimedia) return S_OK; + + if (change->is_hotplug) { + MP_VERBOSE(ao, + "OnDefaultDeviceChanged triggered: sending hotplug event\n"); + ao_hotplug_event(ao); + } else { + // stay on the device the user specified + bstr device = wasapi_get_specified_device_string(ao); + if (device.len) { + MP_VERBOSE(ao, "OnDefaultDeviceChanged triggered: " + "staying on specified device %.*s\n", BSTR_P(device)); + return S_OK; + } + + // don't reload if already on the new default + if (pwstrDeviceId && !wcscmp(pwstrDeviceId, change->monitored)) { + MP_VERBOSE(ao, "OnDefaultDeviceChanged triggered: " + "already using default device, no reload required\n"); + return S_OK; + } + + // if we got here, we need to reload + MP_VERBOSE(ao, + "OnDefaultDeviceChanged triggered: requesting ao reload\n"); + ao_request_reload(ao); + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE sIMMNotificationClient_OnPropertyValueChanged( + IMMNotificationClient *This, + LPCWSTR pwstrDeviceId, + const PROPERTYKEY key) +{ + change_notify *change = (change_notify *)This; + struct ao *ao = change->ao; + + if (!change->is_hotplug && pwstrDeviceId && + !wcscmp(pwstrDeviceId, change->monitored)) + { + MP_VERBOSE(ao, "OnPropertyValueChanged triggered on device %ls\n", + pwstrDeviceId); + if (IsEqualPropertyKey(PKEY_AudioEngine_DeviceFormat, key)) { + MP_VERBOSE(change->ao, + "Changed property: PKEY_AudioEngine_DeviceFormat " + "- requesting ao reload\n"); + ao_request_reload(change->ao); + } else { + MP_VERBOSE(ao, "Changed property: %s\n", mp_PKEY_to_str(&key)); + } + } + + return S_OK; +} + +static CONST_VTBL IMMNotificationClientVtbl sIMMNotificationClientVtbl = { + .QueryInterface = sIMMNotificationClient_QueryInterface, + .AddRef = sIMMNotificationClient_AddRef, + .Release = sIMMNotificationClient_Release, + .OnDeviceStateChanged = sIMMNotificationClient_OnDeviceStateChanged, + .OnDeviceAdded = sIMMNotificationClient_OnDeviceAdded, + .OnDeviceRemoved = sIMMNotificationClient_OnDeviceRemoved, + .OnDefaultDeviceChanged = sIMMNotificationClient_OnDefaultDeviceChanged, + .OnPropertyValueChanged = sIMMNotificationClient_OnPropertyValueChanged, +}; + + +HRESULT wasapi_change_init(struct ao *ao, bool is_hotplug) +{ + struct wasapi_state *state = ao->priv; + struct change_notify *change = &state->change; + HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + &IID_IMMDeviceEnumerator, + (void **)&change->pEnumerator); + EXIT_ON_ERROR(hr); + + // so the callbacks can access the ao + change->ao = ao; + + // whether or not this is the hotplug instance + change->is_hotplug = is_hotplug; + + if (is_hotplug) { + MP_DBG(ao, "Monitoring for hotplug events\n"); + } else { + // Get the device string to compare with the pwstrDeviceId + change->monitored = state->deviceID; + MP_VERBOSE(ao, "Monitoring changes in device %ls\n", change->monitored); + } + + // COM voodoo to emulate c++ class + change->client.lpVtbl = &sIMMNotificationClientVtbl; + + // register the change notification client + hr = IMMDeviceEnumerator_RegisterEndpointNotificationCallback( + change->pEnumerator, (IMMNotificationClient *)change); + EXIT_ON_ERROR(hr); + + return hr; +exit_label: + MP_ERR(state, "Error setting up device change monitoring: %s\n", + mp_HRESULT_to_str(hr)); + wasapi_change_uninit(ao); + return hr; +} + +void wasapi_change_uninit(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + struct change_notify *change = &state->change; + + if (change->pEnumerator && change->client.lpVtbl) { + IMMDeviceEnumerator_UnregisterEndpointNotificationCallback( + change->pEnumerator, (IMMNotificationClient *)change); + } + + SAFE_RELEASE(change->pEnumerator); +} diff --git a/audio/out/ao_wasapi_utils.c b/audio/out/ao_wasapi_utils.c new file mode 100644 index 0000000..731fe8a --- /dev/null +++ b/audio/out/ao_wasapi_utils.c @@ -0,0 +1,1063 @@ +/* + * This file is part of mpv. + * + * Original author: Jonathan Yong <10walls@gmail.com> + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> +#include <wchar.h> +#include <windows.h> +#include <errors.h> +#include <ksguid.h> +#include <ksmedia.h> +#include <avrt.h> + +#include "audio/format.h" +#include "osdep/timer.h" +#include "osdep/io.h" +#include "osdep/strnlen.h" +#include "ao_wasapi.h" + +DEFINE_PROPERTYKEY(mp_PKEY_Device_FriendlyName, + 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, + 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); +DEFINE_PROPERTYKEY(mp_PKEY_Device_DeviceDesc, + 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, + 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); +// CEA 861 subformats +// should work on vista +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS, + 0x00000008, 0x0000, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL, + 0x00000092, 0x0000, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +// might require 7+ +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_AAC, + 0x00000006, 0x0cea, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_MPEG3, + 0x00000004, 0x0cea, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS, + 0x0000000a, 0x0cea, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD, + 0x0000000b, 0x0cea, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP, + 0x0000000c, 0x0cea, 0x0010, 0x80, 0x00, + 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); + +struct wasapi_sample_fmt { + int mp_format; // AF_FORMAT_* + int bits; // aka wBitsPerSample + int used_msb; // aka wValidBitsPerSample + const GUID *subtype; +}; + +// some common bit depths / container sizes (requests welcome) +// Entries that have the same mp_format must be: +// 1. consecutive +// 2. sorted by preferred format (worst comes last) +static const struct wasapi_sample_fmt wasapi_formats[] = { + {AF_FORMAT_U8, 8, 8, &KSDATAFORMAT_SUBTYPE_PCM}, + {AF_FORMAT_S16, 16, 16, &KSDATAFORMAT_SUBTYPE_PCM}, + {AF_FORMAT_S32, 32, 32, &KSDATAFORMAT_SUBTYPE_PCM}, + // compatible, assume LSBs are ignored + {AF_FORMAT_S32, 32, 24, &KSDATAFORMAT_SUBTYPE_PCM}, + // aka S24 (with conversion on output) + {AF_FORMAT_S32, 24, 24, &KSDATAFORMAT_SUBTYPE_PCM}, + {AF_FORMAT_FLOAT, 32, 32, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT}, + {AF_FORMAT_S_AC3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL}, + {AF_FORMAT_S_DTS, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS}, + {AF_FORMAT_S_AAC, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_AAC}, + {AF_FORMAT_S_MP3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_MPEG3}, + {AF_FORMAT_S_TRUEHD, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP}, + {AF_FORMAT_S_EAC3, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS}, + {AF_FORMAT_S_DTSHD, 16, 16, &mp_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD}, + {0}, +}; + +static void wasapi_get_best_sample_formats( + int src_format, struct wasapi_sample_fmt *out_formats) +{ + int mp_formats[AF_FORMAT_COUNT + 1]; + af_get_best_sample_formats(src_format, mp_formats); + for (int n = 0; mp_formats[n]; n++) { + for (int i = 0; wasapi_formats[i].mp_format; i++) { + if (wasapi_formats[i].mp_format == mp_formats[n]) + *out_formats++ = wasapi_formats[i]; + } + } + *out_formats = (struct wasapi_sample_fmt) {0}; +} + +static const GUID *format_to_subtype(int format) +{ + for (int i = 0; wasapi_formats[i].mp_format; i++) { + if (format == wasapi_formats[i].mp_format) + return wasapi_formats[i].subtype; + } + return &KSDATAFORMAT_SPECIFIER_NONE; +} + +char *mp_PKEY_to_str_buf(char *buf, size_t buf_size, const PROPERTYKEY *pkey) +{ + buf = mp_GUID_to_str_buf(buf, buf_size, &pkey->fmtid); + size_t guid_len = strnlen(buf, buf_size); + snprintf(buf + guid_len, buf_size - guid_len, ",%"PRIu32, + (uint32_t) pkey->pid); + return buf; +} + +static void update_waveformat_datarate(WAVEFORMATEXTENSIBLE *wformat) +{ + WAVEFORMATEX *wf = &wformat->Format; + wf->nBlockAlign = wf->nChannels * wf->wBitsPerSample / 8; + wf->nAvgBytesPerSec = wf->nSamplesPerSec * wf->nBlockAlign; +} + +static void set_waveformat(WAVEFORMATEXTENSIBLE *wformat, + struct wasapi_sample_fmt *format, + DWORD samplerate, struct mp_chmap *channels) +{ + wformat->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wformat->Format.nChannels = channels->num; + wformat->Format.nSamplesPerSec = samplerate; + wformat->Format.wBitsPerSample = format->bits; + wformat->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + + wformat->SubFormat = *format_to_subtype(format->mp_format); + wformat->Samples.wValidBitsPerSample = format->used_msb; + + uint64_t chans = mp_chmap_to_waveext(channels); + wformat->dwChannelMask = chans; + + if (wformat->Format.nChannels > 8 || wformat->dwChannelMask != chans) { + // IAudioClient::IsFormatSupported tend to fallback to stereo for closest + // format match when there are more channels. Remix to standard layout. + // Also if input channel mask has channels outside 32-bits override it + // and hope for the best... + wformat->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND; + wformat->Format.nChannels = 8; + } + + update_waveformat_datarate(wformat); +} + +// other wformat parameters must already be set with set_waveformat +static void change_waveformat_samplerate(WAVEFORMATEXTENSIBLE *wformat, + DWORD samplerate) +{ + wformat->Format.nSamplesPerSec = samplerate; + update_waveformat_datarate(wformat); +} + +// other wformat parameters must already be set with set_waveformat +static void change_waveformat_channels(WAVEFORMATEXTENSIBLE *wformat, + struct mp_chmap *channels) +{ + wformat->Format.nChannels = channels->num; + wformat->dwChannelMask = mp_chmap_to_waveext(channels); + update_waveformat_datarate(wformat); +} + +static struct wasapi_sample_fmt format_from_waveformat(WAVEFORMATEX *wf) +{ + struct wasapi_sample_fmt res = {0}; + + for (int n = 0; wasapi_formats[n].mp_format; n++) { + const struct wasapi_sample_fmt *fmt = &wasapi_formats[n]; + int valid_bits = 0; + + if (wf->wBitsPerSample != fmt->bits) + continue; + + const GUID *wf_guid = NULL; + + switch (wf->wFormatTag) { + case WAVE_FORMAT_EXTENSIBLE: { + WAVEFORMATEXTENSIBLE *wformat = (WAVEFORMATEXTENSIBLE *)wf; + wf_guid = &wformat->SubFormat; + if (IsEqualGUID(wf_guid, &KSDATAFORMAT_SUBTYPE_PCM)) + valid_bits = wformat->Samples.wValidBitsPerSample; + break; + } + case WAVE_FORMAT_PCM: + wf_guid = &KSDATAFORMAT_SUBTYPE_PCM; + break; + case WAVE_FORMAT_IEEE_FLOAT: + wf_guid = &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + break; + } + + if (!wf_guid || !IsEqualGUID(wf_guid, fmt->subtype)) + continue; + + res = *fmt; + if (valid_bits > 0 && valid_bits < fmt->bits) + res.used_msb = valid_bits; + break; + } + + return res; +} + +static bool chmap_from_waveformat(struct mp_chmap *channels, + const WAVEFORMATEX *wf) +{ + if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + WAVEFORMATEXTENSIBLE *wformat = (WAVEFORMATEXTENSIBLE *)wf; + mp_chmap_from_waveext(channels, wformat->dwChannelMask); + } else { + mp_chmap_from_channels(channels, wf->nChannels); + } + + if (channels->num != wf->nChannels) { + mp_chmap_from_str(channels, bstr0("empty")); + return false; + } + + return true; +} + +static char *waveformat_to_str_buf(char *buf, size_t buf_size, WAVEFORMATEX *wf) +{ + struct mp_chmap channels; + chmap_from_waveformat(&channels, wf); + + struct wasapi_sample_fmt format = format_from_waveformat(wf); + + snprintf(buf, buf_size, "%s %s (%d/%d bits) @ %uhz", + mp_chmap_to_str(&channels), + af_fmt_to_str(format.mp_format), format.bits, format.used_msb, + (unsigned) wf->nSamplesPerSec); + return buf; +} +#define waveformat_to_str_(wf, sz) waveformat_to_str_buf((char[sz]){0}, sz, (wf)) +#define waveformat_to_str(wf) waveformat_to_str_(wf, MP_NUM_CHANNELS * 4 + 42) + +static void waveformat_copy(WAVEFORMATEXTENSIBLE* dst, WAVEFORMATEX* src) +{ + if (src->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + *dst = *(WAVEFORMATEXTENSIBLE *)src; + } else { + dst->Format = *src; + } +} + +static bool set_ao_format(struct ao *ao, WAVEFORMATEX *wf, + AUDCLNT_SHAREMODE share_mode) +{ + struct wasapi_state *state = ao->priv; + struct wasapi_sample_fmt format = format_from_waveformat(wf); + if (!format.mp_format) { + MP_ERR(ao, "Unable to construct sample format from WAVEFORMAT %s\n", + waveformat_to_str(wf)); + return false; + } + + // Do not touch the ao for passthrough, just assume that we set WAVEFORMATEX + // correctly. + if (af_fmt_is_pcm(format.mp_format)) { + struct mp_chmap channels; + if (!chmap_from_waveformat(&channels, wf)) { + MP_ERR(ao, "Unable to construct channel map from WAVEFORMAT %s\n", + waveformat_to_str(wf)); + return false; + } + + struct ao_convert_fmt conv = { + .src_fmt = format.mp_format, + .channels = channels.num, + .dst_bits = format.bits, + .pad_lsb = format.bits - format.used_msb, + }; + if (!ao_can_convert_inplace(&conv)) { + MP_ERR(ao, "Unable to convert to %s\n", waveformat_to_str(wf)); + return false; + } + + state->convert_format = conv; + ao->samplerate = wf->nSamplesPerSec; + ao->format = format.mp_format; + ao->channels = channels; + } + waveformat_copy(&state->format, wf); + state->share_mode = share_mode; + + MP_VERBOSE(ao, "Accepted as %s %s @ %dhz -> %s (%s)\n", + mp_chmap_to_str(&ao->channels), + af_fmt_to_str(ao->format), ao->samplerate, + waveformat_to_str(wf), + state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE + ? "exclusive" : "shared"); + return true; +} + +#define mp_format_res_str(hres) \ + (SUCCEEDED(hres) ? ((hres) == S_OK) ? "ok" : "close" \ + : ((hres) == AUDCLNT_E_UNSUPPORTED_FORMAT) \ + ? "unsupported" : mp_HRESULT_to_str(hres)) + +static bool try_format_exclusive(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat) +{ + struct wasapi_state *state = ao->priv; + HRESULT hr = IAudioClient_IsFormatSupported(state->pAudioClient, + AUDCLNT_SHAREMODE_EXCLUSIVE, + &wformat->Format, NULL); + MP_VERBOSE(ao, "Trying %s (exclusive) -> %s\n", + waveformat_to_str(&wformat->Format), mp_format_res_str(hr)); + return SUCCEEDED(hr); +} + +static bool search_sample_formats(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat, + int samplerate, struct mp_chmap *channels) +{ + struct wasapi_sample_fmt alt_formats[MP_ARRAY_SIZE(wasapi_formats)]; + wasapi_get_best_sample_formats(ao->format, alt_formats); + for (int n = 0; alt_formats[n].mp_format; n++) { + set_waveformat(wformat, &alt_formats[n], samplerate, channels); + if (try_format_exclusive(ao, wformat)) + return true; + } + + wformat->Format.wBitsPerSample = 0; + return false; +} + +static bool search_samplerates(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat, + struct mp_chmap *channels) +{ + // put common samplerates first so that we find format early + int try[] = {48000, 44100, 96000, 88200, 192000, 176400, + 32000, 22050, 11025, 8000, 16000, 352800, 384000, 0}; + + // get a list of supported rates + int n = 0; + int supported[MP_ARRAY_SIZE(try)] = {0}; + + wformat->Format.wBitsPerSample = 0; + for (int i = 0; try[i]; i++) { + if (!wformat->Format.wBitsPerSample) { + if (search_sample_formats(ao, wformat, try[i], channels)) + supported[n++] = try[i]; + } else { + change_waveformat_samplerate(wformat, try[i]); + if (try_format_exclusive(ao, wformat)) + supported[n++] = try[i]; + } + } + + int samplerate = af_select_best_samplerate(ao->samplerate, supported); + if (samplerate > 0) { + change_waveformat_samplerate(wformat, samplerate); + return true; + } + + // otherwise, this is probably an unsupported channel map + wformat->Format.nSamplesPerSec = 0; + return false; +} + +static bool search_channels(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat) +{ + struct wasapi_state *state = ao->priv; + struct mp_chmap_sel chmap_sel = {.tmp = state}; + struct mp_chmap entry; + // put common layouts first so that we find sample rate/format early + char *channel_layouts[] = + {"stereo", "5.1", "7.1", "6.1", "mono", "2.1", "4.0", "5.0", + "3.0", "3.0(back)", + "quad", "quad(side)", "3.1", + "5.0(side)", "4.1", + "5.1(side)", "6.0", "6.0(front)", "hexagonal", + "6.1(back)", "6.1(front)", "7.0", "7.0(front)", + "7.1(wide)", "7.1(wide-side)", "7.1(rear)", "octagonal", NULL}; + + wformat->Format.nSamplesPerSec = 0; + for (int j = 0; channel_layouts[j]; j++) { + mp_chmap_from_str(&entry, bstr0(channel_layouts[j])); + if (!wformat->Format.nSamplesPerSec) { + if (search_samplerates(ao, wformat, &entry)) + mp_chmap_sel_add_map(&chmap_sel, &entry); + } else { + change_waveformat_channels(wformat, &entry); + if (try_format_exclusive(ao, wformat)) + mp_chmap_sel_add_map(&chmap_sel, &entry); + } + } + + entry = ao->channels; + if (ao_chmap_sel_adjust2(ao, &chmap_sel, &entry, !state->opt_exclusive)){ + change_waveformat_channels(wformat, &entry); + return true; + } + + MP_ERR(ao, "No suitable audio format found\n"); + return false; +} + +static bool find_formats_exclusive(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat) +{ + // Try the specified format as is + if (try_format_exclusive(ao, wformat)) + return true; + + if (af_fmt_is_spdif(ao->format)) { + if (ao->format != AF_FORMAT_S_AC3) { + // If the requested format failed and it is passthrough, but not + // AC3, try lying and saying it is. + MP_VERBOSE(ao, "Retrying as AC3.\n"); + wformat->SubFormat = *format_to_subtype(AF_FORMAT_S_AC3); + if (try_format_exclusive(ao, wformat)) + return true; + } + return false; + } + + // Fallback on the PCM format search + return search_channels(ao, wformat); +} + +static bool find_formats_shared(struct ao *ao, WAVEFORMATEXTENSIBLE *wformat) +{ + struct wasapi_state *state = ao->priv; + + struct mp_chmap channels; + if (!chmap_from_waveformat(&channels, &wformat->Format)) { + MP_ERR(ao, "Error converting channel map\n"); + return false; + } + + HRESULT hr; + WAVEFORMATEX *mix_format; + hr = IAudioClient_GetMixFormat(state->pAudioClient, &mix_format); + EXIT_ON_ERROR(hr); + + // WASAPI doesn't do any sample rate conversion on its own and + // will typically only accept the mix format samplerate. Although + // it will accept any PCM sample format, everything gets converted + // to the mix format anyway (pretty much always float32), so just + // use that. + WAVEFORMATEXTENSIBLE try_format; + waveformat_copy(&try_format, mix_format); + CoTaskMemFree(mix_format); + + // WASAPI may accept channel maps other than the mix format + // if a surround emulator is enabled. + change_waveformat_channels(&try_format, &channels); + + hr = IAudioClient_IsFormatSupported(state->pAudioClient, + AUDCLNT_SHAREMODE_SHARED, + &try_format.Format, + &mix_format); + MP_VERBOSE(ao, "Trying %s (shared) -> %s\n", + waveformat_to_str(&try_format.Format), mp_format_res_str(hr)); + if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT) + EXIT_ON_ERROR(hr); + + switch (hr) { + case S_OK: + waveformat_copy(wformat, &try_format.Format); + break; + case S_FALSE: + waveformat_copy(wformat, mix_format); + CoTaskMemFree(mix_format); + MP_VERBOSE(ao, "Closest match is %s\n", + waveformat_to_str(&wformat->Format)); + break; + default: + hr = IAudioClient_GetMixFormat(state->pAudioClient, &mix_format); + EXIT_ON_ERROR(hr); + waveformat_copy(wformat, mix_format); + CoTaskMemFree(mix_format); + MP_VERBOSE(ao, "Fallback to mix format %s\n", + waveformat_to_str(&wformat->Format)); + + } + + return true; +exit_label: + MP_ERR(state, "Error finding shared mode format: %s\n", + mp_HRESULT_to_str(hr)); + return false; +} + +static bool find_formats(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + struct mp_chmap channels = ao->channels; + + if (mp_chmap_is_unknown(&channels)) + mp_chmap_from_channels(&channels, channels.num); + mp_chmap_reorder_to_waveext(&channels); + if (!mp_chmap_is_valid(&channels)) + mp_chmap_from_channels(&channels, 2); + + struct wasapi_sample_fmt alt_formats[MP_ARRAY_SIZE(wasapi_formats)]; + wasapi_get_best_sample_formats(ao->format, alt_formats); + struct wasapi_sample_fmt wasapi_format = + {AF_FORMAT_S16, 16, 16, &KSDATAFORMAT_SUBTYPE_PCM};; + if (alt_formats[0].mp_format) + wasapi_format = alt_formats[0]; + + AUDCLNT_SHAREMODE share_mode; + WAVEFORMATEXTENSIBLE wformat; + set_waveformat(&wformat, &wasapi_format, ao->samplerate, &channels); + + if (state->opt_exclusive || af_fmt_is_spdif(ao->format)) { + share_mode = AUDCLNT_SHAREMODE_EXCLUSIVE; + if(!find_formats_exclusive(ao, &wformat)) + return false; + } else { + share_mode = AUDCLNT_SHAREMODE_SHARED; + if(!find_formats_shared(ao, &wformat)) + return false; + } + + return set_ao_format(ao, &wformat.Format, share_mode); +} + +static HRESULT init_clock(struct wasapi_state *state) { + HRESULT hr = IAudioClient_GetService(state->pAudioClient, + &IID_IAudioClock, + (void **)&state->pAudioClock); + EXIT_ON_ERROR(hr); + hr = IAudioClock_GetFrequency(state->pAudioClock, &state->clock_frequency); + EXIT_ON_ERROR(hr); + + QueryPerformanceFrequency(&state->qpc_frequency); + + atomic_store(&state->sample_count, 0); + + MP_VERBOSE(state, + "IAudioClock::GetFrequency gave a frequency of %"PRIu64".\n", + (uint64_t) state->clock_frequency); + + return S_OK; +exit_label: + MP_ERR(state, "Error obtaining the audio device's timing: %s\n", + mp_HRESULT_to_str(hr)); + return hr; +} + +static void init_session_display(struct wasapi_state *state, const char *name) { + HRESULT hr = IAudioClient_GetService(state->pAudioClient, + &IID_IAudioSessionControl, + (void **)&state->pSessionControl); + EXIT_ON_ERROR(hr); + + wchar_t path[MAX_PATH] = {0}; + GetModuleFileNameW(NULL, path, MAX_PATH); + hr = IAudioSessionControl_SetIconPath(state->pSessionControl, path, NULL); + if (FAILED(hr)) { + // don't goto exit_label here since SetDisplayName might still work + MP_WARN(state, "Error setting audio session icon: %s\n", + mp_HRESULT_to_str(hr)); + } + + assert(name); + if (!name) + return; + + wchar_t *title = mp_from_utf8(NULL, name); + hr = IAudioSessionControl_SetDisplayName(state->pSessionControl, title, NULL); + talloc_free(title); + + EXIT_ON_ERROR(hr); + return; +exit_label: + // if we got here then the session control is useless - release it + SAFE_RELEASE(state->pSessionControl); + MP_WARN(state, "Error setting audio session name: %s\n", + mp_HRESULT_to_str(hr)); + return; +} + +static void init_volume_control(struct wasapi_state *state) +{ + HRESULT hr; + if (state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + MP_DBG(state, "Activating pEndpointVolume interface\n"); + hr = IMMDeviceActivator_Activate(state->pDevice, + &IID_IAudioEndpointVolume, + CLSCTX_ALL, NULL, + (void **)&state->pEndpointVolume); + EXIT_ON_ERROR(hr); + + MP_DBG(state, "IAudioEndpointVolume::QueryHardwareSupport\n"); + hr = IAudioEndpointVolume_QueryHardwareSupport(state->pEndpointVolume, + &state->vol_hw_support); + EXIT_ON_ERROR(hr); + } else { + MP_DBG(state, "IAudioClient::Initialize pAudioVolume\n"); + hr = IAudioClient_GetService(state->pAudioClient, + &IID_ISimpleAudioVolume, + (void **)&state->pAudioVolume); + EXIT_ON_ERROR(hr); + } + return; +exit_label: + state->vol_hw_support = 0; + SAFE_RELEASE(state->pEndpointVolume); + SAFE_RELEASE(state->pAudioVolume); + MP_WARN(state, "Error setting up volume control: %s\n", + mp_HRESULT_to_str(hr)); +} + +static HRESULT fix_format(struct ao *ao, bool align_hack) +{ + struct wasapi_state *state = ao->priv; + + MP_DBG(state, "IAudioClient::GetDevicePeriod\n"); + REFERENCE_TIME devicePeriod; + HRESULT hr = IAudioClient_GetDevicePeriod(state->pAudioClient,&devicePeriod, + NULL); + MP_VERBOSE(state, "Device period: %.2g ms\n", + (double) devicePeriod / 10000.0 ); + + REFERENCE_TIME bufferDuration = devicePeriod; + if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) { + // for shared mode, use integer multiple of device period close to 50ms + bufferDuration = devicePeriod * ceil(50.0 * 10000.0 / devicePeriod); + } + + // handle unsupported buffer size if AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED was + // returned in a previous attempt. hopefully this shouldn't happen because + // of the above integer device period + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd370875%28v=vs.85%29.aspx + if (align_hack) { + bufferDuration = (REFERENCE_TIME) (0.5 + + (10000.0 * 1000 / state->format.Format.nSamplesPerSec + * state->bufferFrameCount)); + } + + REFERENCE_TIME bufferPeriod = + state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ? bufferDuration : 0; + + MP_DBG(state, "IAudioClient::Initialize\n"); + hr = IAudioClient_Initialize(state->pAudioClient, + state->share_mode, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + bufferDuration, + bufferPeriod, + &(state->format.Format), + NULL); + EXIT_ON_ERROR(hr); + + MP_DBG(state, "IAudioClient::Initialize pRenderClient\n"); + hr = IAudioClient_GetService(state->pAudioClient, + &IID_IAudioRenderClient, + (void **)&state->pRenderClient); + EXIT_ON_ERROR(hr); + + MP_DBG(state, "IAudioClient::Initialize IAudioClient_SetEventHandle\n"); + hr = IAudioClient_SetEventHandle(state->pAudioClient, state->hWake); + EXIT_ON_ERROR(hr); + + MP_DBG(state, "IAudioClient::Initialize IAudioClient_GetBufferSize\n"); + hr = IAudioClient_GetBufferSize(state->pAudioClient, + &state->bufferFrameCount); + EXIT_ON_ERROR(hr); + + ao->device_buffer = state->bufferFrameCount; + bufferDuration = (REFERENCE_TIME) (0.5 + + (10000.0 * 1000 / state->format.Format.nSamplesPerSec + * state->bufferFrameCount)); + MP_VERBOSE(state, "Buffer frame count: %"PRIu32" (%.2g ms)\n", + state->bufferFrameCount, (double) bufferDuration / 10000.0 ); + + hr = init_clock(state); + EXIT_ON_ERROR(hr); + + init_session_display(state, ao->client_name); + init_volume_control(state); + +#if !HAVE_UWP + state->hTask = AvSetMmThreadCharacteristics(L"Pro Audio", &(DWORD){0}); + if (!state->hTask) { + MP_WARN(state, "Failed to set AV thread to Pro Audio: %s\n", + mp_LastError_to_str()); + } +#endif + + return S_OK; +exit_label: + MP_ERR(state, "Error initializing device: %s\n", mp_HRESULT_to_str(hr)); + return hr; +} + +struct device_desc { + LPWSTR deviceID; + char *id; + char *name; +}; + +static char* get_device_name(struct mp_log *l, void *talloc_ctx, IMMDevice *pDevice) +{ + char *namestr = NULL; + IPropertyStore *pProps = NULL; + PROPVARIANT devname; + PropVariantInit(&devname); + + HRESULT hr = IMMDevice_OpenPropertyStore(pDevice, STGM_READ, &pProps); + EXIT_ON_ERROR(hr); + + hr = IPropertyStore_GetValue(pProps, &mp_PKEY_Device_FriendlyName, + &devname); + EXIT_ON_ERROR(hr); + + namestr = mp_to_utf8(talloc_ctx, devname.pwszVal); + +exit_label: + if (FAILED(hr)) + mp_warn(l, "Failed getting device name: %s\n", mp_HRESULT_to_str(hr)); + PropVariantClear(&devname); + SAFE_RELEASE(pProps); + return namestr ? namestr : talloc_strdup(talloc_ctx, ""); +} + +static struct device_desc *get_device_desc(struct mp_log *l, IMMDevice *pDevice) +{ + LPWSTR deviceID; + HRESULT hr = IMMDevice_GetId(pDevice, &deviceID); + if (FAILED(hr)) { + mp_err(l, "Failed getting device id: %s\n", mp_HRESULT_to_str(hr)); + return NULL; + } + struct device_desc *d = talloc_zero(NULL, struct device_desc); + d->deviceID = talloc_memdup(d, deviceID, + (wcslen(deviceID) + 1) * sizeof(wchar_t)); + SAFE_DESTROY(deviceID, CoTaskMemFree(deviceID)); + + char *full_id = mp_to_utf8(NULL, d->deviceID); + bstr id = bstr0(full_id); + bstr_eatstart0(&id, "{0.0.0.00000000}."); + d->id = bstrdup0(d, id); + talloc_free(full_id); + + d->name = get_device_name(l, d, pDevice); + return d; +} + +struct enumerator { + struct mp_log *log; + IMMDeviceEnumerator *pEnumerator; + IMMDeviceCollection *pDevices; + UINT count; +}; + +static void destroy_enumerator(struct enumerator *e) +{ + if (!e) + return; + SAFE_RELEASE(e->pDevices); + SAFE_RELEASE(e->pEnumerator); + talloc_free(e); +} + +static struct enumerator *create_enumerator(struct mp_log *log) +{ + struct enumerator *e = talloc_zero(NULL, struct enumerator); + e->log = log; + HRESULT hr = CoCreateInstance( + &CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &IID_IMMDeviceEnumerator, + (void **)&e->pEnumerator); + EXIT_ON_ERROR(hr); + + hr = IMMDeviceEnumerator_EnumAudioEndpoints( + e->pEnumerator, eRender, DEVICE_STATE_ACTIVE, &e->pDevices); + EXIT_ON_ERROR(hr); + + hr = IMMDeviceCollection_GetCount(e->pDevices, &e->count); + EXIT_ON_ERROR(hr); + + return e; +exit_label: + mp_err(log, "Error getting device enumerator: %s\n", mp_HRESULT_to_str(hr)); + destroy_enumerator(e); + return NULL; +} + +static struct device_desc *device_desc_for_num(struct enumerator *e, UINT i) +{ + IMMDevice *pDevice = NULL; + HRESULT hr = IMMDeviceCollection_Item(e->pDevices, i, &pDevice); + if (FAILED(hr)) { + MP_ERR(e, "Failed getting device #%d: %s\n", i, mp_HRESULT_to_str(hr)); + return NULL; + } + struct device_desc *d = get_device_desc(e->log, pDevice); + SAFE_RELEASE(pDevice); + return d; +} + +static struct device_desc *default_device_desc(struct enumerator *e) +{ + IMMDevice *pDevice = NULL; + HRESULT hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint( + e->pEnumerator, eRender, eMultimedia, &pDevice); + if (FAILED(hr)) { + MP_ERR(e, "Error from GetDefaultAudioEndpoint: %s\n", + mp_HRESULT_to_str(hr)); + return NULL; + } + struct device_desc *d = get_device_desc(e->log, pDevice); + SAFE_RELEASE(pDevice); + return d; +} + +void wasapi_list_devs(struct ao *ao, struct ao_device_list *list) +{ + struct enumerator *enumerator = create_enumerator(ao->log); + if (!enumerator) + return; + + for (UINT i = 0; i < enumerator->count; i++) { + struct device_desc *d = device_desc_for_num(enumerator, i); + if (!d) + goto exit_label; + ao_device_list_add(list, ao, &(struct ao_device_desc){d->id, d->name}); + talloc_free(d); + } + +exit_label: + destroy_enumerator(enumerator); +} + +static bool load_device(struct mp_log *l, + IMMDevice **ppDevice, LPWSTR deviceID) +{ + IMMDeviceEnumerator *pEnumerator = NULL; + HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + &IID_IMMDeviceEnumerator, + (void **)&pEnumerator); + EXIT_ON_ERROR(hr); + + hr = IMMDeviceEnumerator_GetDevice(pEnumerator, deviceID, ppDevice); + EXIT_ON_ERROR(hr); + +exit_label: + if (FAILED(hr)) + mp_err(l, "Error loading selected device: %s\n", mp_HRESULT_to_str(hr)); + SAFE_RELEASE(pEnumerator); + return SUCCEEDED(hr); +} + +static LPWSTR select_device(struct mp_log *l, struct device_desc *d) +{ + if (!d) + return NULL; + mp_verbose(l, "Selecting device \'%s\' (%s)\n", d->id, d->name); + return talloc_memdup(NULL, d->deviceID, + (wcslen(d->deviceID) + 1) * sizeof(wchar_t)); +} + +bstr wasapi_get_specified_device_string(struct ao *ao) +{ + return bstr_strip(bstr0(ao->device)); +} + +LPWSTR wasapi_find_deviceID(struct ao *ao) +{ + LPWSTR deviceID = NULL; + bstr device = wasapi_get_specified_device_string(ao); + MP_DBG(ao, "Find device \'%.*s\'\n", BSTR_P(device)); + + struct device_desc *d = NULL; + struct enumerator *enumerator = create_enumerator(ao->log); + if (!enumerator) + goto exit_label; + + if (!enumerator->count) { + MP_ERR(ao, "There are no playback devices available\n"); + goto exit_label; + } + + if (!device.len) { + MP_VERBOSE(ao, "No device specified. Selecting default.\n"); + d = default_device_desc(enumerator); + deviceID = select_device(ao->log, d); + goto exit_label; + } + + // try selecting by number + bstr rest; + long long devno = bstrtoll(device, &rest, 10); + if (!rest.len && 0 <= devno && devno < (long long)enumerator->count) { + MP_VERBOSE(ao, "Selecting device by number: #%lld\n", devno); + d = device_desc_for_num(enumerator, devno); + deviceID = select_device(ao->log, d); + goto exit_label; + } + + // select by id or name + bstr_eatstart0(&device, "{0.0.0.00000000}."); + for (UINT i = 0; i < enumerator->count; i++) { + d = device_desc_for_num(enumerator, i); + if (!d) + goto exit_label; + + if (bstrcmp(device, bstr_strip(bstr0(d->id))) == 0) { + MP_VERBOSE(ao, "Selecting device by id: \'%.*s\'\n", BSTR_P(device)); + deviceID = select_device(ao->log, d); + goto exit_label; + } + + if (bstrcmp(device, bstr_strip(bstr0(d->name))) == 0) { + if (!deviceID) { + MP_VERBOSE(ao, "Selecting device by name: \'%.*s\'\n", BSTR_P(device)); + deviceID = select_device(ao->log, d); + } else { + MP_WARN(ao, "Multiple devices matched \'%.*s\'." + "Ignoring device \'%s\' (%s).\n", + BSTR_P(device), d->id, d->name); + } + } + SAFE_DESTROY(d, talloc_free(d)); + } + + if (!deviceID) + MP_ERR(ao, "Failed to find device \'%.*s\'\n", BSTR_P(device)); + +exit_label: + talloc_free(d); + destroy_enumerator(enumerator); + return deviceID; +} + +bool wasapi_thread_init(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + MP_DBG(ao, "Init wasapi thread\n"); + int64_t retry_wait = MP_TIME_US_TO_NS(1); + bool align_hack = false; + HRESULT hr; + + ao->format = af_fmt_from_planar(ao->format); + +retry: + if (state->deviceID) { + if (!load_device(ao->log, &state->pDevice, state->deviceID)) + return false; + + MP_DBG(ao, "Activating pAudioClient interface\n"); + hr = IMMDeviceActivator_Activate(state->pDevice, &IID_IAudioClient, + CLSCTX_ALL, NULL, + (void **)&state->pAudioClient); + if (FAILED(hr)) { + MP_FATAL(ao, "Error activating device: %s\n", + mp_HRESULT_to_str(hr)); + return false; + } + } else { + MP_VERBOSE(ao, "Trying UWP wrapper.\n"); + + HRESULT (*wuCreateDefaultAudioRenderer)(IUnknown **res) = NULL; + HANDLE lib = LoadLibraryW(L"wasapiuwp2.dll"); + if (!lib) { + MP_ERR(ao, "Wrapper not found: %d\n", (int)GetLastError()); + return false; + } + + wuCreateDefaultAudioRenderer = + (void*)GetProcAddress(lib, "wuCreateDefaultAudioRenderer"); + if (!wuCreateDefaultAudioRenderer) { + MP_ERR(ao, "Function not found.\n"); + return false; + } + IUnknown *res = NULL; + hr = wuCreateDefaultAudioRenderer(&res); + MP_VERBOSE(ao, "Device: %s %p\n", mp_HRESULT_to_str(hr), res); + if (FAILED(hr)) { + MP_FATAL(ao, "Error activating device: %s\n", + mp_HRESULT_to_str(hr)); + return false; + } + hr = IUnknown_QueryInterface(res, &IID_IAudioClient, + (void **)&state->pAudioClient); + IUnknown_Release(res); + if (FAILED(hr)) { + MP_FATAL(ao, "Failed to get UWP audio client: %s\n", + mp_HRESULT_to_str(hr)); + return false; + } + } + + // In the event of an align hack, we've already done this. + if (!align_hack) { + MP_DBG(ao, "Probing formats\n"); + if (!find_formats(ao)) + return false; + } + + MP_DBG(ao, "Fixing format\n"); + hr = fix_format(ao, align_hack); + switch (hr) { + case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED: + if (align_hack) { + MP_FATAL(ao, "Align hack failed\n"); + break; + } + // According to MSDN, we must use this as base after the failure. + hr = IAudioClient_GetBufferSize(state->pAudioClient, + &state->bufferFrameCount); + if (FAILED(hr)) { + MP_FATAL(ao, "Error getting buffer size for align hack: %s\n", + mp_HRESULT_to_str(hr)); + return false; + } + wasapi_thread_uninit(ao); + align_hack = true; + MP_WARN(ao, "This appears to require a weird Windows 7 hack. Retrying.\n"); + goto retry; + case AUDCLNT_E_DEVICE_IN_USE: + case AUDCLNT_E_DEVICE_INVALIDATED: + if (retry_wait > MP_TIME_US_TO_NS(8)) { + MP_FATAL(ao, "Bad device retry failed\n"); + return false; + } + wasapi_thread_uninit(ao); + MP_WARN(ao, "Retrying in %"PRId64" ns\n", retry_wait); + mp_sleep_ns(retry_wait); + retry_wait *= 2; + goto retry; + } + return SUCCEEDED(hr); +} + +void wasapi_thread_uninit(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + MP_DBG(ao, "Thread shutdown\n"); + + if (state->pAudioClient) + IAudioClient_Stop(state->pAudioClient); + + SAFE_RELEASE(state->pRenderClient); + SAFE_RELEASE(state->pAudioClock); + SAFE_RELEASE(state->pAudioVolume); + SAFE_RELEASE(state->pEndpointVolume); + SAFE_RELEASE(state->pSessionControl); + SAFE_RELEASE(state->pAudioClient); + SAFE_RELEASE(state->pDevice); +#if !HAVE_UWP + SAFE_DESTROY(state->hTask, AvRevertMmThreadCharacteristics(state->hTask)); +#endif + MP_DBG(ao, "Thread uninit done\n"); +} diff --git a/audio/out/buffer.c b/audio/out/buffer.c new file mode 100644 index 0000000..5b8b523 --- /dev/null +++ b/audio/out/buffer.c @@ -0,0 +1,736 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stddef.h> +#include <inttypes.h> +#include <math.h> +#include <unistd.h> +#include <errno.h> +#include <assert.h> + +#include "ao.h" +#include "internal.h" +#include "audio/aframe.h" +#include "audio/format.h" + +#include "common/msg.h" +#include "common/common.h" + +#include "filters/f_async_queue.h" +#include "filters/filter_internal.h" + +#include "osdep/timer.h" +#include "osdep/threads.h" + +struct buffer_state { + // Buffer and AO + mp_mutex lock; + mp_cond wakeup; + + // Playthread sleep + mp_mutex pt_lock; + mp_cond pt_wakeup; + + // Access from AO driver's thread only. + char *convert_buffer; + + // Immutable. + struct mp_async_queue *queue; + + // --- protected by lock + + struct mp_filter *filter_root; + struct mp_filter *input; // connected to queue + struct mp_aframe *pending; // last, not fully consumed output + + bool streaming; // AO streaming active + bool playing; // logically playing audio from buffer + bool paused; // logically paused + + int64_t end_time_ns; // absolute output time of last played sample + + bool initial_unblocked; + + // "Push" AOs only (AOs with driver->write). + bool hw_paused; // driver->set_pause() was used successfully + bool recover_pause; // non-hw_paused: needs to recover delay + struct mp_pcm_state prepause_state; + mp_thread thread; // thread shoveling data to AO + bool thread_valid; // thread is running + struct mp_aframe *temp_buf; + + // --- protected by pt_lock + bool need_wakeup; + bool terminate; // exit thread +}; + +static MP_THREAD_VOID playthread(void *arg); + +void ao_wakeup_playthread(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + mp_mutex_lock(&p->pt_lock); + p->need_wakeup = true; + mp_cond_broadcast(&p->pt_wakeup); + mp_mutex_unlock(&p->pt_lock); +} + +// called locked +static void get_dev_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct buffer_state *p = ao->buffer_state; + + if (p->paused && p->playing && !ao->stream_silence) { + *state = p->prepause_state; + return; + } + + *state = (struct mp_pcm_state){ + .free_samples = -1, + .queued_samples = -1, + .delay = -1, + }; + ao->driver->get_state(ao, state); +} + +struct mp_async_queue *ao_get_queue(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + return p->queue; +} + +// Special behavior with data==NULL: caller uses p->pending. +static int read_buffer(struct ao *ao, void **data, int samples, bool *eof, + bool pad_silence) +{ + struct buffer_state *p = ao->buffer_state; + int pos = 0; + *eof = false; + + while (p->playing && !p->paused && pos < samples) { + if (!p->pending || !mp_aframe_get_size(p->pending)) { + TA_FREEP(&p->pending); + struct mp_frame frame = mp_pin_out_read(p->input->pins[0]); + if (!frame.type) + break; // we can't/don't want to block + if (frame.type != MP_FRAME_AUDIO) { + if (frame.type == MP_FRAME_EOF) + *eof = true; + mp_frame_unref(&frame); + continue; + } + p->pending = frame.data; + } + + if (!data) + break; + + int copy = mp_aframe_get_size(p->pending); + uint8_t **fdata = mp_aframe_get_data_ro(p->pending); + copy = MPMIN(copy, samples - pos); + for (int n = 0; n < ao->num_planes; n++) { + memcpy((char *)data[n] + pos * ao->sstride, + fdata[n], copy * ao->sstride); + } + mp_aframe_skip_samples(p->pending, copy); + pos += copy; + *eof = false; + } + + if (!data) { + if (!p->pending) + return 0; + void **pd = (void *)mp_aframe_get_data_rw(p->pending); + if (pd) + ao_post_process_data(ao, pd, mp_aframe_get_size(p->pending)); + return 1; + } + + // pad with silence (underflow/paused/eof) + if (pad_silence) { + for (int n = 0; n < ao->num_planes; n++) { + af_fill_silence((char *)data[n] + pos * ao->sstride, + (samples - pos) * ao->sstride, + ao->format); + } + } + + ao_post_process_data(ao, data, pos); + return pos; +} + +static int ao_read_data_unlocked(struct ao *ao, void **data, int samples, + int64_t out_time_ns, bool pad_silence) +{ + struct buffer_state *p = ao->buffer_state; + assert(!ao->driver->write); + + int pos = read_buffer(ao, data, samples, &(bool){0}, pad_silence); + + if (pos > 0) + p->end_time_ns = out_time_ns; + + if (pos < samples && p->playing && !p->paused) { + p->playing = false; + ao->wakeup_cb(ao->wakeup_ctx); + // For ao_drain(). + mp_cond_broadcast(&p->wakeup); + } + + return pos; +} + +// 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_ns to the expected delay until the last sample +// reaches the speakers, in nanoseconds, using mp_time_ns() as reference. +int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_ns) +{ + struct buffer_state *p = ao->buffer_state; + + mp_mutex_lock(&p->lock); + + int pos = ao_read_data_unlocked(ao, data, samples, out_time_ns, true); + + mp_mutex_unlock(&p->lock); + + return pos; +} + +// Like ao_read_data() but does not block and also may return partial data. +// Callers have to check the return value. +int ao_read_data_nonblocking(struct ao *ao, void **data, int samples, int64_t out_time_ns) +{ + struct buffer_state *p = ao->buffer_state; + + if (mp_mutex_trylock(&p->lock)) + return 0; + + int pos = ao_read_data_unlocked(ao, data, samples, out_time_ns, false); + + mp_mutex_unlock(&p->lock); + + return pos; +} + +// Same as ao_read_data(), but convert data according to *fmt. +// fmt->src_fmt and fmt->channels must be the same as the AO parameters. +int ao_read_data_converted(struct ao *ao, struct ao_convert_fmt *fmt, + void **data, int samples, int64_t out_time_ns) +{ + struct buffer_state *p = ao->buffer_state; + void *ndata[MP_NUM_CHANNELS] = {0}; + + if (!ao_need_conversion(fmt)) + return ao_read_data(ao, data, samples, out_time_ns); + + assert(ao->format == fmt->src_fmt); + assert(ao->channels.num == fmt->channels); + + bool planar = af_fmt_is_planar(fmt->src_fmt); + int planes = planar ? fmt->channels : 1; + int plane_samples = samples * (planar ? 1: fmt->channels); + int src_plane_size = plane_samples * af_fmt_to_bytes(fmt->src_fmt); + int dst_plane_size = plane_samples * fmt->dst_bits / 8; + + int needed = src_plane_size * planes; + if (needed > talloc_get_size(p->convert_buffer) || !p->convert_buffer) { + talloc_free(p->convert_buffer); + p->convert_buffer = talloc_size(NULL, needed); + } + + for (int n = 0; n < planes; n++) + ndata[n] = p->convert_buffer + n * src_plane_size; + + int res = ao_read_data(ao, ndata, samples, out_time_ns); + + ao_convert_inplace(fmt, ndata, samples); + for (int n = 0; n < planes; n++) + memcpy(data[n], ndata[n], dst_plane_size); + + return res; +} + +int ao_control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct buffer_state *p = ao->buffer_state; + int r = CONTROL_UNKNOWN; + if (ao->driver->control) { + // Only need to lock in push mode. + if (ao->driver->write) + mp_mutex_lock(&p->lock); + + r = ao->driver->control(ao, cmd, arg); + + if (ao->driver->write) + mp_mutex_unlock(&p->lock); + } + return r; +} + +double ao_get_delay(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + mp_mutex_lock(&p->lock); + + double driver_delay; + if (ao->driver->write) { + struct mp_pcm_state state; + get_dev_state(ao, &state); + driver_delay = state.delay; + } else { + int64_t end = p->end_time_ns; + int64_t now = mp_time_ns(); + driver_delay = MPMAX(0, MP_TIME_NS_TO_S(end - now)); + } + + int pending = mp_async_queue_get_samples(p->queue); + if (p->pending) + pending += mp_aframe_get_size(p->pending); + + mp_mutex_unlock(&p->lock); + return driver_delay + pending / (double)ao->samplerate; +} + +// Fully stop playback; clear buffers, including queue. +void ao_reset(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + bool wakeup = false; + bool do_reset = false; + + mp_mutex_lock(&p->lock); + + TA_FREEP(&p->pending); + mp_async_queue_reset(p->queue); + mp_filter_reset(p->filter_root); + mp_async_queue_resume_reading(p->queue); + + if (!ao->stream_silence && ao->driver->reset) { + if (ao->driver->write) { + ao->driver->reset(ao); + } else { + // Pull AOs may wait for ao_read_data() to return. + // That would deadlock if called from within the lock. + do_reset = true; + } + p->streaming = false; + } + wakeup = p->playing; + p->playing = false; + p->recover_pause = false; + p->hw_paused = false; + p->end_time_ns = 0; + + mp_mutex_unlock(&p->lock); + + if (do_reset) + ao->driver->reset(ao); + + if (wakeup) + ao_wakeup_playthread(ao); +} + +// Initiate playback. This moves from the stop/underrun state to actually +// playing (orthogonally taking the paused state into account). Plays all +// data in the queue, and goes into underrun state if no more data available. +// No-op if already running. +void ao_start(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + bool do_start = false; + + mp_mutex_lock(&p->lock); + + p->playing = true; + + if (!ao->driver->write && !p->paused && !p->streaming) { + p->streaming = true; + do_start = true; + } + + mp_mutex_unlock(&p->lock); + + // Pull AOs might call ao_read_data() so do this outside the lock. + if (do_start) + ao->driver->start(ao); + + ao_wakeup_playthread(ao); +} + +void ao_set_paused(struct ao *ao, bool paused, bool eof) +{ + struct buffer_state *p = ao->buffer_state; + bool wakeup = false; + bool do_reset = false, do_start = false; + + // If we are going to pause on eof and ao is still playing, + // be sure to drain the ao first for gapless. + if (eof && paused && ao_is_playing(ao)) + ao_drain(ao); + + mp_mutex_lock(&p->lock); + + if ((p->playing || !ao->driver->write) && !p->paused && paused) { + if (p->streaming && !ao->stream_silence) { + if (ao->driver->write) { + if (!p->recover_pause) + get_dev_state(ao, &p->prepause_state); + if (ao->driver->set_pause && ao->driver->set_pause(ao, true)) { + p->hw_paused = true; + } else { + ao->driver->reset(ao); + p->streaming = false; + p->recover_pause = !ao->untimed; + } + } else if (ao->driver->reset) { + // See ao_reset() why this is done outside of the lock. + do_reset = true; + p->streaming = false; + } + } + wakeup = true; + } else if (p->playing && p->paused && !paused) { + if (ao->driver->write) { + if (p->hw_paused) + ao->driver->set_pause(ao, false); + p->hw_paused = false; + } else { + if (!p->streaming) + do_start = true; + p->streaming = true; + } + wakeup = true; + } + p->paused = paused; + + mp_mutex_unlock(&p->lock); + + if (do_reset) + ao->driver->reset(ao); + if (do_start) + ao->driver->start(ao); + + if (wakeup) + ao_wakeup_playthread(ao); +} + +// Whether audio is playing. This means that there is still data in the buffers, +// and ao_start() was called. This returns true even if playback was logically +// paused. On false, EOF was reached, or an underrun happened, or ao_reset() +// was called. +bool ao_is_playing(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + mp_mutex_lock(&p->lock); + bool playing = p->playing; + mp_mutex_unlock(&p->lock); + + return playing; +} + +// Block until the current audio buffer has played completely. +void ao_drain(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + mp_mutex_lock(&p->lock); + while (!p->paused && p->playing) { + mp_mutex_unlock(&p->lock); + double delay = ao_get_delay(ao); + mp_mutex_lock(&p->lock); + + // Limit to buffer + arbitrary ~250ms max. waiting for robustness. + delay += mp_async_queue_get_samples(p->queue) / (double)ao->samplerate; + + // Wait for EOF signal from AO. + if (mp_cond_timedwait(&p->wakeup, &p->lock, + MP_TIME_S_TO_NS(MPMAX(delay, 0) + 0.25))) + { + MP_VERBOSE(ao, "drain timeout\n"); + break; + } + + if (!p->playing && mp_async_queue_get_samples(p->queue)) { + MP_WARN(ao, "underrun during draining\n"); + mp_mutex_unlock(&p->lock); + ao_start(ao); + mp_mutex_lock(&p->lock); + } + } + mp_mutex_unlock(&p->lock); + + ao_reset(ao); +} + +static void wakeup_filters(void *ctx) +{ + struct ao *ao = ctx; + ao_wakeup_playthread(ao); +} + +void ao_uninit(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + if (p && p->thread_valid) { + mp_mutex_lock(&p->pt_lock); + p->terminate = true; + mp_cond_broadcast(&p->pt_wakeup); + mp_mutex_unlock(&p->pt_lock); + + mp_thread_join(p->thread); + p->thread_valid = false; + } + + if (ao->driver_initialized) + ao->driver->uninit(ao); + + if (p) { + talloc_free(p->filter_root); + talloc_free(p->queue); + talloc_free(p->pending); + talloc_free(p->convert_buffer); + talloc_free(p->temp_buf); + + mp_cond_destroy(&p->wakeup); + mp_mutex_destroy(&p->lock); + + mp_cond_destroy(&p->pt_wakeup); + mp_mutex_destroy(&p->pt_lock); + } + + talloc_free(ao); +} + +void init_buffer_pre(struct ao *ao) +{ + ao->buffer_state = talloc_zero(ao, struct buffer_state); +} + +bool init_buffer_post(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + assert(ao->driver->start); + if (ao->driver->write) { + assert(ao->driver->reset); + assert(ao->driver->get_state); + } + + mp_mutex_init(&p->lock); + mp_cond_init(&p->wakeup); + + mp_mutex_init(&p->pt_lock); + mp_cond_init(&p->pt_wakeup); + + p->queue = mp_async_queue_create(); + p->filter_root = mp_filter_create_root(ao->global); + p->input = mp_async_queue_create_filter(p->filter_root, MP_PIN_OUT, p->queue); + + mp_async_queue_resume_reading(p->queue); + + struct mp_async_queue_config cfg = { + .sample_unit = AQUEUE_UNIT_SAMPLES, + .max_samples = ao->buffer, + .max_bytes = INT64_MAX, + }; + mp_async_queue_set_config(p->queue, cfg); + + if (ao->driver->write) { + mp_filter_graph_set_wakeup_cb(p->filter_root, wakeup_filters, ao); + + p->thread_valid = true; + if (mp_thread_create(&p->thread, playthread, ao)) { + p->thread_valid = false; + return false; + } + } else { + if (ao->stream_silence) { + ao->driver->start(ao); + p->streaming = true; + } + } + + if (ao->stream_silence) { + MP_WARN(ao, "The --audio-stream-silence option is set. This will break " + "certain player behavior.\n"); + } + + return true; +} + +static bool realloc_buf(struct ao *ao, int samples) +{ + struct buffer_state *p = ao->buffer_state; + + samples = MPMAX(1, samples); + + if (!p->temp_buf || samples > mp_aframe_get_size(p->temp_buf)) { + TA_FREEP(&p->temp_buf); + p->temp_buf = mp_aframe_create(); + if (!mp_aframe_set_format(p->temp_buf, ao->format) || + !mp_aframe_set_chmap(p->temp_buf, &ao->channels) || + !mp_aframe_set_rate(p->temp_buf, ao->samplerate) || + !mp_aframe_alloc_data(p->temp_buf, samples)) + { + TA_FREEP(&p->temp_buf); + return false; + } + } + + return true; +} + +// called locked +static bool ao_play_data(struct ao *ao) +{ + struct buffer_state *p = ao->buffer_state; + + if ((!p->playing || p->paused) && !ao->stream_silence) + return false; + + struct mp_pcm_state state; + get_dev_state(ao, &state); + + if (p->streaming && !state.playing && !ao->untimed) + goto eof; + + void **planes = NULL; + int space = state.free_samples; + if (!space) + return false; + assert(space >= 0); + + int samples = 0; + bool got_eof = false; + if (ao->driver->write_frames) { + TA_FREEP(&p->pending); + samples = read_buffer(ao, NULL, 1, &got_eof, false); + planes = (void **)&p->pending; + } else { + if (!realloc_buf(ao, space)) { + MP_ERR(ao, "Failed to allocate buffer.\n"); + return false; + } + planes = (void **)mp_aframe_get_data_rw(p->temp_buf); + assert(planes); + + if (p->recover_pause) { + samples = MPCLAMP(p->prepause_state.delay * ao->samplerate, 0, space); + p->recover_pause = false; + mp_aframe_set_silence(p->temp_buf, 0, space); + } + + if (!samples) { + samples = read_buffer(ao, planes, space, &got_eof, true); + if (p->paused || (ao->stream_silence && !p->playing)) + samples = space; // read_buffer() sets remainder to silent + } + } + + if (samples) { + MP_STATS(ao, "start ao fill"); + if (!ao->driver->write(ao, planes, samples)) + MP_ERR(ao, "Error writing audio to device.\n"); + MP_STATS(ao, "end ao fill"); + + if (!p->streaming) { + MP_VERBOSE(ao, "starting AO\n"); + ao->driver->start(ao); + p->streaming = true; + state.playing = true; + } + } + + MP_TRACE(ao, "in=%d space=%d(%d) pl=%d, eof=%d\n", + samples, space, state.free_samples, p->playing, got_eof); + + if (got_eof) + goto eof; + + return samples > 0 && (samples < space || ao->untimed); + +eof: + MP_VERBOSE(ao, "audio end or underrun\n"); + // Normal AOs signal EOF on underrun, untimed AOs never signal underruns. + if (ao->untimed || !state.playing || ao->stream_silence) { + p->streaming = state.playing && !ao->untimed; + p->playing = false; + } + ao->wakeup_cb(ao->wakeup_ctx); + // For ao_drain(). + mp_cond_broadcast(&p->wakeup); + return true; +} + +static MP_THREAD_VOID playthread(void *arg) +{ + struct ao *ao = arg; + struct buffer_state *p = ao->buffer_state; + mp_thread_set_name("ao"); + while (1) { + mp_mutex_lock(&p->lock); + + bool retry = false; + if (!ao->driver->initially_blocked || p->initial_unblocked) + retry = ao_play_data(ao); + + // Wait until the device wants us to write more data to it. + // Fallback to guessing. + int64_t timeout = INT64_MAX; + if (p->streaming && !retry && (!p->paused || ao->stream_silence)) { + // Wake up again if half of the audio buffer has been played. + // Since audio could play at a faster or slower pace, wake up twice + // as often as ideally needed. + timeout = MP_TIME_S_TO_NS(ao->device_buffer / (double)ao->samplerate * 0.25); + } + + mp_mutex_unlock(&p->lock); + + mp_mutex_lock(&p->pt_lock); + if (p->terminate) { + mp_mutex_unlock(&p->pt_lock); + break; + } + if (!p->need_wakeup && !retry) { + MP_STATS(ao, "start audio wait"); + mp_cond_timedwait(&p->pt_wakeup, &p->pt_lock, timeout); + MP_STATS(ao, "end audio wait"); + } + p->need_wakeup = false; + mp_mutex_unlock(&p->pt_lock); + } + MP_THREAD_RETURN(); +} + +void ao_unblock(struct ao *ao) +{ + if (ao->driver->write) { + struct buffer_state *p = ao->buffer_state; + mp_mutex_lock(&p->lock); + p->initial_unblocked = true; + mp_mutex_unlock(&p->lock); + ao_wakeup_playthread(ao); + } +} diff --git a/audio/out/internal.h b/audio/out/internal.h new file mode 100644 index 0000000..7951b38 --- /dev/null +++ b/audio/out/internal.h @@ -0,0 +1,237 @@ +/* + * This file is part of mpv. + * + * mpv 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. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_AO_INTERNAL_H_ +#define MP_AO_INTERNAL_H_ + +#include <stdatomic.h> +#include <stdbool.h> + +#include "audio/out/ao.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 (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 + int device_buffer; // device buffer in samples (guessed by + // common init code if not set by driver) + const struct ao_driver *driver; + bool driver_initialized; + void *priv; + struct mpv_global *global; + struct encode_lavc_context *encode_lavc_ctx; + void (*wakeup_cb)(void *ctx); + void *wakeup_ctx; + struct mp_log *log; // Using e.g. "[ao/coreaudio]" as prefix + int init_flags; // AO_INIT_* flags + bool stream_silence; // if audio inactive, just play silence + + // The device as selected by the user, usually using ao_device_desc.name + // from an entry from the list returned by driver->list_devices. If the + // default device should be used, this is set to NULL. + char *device; + + // Application name to report to the audio API. + char *client_name; + + // Used during init: if init fails, redirect to this ao + char *redirect; + + // Internal events (use ao_request_reload(), ao_hotplug_event()) + atomic_uint events_; + + // Float gain multiplicator + _Atomic float gain; + + int buffer; + double def_buffer; + struct buffer_state *buffer_state; +}; + +void init_buffer_pre(struct ao *ao); +bool init_buffer_post(struct ao *ao); + +struct mp_pcm_state { + // Note: free_samples+queued_samples <= ao->device_buffer; the sum may be + // less if the audio API can report partial periods played, while + // free_samples should be period-size aligned. If free_samples is not + // period-size aligned, the AO thread might get into a situation where + // it writes a very small number of samples in each iteration, leading + // to extremely inefficient behavior. + // Keep in mind that write() may write less than free_samples (or your + // period size alignment) anyway. + int free_samples; // number of free space in ring buffer + int queued_samples; // number of samples to play in ring buffer + double delay; // total latency in seconds (includes queued_samples) + bool playing; // set if underlying API is actually playing audio; + // the AO must unset it on underrun (accidental + // underrun and EOF are indistinguishable; the upper + // layers decide what it was) + // real pausing may assume playing=true +}; + +/* 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->write is NULL, it assumes + * that the driver uses a callback based audio API, otherwise push based. + * + * Requirements: + * a+b) Mandatory for both types: + * init + * uninit + * start + * Optional for both types: + * control + * a) ->write is called to queue audio. push.c creates a thread to regularly + * refill audio device buffers with ->write, but all driver functions are + * always called under an exclusive lock. + * Mandatory: + * reset + * write + * get_state + * Optional: + * set_pause + * b) ->write must be NULL. ->start must be provided, and should make the + * audio API start calling the audio callback. Your audio callback should + * in turn call ao_read_data() to get audio data. Most functions are + * optional and will be emulated if missing (e.g. pausing is emulated as + * silence). + * Also, the following optional callbacks can be provided: + * reset (stops the audio callback, start() restarts it) + */ +struct ao_driver { + // If true, use with encoding only. + bool encode; + // Name used for --ao. + const char *name; + // Description shown with --ao=help. + const char *description; + // This requires waiting for a AO_EVENT_INITIAL_UNBLOCK event before the + // first write() call is done. Encode mode uses this, and push mode + // respects it automatically (don't use with pull mode). + bool initially_blocked; + // If true, write units of entire frames. The write() call is modified to + // use data==mp_aframe. Useful for encoding AO only. + bool write_frames; + // Init the device using ao->format/ao->channels/ao->samplerate. If the + // 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); + // Optional. See ao_control() etc. in ao.c + int (*control)(struct ao *ao, enum aocontrol cmd, void *arg); + void (*uninit)(struct ao *ao); + // Stop all audio playback, clear buffers, back to state after init(). + // Optional for pull AOs. + void (*reset)(struct ao *ao); + // push based: set pause state. Only called after start() and before reset(). + // returns success (this is intended for paused=true; if it + // returns false, playback continues, and the core emulates via + // reset(); unpausing always works) + // The pausing state is also cleared by reset(). + bool (*set_pause)(struct ao *ao, bool paused); + // pull based: start the audio callback + // push based: start playing queued data + // AO should call ao_wakeup_playthread() if a period boundary + // is crossed, or playback stops due to external reasons + // (including underruns or device removal) + // must set mp_pcm_state.playing; unset on error/underrun/end + void (*start)(struct ao *ao); + // push based: queue new data. This won't try to write more data than the + // reported free space (samples <= mp_pcm_state.free_samples). + // This must NOT start playback. start() does that, and write() may be + // called multiple times before start() is called. It may also happen that + // reset() is called to discard the buffer. start() without write() will + // immediately reported an underrun. + // Return false on failure. + bool (*write)(struct ao *ao, void **data, int samples); + // push based: return mandatory stream information + void (*get_state)(struct ao *ao, struct mp_pcm_state *state); + + // Return the list of devices currently available in the system. Use + // ao_device_list_add() to add entries. The selected device will be set as + // ao->device (using ao_device_desc.name). + // Warning: the ao struct passed is not initialized with ao_driver->init(). + // Instead, hotplug_init/hotplug_uninit is called. If these + // callbacks are not set, no driver initialization call is done + // on the ao struct. + void (*list_devs)(struct ao *ao, struct ao_device_list *list); + + // If set, these are called before/after ao_driver->list_devs is called. + // It is also assumed that the driver can do hotplugging - which means + // it is expected to call ao_hotplug_event(ao) whenever the system's + // audio device list changes. The player will then call list_devs() again. + int (*hotplug_init)(struct ao *ao); + void (*hotplug_uninit)(struct ao *ao); + + // For option parsing (see vo.h) + int priv_size; + const void *priv_defaults; + const struct m_option *options; + const char *options_prefix; + const struct m_sub_options *global_opts; +}; + +// These functions can be called by AOs. + +int ao_read_data(struct ao *ao, void **data, int samples, int64_t out_time_ns); +MP_WARN_UNUSED_RESULT +int ao_read_data_nonblocking(struct ao *ao, void **data, int samples, int64_t out_time_ns); + +bool ao_chmap_sel_adjust(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map); +bool ao_chmap_sel_adjust2(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map, bool safe_multichannel); +bool ao_chmap_sel_get_def(struct ao *ao, const struct mp_chmap_sel *s, + struct mp_chmap *map, int num); + +// Add a deep copy of e to the list. +// Call from ao_driver->list_devs callback only. +void ao_device_list_add(struct ao_device_list *list, struct ao *ao, + struct ao_device_desc *e); + +void ao_post_process_data(struct ao *ao, void **data, int num_samples); + +struct ao_convert_fmt { + int src_fmt; // source AF_FORMAT_* + int channels; // number of channels + int dst_bits; // total target data sample size + int pad_msb; // padding in the MSB (i.e. required shifting) + int pad_lsb; // padding in LSB (required 0 bits) (ignored) +}; + +bool ao_can_convert_inplace(struct ao_convert_fmt *fmt); +bool ao_need_conversion(struct ao_convert_fmt *fmt); +void ao_convert_inplace(struct ao_convert_fmt *fmt, void **data, int num_samples); + +void ao_wakeup_playthread(struct ao *ao); + +int ao_read_data_converted(struct ao *ao, struct ao_convert_fmt *fmt, + void **data, int samples, int64_t out_time_ns); + +#endif |