Skip to content

Commit

Permalink
ao_avfoundation: initial avfoundation ao support
Browse files Browse the repository at this point in the history
  • Loading branch information
ruihe774 committed Mar 28, 2024
1 parent 2af3a6e commit e022a6e
Show file tree
Hide file tree
Showing 10 changed files with 599 additions and 28 deletions.
12 changes: 11 additions & 1 deletion DOCS/man/ao.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,21 @@ Available audio output drivers are:
passthrough (even if the device reports it as supported). Use with
extreme care.


``coreaudio_exclusive`` (macOS only)
Native macOS audio output driver using direct device access and
exclusive mode (bypasses the sound server).

``avfoundation`` (macOS only)
Native macOS audio output driver using ``AVSampleBufferAudioRenderer``
in AVFoundation, which supports `spatial audio
<https://support.apple.com/en-us/HT211775>`_.

.. warning::

Turning on spatial audio may hang the playback
if mpv is not started out of the bundle,
though playback with spatial audio off always works.

``openal``
OpenAL audio output driver.

Expand Down
4 changes: 4 additions & 0 deletions audio/out/ao.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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_avfoundation;
extern const struct ao_driver audio_out_rsound;
extern const struct ao_driver audio_out_pipewire;
extern const struct ao_driver audio_out_sndio;
Expand All @@ -65,6 +66,9 @@ static const struct ao_driver * const audio_out_drivers[] = {
#if HAVE_COREAUDIO
&audio_out_coreaudio,
#endif
#if HAVE_AVFOUNDATION
&audio_out_avfoundation,
#endif
#if HAVE_PIPEWIRE
&audio_out_pipewire,
#endif
Expand Down
358 changes: 358 additions & 0 deletions audio/out/ao_avfoundation.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
/*
* 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 "audio/format.h"
#include "audio/out/ao_coreaudio_chmap.h"
#include "audio/out/ao_coreaudio_utils.h"
#include "common/common.h"
#include "common/msg.h"
#include "internal.h"
#include "osdep/timer.h"
#include "ta/ta_talloc.h"

#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#import <CoreAudioTypes/CoreAudioTypes.h>
#import <CoreFoundation/CoreFoundation.h>
#import <CoreMedia/CoreMedia.h>


@interface AVObserver : NSObject {
struct ao *ao;
}
- (void)handleRestartNotification:(NSNotification*)notification;
@end

struct priv {
AVSampleBufferAudioRenderer *renderer;
AVSampleBufferRenderSynchronizer *synchronizer;
dispatch_queue_t queue;
CMAudioFormatDescriptionRef format_description;
AVObserver *observer;
int64_t end_time_av;
};

static int64_t CMTimeGetNanoseconds(CMTime time)
{
time = CMTimeConvertScale(time, 1000000000, kCMTimeRoundingMethod_Default);
return time.value;
}

static CMTime CMTimeFromNanoseconds(int64_t time)
{
return CMTimeMake(time, 1000000000);
}

static void feed(struct ao *ao)
{
struct priv *p = ao->priv;
int samplerate = ao->samplerate;
int sstride = ao->sstride;

CMBlockBufferRef block_buffer = NULL;
CMSampleBufferRef sample_buffer = NULL;
OSStatus err;

int request_sample_count = samplerate / 10;
int buffer_size = request_sample_count * sstride;
void *data[] = {CFAllocatorAllocate(NULL, buffer_size, 0)};

int64_t cur_time_av = CMTimeGetNanoseconds([p->synchronizer currentTime]);
int64_t cur_time_mp = mp_time_ns();
int64_t end_time_av = MPMAX(p->end_time_av, cur_time_av);
int64_t time_delta = CMTimeGetNanoseconds(CMTimeMake(request_sample_count, samplerate));
int real_sample_count = ao_read_data_nonblocking(ao, data, request_sample_count, end_time_av - cur_time_av + cur_time_mp + time_delta);
if (real_sample_count == 0) {
// avoid spinning by blocking the thread
mp_sleep_ns(10000000);
goto finish;
}

if ((err = CMBlockBufferCreateWithMemoryBlock(
NULL,
data[0],
buffer_size,
NULL,
NULL,
0,
real_sample_count * sstride,
0,
&block_buffer
)) != noErr) {
MP_FATAL(ao, "failed to create block buffer\n");
MP_VERBOSE(ao, "CMBlockBufferCreateWithMemoryBlock returned %d\n", err);
goto error;
}
data[0] = NULL;

CMSampleTimingInfo sample_timing_into[] = {(CMSampleTimingInfo) {
.duration = CMTimeMake(1, samplerate),
.presentationTimeStamp = CMTimeFromNanoseconds(end_time_av),
.decodeTimeStamp = kCMTimeInvalid
}};
size_t sample_size_array[] = {sstride};
if ((err = CMSampleBufferCreateReady(
NULL,
block_buffer,
p->format_description,
real_sample_count,
1,
sample_timing_into,
1,
sample_size_array,
&sample_buffer
)) != noErr) {
MP_FATAL(ao, "failed to create sample buffer\n");
MP_VERBOSE(ao, "CMSampleBufferCreateReady returned %d\n", err);
goto error;
}

[p->renderer enqueueSampleBuffer:sample_buffer];

time_delta = CMTimeGetNanoseconds(CMTimeMake(real_sample_count, samplerate));
p->end_time_av = end_time_av + time_delta;

goto finish;

error:
ao_request_reload(ao);
finish:
if (data[0]) CFAllocatorDeallocate(NULL, data[0]);
if (block_buffer) CFRelease(block_buffer);
if (sample_buffer) CFRelease(sample_buffer);
}

static void start(struct ao *ao)
{
struct priv *p = ao->priv;

p->end_time_av = -1;
[p->synchronizer setRate:1];
[p->renderer requestMediaDataWhenReadyOnQueue:p->queue usingBlock:^{
feed(ao);
}];
}

static void stop(struct ao *ao)
{
struct priv *p = ao->priv;

dispatch_sync(p->queue, ^{
[p->renderer stopRequestingMediaData];
[p->renderer flush];
[p->synchronizer setRate:0];
});
}

static int control(struct ao *ao, enum aocontrol cmd, void *arg)
{
struct priv *p = ao->priv;

switch (cmd) {
case AOCONTROL_GET_MUTE:
*(bool*)arg = [p->renderer isMuted];
return CONTROL_OK;
case AOCONTROL_GET_VOLUME:
*(float*)arg = [p->renderer volume] * 100;
return CONTROL_OK;
case AOCONTROL_SET_MUTE:
[p->renderer setMuted:*(bool*)arg];
return CONTROL_OK;
case AOCONTROL_SET_VOLUME:
[p->renderer setVolume:*(float*)arg / 100];
return CONTROL_OK;
default:
return CONTROL_UNKNOWN;
}
}

@implementation AVObserver
- (instancetype)initWithAO:(struct ao*)_ao {
self = [super init];
if (self) {
ao = _ao;
}
return self;
}
- (void)handleRestartNotification:(NSNotification*)notification {
char *name = cfstr_get_cstr((CFStringRef)notification.name);
MP_WARN(ao, "restarting due to system notification; this will cause desync\n");
MP_VERBOSE(ao, "notification name: %s\n", name);
talloc_free(name);
stop(ao);
start(ao);
}
@end

static int init(struct ao *ao)
{
struct priv *p = ao->priv;
AudioChannelLayout *layout = NULL;

#if TARGET_OS_IPHONE
AVAudioSession *instance = AVAudioSession.sharedInstance;
NSInteger maxChannels = instance.maximumOutputNumberOfChannels;
NSInteger prefChannels = MIN(maxChannels, ao->channels.num);
[instance setCategory:AVAudioSessionCategoryPlayback error:nil];
[instance setMode:AVAudioSessionModeMoviePlayback error:nil];
[instance setActive:YES error:nil];
[instance setPreferredOutputNumberOfChannels:prefChannels error:nil];
#endif

if ((p->renderer = [[AVSampleBufferAudioRenderer alloc] init]) == nil) {
MP_FATAL(ao, "failed to create audio renderer\n");
MP_VERBOSE(ao, "AVSampleBufferAudioRenderer failed to initialize\n");
goto error;
}
if ((p->synchronizer = [[AVSampleBufferRenderSynchronizer alloc] init]) == nil) {
MP_FATAL(ao, "failed to create rendering synchronizer\n");
MP_VERBOSE(ao, "AVSampleBufferRenderSynchronizer failed to initialize\n");
goto error;
}
if ((p->queue = dispatch_queue_create(
"avfoundation event",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0)
)) == NULL) {
MP_FATAL(ao, "failed to create dispatch queue\n");
MP_VERBOSE(ao, "dispatch_queue_create failed\n");
goto error;
}

if (ao->device && ao->device[0]) {
[p->renderer setAudioOutputDeviceUniqueID:(NSString*)cfstr_from_cstr(ao->device)];
}

[p->synchronizer addRenderer:p->renderer];
if (@available(tvOS 14.5, iOS 14.5, macOS 11.3, *)) {
[p->synchronizer setDelaysRateChangeUntilHasSufficientMediaData:NO];
}

if (af_fmt_is_spdif(ao->format)) {
MP_FATAL(ao, "avfoundation does not support SPDIF\n");
#if HAVE_COREAUDIO
MP_FATAL(ao, "please use coreaudio_exclusive instead\n");
#endif
goto error;
}

// AVSampleBufferAudioRenderer only supports interleaved formats
ao->format = af_fmt_from_planar(ao->format);
if (af_fmt_is_planar(ao->format)) {
MP_FATAL(ao, "planar audio formats are unsupported\n");
goto error;
}

AudioStreamBasicDescription asbd;
ca_fill_asbd(ao, &asbd);
size_t layout_size = sizeof(AudioChannelLayout)
+ (ao->channels.num - 1) * sizeof(AudioChannelDescription);
layout = talloc_size(ao, layout_size);
layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
layout->mNumberChannelDescriptions = ao->channels.num;
for (int i = 0; i < ao->channels.num; ++i) {
AudioChannelDescription *desc = layout->mChannelDescriptions + i;
desc->mChannelFlags = kAudioChannelFlags_AllOff;
desc->mChannelLabel = mp_speaker_id_to_ca_label(ao->channels.speaker[i]);
}

void *talloc_ctx = talloc_new(NULL);
AudioChannelLayout *std_layout = ca_find_standard_layout(talloc_ctx, layout);
memmove(layout, std_layout, sizeof(AudioChannelLayout));
talloc_free(talloc_ctx);
ca_log_layout(ao, MSGL_V, layout);

OSStatus err;
if ((err = CMAudioFormatDescriptionCreate(
NULL,
&asbd,
layout_size,
layout,
0,
NULL,
NULL,
&p->format_description
)) != noErr) {
MP_FATAL(ao, "failed to create audio format description\n");
MP_VERBOSE(ao, "CMAudioFormatDescriptionCreate returned %d\n", err);
goto error;
}
talloc_free(layout);
layout = NULL;

// AVSampleBufferAudioRenderer read ahead aggressively
ao->device_buffer = ao->samplerate * 2;

p->observer = [[AVObserver alloc] initWithAO:ao];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:p->observer selector:@selector(handleRestartNotification:) name:AVSampleBufferAudioRendererOutputConfigurationDidChangeNotification object:p->renderer];
[center addObserver:p->observer selector:@selector(handleRestartNotification:) name:AVSampleBufferAudioRendererWasFlushedAutomaticallyNotification object:p->renderer];

return CONTROL_OK;

error:
talloc_free(layout);
if (p->renderer) [p->renderer release];
if (p->synchronizer) [p->synchronizer release];
if (p->queue) dispatch_release(p->queue);
if (p->format_description) CFRelease(p->format_description);

#if TARGET_OS_IPHONE
[AVAudioSession.sharedInstance setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:nil
];
#endif

return CONTROL_ERROR;
}

static void uninit(struct ao *ao)
{
struct priv *p = ao->priv;

stop(ao);

[p->renderer release];
[p->synchronizer release];
dispatch_release(p->queue);
CFRelease(p->format_description);

[[NSNotificationCenter defaultCenter] removeObserver:p->observer];
[p->observer release];

#if TARGET_OS_IPHONE
[AVAudioSession.sharedInstance setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:nil
];
#endif
}

#define OPT_BASE_STRUCT struct priv

const struct ao_driver audio_out_avfoundation = {
.description = "AVFoundation AVSampleBufferAudioRenderer",
.name = "avfoundation",
.uninit = uninit,
.init = init,
.control = control,
.reset = stop,
.start = start,
.list_devs = ca_get_device_list,
.priv_size = sizeof(struct priv),
};
Loading

0 comments on commit e022a6e

Please sign in to comment.