From ab419a6660c6f8f78b30ba0838ab3c274746af89 Mon Sep 17 00:00:00 2001 From: Vilius Date: Tue, 9 May 2023 01:06:22 +0300 Subject: [PATCH] ao_coreaudio: stop audio unit after idle timeout Commit 39f7f83 changed ao_driver.reset to use AudioUnitReset instead of AudioOutputUnitStop. The problem with calling AudioOutputUnitStop was that AudioOutputUnitStart takes a significant amount of time after a stop when a wireless audio device is being used. This resulted in lagging that was noticeable to users during seeking and short pause/resume cycles. Switching to AudioUnitReset eliminated this lagging. However with the switch to AudioUnitReset the macOS daemon coreaudiod continued to consume CPU time and did not release a powerd assertion that it created on behalf of mpv, preventing macOS from sleeping. This commit will change ao_coreaudio.reset to call AudioOutputUnitStop after a delay if playback has not resumed. This preserves the faster restart of playback for seeking and short pause/resume cycles and avoids preventing sleep and needless CPU consumption. Fixes #11617 The code changes were authored by @orion1vi and @lhc70000. Co-authored-by: Collider LI --- audio/out/ao_coreaudio.c | 81 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/audio/out/ao_coreaudio.c b/audio/out/ao_coreaudio.c index 37f1313ca54d4..4d983faba3b32 100644 --- a/audio/out/ao_coreaudio.c +++ b/audio/out/ao_coreaudio.c @@ -27,6 +27,11 @@ #include "ao_coreaudio_properties.h" #include "ao_coreaudio_utils.h" +// The timeout for stopping the audio unit after being reset. This allows the +// device to sleep after playback paused. The duration is chosen to match the +// behavior of AVFoundation. +#define IDLE_TIME 7 * NSEC_PER_SEC + struct priv { AudioDeviceID device; AudioUnit audio_unit; @@ -37,6 +42,10 @@ struct priv { AudioStreamID original_asbd_stream; bool change_physical_format; + + // Block that is executed after `IDLE_TIME` to stop audio output unit. + dispatch_block_t idle_work; + dispatch_queue_t queue; }; static int64_t ca_get_hardware_latency(struct ao *ao) { @@ -166,6 +175,9 @@ static int init(struct ao *ao) if (!init_audiounit(ao, asbd)) goto coreaudio_error; + p->queue = dispatch_queue_create("io.mpv.coreaudio_stop_during_idle", + DISPATCH_QUEUE_SERIAL); + return CONTROL_OK; coreaudio_error: @@ -320,24 +332,89 @@ static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd) return false; } -static void reset(struct ao *ao) +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 cancel_and_release_idle_work(struct priv *p) +{ + if (!p->idle_work) + return; + + dispatch_block_cancel(p->idle_work); + Block_release(p->idle_work); + p->idle_work = NULL; +} + +static void stop_after_idle_time(struct ao *ao) +{ + struct priv *p = ao->priv; + + cancel_and_release_idle_work(p); + + p->idle_work = dispatch_block_create(0, ^{ + MP_VERBOSE(ao, "Stopping audio unit due to idle timeout\n"); + stop(ao); + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, IDLE_TIME), + p->queue, p->idle_work); +} + +static void _reset(void *_ao) { + struct ao *ao = (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"); + + // Until the audio unit is stopped the macOS daemon coreaudiod continues to + // consume CPU and prevent macOS from sleeping. Immediately stopping the + // audio unit would be disruptive for short pause/resume cycles as + // restarting the audio unit takes a noticeable amount of time when a + // wireless audio device is being used. Instead the audio unit is stopped + // after a delay if it remains idle. + stop_after_idle_time(ao); } -static void start(struct ao *ao) +static void reset(struct ao *ao) +{ + struct priv *p = ao->priv; + // Must dispatch to serialize reset, start and stop operations. + dispatch_sync_f(p->queue, ao, &_reset); +} + +static void _start(void *_ao) { + struct ao *ao = (struct ao *)_ao; struct priv *p = ao->priv; + + if (p->idle_work) + dispatch_block_cancel(p->idle_work); + OSStatus err = AudioOutputUnitStart(p->audio_unit); CHECK_CA_WARN("can't start audio unit"); } +static void start(struct ao *ao) +{ + struct priv *p = ao->priv; + // Must dispatch to serialize reset, start and stop operations. + dispatch_sync_f(p->queue, ao, &_start); +} static void uninit(struct ao *ao) { struct priv *p = ao->priv; + + dispatch_sync(p->queue, ^{ + cancel_and_release_idle_work(p); + }); + dispatch_release(p->queue); + AudioOutputUnitStop(p->audio_unit); AudioUnitUninitialize(p->audio_unit); AudioComponentInstanceDispose(p->audio_unit);