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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容