原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/

前言

本篇为《iOS音频播放》系列的第二篇。

在实施前一篇中所述的7个步骤之前还必须面对一个麻烦的问题,AudioSession。


AudioSession简单介绍

AudioSession这个玩意的主要功能包含下面几点(图片来自官方文档):

  1. 确定你的app怎样使用音频(是播放?还是录音?)
  2. 为你的app选择合适的输入输出设备(比方输入用的麦克风,输出是耳机、手机功放或者airplay)
  3. 协调你的app的音频播放和系统以及其它app行为(比如有电话时须要打断,电话结束时须要恢复,按下静音button时是否歌曲也要静音等)

AudioSession

AudioSession相关的类有两个:

  1. AudioToolBox中的AudioSession
  2. AVFoundation中的AVAudioSession

当中AudioSession在SDK 7中已经被标注为depracated,而AVAudioSession这个类尽管iOS 3開始就已经存在了。但当中非常多方法和变量都是在iOS 6以后甚至是iOS 7才有的。

所以各位能够按照下面标准选择:

  • 假设最低版本号支持iOS 5,能够使用AudioSession,也能够使用AVAudioSession
  • 假设最低版本号支持iOS 6及以上。请使用AVAudioSession

以下以AudioSession类为例来讲述AudioSession相关功能的使用(非常不幸我须要支持iOS 5。

T-T。使用AVAudioSession的同学能够在其头文件里寻找相应的方法使用就可以,须要注意的点我会加以说明).

注意:在使用AVAudioPlayer/AVPlayer时能够不用关心AudioSession的相关问题,Apple已经把AudioSession的处理过程封装了。但音乐打断后的响应还是要做的(比方打断后音乐暂停了UI状态也要变化。这个应该通过KVO就能够搞定了吧。。

我没试过瞎猜的>_<)。


初始化AudioSession

使用AudioSession类首先须要调用初始化方法:

1
2
3
4
extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop,
CFStringRef inRunLoopMode,
AudioSessionInterruptionListener inInterruptionListener,
void *inClientData);

前两个參数一般填NULL表示AudioSession执行在主线程上(但并不代表音频的相关处理执行在主线程上。仅仅是AudioSession)。第三个參数须要传入一个一个AudioSessionInterruptionListener类型的方法。作为AudioSession被打断时的回调,第四个參数则是代表打断回调时须要附带的对象(即回到方法中的inClientData。例如以下所看到的,能够理解为UIView
animation中的context)。

1
typedef void (*AudioSessionInterruptionListener)(void * inClientData, UInt32 inInterruptionState);

这才刚開始。坑就来了。

这里会有两个问题:

第一。AudioSessionInitialize能够被多次运行,但AudioSessionInterruptionListener仅仅能被设置一次,这就意味着这个打断回调方法是一个静态方法。一旦初始化成功以后全部的打断都会回调到这种方法,即便下一次再次调用AudioSessionInitialize而且把还有一个静态方法作为參数传入,当打断到来时还是会回调到第一次设置的方法上。

这样的场景并不少见,比如你的app既须要播放歌曲又须要录音,当然你不可能知道用户会先调用哪个功能,所以你必须在播放和录音的模块中都调用AudioSessionInitialize注冊打断方法。但终于打断回调仅仅会作用在先注冊的那个模块中,非常蛋疼吧。。。

所以对于AudioSession的使用最好的方法是生成一个类单独进行管理,统一接收打断回调并发送自己定义的打断通知,在须要用到AudioSession的模块中接收通知并做对应的操作。

Apple也察觉到了这一点,所以在AVAudioSession中首先取消了Initialize方法,改为了单例方法sharedInstance。在iOS 5上全部的打断都须要通过设置id<AVAudioSessionDelegate>
delegate
并实现回调方法来实现,这相同会有上述的问题。所以在iOS 5使用AVAudioSession下仍然须要一个单独管理AudioSession的类存在。

在iOS 6以后Apple最终把打断改成了通知的形式。。

这下科学了。

第二,AudioSessionInitialize方法的第四个參数inClientData,也就是回调方法的第一个參数。

上面已经说了打断回调是一个静态方法,而这个參数的目的是为了能让回调时拿到context(上下文信息),所以这个inClientData须要是一个有足够长生命周期的对象(当然前提是你确实须要用到这个參数),假设这个对象被dealloc了,那么回调时拿到的inClientData会是一个野指针。

就这一点来说构造一个单独管理AudioSession的类也是有必要的。由于这个类的生命周期和AudioSession一样长,我们能够把context保存在这个类中。


监听RouteChange事件

假设想要实现类似于“拔掉耳机就把歌曲暂停”的功能就须要监听RouteChange事件:

1
2
3
4
5
6
7
8
extern OSStatus AudioSessionAddPropertyListener(AudioSessionPropertyID inID,
AudioSessionPropertyListener inProc,
void *inClientData); typedef void (*AudioSessionPropertyListener)(void * inClientData,
AudioSessionPropertyID inID,
UInt32 inDataSize,
const void * inData);

调用上述方法。AudioSessionPropertyID參数传kAudioSessionProperty_AudioRouteChange,AudioSessionPropertyListener參数传相应的回调方法。inClientData參数同AudioSessionInitialize方法。

相同作为静态回调方法还是须要统一管理,接到回调时能够把第一个參数inData转换成CFDictionaryRef并从中获取kAudioSession_AudioRouteChangeKey_Reason键值相应的value(应该是一个CFNumberRef),得到这些信息后就能够发送自己定义通知给其它模块进行相应操作(比如kAudioSessionRouteChangeReason_OldDeviceUnavailable就能够用来做“拔掉耳机就把歌曲暂停”)。

1
2
3
4
5
6
7
8
9
10
11
//AudioSession的AudioRouteChangeReason枚举
enum {
kAudioSessionRouteChangeReason_Unknown = 0,
kAudioSessionRouteChangeReason_NewDeviceAvailable = 1,
kAudioSessionRouteChangeReason_OldDeviceUnavailable = 2,
kAudioSessionRouteChangeReason_CategoryChange = 3,
kAudioSessionRouteChangeReason_Override = 4,
kAudioSessionRouteChangeReason_WakeFromSleep = 6,
kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7,
kAudioSessionRouteChangeReason_RouteConfigurationChange = 8
};
1
2
3
4
5
6
7
8
9
10
11
12
//AVAudioSession的AudioRouteChangeReason枚举
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
AVAudioSessionRouteChangeReasonUnknown = 0,
AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
AVAudioSessionRouteChangeReasonCategoryChange = 3,
AVAudioSessionRouteChangeReasonOverride = 4,
AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8
}

注意:iOS 5下假设使用了AVAudioSession因为AVAudioSessionDelegate中并未定义相关的方法,还是须要用这种方法来实现监听。

iOS
6下直接监听AVAudioSession的通知就能够了。


这里附带两个方法的实现。都是基于AudioSession类的(使用AVAudioSession的同学帮不到你们啦)。

1、推断是否插了耳机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+ (BOOL)usingHeadset
{
#if TARGET_IPHONE_SIMULATOR
return NO;
#endif CFStringRef route;
UInt32 propertySize = sizeof(CFStringRef);
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route); BOOL hasHeadset = NO;
if((route == NULL) || (CFStringGetLength(route) == 0))
{
// Silent Mode
}
else
{
/* Known values of route:
* "Headset"
* "Headphone"
* "Speaker"
* "SpeakerAndMicrophone"
* "HeadphonesAndMicrophone"
* "HeadsetInOut"
* "ReceiverAndMicrophone"
* "Lineout"
*/
NSString* routeStr = (__bridge NSString*)route;
NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"];
NSRange headsetRange = [routeStr rangeOfString : @"Headset"]; if (headphoneRange.location != NSNotFound)
{
hasHeadset = YES;
}
else if(headsetRange.location != NSNotFound)
{
hasHeadset = YES;
}
} if (route)
{
CFRelease(route);
} return hasHeadset;
}

2、推断是否开了Airplay(来自StackOverflow):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (BOOL)isAirplayActived
{
CFDictionaryRef currentRouteDescriptionDictionary = nil;
UInt32 dataSize = sizeof(currentRouteDescriptionDictionary);
AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, &currentRouteDescriptionDictionary); BOOL airplayActived = NO;
if (currentRouteDescriptionDictionary)
{
CFArrayRef outputs = CFDictionaryGetValue(currentRouteDescriptionDictionary, kAudioSession_AudioRouteKey_Outputs);
if(outputs != NULL && CFArrayGetCount(outputs) > 0)
{
CFDictionaryRef currentOutput = CFArrayGetValueAtIndex(outputs, 0);
//Get the output type (will show airplay / hdmi etc
CFStringRef outputType = CFDictionaryGetValue(currentOutput, kAudioSession_AudioRouteKey_Type); airplayActived = (CFStringCompare(outputType, kAudioSessionOutputRoute_AirPlay, 0) == kCFCompareEqualTo);
}
CFRelease(currentRouteDescriptionDictionary);
}
return airplayActived;
}

设置类别

下一步要设置AudioSession的Category。使用AudioSession时调用以下的接口

1
2
3
extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID,
UInt32 inDataSize,
const void *inData);

假设我须要的功能是播放,运行例如以下代码

1
2
3
4
UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,
sizeof(sessionCategory),
&sessionCategory);

使用AVAudioSession时调用以下的接口

1
2
3
4
/* set session category */
- (BOOL)setCategory:(NSString *)category error:(NSError **)outError;
/* set session category with options */
- (BOOL)setCategory:(NSString *)category withOptions: (AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);

至于Category的类型在官方文档中都有介绍。我这里也仅仅罗列一下详细就不赘述了,各位在使用时能够按照自己须要的功能设置Category。

1
2
3
4
5
6
7
8
9
//AudioSession的AudioSessionCategory枚举
enum {
kAudioSessionCategory_AmbientSound = 'ambi',
kAudioSessionCategory_SoloAmbientSound = 'solo',
kAudioSessionCategory_MediaPlayback = 'medi',
kAudioSessionCategory_RecordAudio = 'reca',
kAudioSessionCategory_PlayAndRecord = 'plar',
kAudioSessionCategory_AudioProcessing = 'proc'
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//AudioSession的AudioSessionCategory字符串
/* Use this category for background sounds such as rain, car engine noise, etc.
Mixes with other music. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient; /* Use this category for background sounds. Other music will stop playing. */
AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient; /* Use this category for music tracks.*/
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback; /* Use this category when recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryRecord; /* Use this category when recording and playing back audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord; /* Use this category when using a hardware codec or signal processor while
not playing or recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing;

启用

有了Category就能够启动AudioSession了,启动方法:

1
2
3
4
5
6
7
8
//AudioSession的启动方法
extern OSStatus AudioSessionSetActive(Boolean active);
extern OSStatus AudioSessionSetActiveWithFlags(Boolean active, UInt32 inFlags); //AVAudioSession的启动方法
- (BOOL)setActive:(BOOL)active error:(NSError **)outError;
- (BOOL)setActive:(BOOL)active withFlags:(NSInteger)flags error:(NSError **)outError NS_DEPRECATED_IOS(4_0, 6_0);
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);

启动方法调用后必需要推断是否启动成功。启动不成功的情况常常存在。比如一个前台的app正在播放,你的app正在后台想要启动AudioSession那就会返回失败。

普通情况下我们在启动和停止AudioSession调用第一个方法就能够了。但假设你正在做一个即时语音通讯app的话(类似于微信、易信)就须要注意在deactive AudioSession的时候须要使用第二个方法,inFlags參数传入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivationAVAudioSession给options參数传入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)。当你的app
deactive自己的AudioSession时系统会通知上一个被打断播放app打断结束(就是上面说到的打断回调)。假设你的app在deactive时传入了NotifyOthersOnDeactivation參数。那么其它app在接到打断结束回调时会多得到一个參数kAudioSessionInterruptionType_ShouldResume否则就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume),依据參数的值能够决定是否继续播放。

大概流程是这种:

  1. 一个音乐软件A正在播放。
  2. 用户打开你的软件播放对话语音,AudioSession active。
  3. 音乐软件A音乐被打断并收到InterruptBegin事件。
  4. 对话语音播放结束,AudioSession deactive而且传入NotifyOthersOnDeactivation參数;
  5. 音乐软件A收到InterruptEnd事件。查看Resume參数,假设是ShouldResume控制音频继续播放。假设是ShouldNotResume就维持打断状态;

官方文档中有一张非常形象的图来阐述这个现象:

然而如今某些语音通讯软件和某些音乐软件却无视了NotifyOthersOnDeactivationShouldResume的正确使用方法,导致我们常常接到这种用户反馈:

你们的app在使用xx语音软件听了一段话后就不会继续播放了。但xx音乐软件能够继续播放啊。

好吧,上面仅仅是吐槽一下。请无视我吧。

2014.7.14补充,7.19更新:

发现即使之前已经调用过AudioSessionInitialize方法,在某些情况下被打断之后可能出现AudioSession失效的情况。须要再次调用AudioSessionInitialize方法来又一次生成AudioSession。

否则调用AudioSessionSetActive会返回560557673(其它AudioSession方法也雷同,全部方法调用前必须首先初始化AudioSession),转换成string后为”!ini”即kAudioSessionNotInitialized。这个情况在iOS
5.1.x上比較easy发生,iOS 6.x 和 7.x也偶有发生(详细的原因还不知晓好像和打断时直接调用AudioOutputUnitStop有关,又是个坑啊)。

所以每次在调用AudioSessionSetActive时应该推断一下错误码,假设是上述的错误码须要又一次初始化一下AudioSession。

附上OSStatus转成string的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <Endian.h>

NSString * OSStatusToString(OSStatus status)
{
size_t len = sizeof(UInt32);
long addr = (unsigned long)&status;
char cstring[5]; len = (status >> 24) == 0 ? len - 1 : len;
len = (status >> 16) == 0 ? len - 1 : len;
len = (status >> 8) == 0 ? len - 1 : len;
len = (status >> 0) == 0 ? len - 1 : len; addr += (4 - len); status = EndianU32_NtoB(status); // strings are big endian strncpy(cstring, (char *)addr, len);
cstring[len] = 0; return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding];
}

打断处理

正常启动AudioSession之后就能够播放音频了,以下要讲的是对于打断的处理。之前我们说到打断的回调在iOS 5下须要统一管理。在收到打断開始和结束时须要发送自己定义的通知。

使用AudioSession时打断回调应该首先获取kAudioSessionProperty_InterruptionType。然后发送一个自己定义的通知并带上相应的參数。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState)
{
AudioSessionInterruptionType interruptionType = kAudioSessionInterruptionType_ShouldNotResume;
UInt32 interruptionTypeSize = sizeof(interruptionType);
AudioSessionGetProperty(kAudioSessionProperty_InterruptionType,
&interruptionTypeSize,
&interruptionType); NSDictionary *userInfo = @{MyAudioInterruptionStateKey:@(inInterruptionState),
MyAudioInterruptionTypeKey:@(interruptionType)}; [[NSNotificationCenter defaultCenter] postNotificationName:MyAudioInterruptionNotification object:nil userInfo:userInfo];
}

收到通知后的处理方法例如以下(注意ShouldResume參数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)interruptionNotificationReceived:(NSNotification *)notification
{
UInt32 interruptionState = [notification.userInfo[MyAudioInterruptionStateKey] unsignedIntValue];
AudioSessionInterruptionType interruptionType = [notification.userInfo[MyAudioInterruptionTypeKey] unsignedIntValue];
[self handleAudioSessionInterruptionWithState:interruptionState type:interruptionType];
} - (void)handleAudioSessionInterruptionWithState:(UInt32)interruptionState type:(AudioSessionInterruptionType)interruptionType
{
if (interruptionState == kAudioSessionBeginInterruption)
{
//控制UI。暂停播放
}
else if (interruptionState == kAudioSessionEndInterruption)
{
if (interruptionType == kAudioSessionInterruptionType_ShouldResume)
{
OSStatus status = AudioSessionSetActive(true);
if (status == noErr)
{
//控制UI,继续播放
}
}
}
}

小结

关于AudioSession的话题到此结束(码字果然非常累。

)。小结一下:

  • 假设最低版本号支持iOS 5,能够使用AudioSession也能够考虑使用AVAudioSession。须要有一个类统一管理AudioSession的全部回调,在接到回调后发送相应的自己定义通知;
  • 假设最低版本号支持iOS 6及以上。请使用AVAudioSession,不用统一管理,接AVAudioSession的通知就可以;
  • 依据app的应用场景合理选择Category
  • 在deactive时须要注意app的应用场景来合理的选择是否使用NotifyOthersOnDeactivation參数。
  • 在处理InterruptEnd事件时须要注意ShouldResume的值。


演示样例代码

这里有我自己写的AudioSession的封装,假设各位须要支持iOS
5的话能够使用一下。


下篇预告

下一篇将讲述怎样使用AudioFileStreamer分离音频帧。以及怎样使用AudioQueue进行播放。

下一篇将讲述怎样使用AudioFileStreamer提取音频文件格式信息和分离音频帧。


參考资料

AudioSession

iOS音频播放 (二):AudioSession 转的更多相关文章

  1. iOS音频播放(二):AudioSession

    (本文转自码农人生) 前言 在实施前一篇中所述的7个步骤步之前还必须面对一个麻烦的问题,AudioSession.   AudioSession简介 AudioSession这个玩意的主要功能包括以下 ...

  2. IOS 音频播放

    iOS音频播放 (一):概述 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也因此对于iOS下的音频播放实现有了一定的研究.写这个系列的博客目的一方面希望能够抛砖 ...

  3. iOS音频播放(一):概述

    (本文转自码农人生) 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改,我也因此对于iOS下的音频播放实现有了一定的研究.写这个 系列的博客目的一方面希望能够抛砖引玉 ...

  4. iOS音频播放 (五):AudioQueue

    码农人生 ChengYin's coding life 主页 Blog 分类 Categories 归档 Archives 关于 About Weibo GitHub RSS Where there ...

  5. iOS音频播放、录音、视频播放、拍照、视频录制

    随着移动互联网的发展,如今的手机早已不是打电话.发短信那么简单了,播放音乐.视频.录音.拍照等都是很常用的功能.在iOS中对于多媒体的支持是非常强大的,无论是音视频播放.录制,还是对麦克风.摄像头的操 ...

  6. iOS音频播放概述

    在iOS系统中apple对音频播放需要的操作进行了封装并提供了不同层次的接口 下面对其中的中高层接口进行功能说明: Audio File Services:读写音频数据,可以完成播放流程中的第2步: ...

  7. iOS音频播放 (四):AudioFile 转

    原文出处 : http://msching.github.io/blog/2014/07/19/audio-in-ios-4/ 前言 接着第三篇的AudioStreamFile这一篇要来聊一下Audi ...

  8. iOS音频播放之AudioQueue(一):播放本地音乐

    AudioQueue简单介绍 AudioStreamer说明 AudioQueue具体解释 AudioQueue工作原理 AudioQueue主要接口 AudioQueueNewOutput Audi ...

  9. iOS音频播放 (三):AudioFileStream 转

    原文出处 :http://msching.github.io/blog/2014/07/09/audio-in-ios-3/ 前言 本来说好是要在第三篇中讲AudioFileStream和AudioQ ...

随机推荐

  1. Android(java)学习笔记192:ContentProvider使用之虚拟短信

    1.虚拟短信应用场景:   急着脱身?应付老婆(老公.男女朋友查岗)?   使用虚拟通话短信吧.您只需通过简单设置,软件就会在指定时间会模拟一个“真实”来电或短信来迷惑对方,通过“真实”的证据让对方相 ...

  2. WPS 常用操作

    1.WPS屏保太美了,如何保存 网上搜到如下资料,发现可以在电脑中找到若干个被缓存的图片,kwallpaper可能为kscreensaver

  3. 第一节、ES6的开发环境搭建

    https://blog.csdn.net/zls986992484/article/details/70819462 下面这个不好使 https://blog.csdn.net/gao5311624 ...

  4. 【东软实训】SQLselect及其相关操作

    SQL select 及相关操作 SQL是用于访问和处理数据库的标准的计算机语言,我们所使用的的是Oracle SQL 一个数据库通常包含一个或多个表,每个表有一个名字表示,下图即为一个名为“emp” ...

  5. 16.04 下 ufw 防火墙的的开启、禁用、开放端口、关闭端口

    16.04 下的 ufw 防火墙相关操作使用ufw命令.通过ufw --help可以查看所有相关命令. 打开防火墙 sudo ufw enable 重启防火墙 sudo ufw reload 打开指定 ...

  6. 启动myeclipse弹窗Please allow Subclipse team to receive anonymous usage statistics for this Eclipse intance

    Please allow Subclipse team to receive anonymous usage statistics for this Eclipse intance(翻译:请允许Sub ...

  7. 五、面向切面的spring(1)

    spring的依赖注入看完了,接下来是spring中与DI一并重要的AOP了,开始吧,GO. 在软件开发中,散布于应用中多处的功能被称为横切发关注点,通常来讲,这些横切关注点从概念上市与应用的业务逻辑 ...

  8. x264介绍

    x264 编辑   H.264是ITU(International Telecommunication Unite 国际通信联盟)和MPEG(Motion Picture Experts Group ...

  9. 制作framework&静态库

    http://blog.csdn.net/justinjing0612/article/details/7880712     (制作framework) http://blog.sina.com.c ...

  10. javascript脚本的延时加载

    javascript脚本的延时加载 向HTML页面中插入js代码的主要方法是使用<script>标签,在实际的开发中多采用外部文件的方式,主要考虑到外部js代码的可维护性及可缓存性等优点. ...