iOS音频工具箱
我们都知道,微信语音播放下可以切换听筒和扬声器,那么它是怎么实现的呢?
微信语音播放之切换听筒和扬声器的方法解决方案
其中还包括使用近距离传感器(主要针对贴近耳部),提供耳机检测功能。
但是微信是一个业务级的东西,如何脱离做到业务,只提供工具类,到哪里都可以使用的一个工具类。
本文是基于IM聊天功能块,拆分出来的的工具类。
包括2个类,录音和播放
播放
播放要解决的如下问题:
- 1.当前正在播放的消息,播放状态的维护,播放动画的开启
- 2.播放完成消息的状态回归到正常,播放动画停止
- 3.停止播放时播放状态回归正常,播放动画停止
- 4.播放过程中切换不同的语音消息,播放状态改变,上一个回归正常态,播放动画停止;新的播放开始,播放动画开启,状态改为播放态。
- 5.自动播放下一条语音方案。
我们先从播放开始介绍:
接口设计
具体设计如下:
////////////////////////////////////////////////////////////////
//
// 播放语音
//
////////////////////////////////////////////////////////////////
typedef void(^EndStop)(const char *);
@interface ZWAudioPlayerTool : NSObject
@property (nonatomic, copy) EndStop endStop;
/**
* @brief 播放语音消息
*
* @param filePath 语音路径
* @return 播放成功还是失败
*/
- (BOOL)play:(const char *)filePath;
/**
* @brief 停止播放语音消息
*/
- (void)stop;
/**
* @brief 注册近距离传感器,耳机检测
*/
- (void)registPlayer;
@end
既然是工具方法,那么就需要不依赖与业务。
首先play传入的是音频文件的绝对路径。
按照微信的消息系统,别人给你发了音频消息,那么需要先下载到本地,这点需要单独写一个业务下载的工具。然后把下载好的音频和音频消息关联起来(通过消息可以映射出音频文件,一一对应)
其次,播放的话可能存在无法初始化,传入路径错误等,所以返回bool值。
停止播放的话,是停止正在播放的,如果直接调用stop没有意义。
registPlayer的注册,后面单独讲到,这里就是注册近距离传感器,耳机检测
我们都知道,微信的语音消息可以连续播放。但工具类不可能知道业务如何处理,所以播放完成了,回调一个播放结束的回调。然后业务层继续播放下一个就可以了。这就形成了一个循环。
再者:如果当前正在播放一个,你突然切换了一个,那么之前消息的状态为正在播放,那么需要修改为正常状态。
以下回调就是处理这个问题的。
typedef void(^EndStop)(const char *)
还有就是这个endStop是block,有的人喜欢用通知,那么又增加了一个通知的机制。
/**
* @brief 开始一次语音播放的通知
*/
extern NSString *const ZWAudioStartPlay;
/**
* @brief 结束一次语音播放的通知
*/
extern NSString *const ZWAudioEndPlay;
通过上面的接口已经完成了播放语音接口说明,
由于笔者没有做音乐播放类的pause和resume的点,所以这里没有加入这两个接口。但加入也不难,就是调用系统api的问题。
近距离传感器
近距离传感器问题,其实很简单,只需要注册一下,并开启就可以了。
代码如下
//注册使用近距离传感器,红外感应
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(sensorStateChange:)
name:UIDeviceProximityStateDidChangeNotification
object:nil];
记住一定要调用如下代码,先开启红外感应。近距离传感器
//建议在播放之前设置yes,播放结束设置NO,这个功能是开启红外感应
[[UIDevice currentDevice] setProximityMonitoringEnabled:YES];
实现方法如下:
//如果此时手机靠近面部放在耳朵旁,那么声音将通过听筒输出,并将屏幕变暗
if ([UIDevice currentDevice].isProximityMonitoringEnabled)
{
if ([[UIDevice currentDevice] proximityState] == YES)
{
isSpeakLoudly = NO;
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
// NSLog(@"isSpeakLoudly = no,贴近面部,屏幕变暗");
}
else
{
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
// NSLog(@"isSpeakLoudly = yes,正常状态下,扬声器模式");
if (!isSpeakLoudly)
{
isSpeakLoudly = YES;
[[NSNotificationCenter defaultCenter] postNotificationName:ZWSpeakLoudly object:nil];
}
}
监听输出设备变化(耳机插拔,蓝牙设备连接和断开等)
由于市面上的iOS设备低于6.0的版本基本已经没有了,或者说我们的应用很快已经不支持iOS7 了都,所以,我们不在考虑之前的api了。
iOS 6以后的api如下,
也是注册通知
//监听输出设备变化(耳机插拔,蓝牙设备连接和断开等)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(sessionRouteChange:)
name:AVAudioSessionRouteChangeNotification
object:nil];
- (void)sessionRouteChange:(NSNotification *)notification {
NSTimeInterval currentTime = self.player.currentTime;
AVAudioSessionRouteChangeReason routeChangeReason = [notification.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
if (AVAudioSessionRouteChangeReasonNewDeviceAvailable == routeChangeReason) {//新设备插入
if ([self isNotUseBuiltInPort]) {
if (currentTime > 0.35) {
currentTime = currentTime - 0.35; //插入耳机需要时间,切换默认减去0.35s,继续播放
} else{
currentTime = 0;
}
if (self.player && [self.player isPlaying]) {
self.player.currentTime = currentTime;
}
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
}
}else if (AVAudioSessionRouteChangeReasonOldDeviceUnavailable == routeChangeReason) { //新设备拔出
if (![self isNotUseBuiltInPort]) {
//拔出耳机切换默认减去1s,继续播放
if (currentTime > 1.0) {
currentTime = currentTime - 1.0;
}else {
currentTime = 0;
}
if (self.player && self.player.isPlaying){
self.player.currentTime = currentTime;
}
[self sensorStateChange:nil];
}
} else {
// NSLog(@"没有设备音频变化");
}
}
电话等打断
由于电话是手机的必备功能,如果接听电话过程中,我们的身影还在
//电话打断
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(sessionInterruption:)
name:AVAudioSessionInterruptionNotification
object:[AVAudioSession sharedInstance]];
//电话等中断程序
- (void)sessionInterruption:(NSNotification *)notification {
AVAudioSessionInterruptionType interruptionType = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (AVAudioSessionInterruptionTypeBegan == interruptionType) {
// NSLog(@"begin interruption");
//直接停止播放
[self stop];
} else if (AVAudioSessionInterruptionTypeEnded == interruptionType) {
// NSLog(@"end interruption");
}
}
检测耳机设备
//设备是否存在,耳麦,耳机等
- (BOOL)isHeadsetPluggedIn
{
AVAudioSessionRouteDescription *route = [[AVAudioSession sharedInstance] currentRoute];
for (AVAudioSessionPortDescription *desc in [route outputs]) {
if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones]) {
return YES;
}
else if([[desc portType] isEqualToString:AVAudioSessionPortHeadsetMic]) {
return YES;
}else {
continue;
}
}
return NO;
}
//检测是否有耳机,只需在route中是否有Headphone或Headset存在
- (BOOL)hasHeadset {
#if TARGET_IPHONE_SIMULATOR
// #warning *** Simulator mode: audio session code works only on a device
return NO;
#else
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
return [self isHeadsetPluggedIn];
#else
CFStringRef route;
UInt32 propertySize = sizeof(CFStringRef);
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route);
if((route == NULL) || (CFStringGetLength(route) == 0)){
// Silent Mode
// NSLog(@"AudioRoute: SILENT, do nothing!");
} else {
NSString* routeStr = (__bridge 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;
}
}
return NO;
#endif
#endif
}
//拔出耳机,强制修改系统声音输出设备:
- (void)resetOutputTarget {
BOOL hasHeadset = [self hasHeadset];
NSLog(@"Will Set output target is_headset = %@ .", hasHeadset ? @"YES" : @"NO");
//None:听筒,耳机 Speaker:扬声器
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
if (hasHeadset) {
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
NSLog(@"isSpeakLoudly = no,贴近面部,屏幕变暗");
}else {
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
}
#else
UInt32 audioRouteOverride = hasHeadset ?kAudioSessionOverrideAudioRoute_None:kAudioSessionOverrideAudioRoute_Speaker;
AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
#endif
}
- (BOOL)isNotUseBuiltInPort{
NSArray *outputs = [[AVAudioSession sharedInstance] currentRoute].outputs;
if (outputs.count <= 0) {
return NO;
}
AVAudioSessionPortDescription *port = (AVAudioSessionPortDescription*)outputs[0];
return ![port.portType isEqualToString:AVAudioSessionPortBuiltInReceiver]&&![port.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker];
}
至于play和stop都干了啥,直接看github的代码吧。这里不做解释了。
录音
首先要看,需要解决的问题,
- 1.开始录音,结束录音,取消录音
- 2.录音过程中,需要把音量返回,用于显示音量小喇叭
- 3.录音结束返回的是录音时长还有录的内容。
- 4.取消录音的话,需要把已经录的源文件删除。
其次我们看接口设计
////////////////////////////////////////////////////
//
// 语音工具类 //
//
////////////////////////////////////////////////////
@class ZWAudioRecorderTool;
@protocol ZWAudioRecorderToolProtocool <NSObject>
/**
* @brief 当音量发生变化时, 会收到回调
*
* @param helper 语音消息实例
* @param volume 变化后的音量
* @param duration 录音时长
*/
- (void)audioTool:(ZWAudioRecorderTool *)helper volumeChanged:(float)volume duration:(float)duration;
@end
typedef struct ZWRecordInfo_t
{
const char *filePath;// urf 8
float duration;
} ZWRecordInfo;
@interface ZWAudioRecorderTool : NSObject
@property (nonatomic, weak)id <ZWAudioRecorderToolProtocool> delegate;
@property (nonatomic, assign, readonly)float duration;
/**
* @brief 用户是否开启录音权限
*
* @return 是否开启录音,yes:开启,no:禁用,仅适用与iOS7及以后,iOS 7之前默认开启
*/
+ (BOOL)canRecord;
/**
* @brief 开始录制
* @return 录制成功还是失败
*/
- (BOOL)startRecord;
/**
* @brief 停止录制
*
* @return 录制完成后得到的语音消息
*/
- (ZWRecordInfo)stopRecord;
/**
* @brief 取消录制
*/
- (void)cancelRecord;
@end
关注下stopRecord 返回的是一个结构体,包括录制时长和录制的源文件地址。
当然目前写死的是m4a,如果对格式有要求,可以进行转码,或者把录制部分直接边录制边转码都可以。
其中的delegate用于实现音量和录制时长的更新的。
由于水平有限,同时使用场景限制,目前只适合类似聊天流的项目中,对于音乐播放器的不适合。
git hub地址:
https://github.com/ziwen/ZWAudioTools
欢迎围观
本文解释权归:子文
转载请注明出处,谢谢
网友评论