91

AVAudioSession音频配置小技巧

 3 years ago
source link: http://cocoa-chen.github.io/2020/08/20/AVAudioSession音频配置小技巧/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

iOS在音频配置方面的核心逻辑在AVAudioSession,它用来管理多个App对音频硬件设备(麦克风,扬声器)的资源使用。这篇文章不是AVAudioSession的科普,如果你还不了解它,可以google下,会有很多文章专门介绍它。这里总结一些网络上不常见的音频配置技巧点,帮助你优化App的体验问题。

jiEVBvn.jpg!mobile

技巧点总结

技巧一:适配蓝牙设备

背景:

在录音时,我们常会使用下面的代码来支持蓝牙设备,但是这样适配的方案有问题。在一些蓝牙设备或者汽车的车载蓝牙音箱上会出现录音被识别为电话优先级的事情出现,无法使用车载的FM,音乐播放也会出现音质变差的问题,用户体验极差。

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetooth error:nil];

问题原因:

为了解决这个问题,我们首先需要了解下蓝牙协议的三大规格:HSP、HFP和A2DP

HeadsetPro-file(HSP)代表耳机功能,提供手机与耳机之间通信所需的基本功能。

HandProfile(HFP)则代表免提功能,HFP在HSP的基础上增加了某些扩展功能。

Advanced Audio Distribution Profile(A2DP),指的是蓝牙音频传输模型协定。

HFP格式的蓝牙耳机支持手机功能比较完整,消费者可在耳机上操作手机设定好的重拨、来电保留、来电拒听等免提选项功能。

A2DP是高级音频传送规格,允许传输立体声音频信号,相比用于 HSP 和 HFP 的单声道加密,质量要好得多。

在录音时,AVAudioSessionCategoryOptionAllowBluetooth会让配对的HFP蓝牙设备作为音频输入源,由于苹果的设定,如果采用了HFP来进行音频输入,那么输出会自动切回为HFP。导致车载中控成为了一个免提设备,同时在进行音频输入和输出,中控台也一直显示拨打电话的界面,也间接导致了FM功能的不可用。

相关内容可参见苹果的官方文档描述,如下:

AVAudioSessionCategoryOptionAllowBluetooth – This allows an application to change the default behaviour of some audio session categories with regards to showing bluetooth Hands-Free Profile (HFP) devices as available routes. The current category behavior is: (1) AVAudioSessionCategoryPlayAndRecord this will default to false, but can be set to true. This will allow a paired bluetooth HFP device to show up as an available route for input, while playing through the category-appropriate output
“If an application uses the setPreferredInput:error: method to select a Bluetooth HFP input, the output will automatically be changed to the Bluetooth HFP output. Moreover, selecting a Bluetooth HFP output using the MPVolumeView’s route picker will automatically change the input to the Bluetooth HFP input. Therefore both the input and output will always end up on the Bluetooth HFP device even though only the input or output was set individually.”

解决方案:

修改AVAudioSession的音频配置为AVAudioSessionCategoryOptionAllowBluetoothA2DP,系统会使音频输出通道切换为A2DP的蓝牙设备,同时将输入切换到最适合音频Category的通道(基本都是内置的麦克风),这样可以将音频输入输出通道作区分,解决上面提到的车载蓝牙设备适配问题。

同时由于蓝牙输入设备的限制,采用HFP蓝牙传输协议时,音频将以8000 Hz的频率单声道播放;而对于支持A2DP的已连接设备,采用A2DP协议可以44100 Hz采样率立体声播放,在将音频输出时的蓝牙传输协议从HFP切换到A2DP后,音质会更好,体验也更佳。

注意:AVAudioSessionCategoryOptionAllowBluetooth 和 AVAudioSessionCategoryOptionAllowBluetoothA2DP 可以同时设置。如果有些设备同时支持 HFP 和 A2DP,在两个Option都设置的情况下,HFP port会有更高的优先级。在我们上述的情景下,不能同时设置AllowBluetooth及AllowBluetoothA2DP,不然A2DP会自动失效导致解决方案无效。

相关内容可参见苹果的官方文档描述,如下:

AVAudioSessionCategoryOptionAllowBluetoothA2DP – This allows an application to change the default behaviour of some audio session categories with regards to showing bluetooth Advanced Audio Distribution Profile (A2DP), i.e. stereo Bluetooth, devices as available routes. The current category behavior is: (1) AVAudioSessionCategoryPlayAndRecord this will default to false, but can be set to true. This will allow a paired bluetooth A2DP device to show up as an available route for output, while recording through the category-appropriate input

最终的适配代码如下:

if (@available(iOS 10.0, *)) {
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:nil];
}else{
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetooth error:nil];
}

技巧二:Carplay支持

随着车企对Carplay的陆续支持,越来越多的用户开始使用Carplay功能,在音频播报时我们可以采用以下的代码来适配支持

适配方案可参见官方文档: https://developer.apple.com/carplay/documentation/CarPlay-Navigation-App-Programming-Guide.pdf

if (@available(iOS 12.0, *)) {
    if([AVAudioSession sharedInstance].category == AVAudioSessionCategoryPlayback &&
       [[AVAudioSession sharedInstance] mode] != AVAudioSessionModeVoicePrompt) {
        //AVAudioSessionModeVoicePrompt只有在AVAudioSessionCategoryPlayback的类型下才会生效
        [[AVAudioSession sharedInstance] setMode:AVAudioSessionModeVoicePrompt error:nil];
    }
}

技巧三:port为空防崩溃

网络上有些文章在设备断开时会读取之前断开的通道是什么,示例代码如下:

@implementation YourClass

- (instancetype)init {
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(p_routeChangeNotification:) name:AVAudioSessionRouteChangeNotification object:nil];
    }
    return self;
}

//音频通道发生变化
- (void)p_routeChangeNotification:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    AVAudioSessionRouteChangeReason reason = [userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey];
        //这里用的是outputs[0]
        AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
    }
}

但其实以上的代码是有问题的,在某些极端情况outputs的数据不是>=1的,直接用[0]会出现数组越界问题而崩溃。建议改成以下的方式:

AVAudioSessionPortDescription *previousOutput = previousRoute.outputs.firstObject;
if (!previousOutput) {
    //your logic code
}

技巧四:overrideOutputAudioPort

现在主流的AVAudioSession配置都考虑了对耳机插拔的支持,处理逻辑是在新设备连接时调用[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil]; 在设备断开时调用[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];

这样的处理简单粗暴,会导致设备断开时输出通道强制路由到扬声器,可能会出现错误路由的问题。可判断下当前通道是否为听筒,如果为听筒再重置到扬声器,如果不是忽略即可。代码示意如下:

AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
AVAudioSessionPortDescription *port = currentRoute.outputs.firstObject;
if (!port) {
   return;
}
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
   [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
   self.overrideToSpeaker = YES;
}else{
   if (self.overrideToSpeaker) {
       self.overrideToSpeaker = NO;
       [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
   }
}

技巧五:录音中震动配置

在设置Category为AVAudioSessionCategoryPlayAndRecord时,调用震动的代码会不生效。在iOS13系统版本以上,系统添加了新的方法,可以配置录音时支持震动功能来解决。代码如下:

if (@available(iOS 13.0, *)) {
    [[AVAudioSession sharedInstance] setAllowHapticsAndSystemSoundsDuringRecording:YES error:nil];
}

总结

AVAudioSession是管理音频配置的核心类,官方文档里有很多的细节点供挖掘,这里简单总结一些技巧点,如有内容错误,欢迎指正。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK