1 /*****************************************************************************
2 * audiounit_ios.m: AudioUnit output plugin for iOS
3 *****************************************************************************
4 * Copyright (C) 2012 - 2017 VLC authors and VideoLAN
6 * Authors: Felix Paul Kühne <fkuehne at videolan dot org>
8 * This program is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 2.1 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Lesser General Public License for more details.
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this program; if not, write to the Free Software Foundation,
20 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
21 *****************************************************************************/
25 #import "coreaudio_common.h"
27 #import <vlc_plugin.h>
29 #import <CoreAudio/CoreAudioTypes.h>
30 #import <Foundation/Foundation.h>
31 #import <AVFoundation/AVFoundation.h>
32 #import <mach/mach_time.h>
35 #pragma mark private declarations
37 /* aout wrapper: used as observer for notifications */
38 @interface AoutWrapper : NSObject
39 - (instancetype)initWithAout:(audio_output_t *)aout;
40 @property (readonly, assign) audio_output_t* aout;
54 { "pcm", "Up to 9 channels PCM output", AU_DEV_PCM },
55 { "encoded", "Encoded output if available (via HDMI/SPDIF) or PCM output",
56 AU_DEV_ENCODED }, /* This can also be forced with the --spdif option */
59 @interface SessionManager : NSObject
61 NSMutableSet *_registeredInstances;
63 + (SessionManager *)sharedInstance;
64 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance;
65 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance;
68 @implementation SessionManager
69 + (SessionManager *)sharedInstance
71 static SessionManager *sharedInstance = nil;
72 static dispatch_once_t pred;
74 dispatch_once(&pred, ^{
75 sharedInstance = [SessionManager new];
78 return sharedInstance;
85 _registeredInstances = [[NSMutableSet alloc] init];
90 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance
92 @synchronized(_registeredInstances) {
93 [_registeredInstances addObject:wrapperInstance];
97 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance
99 @synchronized(_registeredInstances) {
100 [_registeredInstances removeObject:wrapperInstance];
101 return _registeredInstances.count;
106 /*****************************************************************************
107 * aout_sys_t: private audio output method descriptor
108 *****************************************************************************
109 * This structure is part of the audio output thread descriptor.
110 * It describes the CoreAudio specific properties of an output thread.
111 *****************************************************************************/
114 struct aout_sys_common c;
116 AVAudioSession *avInstance;
117 AoutWrapper *aoutWrapper;
118 /* The AudioUnit we use */
122 bool b_preferred_channels_set;
130 /* Soft volume helper */
131 #include "audio_output/volume.h"
142 #pragma mark AVAudioSession route and output handling
144 @implementation AoutWrapper
146 - (instancetype)initWithAout:(audio_output_t *)aout
154 - (void)audioSessionRouteChange:(NSNotification *)notification
156 audio_output_t *p_aout = [self aout];
157 aout_sys_t *p_sys = p_aout->sys;
158 NSDictionary *userInfo = notification.userInfo;
159 NSInteger routeChangeReason =
160 [[userInfo valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
162 msg_Dbg(p_aout, "Audio route changed: %ld", (long) routeChangeReason);
164 if (routeChangeReason == AVAudioSessionRouteChangeReasonNewDeviceAvailable
165 || routeChangeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
166 aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
169 const vlc_tick_t latency_us =
170 vlc_tick_from_sec([p_sys->avInstance outputLatency]);
171 ca_SetDeviceLatency(p_aout, latency_us);
172 msg_Dbg(p_aout, "Current device has a new latency of %lld us", latency_us);
176 - (void)handleInterruption:(NSNotification *)notification
178 audio_output_t *p_aout = [self aout];
179 NSDictionary *userInfo = notification.userInfo;
180 if (!userInfo || !userInfo[AVAudioSessionInterruptionTypeKey]) {
184 NSUInteger interruptionType = [userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
186 if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
187 ca_SetAliveState(p_aout, false);
188 } else if (interruptionType == AVAudioSessionInterruptionTypeEnded
189 && [userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] == AVAudioSessionInterruptionOptionShouldResume) {
190 ca_SetAliveState(p_aout, true);
196 avas_setPreferredNumberOfChannels(audio_output_t *p_aout,
197 const audio_sample_format_t *fmt)
199 aout_sys_t *p_sys = p_aout->sys;
201 if (aout_BitsPerSample(fmt->i_format) == 0)
202 return; /* Don't touch the number of channels for passthrough */
204 AVAudioSession *instance = p_sys->avInstance;
205 NSInteger max_channel_count = [instance maximumOutputNumberOfChannels];
206 unsigned channel_count = aout_FormatNbChannels(fmt);
208 /* Increase the preferred number of output channels if possible */
209 if (channel_count > 2 && max_channel_count > 2)
211 channel_count = __MIN(channel_count, max_channel_count);
212 bool success = [instance setPreferredOutputNumberOfChannels:channel_count
214 if (success && [instance outputNumberOfChannels] == channel_count)
215 p_sys->b_preferred_channels_set = true;
218 /* Not critical, output channels layout will be Stereo */
219 msg_Warn(p_aout, "setPreferredOutputNumberOfChannels failed");
225 avas_resetPreferredNumberOfChannels(audio_output_t *p_aout)
227 aout_sys_t *p_sys = p_aout->sys;
228 AVAudioSession *instance = p_sys->avInstance;
230 if (p_sys->b_preferred_channels_set)
232 [instance setPreferredOutputNumberOfChannels:2 error:nil];
233 p_sys->b_preferred_channels_set = false;
238 avas_GetOptimalChannelLayout(audio_output_t *p_aout, enum port_type *pport_type,
239 AudioChannelLayout **playout)
241 aout_sys_t * p_sys = p_aout->sys;
242 AVAudioSession *instance = p_sys->avInstance;
243 AudioChannelLayout *layout = NULL;
244 *pport_type = PORT_TYPE_DEFAULT;
246 long last_channel_count = 0;
247 for (AVAudioSessionPortDescription *out in [[instance currentRoute] outputs])
249 /* Choose the layout with the biggest number of channels or the HDMI
252 enum port_type port_type;
253 if ([out.portType isEqualToString: AVAudioSessionPortUSBAudio])
254 port_type = PORT_TYPE_USB;
255 else if ([out.portType isEqualToString: AVAudioSessionPortHDMI])
256 port_type = PORT_TYPE_HDMI;
257 else if ([out.portType isEqualToString: AVAudioSessionPortHeadphones])
258 port_type = PORT_TYPE_HEADPHONES;
260 port_type = PORT_TYPE_DEFAULT;
262 NSArray<AVAudioSessionChannelDescription *> *chans = [out channels];
264 if (chans.count > last_channel_count || port_type == PORT_TYPE_HDMI)
266 /* We don't need a layout specification for stereo */
269 bool labels_valid = false;
270 for (AVAudioSessionChannelDescription *chan in chans)
272 if ([chan channelLabel] != kAudioChannelLabel_Unknown)
280 /* TODO: Guess labels ? */
281 msg_Warn(p_aout, "no valid channel labels");
286 || layout->mNumberChannelDescriptions < chans.count)
288 const size_t layout_size = sizeof(AudioChannelLayout)
289 + chans.count * sizeof(AudioChannelDescription);
290 layout = realloc_or_free(layout, layout_size);
295 layout->mChannelLayoutTag =
296 kAudioChannelLayoutTag_UseChannelDescriptions;
297 layout->mNumberChannelDescriptions = chans.count;
300 for (AVAudioSessionChannelDescription *chan in chans)
301 layout->mChannelDescriptions[i++].mChannelLabel
302 = [chan channelLabel];
304 last_channel_count = chans.count;
306 *pport_type = port_type;
309 if (port_type == PORT_TYPE_HDMI) /* Prefer HDMI */
313 msg_Dbg(p_aout, "Output on %s, channel count: %u",
314 *pport_type == PORT_TYPE_HDMI ? "HDMI" :
315 *pport_type == PORT_TYPE_USB ? "USB" :
316 *pport_type == PORT_TYPE_HEADPHONES ? "Headphones" : "Default",
317 layout ? (unsigned) layout->mNumberChannelDescriptions : 2);
324 avas_SetActive(audio_output_t *p_aout, bool active, NSUInteger options)
326 aout_sys_t * p_sys = p_aout->sys;
327 AVAudioSession *instance = p_sys->avInstance;
329 NSError *error = nil;
333 ret = [instance setCategory:AVAudioSessionCategoryPlayback error:&error];
334 ret = ret && [instance setMode:AVAudioSessionModeMoviePlayback error:&error];
335 ret = ret && [instance setActive:YES withOptions:options error:&error];
336 [[SessionManager sharedInstance] addAoutInstance: p_sys->aoutWrapper];
338 NSInteger numberOfRegisteredInstances = [[SessionManager sharedInstance] removeAoutInstance: p_sys->aoutWrapper];
339 if (numberOfRegisteredInstances == 0) {
340 ret = [instance setActive:NO withOptions:options error:&error];
348 msg_Err(p_aout, "AVAudioSession playback change failed: %s(%d)",
349 error.domain.UTF8String, (int)error.code);
357 #pragma mark actual playback
360 Pause (audio_output_t *p_aout, bool pause, vlc_tick_t date)
362 aout_sys_t * p_sys = p_aout->sys;
364 /* We need to start / stop the audio unit here because otherwise the OS
365 * won't believe us that we stopped the audio output so in case of an
366 * interruption, our unit would be permanently silenced. In case of
367 * multi-tasking, the multi-tasking view would still show a playing state
368 * despite we are paused, same for lock screen */
370 if (pause == p_sys->b_stopped)
376 err = AudioOutputUnitStop(p_sys->au_unit);
378 ca_LogErr("AudioOutputUnitStart failed");
379 avas_SetActive(p_aout, false, 0);
383 if (avas_SetActive(p_aout, true, 0) == VLC_SUCCESS)
385 err = AudioOutputUnitStart(p_sys->au_unit);
388 ca_LogErr("AudioOutputUnitStart failed");
389 avas_SetActive(p_aout, false, 0);
390 /* Do not un-pause, the Render Callback won't run, and next call
391 * of ca_Play will deadlock */
396 p_sys->b_stopped = pause;
397 ca_Pause(p_aout, pause, date);
399 /* Since we stopped the AudioUnit, we can't really recover the delay from
400 * the last playback. So it's better to flush everything now to avoid
401 * synchronization glitches when resuming from pause. The main drawback is
402 * that we loose 1-2 sec of audio when resuming. The order is important
403 * here, ca_Flush need to be called when paused. */
409 MuteSet(audio_output_t *p_aout, bool mute)
411 aout_sys_t * p_sys = p_aout->sys;
413 p_sys->b_muted = mute;
414 if (p_sys->au_unit != NULL)
416 Pause(p_aout, mute, 0);
425 Play(audio_output_t * p_aout, block_t * p_block, vlc_tick_t date)
427 aout_sys_t * p_sys = p_aout->sys;
430 block_Release(p_block);
432 ca_Play(p_aout, p_block, date);
435 #pragma mark initialization
438 Stop(audio_output_t *p_aout)
440 aout_sys_t *p_sys = p_aout->sys;
443 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
445 if (!p_sys->b_stopped)
447 err = AudioOutputUnitStop(p_sys->au_unit);
449 ca_LogWarn("AudioOutputUnitStop failed");
452 au_Uninitialize(p_aout, p_sys->au_unit);
454 err = AudioComponentInstanceDispose(p_sys->au_unit);
456 ca_LogWarn("AudioComponentInstanceDispose failed");
458 avas_resetPreferredNumberOfChannels(p_aout);
460 avas_SetActive(p_aout, false,
461 AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
465 Start(audio_output_t *p_aout, audio_sample_format_t *restrict fmt)
467 aout_sys_t *p_sys = p_aout->sys;
470 AudioChannelLayout *layout = NULL;
472 if (aout_FormatNbChannels(fmt) == 0 || AOUT_FMT_HDMI(fmt))
475 /* XXX: No more passthrough since iOS 11 */
476 if (AOUT_FMT_SPDIF(fmt))
479 aout_FormatPrint(p_aout, "VLC is looking for:", fmt);
481 p_sys->au_unit = NULL;
483 [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
484 selector:@selector(audioSessionRouteChange:)
485 name:AVAudioSessionRouteChangeNotification
487 [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
488 selector:@selector(handleInterruption:)
489 name:AVAudioSessionInterruptionNotification
492 /* Activate the AVAudioSession */
493 if (avas_SetActive(p_aout, true, 0) != VLC_SUCCESS)
495 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
499 /* Set the preferred number of channels, then fetch the channel layout that
500 * should correspond to this number */
501 avas_setPreferredNumberOfChannels(p_aout, fmt);
503 BOOL success = [p_sys->avInstance setPreferredSampleRate:fmt->i_rate error:nil];
506 /* Not critical, we can use any sample rates */
507 msg_Dbg(p_aout, "failed to set preferred sample rate");
510 enum port_type port_type;
511 int ret = avas_GetOptimalChannelLayout(p_aout, &port_type, &layout);
512 if (ret != VLC_SUCCESS)
515 if (AOUT_FMT_SPDIF(fmt))
517 if (p_sys->au_dev != AU_DEV_ENCODED
518 || (port_type != PORT_TYPE_USB && port_type != PORT_TYPE_HDMI))
522 p_aout->current_sink_info.headphones = port_type == PORT_TYPE_HEADPHONES;
524 p_sys->au_unit = au_NewOutputInstance(p_aout, kAudioUnitSubType_RemoteIO);
525 if (p_sys->au_unit == NULL)
528 err = AudioUnitSetProperty(p_sys->au_unit,
529 kAudioOutputUnitProperty_EnableIO,
530 kAudioUnitScope_Output, 0,
531 &(UInt32){ 1 }, sizeof(UInt32));
533 ca_LogWarn("failed to set IO mode");
535 const vlc_tick_t latency_us =
536 vlc_tick_from_sec([p_sys->avInstance outputLatency]);
537 msg_Dbg(p_aout, "Current device has a latency of %lld us", latency_us);
539 ret = au_Initialize(p_aout, p_sys->au_unit, fmt, layout, latency_us, NULL);
540 if (ret != VLC_SUCCESS)
545 err = AudioOutputUnitStart(p_sys->au_unit);
548 ca_LogErr("AudioOutputUnitStart failed");
549 au_Uninitialize(p_aout, p_sys->au_unit);
554 Pause(p_aout, true, 0);
557 fmt->channel_type = AUDIO_CHANNEL_TYPE_BITMAP;
558 p_aout->mute_set = MuteSet;
559 p_aout->pause = Pause;
561 aout_SoftVolumeStart( p_aout );
563 msg_Dbg(p_aout, "analog AudioUnit output successfully opened for %4.4s %s",
564 (const char *)&fmt->i_format, aout_FormatPrintChannels(fmt));
569 if (p_sys->au_unit != NULL)
570 AudioComponentInstanceDispose(p_sys->au_unit);
571 avas_resetPreferredNumberOfChannels(p_aout);
572 avas_SetActive(p_aout, false,
573 AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
574 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
575 msg_Err(p_aout, "opening AudioUnit output failed");
579 static int DeviceSelect(audio_output_t *p_aout, const char *psz_id)
581 aout_sys_t *p_sys = p_aout->sys;
582 enum au_dev au_dev = AU_DEV_PCM;
586 for (unsigned int i = 0; i < sizeof(au_devs) / sizeof(au_devs[0]); ++i)
588 if (!strcmp(psz_id, au_devs[i].psz_id))
590 au_dev = au_devs[i].au_dev;
596 if (au_dev != p_sys->au_dev)
598 p_sys->au_dev = au_dev;
599 aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
600 msg_Dbg(p_aout, "selected audiounit device: %s", psz_id);
602 aout_DeviceReport(p_aout, psz_id);
607 Close(vlc_object_t *obj)
609 audio_output_t *aout = (audio_output_t *)obj;
610 aout_sys_t *sys = aout->sys;
612 [sys->aoutWrapper release];
618 Open(vlc_object_t *obj)
620 audio_output_t *aout = (audio_output_t *)obj;
622 aout_sys_t *sys = aout->sys = calloc(1, sizeof (*sys));
623 if (unlikely(sys == NULL))
626 if (ca_Open(aout) != VLC_SUCCESS)
632 sys->avInstance = [AVAudioSession sharedInstance];
633 assert(sys->avInstance != NULL);
635 sys->aoutWrapper = [[AoutWrapper alloc] initWithAout:aout];
636 if (sys->aoutWrapper == NULL)
642 sys->b_muted = false;
643 sys->b_preferred_channels_set = false;
644 sys->au_dev = var_InheritBool(aout, "spdif") ? AU_DEV_ENCODED : AU_DEV_PCM;
647 aout->device_select = DeviceSelect;
649 aout_SoftVolumeInit( aout );
651 for (unsigned int i = 0; i< sizeof(au_devs) / sizeof(au_devs[0]); ++i)
652 aout_HotplugReport(aout, au_devs[i].psz_id, au_devs[i].psz_name);
658 #pragma mark module descriptor
661 set_shortname("audiounit_ios")
662 set_description("AudioUnit output for iOS")
663 set_capability("audio output", 101)
664 set_category(CAT_AUDIO)
665 set_subcategory(SUBCAT_AUDIO_AOUT)
667 set_callbacks(Open, Close)