summaryrefslogtreecommitdiffstats
path: root/audio/out/ao_avfoundation.m
diff options
context:
space:
mode:
Diffstat (limited to 'audio/out/ao_avfoundation.m')
-rw-r--r--audio/out/ao_avfoundation.m372
1 files changed, 372 insertions, 0 deletions
diff --git a/audio/out/ao_avfoundation.m b/audio/out/ao_avfoundation.m
new file mode 100644
index 0000000000..7654916519
--- /dev/null
+++ b/audio/out/ao_avfoundation.m
@@ -0,0 +1,372 @@
+/*
+ * 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 bool set_pause(struct ao *ao, bool paused)
+{
+ struct priv *p = ao->priv;
+
+ if (paused) {
+ [p->synchronizer setRate:0];
+ } else {
+ [p->synchronizer setRate:1];
+ }
+
+ return true;
+}
+
+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,
+ .set_pause = set_pause,
+ .list_devs = ca_get_device_list,
+ .priv_size = sizeof(struct priv),
+};