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 local prototypes & module descriptor
37 static int Open (vlc_object_t *);
38 static void Close (vlc_object_t *);
41 set_shortname("audiounit_ios")
42 set_description("AudioUnit output for iOS")
43 set_capability("audio output", 101)
44 set_category(CAT_AUDIO)
45 set_subcategory(SUBCAT_AUDIO_AOUT)
46 set_callbacks(Open, Close)
50 #pragma mark private declarations
52 /* aout wrapper: used as observer for notifications */
53 @interface AoutWrapper : NSObject
54 - (instancetype)initWithAout:(audio_output_t *)aout;
55 @property (readonly, assign) audio_output_t* aout;
69 { "pcm", "Up to 9 channels PCM output", AU_DEV_PCM },
70 { "encoded", "Encoded output if available (via HDMI/SPDIF) or PCM output",
71 AU_DEV_ENCODED }, /* This can also be forced with the --spdif option */
74 @interface SessionManager : NSObject
76 NSMutableSet *_registeredInstances;
78 + (SessionManager *)sharedInstance;
79 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance;
80 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance;
83 @implementation SessionManager
84 + (SessionManager *)sharedInstance
86 static SessionManager *sharedInstance = nil;
87 static dispatch_once_t pred;
89 dispatch_once(&pred, ^{
90 sharedInstance = [SessionManager new];
93 return sharedInstance;
100 _registeredInstances = [[NSMutableSet alloc] init];
105 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance
107 @synchronized(_registeredInstances) {
108 [_registeredInstances addObject:wrapperInstance];
112 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance
114 @synchronized(_registeredInstances) {
115 [_registeredInstances removeObject:wrapperInstance];
116 return _registeredInstances.count;
121 /*****************************************************************************
122 * aout_sys_t: private audio output method descriptor
123 *****************************************************************************
124 * This structure is part of the audio output thread descriptor.
125 * It describes the CoreAudio specific properties of an output thread.
126 *****************************************************************************/
129 struct aout_sys_common c;
131 AVAudioSession *avInstance;
132 AoutWrapper *aoutWrapper;
133 /* The AudioUnit we use */
137 bool b_preferred_channels_set;
145 /* Soft volume helper */
146 #include "audio_output/volume.h"
157 #pragma mark AVAudioSession route and output handling
159 @implementation AoutWrapper
161 - (instancetype)initWithAout:(audio_output_t *)aout
169 - (void)audioSessionRouteChange:(NSNotification *)notification
171 audio_output_t *p_aout = [self aout];
172 NSDictionary *userInfo = notification.userInfo;
173 NSInteger routeChangeReason =
174 [[userInfo valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
176 msg_Dbg(p_aout, "Audio route changed: %ld", (long) routeChangeReason);
178 if (routeChangeReason == AVAudioSessionRouteChangeReasonNewDeviceAvailable
179 || routeChangeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
180 aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
183 - (void)handleInterruption:(NSNotification *)notification
185 audio_output_t *p_aout = [self aout];
186 NSDictionary *userInfo = notification.userInfo;
187 if (!userInfo || !userInfo[AVAudioSessionInterruptionTypeKey]) {
191 NSUInteger interruptionType = [userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
193 if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
194 ca_SetAliveState(p_aout, false);
195 } else if (interruptionType == AVAudioSessionInterruptionTypeEnded
196 && [userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue] == AVAudioSessionInterruptionOptionShouldResume) {
197 ca_SetAliveState(p_aout, true);
203 avas_setPreferredNumberOfChannels(audio_output_t *p_aout,
204 const audio_sample_format_t *fmt)
206 aout_sys_t *p_sys = p_aout->sys;
208 if (aout_BitsPerSample(fmt->i_format) == 0)
209 return; /* Don't touch the number of channels for passthrough */
211 AVAudioSession *instance = p_sys->avInstance;
212 NSInteger max_channel_count = [instance maximumOutputNumberOfChannels];
213 unsigned channel_count = aout_FormatNbChannels(fmt);
215 /* Increase the preferred number of output channels if possible */
216 if (channel_count > 2 && max_channel_count > 2)
218 channel_count = __MIN(channel_count, max_channel_count);
219 bool success = [instance setPreferredOutputNumberOfChannels:channel_count
221 if (success && [instance outputNumberOfChannels] == channel_count)
222 p_sys->b_preferred_channels_set = true;
225 /* Not critical, output channels layout will be Stereo */
226 msg_Warn(p_aout, "setPreferredOutputNumberOfChannels failed");
232 avas_resetPreferredNumberOfChannels(audio_output_t *p_aout)
234 aout_sys_t *p_sys = p_aout->sys;
235 AVAudioSession *instance = p_sys->avInstance;
237 if (p_sys->b_preferred_channels_set)
239 [instance setPreferredOutputNumberOfChannels:2 error:nil];
240 p_sys->b_preferred_channels_set = false;
245 avas_GetOptimalChannelLayout(audio_output_t *p_aout, enum port_type *pport_type,
246 AudioChannelLayout **playout)
248 aout_sys_t * p_sys = p_aout->sys;
249 AVAudioSession *instance = p_sys->avInstance;
250 AudioChannelLayout *layout = NULL;
251 *pport_type = PORT_TYPE_DEFAULT;
253 long last_channel_count = 0;
254 for (AVAudioSessionPortDescription *out in [[instance currentRoute] outputs])
256 /* Choose the layout with the biggest number of channels or the HDMI
259 enum port_type port_type;
260 if ([out.portType isEqualToString: AVAudioSessionPortUSBAudio])
261 port_type = PORT_TYPE_USB;
262 else if ([out.portType isEqualToString: AVAudioSessionPortHDMI])
263 port_type = PORT_TYPE_HDMI;
264 else if ([out.portType isEqualToString: AVAudioSessionPortHeadphones])
265 port_type = PORT_TYPE_HEADPHONES;
267 port_type = PORT_TYPE_DEFAULT;
269 NSArray<AVAudioSessionChannelDescription *> *chans = [out channels];
271 if (chans.count > last_channel_count || port_type == PORT_TYPE_HDMI)
273 /* We don't need a layout specification for stereo */
276 bool labels_valid = false;
277 for (AVAudioSessionChannelDescription *chan in chans)
279 if ([chan channelLabel] != kAudioChannelLabel_Unknown)
287 /* TODO: Guess labels ? */
288 msg_Warn(p_aout, "no valid channel labels");
293 || layout->mNumberChannelDescriptions < chans.count)
295 const size_t layout_size = sizeof(AudioChannelLayout)
296 + chans.count * sizeof(AudioChannelDescription);
297 layout = realloc_or_free(layout, layout_size);
302 layout->mChannelLayoutTag =
303 kAudioChannelLayoutTag_UseChannelDescriptions;
304 layout->mNumberChannelDescriptions = chans.count;
307 for (AVAudioSessionChannelDescription *chan in chans)
308 layout->mChannelDescriptions[i++].mChannelLabel
309 = [chan channelLabel];
311 last_channel_count = chans.count;
313 *pport_type = port_type;
316 if (port_type == PORT_TYPE_HDMI) /* Prefer HDMI */
320 msg_Dbg(p_aout, "Output on %s, channel count: %u",
321 *pport_type == PORT_TYPE_HDMI ? "HDMI" :
322 *pport_type == PORT_TYPE_USB ? "USB" :
323 *pport_type == PORT_TYPE_HEADPHONES ? "Headphones" : "Default",
324 layout ? (unsigned) layout->mNumberChannelDescriptions : 2);
331 avas_SetActive(audio_output_t *p_aout, bool active, NSUInteger options)
333 aout_sys_t * p_sys = p_aout->sys;
334 AVAudioSession *instance = p_sys->avInstance;
336 NSError *error = nil;
340 ret = [instance setCategory:AVAudioSessionCategoryPlayback error:&error];
341 ret = ret && [instance setMode:AVAudioSessionModeMoviePlayback error:&error];
342 ret = ret && [instance setActive:YES withOptions:options error:&error];
343 [[SessionManager sharedInstance] addAoutInstance: p_sys->aoutWrapper];
345 NSInteger numberOfRegisteredInstances = [[SessionManager sharedInstance] removeAoutInstance: p_sys->aoutWrapper];
346 if (numberOfRegisteredInstances == 0) {
347 ret = [instance setActive:NO withOptions:options error:&error];
355 msg_Err(p_aout, "AVAudioSession playback change failed: %s(%d)",
356 error.domain.UTF8String, (int)error.code);
364 #pragma mark actual playback
367 Pause (audio_output_t *p_aout, bool pause, vlc_tick_t date)
369 aout_sys_t * p_sys = p_aout->sys;
371 /* We need to start / stop the audio unit here because otherwise the OS
372 * won't believe us that we stopped the audio output so in case of an
373 * interruption, our unit would be permanently silenced. In case of
374 * multi-tasking, the multi-tasking view would still show a playing state
375 * despite we are paused, same for lock screen */
377 if (pause == p_sys->b_paused)
383 err = AudioOutputUnitStop(p_sys->au_unit);
385 ca_LogErr("AudioOutputUnitStart failed");
386 avas_SetActive(p_aout, false, 0);
390 if (avas_SetActive(p_aout, true, 0) == VLC_SUCCESS)
392 err = AudioOutputUnitStart(p_sys->au_unit);
395 ca_LogErr("AudioOutputUnitStart failed");
396 avas_SetActive(p_aout, false, 0);
397 /* Do not un-pause, the Render Callback won't run, and next call
398 * of ca_Play will deadlock */
403 p_sys->b_paused = pause;
404 ca_Pause(p_aout, pause, date);
408 Flush(audio_output_t *p_aout, bool wait)
410 aout_sys_t * p_sys = p_aout->sys;
412 ca_Flush(p_aout, wait);
416 MuteSet(audio_output_t *p_aout, bool mute)
418 aout_sys_t * p_sys = p_aout->sys;
420 p_sys->b_muted = mute;
421 if (p_sys->au_unit != NULL)
423 Pause(p_aout, mute, 0);
425 ca_Flush(p_aout, false);
432 Play(audio_output_t * p_aout, block_t * p_block, vlc_tick_t date)
434 aout_sys_t * p_sys = p_aout->sys;
437 block_Release(p_block);
439 ca_Play(p_aout, p_block, date);
442 #pragma mark initialization
445 Stop(audio_output_t *p_aout)
447 aout_sys_t *p_sys = p_aout->sys;
450 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
452 if (!p_sys->b_paused)
454 err = AudioOutputUnitStop(p_sys->au_unit);
456 ca_LogWarn("AudioOutputUnitStop failed");
459 au_Uninitialize(p_aout, p_sys->au_unit);
461 err = AudioComponentInstanceDispose(p_sys->au_unit);
463 ca_LogWarn("AudioComponentInstanceDispose failed");
465 avas_resetPreferredNumberOfChannels(p_aout);
467 avas_SetActive(p_aout, false,
468 AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
472 Start(audio_output_t *p_aout, audio_sample_format_t *restrict fmt)
474 aout_sys_t *p_sys = p_aout->sys;
477 AudioChannelLayout *layout = NULL;
479 if (aout_FormatNbChannels(fmt) == 0 || AOUT_FMT_HDMI(fmt))
482 /* XXX: No more passthrough since iOS 11 */
483 if (AOUT_FMT_SPDIF(fmt))
486 aout_FormatPrint(p_aout, "VLC is looking for:", fmt);
488 p_sys->au_unit = NULL;
490 [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
491 selector:@selector(audioSessionRouteChange:)
492 name:AVAudioSessionRouteChangeNotification
494 [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
495 selector:@selector(handleInterruption:)
496 name:AVAudioSessionInterruptionNotification
499 /* Activate the AVAudioSession */
500 if (avas_SetActive(p_aout, true, 0) != VLC_SUCCESS)
502 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
506 /* Set the preferred number of channels, then fetch the channel layout that
507 * should correspond to this number */
508 avas_setPreferredNumberOfChannels(p_aout, fmt);
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 ret = au_Initialize(p_aout, p_sys->au_unit, fmt, layout,
536 vlc_tick_from_sec([p_sys->avInstance outputLatency]), NULL);
537 if (ret != VLC_SUCCESS)
542 err = AudioOutputUnitStart(p_sys->au_unit);
545 ca_LogErr("AudioOutputUnitStart failed");
546 au_Uninitialize(p_aout, p_sys->au_unit);
551 Pause(p_aout, true, 0);
554 fmt->channel_type = AUDIO_CHANNEL_TYPE_BITMAP;
555 p_aout->mute_set = MuteSet;
556 p_aout->pause = Pause;
557 p_aout->flush = Flush;
559 aout_SoftVolumeStart( p_aout );
561 msg_Dbg(p_aout, "analog AudioUnit output successfully opened for %4.4s %s",
562 (const char *)&fmt->i_format, aout_FormatPrintChannels(fmt));
567 if (p_sys->au_unit != NULL)
568 AudioComponentInstanceDispose(p_sys->au_unit);
569 avas_resetPreferredNumberOfChannels(p_aout);
570 avas_SetActive(p_aout, false,
571 AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
572 [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
573 msg_Err(p_aout, "opening AudioUnit output failed");
577 static int DeviceSelect(audio_output_t *p_aout, const char *psz_id)
579 aout_sys_t *p_sys = p_aout->sys;
580 enum au_dev au_dev = AU_DEV_PCM;
584 for (unsigned int i = 0; i < sizeof(au_devs) / sizeof(au_devs[0]); ++i)
586 if (!strcmp(psz_id, au_devs[i].psz_id))
588 au_dev = au_devs[i].au_dev;
594 if (au_dev != p_sys->au_dev)
596 p_sys->au_dev = au_dev;
597 aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
598 msg_Dbg(p_aout, "selected audiounit device: %s", psz_id);
600 aout_DeviceReport(p_aout, psz_id);
605 Close(vlc_object_t *obj)
607 audio_output_t *aout = (audio_output_t *)obj;
608 aout_sys_t *sys = aout->sys;
610 [sys->aoutWrapper release];
617 Open(vlc_object_t *obj)
619 audio_output_t *aout = (audio_output_t *)obj;
620 aout_sys_t *sys = calloc(1, sizeof (*sys));
622 if (unlikely(sys == NULL))
625 sys->avInstance = [AVAudioSession sharedInstance];
626 assert(sys->avInstance != NULL);
628 sys->aoutWrapper = [[AoutWrapper alloc] initWithAout:aout];
629 if (sys->aoutWrapper == NULL)
635 sys->b_muted = false;
636 sys->b_preferred_channels_set = false;
637 sys->au_dev = var_InheritBool(aout, "spdif") ? AU_DEV_ENCODED : AU_DEV_PCM;
641 aout->device_select = DeviceSelect;
643 aout_SoftVolumeInit( aout );
645 for (unsigned int i = 0; i< sizeof(au_devs) / sizeof(au_devs[0]); ++i)
646 aout_HotplugReport(aout, au_devs[i].psz_id, au_devs[i].psz_name);