iOS|iOS 设备 检测声音输出设备及耳机麦克风的处理

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 ?
kAudioSessionOverrideAudioRoute_None:kAudioSessionOverrideAudioRoute_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: kAudioSessionOverrideAudioRoute_None,which specifies that you want to use the normal audio route; andkAudioSessionOverrideAudioRoute_Speaker,when sends output audio to the speaker. Awrite-only 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 withthe kAudioSessionCategory_PlayAndRecord (orthe equivalentAVAudioSessionCategoryRecord)category.

可以看到,该属性只有当category为kAudioSessionCategory_PlayAndRecord或者AVAudioSessionCategoryRecord时才能使用。所以我们还需要能够设置AudioSession的category。
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 AVAudioSessionCategorySoloAmbient; NSString *const AVAudioSessionCategoryPlayback; NSString *const AVAudioSessionCategoryRecord; NSString *const AVAudioSessionCategoryPlayAndRecord; NSString *const AVAudioSessionCategoryAudioProcessing;


具体每一个category的功能请参考iOS文档,其中AVAudioSessionCategoryRecord为独立录音模式,而AVAudioSessionCategoryPlayAndRecord为支持录音盒播放的模式,而AVAudioSessionCategoryPlayback为普通播放模式。
设置category:
- (BOOL)checkAndPrepareCategoryForRecording {
recording =YES;
BOOLhasMicphone = [self hasMicphone];
NSLog(@”WillSet category for recording! hasMicophone = %@”,hasMicphone?@”YES”:@”NO”);
if(hasMicphone) {
[[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPlayAndRecord
error:nil];
}
[selfresetOutputTarget];
returnhasMicphone;
}
- (void)resetCategory {
if(!recording) {
NSLog(@”Will Set category to static value =https://www.it610.com/article/AVAudioSessionCategoryPlayback!”);
[[AVAudioSession sharedInstance]setCategory:AVAudioSessionCategoryPlayback
error:nil];
}
}
5. 检测耳机插入/拔出事件
耳机插入拔出事件是通过监听AudioSession的RouteChange事件然后判断耳机状态实现的。实现步骤分为两步,首先注册监听函数,然后再监听函数中判断耳机状态。
注册监听函数:
AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange,
audioRouteChangeListenerCallback,
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, kAudioSessionRouteChangeReason_NewDeviceAvailable= 1, kAudioSessionRouteChangeReason_OldDeviceUnavailable= 2, kAudioSessionRouteChangeReason_CategoryChange= 3, kAudioSessionRouteChangeReason_Override= 4, // this enum has no constant with a value of 5 kAudioSessionRouteChangeReason_WakeFromSleep= 6, kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7 };

具体每个类型的含义请查阅iOS文档,其中我们关注的是kAudioSessionRouteChangeReason_NewDeviceAvailable有新设备插入、kAudioSessionRouteChangeReason_OldDeviceUnavailable原有设备被拔出以及kAudioSessionRouteChangeReason_NoSuitableRouteForCategory当前工作模式缺少合适设备。
当有新设备接入时,如果检测到耳机,则判定为耳机插入事件;当原有设备移除时,如果无法检测到耳机,则判定为耳机拔出事件;当出现“当前工作模式缺少合适设备时”,直接判定为录音时拔出了麦克风。
很明显,这个判定逻辑实际上不准确,比如原来就有耳机但是插入了一个新的audio设备或者是原来就没有耳机但是拔出了一个原有的audio设备,我们的判定都会出错。但是对于我们的项目来说,其实关注的不是耳机是拔出还是插入,真正关注的是有audio设备插入/拔出时能够根据当前耳机/麦克风状态去调整设置,所以这个判定实现对我们来说是正确的。
监听函数的实现:
【iOS|iOS 设备 检测声音输出设备及耳机麦克风的处理】 void audioRouteChangeListenerCallback (
void*inUserData,
AudioSessionPropertyIDinPropertyID,
UInt32inPropertyValueSize,
constvoid*inPropertyValue
) {

    推荐阅读