1. 检测声音输入设备
- (BOOL)hasMicphone {
return[[AVAudioSession sharedInstance] inputIsAvailable];
}
2. 检测声音输出设备
对于输出设备的检测,我们只考虑了2个情况,一种是设备自身的外放(iTouch/iPad/iPhone都有),一种是当前是否插入了带外放的耳机。iOS已经提供了相关方法用于获取当前的所有声音设备,我们只需要检查在这些设备中是否存在我们所关注的那几个就可以了。
获取当前所有声音设备:
CFStringRef route;
UInt32 propertySize = sizeof(CFStringRef);
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute,&propertySize, &route);
在iOS上所有可能的声音设备包括:
每一项的具体代表的设备请查考iOS文档,此处我们关注的是是否有耳机,所以只需要检查在route中是否有Headphone或Headset存在,具体方法如下:
- (BOOL)hasHeadset {
#ifTARGET_IPHONE_SIMULATOR
#warning *** Simulator mode: audio session code works only on adevice
return NO;
#else
CFStringRefroute;
UInt32propertySize = sizeof(CFStringRef);
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute,&propertySize, &route);
if((route ==NULL) || (CFStringGetLength(route) == 0)){
// Silent Mode
NSLog(@”AudioRoute: SILENT, do nothing!”);
} else{
NSString* routeStr = (NSString*)route;
NSLog(@”AudioRoute: %@”, routeStr);
NSRange headphoneRange = [routeStr rangeOfString :@"Headphone"];
NSRange headsetRange = [routeStr rangeOfString : @"Headset"];
if (headphoneRange.location != NSNotFound) {
return YES;
} else if(headsetRange.location != NSNotFound) {
return YES;
}
}
returnNO;
#endif }
请注意,由于获取AudioRoute的相关方法不能再simulator上运行(会直接crush),所以必须先行处理。
3. 设置声音输出设备
在我们的项目中,存在当正在播放时用户会插入或拔出耳机的情况。如果是播放时用户插入了耳机,苹果会自动将声音输出指向到耳机并自动将音量调整为合适大小;如果是在用耳机的播放过程中用户拔出了耳机,声音会自动从设备自身的外放里面播出,但是其音量并不会自动调大。
经过我们的测试,我们发现当播放时拔出耳机会有两个问题(也许对你来说不是问题,但是会影响我们的app):
- 音乐播放自动停止
- 声音音量大小不会自动变大,系统仍然以较小的声音(在耳机上合适的声音)来进行外放
对于第一个问题,实际上就是需要能够检测到耳机拔出的事件;而第二个问题则是需要当耳机拔出时强制设置系统输出设备修改为系统外放。
强制修改系统声音输出设备:
- (void)resetOutputTarget {
BOOLhasHeadset = [self hasHeadset];
NSLog(@”Will Set output target is_headset = %@ .”, hasHeadset ? @”YES” :@”NO”);
UInt32audioRouteOverride = hasHeadset ?
kAudioSessionOverrideAud ioRoute_None:kAudioSessionOverrideAud ioRoute_Speaker;
AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute,sizeof(audioRouteOverride),&audioRouteOverride);
}
可以看到我们修改了AudioSession的属性“kAudioSessionProperty_OverrideAudioRoute”,该属性在iOS文档上的解释如下:
kAudioSessionProperty_OverrideAudioRoute
Specifieswhether or not to override the audio session category’s normalaudio route. Can be set with one of twovalues:
kAudioSessionOverrideAud
,which specifies that you want to use the normal audio route;andioRoute_None kAudioSessionOverrideAud
,when sends output audio to the speaker. Awrite-onlyioRoute_Speaker UInt32
value.
Upon an audio route change (such as by plugging in or unplugging aheadset), or upon interruption, this property reverts to itsdefault value. This property can be used only withthekAudioSessionCategory_PlayAndRecord
(orthe equivalent AVAudioSessionCategoryRe
)category.cord
可以看到,该属性只有当category为kAudioSessionCategory_PlayAndRecord或者AVAudioSessionCategoryRe
4. 设置Audio工作模式(category,我当做工作模式理解的)
iOS系统中Audio支持多种工作模式(category),要实现某个功能,必须首先将AudioSession设置到支持该功能的工作模式下。所有支持的工作模式如下:
Audio Session Categories
Category identifiers for audio sessions, used as values forthe
setCategory:error:
method. NSString *const AVAudioSessionCategoryAmbient; NSString *const AVAudioSessionCategorySo loAmbient; NSString *const AVAudioSessionCategoryPl ayback; NSString *const AVAudioSessionCategoryRe cord; NSString *const AVAudioSessionCategoryPl ayAndRecord; NSString *const AVAudioSessionCategoryAu dioProcessing;
具体每一个category的功能请参考iOS文档,其中AVAudioSessionCategoryRe
设置category:
- (BOOL)checkAndPrepareCategoryF
orRecording {
recording =YES;
BOOLhasMicphone = [self hasMicphone];
NSLog(@”WillSet category for recording! hasMicophone = %@”,hasMicphone?@”YES”:@”NO”);
if(hasMicphone) {
[[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPl ayAndRecord
error:nil];
}
[selfresetOutputTarget];
returnhasMicphone;
}- (void)resetCategory {
if(!recording) {
NSLog(@”Will Set category to static value =AVAudioSessionCategoryPl ayback!”);
[[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPl ayback
error:nil];
}
}
5. 检测耳机插入/拔出事件
耳机插入拔出事件是通过监听AudioSession的RouteChange事件然后判断耳机状态实现的。实现步骤分为两步,首先注册监听函数,然后再监听函数中判断耳机状态。
注册监听函数:
AudioSessionAddPropertyL
istener(kAudioSessionProperty_AudioRouteChange,
audioRouteChangeListener Callback,
self);
我们的需求是当耳机被插入或拔出时做出响应,而产生AouteChange事件的原因有多种,所以需要对各种类型进行处理并结合当前耳机状态进行判断。在iOS文档中,产生AouteChange事件的原因有如下几种:
Audio Session Route Change Reasons
Identifiers for the various reasons that an audio route can changewhile your iOS application is running.
enum { kAudioSessionRouteChangeReason_Unknown = 0, kAudioSessionRouteChange Reason_NewDeviceAvailable = 1, kAudioSessionRouteChange Reason_OldDeviceUnavailable = 2, kAudioSessionRouteChange Reason_CategoryChange = 3, kAudioSessionRouteChange Reason_Override = 4, // this enum has no constant with a value of 5 kAudioSessionRouteChange Reason_WakeFromSleep = 6, kAudioSessionRouteChange Reason_NoSuitableRouteForCatego ry = 7 };
具体每个类型的含义请查阅iOS文档,其中我们关注的是kAudioSessionRouteChange
当有新设备接入时,如果检测到耳机,则判定为耳机插入事件;当原有设备移除时,如果无法检测到耳机,则判定为耳机拔出事件;当出现“当前工作模式缺少合适设备时”,直接判定为录音时拔出了麦克风。
很明显,这个判定逻辑实际上不准确,比如原来就有耳机但是插入了一个新的audio设备或者是原来就没有耳机但是拔出了一个原有的audio设备,我们的判定都会出错。但是对于我们的项目来说,其实关注的不是耳机是拔出还是插入,真正关注的是有audio设备插入/拔出时能够根据当前耳机/麦克风状态去调整设置,所以这个判定实现对我们来说是正确的。
监听函数的实现:
void audioRouteChangeListener
Callback (
void *inUserData,
AudioSessionPropertyID inPropertyID,
UInt32 inPropertyValueSize,
constvoid *inPropertyValue
) {
if(inPropertyID != kAudioSessionProperty_AudioRouteChange)return;
//Determines the reason for the route change, to ensure that it isnot
// because of a category change.
CFDictionaryRef routeChangeDictionary = inPropertyValue;
CFNumberRefrouteChangeReasonRef =
CFDictionaryGetValue (routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_Reason));
SInt32routeChangeReason;
CFNumberGetValue (routeChangeReasonRef, kCFNumberSInt32Type,&routeChangeReason);
NSLog(@”======================= RouteChangeReason : %d”,routeChangeReason);
AudioHelper*_self = (AudioHelper *) inUserData;
if(routeChangeReason ==kAudioSessionRouteChange Reason_OldDeviceUnavailable) {
[_self resetSettings];
if (![_self hasHeadset]) {
[[NSNotificationCenter defaultCenter]postNotificationName:@”ununpluggingHeadse
object:nil];
}
} else if(routeChangeReason ==kAudioSessionRouteChange Reason_NewDeviceAvailable) {
[_self resetSettings];
if (![_self hasMicphone]) {
[[NSNotificationCenter defaultCenter]postNotificationName:@”pluggInMicrophone”
object:nil];
}
} else if(routeChangeReason ==kAudioSessionRouteChange Reason_NoSuitableRouteForCatego ry) {
[_self resetSettings];
[[NSNotificationCenter defaultCenter]postNotificationName:@”lostMicroPhone”
object:nil];
}
//else if(routeChangeReason ==kAudioSessionRouteChange Reason_CategoryChange ){
// [[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPl ayAndRecorderror:nil];
//}
[_selfprintCurrentCategory];
}
当检测到相关事件后,通过NSNotificationCenter通知observers耳机(有无麦克风)拔出/插入事件拔出事件,从而触发相关操作。
6. 事件处理
对于耳机(有无麦克风)拔出/插入事件,一般需要做如下处理:
- 强制重设系统声音输出设备(防止系统以较小声音在外放中播放)
- 如果拔出前正在播放,则启动已经暂停的播放(当耳机拔出时,系统会自动暂停播放)
- 当拔出前正在录音,则需要检查麦克风情况并决定是否停止录音(如果录音时从iTouch/iPad等设备上拔出了带麦克风的耳机)