misc: medialibrary: ctx does not need dynamic lifetime
[vlc.git] / modules / audio_output / audiounit_ios.m
blobc9a8b7c7df46d4923cbc2ef06429e2fe8f8cebc6
1 /*****************************************************************************
2  * audiounit_ios.m: AudioUnit output plugin for iOS
3  *****************************************************************************
4  * Copyright (C) 2012 - 2017 VLC authors and VideoLAN
5  *
6  * Authors: Felix Paul Kühne <fkuehne at videolan dot org>
7  *
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.
12  *
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.
17  *
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  *****************************************************************************/
23 #pragma mark includes
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>
34 #pragma mark -
35 #pragma mark local prototypes & module descriptor
37 static int  Open  (vlc_object_t *);
38 static void Close (vlc_object_t *);
40 vlc_module_begin ()
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)
47 vlc_module_end ()
49 #pragma mark -
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;
56 @end
58 enum au_dev
60     AU_DEV_PCM,
61     AU_DEV_ENCODED,
64 static const struct {
65     const char *psz_id;
66     const char *psz_name;
67     enum au_dev au_dev;
68 } au_devs[] = {
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;
81 @end
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];
91     });
93     return sharedInstance;
96 - (instancetype)init
98     self = [super init];
99     if (self) {
100         _registeredInstances = [[NSMutableSet alloc] init];
101     }
102     return self;
105 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance
107     @synchronized(_registeredInstances) {
108         [_registeredInstances addObject:wrapperInstance];
109     }
112 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance
114     @synchronized(_registeredInstances) {
115         [_registeredInstances removeObject:wrapperInstance];
116         return _registeredInstances.count;
117     }
119 @end
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  *****************************************************************************/
127 typedef struct
129     struct aout_sys_common c;
131     AVAudioSession *avInstance;
132     AoutWrapper *aoutWrapper;
133     /* The AudioUnit we use */
134     AudioUnit au_unit;
135     bool      b_muted;
136     bool      b_paused;
137     bool      b_preferred_channels_set;
138     enum au_dev au_dev;
140     /* sw gain */
141     float               soft_gain;
142     bool                soft_mute;
143 } aout_sys_t;
145 /* Soft volume helper */
146 #include "audio_output/volume.h"
148 enum port_type
150     PORT_TYPE_DEFAULT,
151     PORT_TYPE_USB,
152     PORT_TYPE_HDMI,
153     PORT_TYPE_HEADPHONES
156 #pragma mark -
157 #pragma mark AVAudioSession route and output handling
159 @implementation AoutWrapper
161 - (instancetype)initWithAout:(audio_output_t *)aout
163     self = [super init];
164     if (self)
165         _aout = aout;
166     return self;
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]) {
188         return;
189     }
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);
198     }
200 @end
202 static void
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)
217     {
218         channel_count = __MIN(channel_count, max_channel_count);
219         bool success = [instance setPreferredOutputNumberOfChannels:channel_count
220                         error:nil];
221         if (success && [instance outputNumberOfChannels] == channel_count)
222             p_sys->b_preferred_channels_set = true;
223         else
224         {
225             /* Not critical, output channels layout will be Stereo */
226             msg_Warn(p_aout, "setPreferredOutputNumberOfChannels failed");
227         }
228     }
231 static void
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)
238     {
239         [instance setPreferredOutputNumberOfChannels:2 error:nil];
240         p_sys->b_preferred_channels_set = false;
241     }
244 static int
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])
255     {
256         /* Choose the layout with the biggest number of channels or the HDMI
257          * one */
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;
266         else
267             port_type = PORT_TYPE_DEFAULT;
269         NSArray<AVAudioSessionChannelDescription *> *chans = [out channels];
271         if (chans.count > last_channel_count || port_type == PORT_TYPE_HDMI)
272         {
273             /* We don't need a layout specification for stereo */
274             if (chans.count > 2)
275             {
276                 bool labels_valid = false;
277                 for (AVAudioSessionChannelDescription *chan in chans)
278                 {
279                     if ([chan channelLabel] != kAudioChannelLabel_Unknown)
280                     {
281                         labels_valid = true;
282                         break;
283                     }
284                 }
285                 if (!labels_valid)
286                 {
287                     /* TODO: Guess labels ? */
288                     msg_Warn(p_aout, "no valid channel labels");
289                     continue;
290                 }
292                 if (layout == NULL
293                  || layout->mNumberChannelDescriptions < chans.count)
294                 {
295                     const size_t layout_size = sizeof(AudioChannelLayout)
296                         + chans.count * sizeof(AudioChannelDescription);
297                     layout = realloc_or_free(layout, layout_size);
298                     if (layout == NULL)
299                         return VLC_ENOMEM;
300                 }
302                 layout->mChannelLayoutTag =
303                     kAudioChannelLayoutTag_UseChannelDescriptions;
304                 layout->mNumberChannelDescriptions = chans.count;
306                 unsigned i = 0;
307                 for (AVAudioSessionChannelDescription *chan in chans)
308                     layout->mChannelDescriptions[i++].mChannelLabel
309                         = [chan channelLabel];
311                 last_channel_count = chans.count;
312             }
313             *pport_type = port_type;
314         }
316         if (port_type == PORT_TYPE_HDMI) /* Prefer HDMI */
317             break;
318     }
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);
326     *playout = layout;
327     return VLC_SUCCESS;
330 static int
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;
335     BOOL ret = false;
336     NSError *error = nil;
338     if (active)
339     {
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];
344     } else {
345         NSInteger numberOfRegisteredInstances = [[SessionManager sharedInstance] removeAoutInstance: p_sys->aoutWrapper];
346         if (numberOfRegisteredInstances == 0) {
347             ret = [instance setActive:NO withOptions:options error:&error];
348         } else {
349             ret = true;
350         }
351     }
353     if (!ret)
354     {
355         msg_Err(p_aout, "AVAudioSession playback change failed: %s(%d)",
356                 error.domain.UTF8String, (int)error.code);
357         return VLC_EGENERIC;
358     }
360     return VLC_SUCCESS;
363 #pragma mark -
364 #pragma mark actual playback
366 static void
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)
378         return;
380     OSStatus err;
381     if (pause)
382     {
383         err = AudioOutputUnitStop(p_sys->au_unit);
384         if (err != noErr)
385             ca_LogErr("AudioOutputUnitStart failed");
386         avas_SetActive(p_aout, false, 0);
387     }
388     else
389     {
390         if (avas_SetActive(p_aout, true, 0) == VLC_SUCCESS)
391         {
392             err = AudioOutputUnitStart(p_sys->au_unit);
393             if (err != noErr)
394             {
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 */
399                 return;
400             }
401         }
402     }
403     p_sys->b_paused = pause;
404     ca_Pause(p_aout, pause, date);
407 static void
408 Flush(audio_output_t *p_aout, bool wait)
410     aout_sys_t * p_sys = p_aout->sys;
412     ca_Flush(p_aout, wait);
415 static int
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)
422     {
423         Pause(p_aout, mute, 0);
424         if (mute)
425             ca_Flush(p_aout, false);
426     }
428     return VLC_SUCCESS;
431 static void
432 Play(audio_output_t * p_aout, block_t * p_block, vlc_tick_t date)
434     aout_sys_t * p_sys = p_aout->sys;
436     if (p_sys->b_muted)
437         block_Release(p_block);
438     else
439         ca_Play(p_aout, p_block, date);
442 #pragma mark initialization
444 static void
445 Stop(audio_output_t *p_aout)
447     aout_sys_t   *p_sys = p_aout->sys;
448     OSStatus err;
450     [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
452     if (!p_sys->b_paused)
453     {
454         err = AudioOutputUnitStop(p_sys->au_unit);
455         if (err != noErr)
456             ca_LogWarn("AudioOutputUnitStop failed");
457     }
459     au_Uninitialize(p_aout, p_sys->au_unit);
461     err = AudioComponentInstanceDispose(p_sys->au_unit);
462     if (err != noErr)
463         ca_LogWarn("AudioComponentInstanceDispose failed");
465     avas_resetPreferredNumberOfChannels(p_aout);
467     avas_SetActive(p_aout, false,
468                    AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
471 static int
472 Start(audio_output_t *p_aout, audio_sample_format_t *restrict fmt)
474     aout_sys_t *p_sys = p_aout->sys;
475     OSStatus err;
476     OSStatus status;
477     AudioChannelLayout *layout = NULL;
479     if (aout_FormatNbChannels(fmt) == 0 || AOUT_FMT_HDMI(fmt))
480         return VLC_EGENERIC;
482     /* XXX: No more passthrough since iOS 11 */
483     if (AOUT_FMT_SPDIF(fmt))
484         return VLC_EGENERIC;
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
493                                                object:nil];
494     [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
495                                              selector:@selector(handleInterruption:)
496                                                  name:AVAudioSessionInterruptionNotification
497                                                object:nil];
499     /* Activate the AVAudioSession */
500     if (avas_SetActive(p_aout, true, 0) != VLC_SUCCESS)
501     {
502         [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
503         return VLC_EGENERIC;
504     }
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)
513         goto error;
515     if (AOUT_FMT_SPDIF(fmt))
516     {
517         if (p_sys->au_dev != AU_DEV_ENCODED
518          || (port_type != PORT_TYPE_USB && port_type != PORT_TYPE_HDMI))
519             goto error;
520     }
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)
526         goto error;
528     err = AudioUnitSetProperty(p_sys->au_unit,
529                                kAudioOutputUnitProperty_EnableIO,
530                                kAudioUnitScope_Output, 0,
531                                &(UInt32){ 1 }, sizeof(UInt32));
532     if (err != noErr)
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)
538         goto error;
540     p_aout->play = Play;
542     err = AudioOutputUnitStart(p_sys->au_unit);
543     if (err != noErr)
544     {
545         ca_LogErr("AudioOutputUnitStart failed");
546         au_Uninitialize(p_aout, p_sys->au_unit);
547         goto error;
548     }
550     if (p_sys->b_muted)
551         Pause(p_aout, true, 0);
553     free(layout);
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));
563     return VLC_SUCCESS;
565 error:
566     free(layout);
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");
574     return VLC_EGENERIC;
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;
582     if (psz_id)
583     {
584         for (unsigned int i = 0; i < sizeof(au_devs) / sizeof(au_devs[0]); ++i)
585         {
586             if (!strcmp(psz_id, au_devs[i].psz_id))
587             {
588                 au_dev = au_devs[i].au_dev;
589                 break;
590             }
591         }
592     }
594     if (au_dev != p_sys->au_dev)
595     {
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);
599     }
600     aout_DeviceReport(p_aout, psz_id);
601     return VLC_SUCCESS;
604 static void
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];
612     ca_Close(aout);
613     free(sys);
616 static int
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))
623         return VLC_ENOMEM;
625     sys->avInstance = [AVAudioSession sharedInstance];
626     assert(sys->avInstance != NULL);
628     sys->aoutWrapper = [[AoutWrapper alloc] initWithAout:aout];
629     if (sys->aoutWrapper == NULL)
630     {
631         free(sys);
632         return VLC_ENOMEM;
633     }
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;
638     aout->sys = sys;
639     aout->start = Start;
640     aout->stop = Stop;
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);
648     ca_Open(aout);
649     return VLC_SUCCESS;