iOS 音频流播(二)
前言
第一篇中介绍了音频基础知识和编码的技术栈,没有看过的同学可以花几分钟浏览一下,把握一下大体方向。接下来的几篇文章,会依次介绍AudioFileStream、AudioFile、AudioConverter和AudioUnit,最后会给大家分析DOUAudioStreamer的源码。不过在这之前,还有一个很重要知识点,那就是AudioSession。
AudioSession简介
iOS中关于AudioSession有两个类可以使用。对于第二个,已经被标注为deprecated,这里就不多做介绍了。
- AVFoundation中的AVAudioSession
- AudioToolBox中的AudioSession
AVAudioSession的作用
一言以概之,AVAudioSession是iOS中的音频小管家。iOS通过AVAudioSession协调应用程序、应用程序之间甚至是设备级别的音频行为。我们知道,手机所处的环境其实非常复杂,比如说:
- 你正听着歌呢,一个电话进来了
- 你正听着歌呢,不小心按下了静音键
- 你正听着歌呢,有人找你,你取下了耳机
- etc,...
通过配置AVAudioSession,可以让你控制你的应用的音频行为,比如:
- 确定你的app如何使用音频(是播放?还是录音?)
- 为你的app选择合适的输入输出设备(比如输入用的麦克风,输出是耳机或者手机功放)
- 协调你的app的音频播放和系统以及其他app行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等)
AVAudioSession的使用
AVAudioSession设计为单例模式。
AVAudioSession *session = [AVAudioSession sharedInstance];
监听打断
当你正播着音频,此时来了个电话,或者启动了其他音乐播放程序,而它是独占式,不与其他应用进行混音(参见下面的设置类别),此时你的AudioSession就会deactive,同时进入打断。你可以注册打断监听,用以在打断时暂停播放,打断结束后继续播放。
// AVFoundation 定义的打断通知
AVF_EXPORT NSString *const AVAudioSessionInterruptionNotification;
// 注册打断通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_audioSessionInterruptionListener:)
name:AVAudioSessionInterruptionNotification object:nil];
// 处理打断
- (void)_audioSessionInterruptionListener:(NSNotification *)notification
{
// 获取打断的描述信息
NSDictionary *interruptionDictionary = [notification userInfo];
// 获取打断的状态
AVAudioSessionInterruptionType type =
[interruptionDictionary [AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
// 打断开始
if (type == AVAudioSessionInterruptionTypeBegan) {
// 更新UI,暂停播放
} else if (type == AVAudioSessionInterruptionTypeEnded){
// 重新激活AudioSession,更新UI,继续播放
}
}
监听route change
我们知道,手机是可以外接耳机,或者蓝牙音箱等等外部输出设备,当你改变输出设备时,比如从手机功放改到耳机,此时iOS会告诉我们音频输出方式发生了变化。
// AVFoundation 定义的route change通知
AVF_EXPORT NSString *const AVAudioSessionRouteChangeNotification;
// 注册route change
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_audioSessionRouteChangeListener:)
name:AVAudioSessionRouteChangeNotification object:nil];
// 处理route change
- (void)_audioSessionRouteChangeListener:(NSNotification*)notification
{
// 取出描述信息
NSDictionary *routeChangeDic = notification.userInfo;
// 取出音频输出方式改变的原因
NSInteger routeChangeReason = [[routeChangeDic
valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
// 耳机插入
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
break;
// 耳机拔出
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
break;
// called at start - also when other audio wants to play
case AVAudioSessionRouteChangeReasonCategoryChange:
break;
}
}
其中AVAudioSessionRouteChangeReasonOldDeviceUnavailable可以用来实现用户拔掉耳机,停止播放这个功能。
设置类别
通过设置类别,可以指明你想要使用音频服务的意图。比如是要录音还是播放,还是录音和播放同时进行等等。
NSError *error = nil;
BOOL status = [session setCategory:AVAudioSessionCategoryPlayback error:&error];
if (!status) {
NSLog(@"%@",error);
// 出错处理
}
以下是常见的几种类别:
// 设置此类别,iOS允许其他后台应用继续播放音频,按下静音键和锁屏状态会停止播放
AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient;
// 基本同AVAudioSessionCategoryAmbient,唯一的不同在于此类别是独占式,它会阻断其他应用播放音频
AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient;
// 允许后台播放
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback;
// 仅提供录音功能,无法进行播放
AVF_EXPORT NSString *const AVAudioSessionCategoryRecord;
// 可以同时进行播放和录制
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord;
注意:如果需要支持后台播放(包括锁屏时继续播放音频),还必须在info.plist-->Required background modes添加App plays audio or streams audio/video using AirPlay或者在Xcode勾选
E9C88AEE-16EF-46A8-B815-6CB8356BB42D.png
设置类别还有另外一个版本
/* set session category with options */
- (BOOL)setCategory:(NSString *)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError;
下面是几个常用的AVAudioSessionCategoryOptions枚举(新增加的AVAudioSessionCategoryOptionAllowBluetoothA2DP和AVAudioSessionCategoryOptionAllowAirPlay是用来支持蓝牙A2DP和AirPlay)
// 在AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute下有效,允许和后台应用混音
AVAudioSessionCategoryOptionMixWithOthers
//在AVAudioSessionCategoryAmbient, AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute 有效,会降低其他应用的声音
AVAudioSessionCategoryOptionDuckOthers
//在AVAudioSessionCategoryRecord and AVAudioSessionCategoryPlayAndRecord 下有效,提供对蓝牙耳机的支持
AVAudioSessionCategoryOptionAllowBluetooth
//在AVAudioSessionCategoryPlayAndRecord 下有效,使用手机扬声器
AVAudioSessionCategoryOptionDefaultToSpeaker
激活
设置完类别以后,通过激活AudioSession就可以使用了。
- (BOOL)setActive:(BOOL)active error:(NSError * *)outError;
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError ;
- 参数active传入YES表示激活AudioSession,传入NO表示解除激活状态
- 传入的error若在返回时有值,说明发生了错误
- 返回值同样表示执行状态
该方法的第二个版本,可以传入一个AVAudioSessionSetActiveOptions的枚举值。
typedef NS_OPTIONS(NSUInteger, AVAudioSessionSetActiveOptions)
{
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation = 1
}
当你的app deactive自己的AudioSession时系统会通知上一个被打断播放app打断结束,如果你的app在deactive时传入了NotifyOthersOnDeactivation参数,那么其他app在接到打断结束回调时会多得到一个参数。在打断处理中我们得到了打断的描述信息interruptionDictionary,通过key AVAudioSessionInterruptionOptionKey可以取出一个AVAudioSessionInterruptionOptions类型的值,如果是AVAudioSessionInterruptionOptionShouldResume,那么就可以重新激活AudioSession,控制UI继续播放,如是ShouldNotResume,那就继续维持打断状态。
// 完整的处理流程
- (void) _audioSessionInterruptionListener:(NSNotification*)notification {
// 获取打断的描述信息
NSDictionary *interruptionDictionary = [notification userInfo];
// 获取打断的状态
AVAudioSessionInterruptionType type =
[interruptionDictionary [AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
// 能否重新激活AudioSession
AVAudioSessionInterruptionOptions option = [interruptionDictionary [AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
// 打断开始
if (type == AVAudioSessionInterruptionTypeBegan) {
// 更新UI,暂停播放
} else if (type == AVAudioSessionInterruptionTypeEnded){
// 如果可以恢复
if (option == AVAudioSessionInterruptionOptionShouldResume){
// 重新激活AudioSession,更新UI,继续播放
}
} else {
NSLog(@"Something else happened");
}
}
大概流程是这样的:
- 一个音乐软件A正在播放;
- 用户打开你的软件播放对话语音,AudioSession active;
- 音乐软件A音乐被打断并收到InterruptBegin事件;
- 对话语音播放结束,AudioSession deactive并且传入NotifyOthersOnDeactivation参数;
- 音乐软件A收到InterruptEnd事件,查看Resume参数,如果是ShouldResume控制音频继续播放,如果是ShouldNotResume就维持打断状态;
注意:启动方法调用后必须要判断是否启动成功,启动不成功的情况经常存在,例如一个前台的app正在播放,你的app正在后台想要启动AudioSession那就会返回失败。
一点关于后台切换上下曲的小tip
按下HOME键后,程序退到后台,但是声音仍在播放。但是如果要实现播放列表的依次播放、循环播放,即放完一首后自动切换到下一首,会出现一个问题,当app在后台放完一首后,就会停下来。原因是在后台运行时,一旦声音停下来,程序也随之suspend。因为在切换文件加载的间隙,程序就会被suspend。
对这个问题,可以通过申请后台taskID达到后台切换播放文件的功能。即声明后台task id,并通过beginBackgroundTaskWithExpirationHandler将App设为后台Task,达到持续后台运行的目的。我们知道一般情况下,按HOME将程序送到后台,可以有5或10秒时间可以进行一些收尾工作,具体时间[[UIApplication sharedApplication] backgroundTimeRemaining]返回值。超时后app会被suspend,现在要做的就是用[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL]开始后台任务,可以将后台运行超时时间长时间的延长,具体延长多少时间还是见返回值,总之对于放段时间音乐应该够了。另一个问题是每个开始的后台任务,都必须用endBackgroundTask来结束。 因此,在每次开始播放后启动新的后台任务,同时结束上一个后台任务。
// 声明上一个taskID
@property (nonatomic) UIBackgroundTaskIdentifier oldTaskId;
// 申请一点后台执行时间
UIBackgroundTaskIdentifier newTaskId = UIBackgroundTaskInvalid;
// 在这里进行播放下一曲操作
newTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
if (newTaskId != UIBackgroundTaskInvalid && oldTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask: oldTaskId];
}
oldTaskId = newTaskId;
下篇将会介绍AudioFileStream。