前言
本篇为《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
类首先须要调用初始化方法:
| extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioSessionInterruptionListener inInterruptionListener, void *inClientData);
|
前两个參数一般填NULL
表示AudioSession执行在主线程上(但并不代表音频的相关处理执行在主线程上。仅仅是AudioSession),第三个參数须要传入一个一个AudioSessionInterruptionListener
类型的方法,作为AudioSession被打断时的回调。第四个參数则是代表打断回调时须要附带的对象(即回到方法中的inClientData,例如以下所看到的。能够理解为UIView animation中的context)。
| 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事件:
| 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
就能够用来做“拔掉耳机就把歌曲暂停”)。
| //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 };
|
| //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:
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, ¤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
时调用以下的接口
| extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID, UInt32 inDataSize, const void *inData);
|
假设我须要的功能是播放,运行例如以下代码
| UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback; AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, sizeof(sessionCategory), &sessionCategory);
|
使用AVAudioSession
时调用以下的接口
| /* 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。
| //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了。启动方法:
| //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 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
,然后发送一个自己定义的通知并带上相应的參数。
| 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
提取音频文件格式信息和分离音频帧。