iOS音频工具箱

作者: zaijianbali | 来源:发表于2017-05-17 14:52 被阅读309次

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

欢迎围观

本文解释权归:子文

转载请注明出处,谢谢

来杯可乐催更吧

请子文喝可乐

相关文章

网友评论

    本文标题:iOS音频工具箱

    本文链接:https://www.haomeiwen.com/subject/atudxxtx.html