diff --git a/DOCS/man/ao.rst b/DOCS/man/ao.rst index 4890747bc9..4ac1cc52ee 100644 --- a/DOCS/man/ao.rst +++ b/DOCS/man/ao.rst @@ -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 + `_. + + .. 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. diff --git a/audio/out/ao.c b/audio/out/ao.c index 3c61ee786c..75fcbac6fa 100644 --- a/audio/out/ao.c +++ b/audio/out/ao.c @@ -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; @@ -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 diff --git a/audio/out/ao_avfoundation.m b/audio/out/ao_avfoundation.m new file mode 100644 index 0000000000..3ec11c4fdf --- /dev/null +++ b/audio/out/ao_avfoundation.m @@ -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 . + */ + +#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 +#import +#import +#import +#import + + +@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), +}; diff --git a/audio/out/ao_coreaudio_chmap.c b/audio/out/ao_coreaudio_chmap.c index fb7f577ea1..5a129c4e67 100644 --- a/audio/out/ao_coreaudio_chmap.c +++ b/audio/out/ao_coreaudio_chmap.c @@ -22,6 +22,7 @@ #include "ao_coreaudio_utils.h" #include "ao_coreaudio_chmap.h" +#include static const int speaker_map[][2] = { { kAudioChannelLabel_Left, MP_SPEAKER_ID_FL }, @@ -65,6 +66,119 @@ static const int speaker_map[][2] = { { 0, -1 }, }; +static const AudioChannelLayoutTag std_layouts[] = { + (100U<<16) | 1, // kAudioChannelLayoutTag_Mono + (101U<<16) | 2, // kAudioChannelLayoutTag_Stereo + (102U<<16) | 2, // kAudioChannelLayoutTag_StereoHeadphones + (103U<<16) | 2, // kAudioChannelLayoutTag_MatrixStereo + (104U<<16) | 2, // kAudioChannelLayoutTag_MidSide + (105U<<16) | 2, // kAudioChannelLayoutTag_XY + (106U<<16) | 2, // kAudioChannelLayoutTag_Binaural + (107U<<16) | 4, // kAudioChannelLayoutTag_Ambisonic_B_Format + (108U<<16) | 4, // kAudioChannelLayoutTag_Quadraphonic + (109U<<16) | 5, // kAudioChannelLayoutTag_Pentagonal + (110U<<16) | 6, // kAudioChannelLayoutTag_Hexagonal + (111U<<16) | 8, // kAudioChannelLayoutTag_Octagonal + (112U<<16) | 8, // kAudioChannelLayoutTag_Cube + (113U<<16) | 3, // kAudioChannelLayoutTag_MPEG_3_0_A + (114U<<16) | 3, // kAudioChannelLayoutTag_MPEG_3_0_B + (115U<<16) | 4, // kAudioChannelLayoutTag_MPEG_4_0_A + (116U<<16) | 4, // kAudioChannelLayoutTag_MPEG_4_0_B + (117U<<16) | 5, // kAudioChannelLayoutTag_MPEG_5_0_A + (118U<<16) | 5, // kAudioChannelLayoutTag_MPEG_5_0_B + (119U<<16) | 5, // kAudioChannelLayoutTag_MPEG_5_0_C + (120U<<16) | 5, // kAudioChannelLayoutTag_MPEG_5_0_D + (121U<<16) | 6, // kAudioChannelLayoutTag_MPEG_5_1_A + (122U<<16) | 6, // kAudioChannelLayoutTag_MPEG_5_1_B + (123U<<16) | 6, // kAudioChannelLayoutTag_MPEG_5_1_C + (124U<<16) | 6, // kAudioChannelLayoutTag_MPEG_5_1_D + (125U<<16) | 7, // kAudioChannelLayoutTag_MPEG_6_1_A + (126U<<16) | 8, // kAudioChannelLayoutTag_MPEG_7_1_A + (127U<<16) | 8, // kAudioChannelLayoutTag_MPEG_7_1_B + (128U<<16) | 8, // kAudioChannelLayoutTag_MPEG_7_1_C + (129U<<16) | 8, // kAudioChannelLayoutTag_Emagic_Default_7_1 + (130U<<16) | 8, // kAudioChannelLayoutTag_SMPTE_DTV + (131U<<16) | 3, // kAudioChannelLayoutTag_ITU_2_1 + (132U<<16) | 4, // kAudioChannelLayoutTag_ITU_2_2 + (133U<<16) | 3, // kAudioChannelLayoutTag_DVD_4 + (134U<<16) | 4, // kAudioChannelLayoutTag_DVD_5 + (135U<<16) | 5, // kAudioChannelLayoutTag_DVD_6 + (136U<<16) | 4, // kAudioChannelLayoutTag_DVD_10 + (137U<<16) | 5, // kAudioChannelLayoutTag_DVD_11 + (138U<<16) | 5, // kAudioChannelLayoutTag_DVD_18 + (139U<<16) | 6, // kAudioChannelLayoutTag_AudioUnit_6_0 + (140U<<16) | 7, // kAudioChannelLayoutTag_AudioUnit_7_0 + (148U<<16) | 7, // kAudioChannelLayoutTag_AudioUnit_7_0_Front + (141U<<16) | 6, // kAudioChannelLayoutTag_AAC_6_0 + (142U<<16) | 7, // kAudioChannelLayoutTag_AAC_6_1 + (143U<<16) | 7, // kAudioChannelLayoutTag_AAC_7_0 + (183U<<16) | 8, // kAudioChannelLayoutTag_AAC_7_1_B + (184U<<16) | 8, // kAudioChannelLayoutTag_AAC_7_1_C + (144U<<16) | 8, // kAudioChannelLayoutTag_AAC_Octagonal + (145U<<16) | 16, // kAudioChannelLayoutTag_TMH_10_2_std + (146U<<16) | 21, // kAudioChannelLayoutTag_TMH_10_2_full + (149U<<16) | 2, // kAudioChannelLayoutTag_AC3_1_0_1 + (150U<<16) | 3, // kAudioChannelLayoutTag_AC3_3_0 + (151U<<16) | 4, // kAudioChannelLayoutTag_AC3_3_1 + (152U<<16) | 4, // kAudioChannelLayoutTag_AC3_3_0_1 + (153U<<16) | 4, // kAudioChannelLayoutTag_AC3_2_1_1 + (154U<<16) | 5, // kAudioChannelLayoutTag_AC3_3_1_1 + (155U<<16) | 6, // kAudioChannelLayoutTag_EAC_6_0_A + (156U<<16) | 7, // kAudioChannelLayoutTag_EAC_7_0_A + (157U<<16) | 7, // kAudioChannelLayoutTag_EAC3_6_1_A + (158U<<16) | 7, // kAudioChannelLayoutTag_EAC3_6_1_B + (159U<<16) | 7, // kAudioChannelLayoutTag_EAC3_6_1_C + (160U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_A + (161U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_B + (162U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_C + (163U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_D + (164U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_E + (165U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_F + (166U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_G + (167U<<16) | 8, // kAudioChannelLayoutTag_EAC3_7_1_H + (168U<<16) | 4, // kAudioChannelLayoutTag_DTS_3_1 + (169U<<16) | 5, // kAudioChannelLayoutTag_DTS_4_1 + (170U<<16) | 6, // kAudioChannelLayoutTag_DTS_6_0_A + (171U<<16) | 6, // kAudioChannelLayoutTag_DTS_6_0_B + (172U<<16) | 6, // kAudioChannelLayoutTag_DTS_6_0_C + (173U<<16) | 7, // kAudioChannelLayoutTag_DTS_6_1_A + (174U<<16) | 7, // kAudioChannelLayoutTag_DTS_6_1_B + (175U<<16) | 7, // kAudioChannelLayoutTag_DTS_6_1_C + (176U<<16) | 7, // kAudioChannelLayoutTag_DTS_7_0 + (177U<<16) | 8, // kAudioChannelLayoutTag_DTS_7_1 + (178U<<16) | 8, // kAudioChannelLayoutTag_DTS_8_0_A + (179U<<16) | 8, // kAudioChannelLayoutTag_DTS_8_0_B + (180U<<16) | 9, // kAudioChannelLayoutTag_DTS_8_1_A + (181U<<16) | 9, // kAudioChannelLayoutTag_DTS_8_1_B + (182U<<16) | 7, // kAudioChannelLayoutTag_DTS_6_1_D + (185U<<16) | 4, // kAudioChannelLayoutTag_WAVE_4_0_B + (186U<<16) | 5, // kAudioChannelLayoutTag_WAVE_5_0_B + (187U<<16) | 6, // kAudioChannelLayoutTag_WAVE_5_1_B + (188U<<16) | 7, // kAudioChannelLayoutTag_WAVE_6_1 + (189U<<16) | 8, // kAudioChannelLayoutTag_WAVE_7_1 + (194U<<16) | 8, // kAudioChannelLayoutTag_Atmos_5_1_2 + (195U<<16) | 10, // kAudioChannelLayoutTag_Atmos_5_1_4 + (196U<<16) | 10, // kAudioChannelLayoutTag_Atmos_7_1_2 + (192U<<16) | 12, // kAudioChannelLayoutTag_Atmos_7_1_4 + (193U<<16) | 16, // kAudioChannelLayoutTag_Atmos_9_1_6 + (197U<<16) | 4, // kAudioChannelLayoutTag_Logic_4_0_C + (198U<<16) | 6, // kAudioChannelLayoutTag_Logic_6_0_B + (199U<<16) | 7, // kAudioChannelLayoutTag_Logic_6_1_B + (200U<<16) | 7, // kAudioChannelLayoutTag_Logic_6_1_D + (201U<<16) | 8, // kAudioChannelLayoutTag_Logic_7_1_B + (202U<<16) | 12, // kAudioChannelLayoutTag_Logic_Atmos_7_1_4_B + (203U<<16) | 14, // kAudioChannelLayoutTag_Logic_Atmos_7_1_6 + (204U<<16) | 24, // kAudioChannelLayoutTag_CICP_13 + (205U<<16) | 8, // kAudioChannelLayoutTag_CICP_14 + (206U<<16) | 12, // kAudioChannelLayoutTag_CICP_15 + (207U<<16) | 10, // kAudioChannelLayoutTag_CICP_16 + (208U<<16) | 12, // kAudioChannelLayoutTag_CICP_17 + (209U<<16) | 14, // kAudioChannelLayoutTag_CICP_18 + (210U<<16) | 12, // kAudioChannelLayoutTag_CICP_19 + (211U<<16) | 14, // kAudioChannelLayoutTag_CICP_20 + kAudioChannelLayoutTag_Unknown +}; + int ca_label_to_mp_speaker_id(AudioChannelLabel label) { for (int i = 0; speaker_map[i][1] >= 0; i++) @@ -73,30 +187,48 @@ int ca_label_to_mp_speaker_id(AudioChannelLabel label) return -1; } +AudioChannelLabel mp_speaker_id_to_ca_label(int speaker_id) +{ + for (int i = 0; speaker_map[i][1] >= 0; i++) + if (speaker_map[i][1] == speaker_id) + return speaker_map[i][0]; + return -1; // kAudioChannelLabel_Unknown +} + #if HAVE_COREAUDIO -static void ca_log_layout(struct ao *ao, int l, AudioChannelLayout *layout) +void ca_log_layout(struct ao *ao, int l, AudioChannelLayout *layout) { if (!mp_msg_test(ao->log, l)) return; - AudioChannelDescription *descs = layout->mChannelDescriptions; + AudioChannelLayoutTag tag = layout->mChannelLayoutTag; + mp_msg(ao->log, l, "audio channel layout: tag: <%u>", tag); - mp_msg(ao->log, l, "layout: tag: <%u>, bitmap: <%u>, " - "descriptions <%u>\n", - (unsigned) layout->mChannelLayoutTag, - (unsigned) layout->mChannelBitmap, - (unsigned) layout->mNumberChannelDescriptions); + if (tag == kAudioChannelLayoutTag_UseChannelDescriptions) { + AudioChannelDescription *descs = layout->mChannelDescriptions; + mp_msg(ao->log, l, ", descriptions <%u>\n", + (unsigned) layout->mNumberChannelDescriptions); - for (int i = 0; i < layout->mNumberChannelDescriptions; i++) { - AudioChannelDescription d = descs[i]; - mp_msg(ao->log, l, " - description %d: label <%u, %u>, " - " flags: <%u>, coords: <%f, %f, %f>\n", i, - (unsigned) d.mChannelLabel, - (unsigned) ca_label_to_mp_speaker_id(d.mChannelLabel), - (unsigned) d.mChannelFlags, - d.mCoordinates[0], - d.mCoordinates[1], - d.mCoordinates[2]); + for (int i = 0; i < layout->mNumberChannelDescriptions; i++) { + AudioChannelDescription d = descs[i]; + mp_msg(ao->log, l, " - description %d: label <%u, %u>, flags: <%u>", + i, + (unsigned) d.mChannelLabel, + (unsigned) ca_label_to_mp_speaker_id(d.mChannelLabel), + (unsigned) d.mChannelFlags); + if (d.mChannelFlags != kAudioChannelFlags_AllOff) { + mp_msg(ao->log, l, ", coords: <%f, %f, %f>\n", + d.mCoordinates[0], + d.mCoordinates[1], + d.mCoordinates[2]); + } else { + mp_msg(ao->log, l, "\n"); + } + } + } else if (tag == kAudioChannelLayoutTag_UseChannelBitmap) { + mp_msg(ao->log, l, ", bitmap <%u>\n", layout->mChannelBitmap); + } else { + mp_msg(ao->log, l, "\n"); } } @@ -117,7 +249,7 @@ static AudioChannelLayout *ca_layout_to_custom_layout(struct ao *ao, kAudioFormatProperty_ChannelLayoutForBitmap, sizeof(uint32_t), &l->mChannelBitmap, &psize); CHECK_CA_ERROR("failed to convert channel bitmap to descriptions (info)"); - r = talloc_size(NULL, psize); + r = talloc_size(talloc_ctx, psize); err = AudioFormatGetProperty( kAudioFormatProperty_ChannelLayoutForBitmap, sizeof(uint32_t), &l->mChannelBitmap, &psize, r); @@ -127,7 +259,7 @@ static AudioChannelLayout *ca_layout_to_custom_layout(struct ao *ao, err = AudioFormatGetPropertyInfo( kAudioFormatProperty_ChannelLayoutForTag, sizeof(AudioChannelLayoutTag), &l->mChannelLayoutTag, &psize); - r = talloc_size(NULL, psize); + r = talloc_size(talloc_ctx, psize); CHECK_CA_ERROR("failed to convert channel tag to descriptions (info)"); err = AudioFormatGetProperty( kAudioFormatProperty_ChannelLayoutForTag, @@ -135,14 +267,53 @@ static AudioChannelLayout *ca_layout_to_custom_layout(struct ao *ao, CHECK_CA_ERROR("failed to convert channel tag to descriptions (get)"); } - MP_VERBOSE(ao, "converted input channel layout:\n"); - ca_log_layout(ao, MSGL_V, l); + if (ao) { + MP_VERBOSE(ao, "converted input channel layout:\n"); + ca_log_layout(ao, MSGL_V, l); + } return r; coreaudio_error: return NULL; } +AudioChannelLayout *ca_find_standard_layout(void *talloc_ctx, AudioChannelLayout *l) +{ + if (l->mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions) + return l; + + AudioChannelLayout *s = talloc_size(talloc_ctx, sizeof(AudioChannelLayout)); + + for (int i = 0; ; ++i) { + if ((s->mChannelLayoutTag = std_layouts[i]) == kAudioChannelLayoutTag_Unknown) { + s = NULL; + break; + } + + AudioChannelLayout *r = ca_layout_to_custom_layout(NULL, talloc_ctx, s); + + if (!r) + goto mismatch; + if (l->mNumberChannelDescriptions != r->mNumberChannelDescriptions) + goto mismatch; + + for (int i = 0; i < l->mNumberChannelDescriptions; ++i) { + AudioChannelDescription *ld = l->mChannelDescriptions + i; + AudioChannelDescription *rd = r->mChannelDescriptions + i; + if (ld->mChannelLabel == rd->mChannelLabel) + continue; + // XXX: we cannot handle channels with coordinates + goto mismatch; + } + + break; + +mismatch:; + } + + return s ? s : l; +} + #define CHMAP(n, ...) &(struct mp_chmap) MP_CONCAT(MP_CHMAP, n) (__VA_ARGS__) diff --git a/audio/out/ao_coreaudio_chmap.h b/audio/out/ao_coreaudio_chmap.h index b6d160c546..0b21e8330c 100644 --- a/audio/out/ao_coreaudio_chmap.h +++ b/audio/out/ao_coreaudio_chmap.h @@ -18,15 +18,22 @@ #ifndef MPV_COREAUDIO_CHMAP_H #define MPV_COREAUDIO_CHMAP_H +#include "config.h" #include -#include "config.h" +#if HAVE_AVFOUNDATION || HAVE_AUDIOUNIT +#undef HAVE_COREAUDIO +#define HAVE_COREAUDIO 1 +#endif struct mp_chmap; int ca_label_to_mp_speaker_id(AudioChannelLabel label); +AudioChannelLabel mp_speaker_id_to_ca_label(int speaker_id); #if HAVE_COREAUDIO +AudioChannelLayout *ca_find_standard_layout(void *talloc_ctx, AudioChannelLayout *l); +void ca_log_layout(struct ao *ao, int l, AudioChannelLayout *layout); bool ca_init_chmap(struct ao *ao, AudioDeviceID device); void ca_get_active_chmap(struct ao *ao, AudioDeviceID device, int channel_count, struct mp_chmap *out_map); diff --git a/audio/out/ao_coreaudio_properties.h b/audio/out/ao_coreaudio_properties.h index f29396883d..2c9c5657bb 100644 --- a/audio/out/ao_coreaudio_properties.h +++ b/audio/out/ao_coreaudio_properties.h @@ -23,6 +23,11 @@ #include "internal.h" +#if HAVE_AVFOUNDATION || HAVE_AUDIOUNIT +#undef HAVE_COREAUDIO +#define HAVE_COREAUDIO 1 +#endif + // CoreAudio names are way too verbose #define ca_sel AudioObjectPropertySelector #define ca_scope AudioObjectPropertyScope diff --git a/audio/out/ao_coreaudio_utils.c b/audio/out/ao_coreaudio_utils.c index f730beca87..e74092a8d2 100644 --- a/audio/out/ao_coreaudio_utils.c +++ b/audio/out/ao_coreaudio_utils.c @@ -138,7 +138,8 @@ bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message) { if (code == noErr) return true; - mp_msg(ao->log, level, "%s (%s/%d)\n", message, mp_tag_str(code), (int)code); + if (ao) + mp_msg(ao->log, level, "%s (%s/%d)\n", message, mp_tag_str(code), (int)code); return false; } diff --git a/audio/out/ao_coreaudio_utils.h b/audio/out/ao_coreaudio_utils.h index c6f1a5ec24..699ffde9a7 100644 --- a/audio/out/ao_coreaudio_utils.h +++ b/audio/out/ao_coreaudio_utils.h @@ -29,6 +29,11 @@ #include "internal.h" #include "osdep/utils-mac.h" +#if HAVE_AVFOUNDATION || HAVE_AUDIOUNIT +#undef HAVE_COREAUDIO +#define HAVE_COREAUDIO 1 +#endif + bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message); #define CHECK_CA_ERROR_L(label, message) \ diff --git a/meson.build b/meson.build index 7822a413c2..0715254c65 100644 --- a/meson.build +++ b/meson.build @@ -799,6 +799,14 @@ if features['audiounit'] sources += files('audio/out/ao_audiounit.m') endif +avfoundation = dependency('appleframeworks', modules: ['CoreMedia', 'AVFoundation'], + required: get_option('avfoundation')) +features += {'avfoundation': avfoundation.found()} +if features['avfoundation'] + dependencies += avfoundation + sources += files('audio/out/ao_avfoundation.m') +endif + coreaudio = dependency('appleframeworks', modules: ['CoreFoundation', 'CoreAudio', 'AudioUnit', 'AudioToolbox'], required: get_option('coreaudio')) features += {'coreaudio': coreaudio.found()} @@ -809,9 +817,10 @@ if features['coreaudio'] 'audio/out/ao_coreaudio_properties.c') endif -if features['audiounit'] or features['coreaudio'] +if features['audiounit'] or features['coreaudio'] or features['avfoundation'] sources += files('audio/out/ao_coreaudio_chmap.c', - 'audio/out/ao_coreaudio_utils.c') + 'audio/out/ao_coreaudio_utils.c', + 'audio/out/ao_coreaudio_properties.c') endif jack_opt = get_option('jack').require( diff --git a/meson_options.txt b/meson_options.txt index 6d00e3bf47..e488f6ee88 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -43,6 +43,7 @@ option('zlib', type: 'feature', value: 'auto', description: 'zlib') option('alsa', type: 'feature', value: 'auto', description: 'ALSA audio output') option('audiounit', type: 'feature', value: 'auto', description: 'AudioUnit output (iOS)') option('coreaudio', type: 'feature', value: 'auto', description: 'CoreAudio audio output') +option('avfoundation', type: 'feature', value: 'auto', description: 'AVFoundation audio output') option('jack', type: 'feature', value: 'auto', description: 'JACK audio output') option('openal', type: 'feature', value: 'disabled', description: 'OpenAL audio output') option('opensles', type: 'feature', value: 'auto', description: 'OpenSL ES audio output')