您的位置:首页 > 移动开发 > IOS开发

iOS音频播放 (二):AudioSession 转

2017-05-14 15:08 281 查看
原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/


前言

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

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


AudioSession简单介绍

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

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



AudioSession

AudioSession相关的类有两个:

AudioToolBox
中的
AudioSession

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
45
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
45
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
45
6
7
8
9
10
1112

//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
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
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
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122

+ (BOOL)isAirplayActived
{
CFDictionaryRef currentRouteDescriptionDictionary = nil;
UInt32 dataSize = sizeof(currentRouteDescriptionDictionary);
AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, ¤tRouteDescriptionDictionary);

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
时调用以下的接口

12
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
45
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
45
6
7
8
9
10
1112
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
45
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_NotifyOthersOnDeactivation
AVAudioSession
给options參数传入
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
)。当你的app
deactive自己的AudioSession时系统会通知上一个被打断播放app打断结束(就是上面说到的打断回调)。假设你的app在deactive时传入了NotifyOthersOnDeactivation參数。那么其它app在接到打断结束回调时会多得到一个參数
kAudioSessionInterruptionType_ShouldResume
否则就是ShouldNotResume(
AVAudioSessionInterruptionOptionShouldResume
),依据參数的值能够决定是否继续播放。

大概流程是这种:

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

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





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

你们的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
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122

#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
45
6
7
8
9
10
1112
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
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: