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行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等)
aspg_intro_2x.png

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。

你可能感兴趣的:(iOS 音频流播(二))