qt: playlist: use item title if available
[vlc.git] / modules / audio_output / audiounit_ios.m
blobe0547b514f025b1653e6eb7d0549ff119d146e41
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 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;
41 @end
43 enum au_dev
45     AU_DEV_PCM,
46     AU_DEV_ENCODED,
49 static const struct {
50     const char *psz_id;
51     const char *psz_name;
52     enum au_dev au_dev;
53 } au_devs[] = {
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;
66 @end
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];
76     });
78     return sharedInstance;
81 - (instancetype)init
83     self = [super init];
84     if (self) {
85         _registeredInstances = [[NSMutableSet alloc] init];
86     }
87     return self;
90 - (void)addAoutInstance:(AoutWrapper *)wrapperInstance
92     @synchronized(_registeredInstances) {
93         [_registeredInstances addObject:wrapperInstance];
94     }
97 - (NSInteger)removeAoutInstance:(AoutWrapper *)wrapperInstance
99     @synchronized(_registeredInstances) {
100         [_registeredInstances removeObject:wrapperInstance];
101         return _registeredInstances.count;
102     }
104 @end
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  *****************************************************************************/
112 typedef struct
114     struct aout_sys_common c;
116     AVAudioSession *avInstance;
117     AoutWrapper *aoutWrapper;
118     /* The AudioUnit we use */
119     AudioUnit au_unit;
120     bool      b_muted;
121     bool      b_stopped;
122     bool      b_preferred_channels_set;
123     enum au_dev au_dev;
125     /* sw gain */
126     float               soft_gain;
127     bool                soft_mute;
128 } aout_sys_t;
130 /* Soft volume helper */
131 #include "audio_output/volume.h"
133 enum port_type
135     PORT_TYPE_DEFAULT,
136     PORT_TYPE_USB,
137     PORT_TYPE_HDMI,
138     PORT_TYPE_HEADPHONES
141 #pragma mark -
142 #pragma mark AVAudioSession route and output handling
144 @implementation AoutWrapper
146 - (instancetype)initWithAout:(audio_output_t *)aout
148     self = [super init];
149     if (self)
150         _aout = aout;
151     return self;
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);
167     else
168     {
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);
173     }
176 - (void)handleInterruption:(NSNotification *)notification
178     audio_output_t *p_aout = [self aout];
179     NSDictionary *userInfo = notification.userInfo;
180     if (!userInfo || !userInfo[AVAudioSessionInterruptionTypeKey]) {
181         return;
182     }
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);
191     }
193 @end
195 static void
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)
210     {
211         channel_count = __MIN(channel_count, max_channel_count);
212         bool success = [instance setPreferredOutputNumberOfChannels:channel_count
213                         error:nil];
214         if (success && [instance outputNumberOfChannels] == channel_count)
215             p_sys->b_preferred_channels_set = true;
216         else
217         {
218             /* Not critical, output channels layout will be Stereo */
219             msg_Warn(p_aout, "setPreferredOutputNumberOfChannels failed");
220         }
221     }
224 static void
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)
231     {
232         [instance setPreferredOutputNumberOfChannels:2 error:nil];
233         p_sys->b_preferred_channels_set = false;
234     }
237 static int
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])
248     {
249         /* Choose the layout with the biggest number of channels or the HDMI
250          * one */
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;
259         else
260             port_type = PORT_TYPE_DEFAULT;
262         NSArray<AVAudioSessionChannelDescription *> *chans = [out channels];
264         if (chans.count > last_channel_count || port_type == PORT_TYPE_HDMI)
265         {
266             /* We don't need a layout specification for stereo */
267             if (chans.count > 2)
268             {
269                 bool labels_valid = false;
270                 for (AVAudioSessionChannelDescription *chan in chans)
271                 {
272                     if ([chan channelLabel] != kAudioChannelLabel_Unknown)
273                     {
274                         labels_valid = true;
275                         break;
276                     }
277                 }
278                 if (!labels_valid)
279                 {
280                     /* TODO: Guess labels ? */
281                     msg_Warn(p_aout, "no valid channel labels");
282                     continue;
283                 }
285                 if (layout == NULL
286                  || layout->mNumberChannelDescriptions < chans.count)
287                 {
288                     const size_t layout_size = sizeof(AudioChannelLayout)
289                         + chans.count * sizeof(AudioChannelDescription);
290                     layout = realloc_or_free(layout, layout_size);
291                     if (layout == NULL)
292                         return VLC_ENOMEM;
293                 }
295                 layout->mChannelLayoutTag =
296                     kAudioChannelLayoutTag_UseChannelDescriptions;
297                 layout->mNumberChannelDescriptions = chans.count;
299                 unsigned i = 0;
300                 for (AVAudioSessionChannelDescription *chan in chans)
301                     layout->mChannelDescriptions[i++].mChannelLabel
302                         = [chan channelLabel];
304                 last_channel_count = chans.count;
305             }
306             *pport_type = port_type;
307         }
309         if (port_type == PORT_TYPE_HDMI) /* Prefer HDMI */
310             break;
311     }
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);
319     *playout = layout;
320     return VLC_SUCCESS;
323 static int
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;
328     BOOL ret = false;
329     NSError *error = nil;
331     if (active)
332     {
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];
337     } else {
338         NSInteger numberOfRegisteredInstances = [[SessionManager sharedInstance] removeAoutInstance: p_sys->aoutWrapper];
339         if (numberOfRegisteredInstances == 0) {
340             ret = [instance setActive:NO withOptions:options error:&error];
341         } else {
342             ret = true;
343         }
344     }
346     if (!ret)
347     {
348         msg_Err(p_aout, "AVAudioSession playback change failed: %s(%d)",
349                 error.domain.UTF8String, (int)error.code);
350         return VLC_EGENERIC;
351     }
353     return VLC_SUCCESS;
356 #pragma mark -
357 #pragma mark actual playback
359 static void
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)
371         return;
373     OSStatus err;
374     if (pause)
375     {
376         err = AudioOutputUnitStop(p_sys->au_unit);
377         if (err != noErr)
378             ca_LogErr("AudioOutputUnitStart failed");
379         avas_SetActive(p_aout, false, 0);
380     }
381     else
382     {
383         if (avas_SetActive(p_aout, true, 0) == VLC_SUCCESS)
384         {
385             err = AudioOutputUnitStart(p_sys->au_unit);
386             if (err != noErr)
387             {
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 */
392                 return;
393             }
394         }
395     }
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. */
404     if (pause)
405         ca_Flush(p_aout);
408 static int
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)
415     {
416         Pause(p_aout, mute, 0);
417         if (mute)
418             ca_Flush(p_aout);
419     }
421     return VLC_SUCCESS;
424 static void
425 Play(audio_output_t * p_aout, block_t * p_block, vlc_tick_t date)
427     aout_sys_t * p_sys = p_aout->sys;
429     if (p_sys->b_muted)
430         block_Release(p_block);
431     else
432         ca_Play(p_aout, p_block, date);
435 #pragma mark initialization
437 static void
438 Stop(audio_output_t *p_aout)
440     aout_sys_t   *p_sys = p_aout->sys;
441     OSStatus err;
443     [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
445     if (!p_sys->b_stopped)
446     {
447         err = AudioOutputUnitStop(p_sys->au_unit);
448         if (err != noErr)
449             ca_LogWarn("AudioOutputUnitStop failed");
450     }
452     au_Uninitialize(p_aout, p_sys->au_unit);
454     err = AudioComponentInstanceDispose(p_sys->au_unit);
455     if (err != noErr)
456         ca_LogWarn("AudioComponentInstanceDispose failed");
458     avas_resetPreferredNumberOfChannels(p_aout);
460     avas_SetActive(p_aout, false,
461                    AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation);
464 static int
465 Start(audio_output_t *p_aout, audio_sample_format_t *restrict fmt)
467     aout_sys_t *p_sys = p_aout->sys;
468     OSStatus err;
469     OSStatus status;
470     AudioChannelLayout *layout = NULL;
472     if (aout_FormatNbChannels(fmt) == 0 || AOUT_FMT_HDMI(fmt))
473         return VLC_EGENERIC;
475     /* XXX: No more passthrough since iOS 11 */
476     if (AOUT_FMT_SPDIF(fmt))
477         return VLC_EGENERIC;
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
486                                                object:nil];
487     [[NSNotificationCenter defaultCenter] addObserver:p_sys->aoutWrapper
488                                              selector:@selector(handleInterruption:)
489                                                  name:AVAudioSessionInterruptionNotification
490                                                object:nil];
492     /* Activate the AVAudioSession */
493     if (avas_SetActive(p_aout, true, 0) != VLC_SUCCESS)
494     {
495         [[NSNotificationCenter defaultCenter] removeObserver:p_sys->aoutWrapper];
496         return VLC_EGENERIC;
497     }
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];
504     if (!success)
505     {
506         /* Not critical, we can use any sample rates */
507         msg_Dbg(p_aout, "failed to set preferred sample rate");
508     }
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     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)
541         goto error;
543     p_aout->play = Play;
545     err = AudioOutputUnitStart(p_sys->au_unit);
546     if (err != noErr)
547     {
548         ca_LogErr("AudioOutputUnitStart failed");
549         au_Uninitialize(p_aout, p_sys->au_unit);
550         goto error;
551     }
553     if (p_sys->b_muted)
554         Pause(p_aout, true, 0);
556     free(layout);
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));
565     return VLC_SUCCESS;
567 error:
568     free(layout);
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");
576     return VLC_EGENERIC;
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;
584     if (psz_id)
585     {
586         for (unsigned int i = 0; i < sizeof(au_devs) / sizeof(au_devs[0]); ++i)
587         {
588             if (!strcmp(psz_id, au_devs[i].psz_id))
589             {
590                 au_dev = au_devs[i].au_dev;
591                 break;
592             }
593         }
594     }
596     if (au_dev != p_sys->au_dev)
597     {
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);
601     }
602     aout_DeviceReport(p_aout, psz_id);
603     return VLC_SUCCESS;
606 static void
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];
614     free(sys);
617 static int
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))
624         return VLC_ENOMEM;
626     if (ca_Open(aout) != VLC_SUCCESS)
627     {
628         free(sys);
629         return VLC_EGENERIC;
630     }
632     sys->avInstance = [AVAudioSession sharedInstance];
633     assert(sys->avInstance != NULL);
635     sys->aoutWrapper = [[AoutWrapper alloc] initWithAout:aout];
636     if (sys->aoutWrapper == NULL)
637     {
638         free(sys);
639         return VLC_ENOMEM;
640     }
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;
645     aout->start = Start;
646     aout->stop = Stop;
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);
654     return VLC_SUCCESS;
657 #pragma mark -
658 #pragma mark module descriptor
660 vlc_module_begin ()
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)
666     add_sw_gain()
667     set_callbacks(Open, Close)
668 vlc_module_end ()