Skip to content

Commit

Permalink
ao_coreaudio: stop audio unit after idle timeout
Browse files Browse the repository at this point in the history
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 mpv-player#11617

The code changes were authored by @orion1vi and @lhc70000.

Co-authored-by: Collider LI <lhc199652@gmail.com>
  • Loading branch information
2 people authored and low-batt committed Mar 8, 2024
1 parent 580bc69 commit 2abfbee
Showing 1 changed file with 79 additions and 2 deletions.
81 changes: 79 additions & 2 deletions audio/out/ao_coreaudio.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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_async_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_async_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);
Expand Down

0 comments on commit 2abfbee

Please sign in to comment.