diff --git a/audio/out/ao_harmony.c b/audio/out/ao_harmony.c new file mode 100644 index 0000000000..856bb92052 --- /dev/null +++ b/audio/out/ao_harmony.c @@ -0,0 +1,446 @@ +/* HarmonyOS OHAudio output for the MPV pull-based audio contract. */ + +#include +#include +#include + +#include "config.h" +#include "ao.h" +#include "audio/format.h" +#include "audio/out/ao_harmony.h" +#include "common/msg.h" +#include "internal.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +#if HAVE_HARMONY_AUDIO + +#include +#include +#include + +struct priv { + OH_AudioRenderer *renderer; + mp_thread clock_thread; + bool clock_thread_valid; + atomic_bool stop_clock; + atomic_bool callback_enabled; + atomic_int callbacks_inflight; + _Atomic int64_t submitted_frames; + _Atomic int64_t played_frames; + _Atomic int64_t raw_played_frames; + _Atomic int64_t timestamp_origin_raw; + _Atomic int64_t audio_timestamp_ns; + _Atomic int64_t estimated_delay_ms; + _Atomic int64_t buffer_starvation; + _Atomic int64_t callback_count; + int32_t latency_ms; + int32_t frame_size; + int32_t channel_count; + uint64_t channel_layout; +}; + +static _Atomic int32_t g_state = MPV_HARMONY_AUDIO_DISABLED; +static _Atomic int32_t g_sample_rate; +static _Atomic int32_t g_sample_format; +static _Atomic int32_t g_channels; +static _Atomic uint64_t g_channel_layout; +static _Atomic int64_t g_submitted_frames; +static _Atomic int64_t g_played_frames; +static _Atomic int64_t g_audio_timestamp_ns; +static _Atomic int64_t g_estimated_delay_ms; +static _Atomic int64_t g_buffer_starvation; +static _Atomic int64_t g_system_underrun; +static _Atomic int64_t g_callback_count; +static _Atomic int64_t g_audio_flush_count; +static _Atomic int64_t g_seek_audio_reset_count; + +static void reset_stats(void) +{ + atomic_store(&g_state, MPV_HARMONY_AUDIO_DISABLED); + atomic_store(&g_sample_rate, 0); + atomic_store(&g_sample_format, AF_FORMAT_UNKNOWN); + atomic_store(&g_channels, 0); + atomic_store(&g_channel_layout, 0); + atomic_store(&g_submitted_frames, 0); + atomic_store(&g_played_frames, 0); + atomic_store(&g_audio_timestamp_ns, 0); + atomic_store(&g_estimated_delay_ms, 0); + atomic_store(&g_buffer_starvation, 0); + atomic_store(&g_system_underrun, 0); + atomic_store(&g_callback_count, 0); + atomic_store(&g_audio_flush_count, 0); + atomic_store(&g_seek_audio_reset_count, 0); +} + +static void set_state(int32_t state) +{ + atomic_store(&g_state, state); +} + +static void log_result(struct ao *ao, const char *operation, OH_AudioStream_Result result) +{ + MP_ERR(ao, "%s failed: %d\n", operation, result); +} + +static OH_AudioStream_SampleFormat sample_format_for(int format) +{ + return af_fmt_is_float(format) ? AUDIOSTREAM_SAMPLE_F32LE : AUDIOSTREAM_SAMPLE_S16LE; +} + +static void fill_silence(struct ao *ao, void *data, int samples) +{ + if (samples > 0) + af_fill_silence(data, (size_t)samples * ao->sstride, ao->format); +} + +static OH_AudioData_Callback_Result write_data(OH_AudioRenderer *renderer, void *user_data, + void *audio_data, int32_t audio_data_size) +{ + (void)renderer; + struct ao *ao = user_data; + struct priv *p = ao->priv; + atomic_fetch_add(&p->callbacks_inflight, 1); + int64_t callback_number = atomic_fetch_add(&p->callback_count, 1) + 1; + atomic_fetch_add(&g_callback_count, 1); + + int stride = ao->sstride; + if (audio_data == NULL || audio_data_size <= 0 || stride <= 0) { + atomic_fetch_sub(&p->callbacks_inflight, 1); + return AUDIO_DATA_CALLBACK_RESULT_INVALID; + } + + int samples = audio_data_size / stride; + int remainder = audio_data_size - samples * stride; + if (!atomic_load(&p->callback_enabled) || samples <= 0) { + memset(audio_data, 0, (size_t)audio_data_size); + atomic_fetch_sub(&p->callbacks_inflight, 1); + return AUDIO_DATA_CALLBACK_RESULT_VALID; + } + + if (callback_number == 1) + MP_VERBOSE(ao, "OHAudio first callback: %d frames, %d bytes\n", samples, audio_data_size); + + void *planes[1] = {audio_data}; + int64_t end_time = mp_time_ns() + MP_TIME_MS_TO_NS(p->latency_ms); + end_time += MP_TIME_S_TO_NS(samples / (double)ao->samplerate); + int got = ao_read_data(ao, planes, samples, end_time, NULL, true, false); + if (got < samples) { + fill_silence(ao, (char *)audio_data + (size_t)got * stride, samples - got); + atomic_fetch_add(&p->buffer_starvation, 1); + atomic_fetch_add(&g_buffer_starvation, 1); + } + if (remainder > 0) + memset((char *)audio_data + (size_t)samples * stride, 0, (size_t)remainder); + atomic_fetch_add(&p->submitted_frames, samples); + atomic_fetch_add(&g_submitted_frames, samples); + atomic_fetch_sub(&p->callbacks_inflight, 1); + return AUDIO_DATA_CALLBACK_RESULT_VALID; +} + +static void renderer_error(OH_AudioRenderer *renderer, void *user_data, + OH_AudioStream_Result error) +{ + (void)renderer; + struct ao *ao = user_data; + log_result(ao, "renderer error callback", error); + set_state(MPV_HARMONY_AUDIO_ERROR); + ao_request_reload(ao); +} + +static void output_device_changed(OH_AudioRenderer *renderer, void *user_data, + OH_AudioStream_DeviceChangeReason reason) +{ + (void)renderer; + struct ao *ao = user_data; + MP_VERBOSE(ao, "output device changed: %d\n", reason); + ao_request_reload(ao); +} + +static MP_THREAD_VOID clock_loop(void *arg) +{ + struct ao *ao = arg; + struct priv *p = ao->priv; + mp_thread_set_name("ao/harmony-clock"); + while (!atomic_load(&p->stop_clock)) { + int64_t played = 0; + int64_t timestamp = 0; + if (OH_AudioRenderer_GetAudioTimestampInfo(p->renderer, &played, ×tamp) == AUDIOSTREAM_SUCCESS) { + atomic_store(&p->raw_played_frames, played); + int64_t origin = atomic_load(&p->timestamp_origin_raw); + int64_t effective_played = played > origin ? played - origin : 0; + atomic_store(&p->played_frames, effective_played); + atomic_store(&p->audio_timestamp_ns, timestamp); + atomic_store(&g_played_frames, effective_played); + atomic_store(&g_audio_timestamp_ns, timestamp); + int64_t submitted = atomic_load(&p->submitted_frames); + int64_t pending = submitted > effective_played ? submitted - effective_played : 0; + int64_t delay = p->latency_ms + pending * 1000 / MPMAX(1, ao->samplerate); + atomic_store(&p->estimated_delay_ms, delay); + atomic_store(&g_estimated_delay_ms, delay); + } + mp_sleep_ns(MP_TIME_MS_TO_NS(20)); + } + MP_THREAD_RETURN(); +} + +static OH_AudioStream_Result create_renderer(struct ao *ao, int format) +{ + struct priv *p = ao->priv; + p->renderer = NULL; + OH_AudioStreamBuilder *builder = NULL; + OH_AudioStream_Result result = OH_AudioStreamBuilder_Create(&builder, AUDIOSTREAM_TYPE_RENDERER); + if (result != AUDIOSTREAM_SUCCESS || builder == NULL) { + log_result(ao, "AUDIO_BUILDER_FAILED", result); + return result; + } + + OH_AudioStream_SampleFormat sample_format = sample_format_for(format); + OH_AudioChannelLayout layout = p->channel_count == 1 ? CH_LAYOUT_MONO : CH_LAYOUT_STEREO; + result = OH_AudioStreamBuilder_SetSamplingRate(builder, ao->samplerate); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetChannelCount(builder, p->channel_count); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetChannelLayout(builder, layout); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetSampleFormat(builder, sample_format); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetEncodingType(builder, AUDIOSTREAM_ENCODING_TYPE_RAW); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetLatencyMode(builder, AUDIOSTREAM_LATENCY_MODE_NORMAL); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetRendererInfo(builder, AUDIOSTREAM_USAGE_MOVIE); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetRendererWriteDataCallback(builder, write_data, ao); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetRendererOutputDeviceChangeCallback(builder, + output_device_changed, ao); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_SetRendererErrorCallback(builder, renderer_error, ao); + if (result == AUDIOSTREAM_SUCCESS) + result = OH_AudioStreamBuilder_GenerateRenderer(builder, &p->renderer); + OH_AudioStreamBuilder_Destroy(builder); + if (result != AUDIOSTREAM_SUCCESS || p->renderer == NULL) { + if (p->renderer != NULL) { + OH_AudioRenderer_Release(p->renderer); + p->renderer = NULL; + } + log_result(ao, "AUDIO_RENDERER_CREATE_FAILED", result); + return result; + } + if (OH_AudioRenderer_SetVolume(p->renderer, 1.0f) != AUDIOSTREAM_SUCCESS) + MP_WARN(ao, "renderer unity volume could not be set\n"); + if (OH_AudioRenderer_GetFrameSizeInCallback(p->renderer, &p->frame_size) != AUDIOSTREAM_SUCCESS) + p->frame_size = 0; + if (OH_AudioRenderer_GetLatency(p->renderer, AUDIOSTREAM_LATENCY_TYPE_ALL, &p->latency_ms) != AUDIOSTREAM_SUCCESS) + p->latency_ms = 0; + MP_VERBOSE(ao, "OHAudio renderer ready: %d Hz, %d channels, %s, frame_size=%d, latency=%d ms\n", + ao->samplerate, p->channel_count, af_fmt_to_str(format), p->frame_size, p->latency_ms); + return AUDIOSTREAM_SUCCESS; +} + +static int init(struct ao *ao) +{ + struct priv *p = ao->priv; + reset_stats(); + atomic_init(&p->stop_clock, false); + atomic_init(&p->callback_enabled, false); + atomic_init(&p->callbacks_inflight, 0); + atomic_init(&p->submitted_frames, 0); + atomic_init(&p->played_frames, 0); + atomic_init(&p->raw_played_frames, 0); + atomic_init(&p->timestamp_origin_raw, 0); + atomic_init(&p->audio_timestamp_ns, 0); + atomic_init(&p->estimated_delay_ms, 0); + atomic_init(&p->buffer_starvation, 0); + atomic_init(&p->callback_count, 0); + p->renderer = NULL; + p->clock_thread_valid = false; + p->frame_size = 0; + p->latency_ms = 0; + + if (!af_fmt_is_pcm(ao->format)) { + MP_ERR(ao, "AUDIO_UNSUPPORTED_FORMAT: %s\n", af_fmt_to_str(ao->format)); + set_state(MPV_HARMONY_AUDIO_ERROR); + return -1; + } + ao->format = af_fmt_is_float(ao->format) ? AF_FORMAT_FLOAT : AF_FORMAT_S16; + if (ao->channels.num < 1) { + MP_ERR(ao, "AUDIO_UNSUPPORTED_CHANNEL_LAYOUT\n"); + set_state(MPV_HARMONY_AUDIO_ERROR); + return -1; + } + p->channel_count = ao->channels.num == 1 ? 1 : 2; + if (ao->channels.num != p->channel_count) + mp_chmap_from_channels(&ao->channels, p->channel_count); + p->channel_layout = p->channel_count == 1 ? CH_LAYOUT_MONO : CH_LAYOUT_STEREO; + + OH_AudioStream_Result result = create_renderer(ao, ao->format); + if (result != AUDIOSTREAM_SUCCESS && ao->format == AF_FORMAT_FLOAT) { + ao->format = AF_FORMAT_S16; + result = create_renderer(ao, ao->format); + } + if (result != AUDIOSTREAM_SUCCESS) { + set_state(MPV_HARMONY_AUDIO_ERROR); + return -1; + } + atomic_store(&g_sample_rate, ao->samplerate); + atomic_store(&g_sample_format, ao->format); + atomic_store(&g_channels, p->channel_count); + atomic_store(&g_channel_layout, p->channel_layout); + atomic_store(&g_estimated_delay_ms, p->latency_ms); + set_state(MPV_HARMONY_AUDIO_READY); + if (mp_thread_create(&p->clock_thread, clock_loop, ao)) { + MP_ERR(ao, "AUDIO_TIMESTAMP_ERROR: clock thread creation failed\n"); + OH_AudioRenderer_Release(p->renderer); + p->renderer = NULL; + set_state(MPV_HARMONY_AUDIO_ERROR); + return -1; + } + p->clock_thread_valid = true; + return 0; +} + +static void uninit(struct ao *ao) +{ + struct priv *p = ao->priv; + MP_VERBOSE(ao, "OHAudio release\n"); + atomic_store(&p->callback_enabled, false); + if (p->renderer != NULL) { + OH_AudioRenderer_Stop(p->renderer); + while (atomic_load(&p->callbacks_inflight) > 0) + mp_sleep_ns(MP_TIME_MS_TO_NS(1)); + } + atomic_store(&p->stop_clock, true); + if (p->clock_thread_valid) + mp_thread_join(p->clock_thread); + if (p->renderer != NULL) { + OH_AudioStream_Result result = OH_AudioRenderer_Release(p->renderer); + if (result != AUDIOSTREAM_SUCCESS) + log_result(ao, "AUDIO_RELEASE_ERROR", result); + p->renderer = NULL; + } + set_state(MPV_HARMONY_AUDIO_RELEASED); +} + +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + if (p->renderer == NULL) + return; + atomic_store(&p->callback_enabled, true); + OH_AudioStream_Result result = OH_AudioRenderer_Start(p->renderer); + if (result != AUDIOSTREAM_SUCCESS) { + log_result(ao, "AUDIO_RENDERER_START_FAILED", result); + set_state(MPV_HARMONY_AUDIO_ERROR); + ao_request_reload(ao); + return; + } + MP_VERBOSE(ao, "OHAudio start\n"); + set_state(MPV_HARMONY_AUDIO_RUNNING); +} + +static bool set_pause(struct ao *ao, bool paused) +{ + struct priv *p = ao->priv; + if (p->renderer == NULL) + return false; + OH_AudioStream_Result result = paused ? OH_AudioRenderer_Pause(p->renderer) + : OH_AudioRenderer_Start(p->renderer); + if (result != AUDIOSTREAM_SUCCESS) { + log_result(ao, paused ? "pause" : "resume", result); + set_state(MPV_HARMONY_AUDIO_ERROR); + return false; + } + MP_VERBOSE(ao, "OHAudio %s\n", paused ? "pause" : "resume"); + set_state(paused ? MPV_HARMONY_AUDIO_PAUSED : MPV_HARMONY_AUDIO_RUNNING); + return true; +} + +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + if (p->renderer == NULL) + return; + atomic_store(&p->callback_enabled, false); + OH_AudioStream_Result stop_result = OH_AudioRenderer_Stop(p->renderer); + if (stop_result != AUDIOSTREAM_SUCCESS && stop_result != AUDIOSTREAM_ERROR_ILLEGAL_STATE) + log_result(ao, "AUDIO_STOP_FAILED", stop_result); + while (atomic_load(&p->callbacks_inflight) > 0) + mp_sleep_ns(MP_TIME_MS_TO_NS(1)); + OH_AudioStream_Result result = OH_AudioRenderer_Flush(p->renderer); + if (result != AUDIOSTREAM_SUCCESS) { + log_result(ao, "AUDIO_FLUSH_FAILED", result); + set_state(MPV_HARMONY_AUDIO_ERROR); + return; + } + atomic_store(&p->timestamp_origin_raw, atomic_load(&p->raw_played_frames)); + atomic_store(&p->submitted_frames, 0); + atomic_store(&p->played_frames, 0); + atomic_store(&p->audio_timestamp_ns, 0); + atomic_store(&g_submitted_frames, 0); + atomic_store(&g_played_frames, 0); + atomic_store(&g_audio_timestamp_ns, 0); + atomic_fetch_add(&g_audio_flush_count, 1); + atomic_fetch_add(&g_seek_audio_reset_count, 1); + MP_VERBOSE(ao, "OHAudio flush\n"); + set_state(MPV_HARMONY_AUDIO_STOPPED); +} + +static void get_state(struct ao *ao, struct mp_pcm_state *state) +{ + struct priv *p = ao->priv; + int64_t submitted = atomic_load(&p->submitted_frames); + int64_t played = atomic_load(&p->played_frames); + int64_t pending = submitted > played ? submitted - played : 0; + state->free_samples = 0; + state->queued_samples = pending; + state->delay = pending / (double)MPMAX(1, ao->samplerate) + p->latency_ms / 1000.0; + state->playing = atomic_load(&g_state) == MPV_HARMONY_AUDIO_RUNNING; +} + +const struct ao_driver audio_out_harmony = { + .description = "HarmonyOS OHAudio output", + .name = "harmony", + .init = init, + .uninit = uninit, + .reset = reset, + .set_pause = set_pause, + .start = start, + .get_state = get_state, + .priv_size = sizeof(struct priv), +}; + +#else + +MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats) +{ + if (stats != NULL) + memset(stats, 0, sizeof(*stats)); + return 0; +} + +#endif + +#if HAVE_HARMONY_AUDIO +MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats) +{ + if (stats == NULL) + return 0; + stats->state = atomic_load(&g_state); + stats->sample_rate = atomic_load(&g_sample_rate); + stats->sample_format = atomic_load(&g_sample_format); + stats->channels = atomic_load(&g_channels); + stats->channel_layout = atomic_load(&g_channel_layout); + stats->submitted_frames = atomic_load(&g_submitted_frames); + stats->played_frames = atomic_load(&g_played_frames); + stats->audio_timestamp_ns = atomic_load(&g_audio_timestamp_ns); + stats->estimated_delay_ms = atomic_load(&g_estimated_delay_ms); + stats->buffer_starvation = atomic_load(&g_buffer_starvation); + stats->system_underrun = atomic_load(&g_system_underrun); + stats->callback_count = atomic_load(&g_callback_count); + stats->audio_flush_count = atomic_load(&g_audio_flush_count); + stats->seek_audio_reset_count = atomic_load(&g_seek_audio_reset_count); + return 1; +} +#endif diff --git a/audio/out/ao_harmony.h b/audio/out/ao_harmony.h new file mode 100644 index 0000000000..ac68588d4a --- /dev/null +++ b/audio/out/ao_harmony.h @@ -0,0 +1,49 @@ +/* + * HarmonyOS audio diagnostics exported for the isolated Playback Lab. + * The AO remains usable without consuming this optional diagnostics API. + */ +#ifndef MPV_AO_HARMONY_H +#define MPV_AO_HARMONY_H + +#include + +#include "mpv/client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +enum mpv_harmony_audio_state { + MPV_HARMONY_AUDIO_DISABLED = 0, + MPV_HARMONY_AUDIO_READY = 1, + MPV_HARMONY_AUDIO_RUNNING = 2, + MPV_HARMONY_AUDIO_PAUSED = 3, + MPV_HARMONY_AUDIO_STOPPED = 4, + MPV_HARMONY_AUDIO_ERROR = 5, + MPV_HARMONY_AUDIO_RELEASED = 6, +}; + +struct mpv_harmony_audio_stats { + int32_t state; + int32_t sample_rate; + int32_t sample_format; + int32_t channels; + uint64_t channel_layout; + int64_t submitted_frames; + int64_t played_frames; + int64_t audio_timestamp_ns; + int64_t estimated_delay_ms; + int64_t buffer_starvation; + int64_t system_underrun; + int64_t callback_count; + int64_t audio_flush_count; + int64_t seek_audio_reset_count; +}; + +MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats); + +#ifdef __cplusplus +} +#endif + +#endif