前言:
在开发iOS ASR 语音转文字SDK中遇到一系列问题,途中尝试解决方案及技术要点进行记录和学习积累 AVAudioSession 相关文章可参考
一、基础:
基础实现部分不再详细堆叠(网上文章较多),以下是主要技术要点和知识点
- websocket(如果需要通过后台网络进行TTS相关语音转换)
- 语音录制相关基础知识 采样率、通道(声到)、声音位数(采样精度)、编码格式(wav,mp3等)
- 录音基础设置相关 AVAudioSession, 系统声音相关设置,包括硬件(话筒、耳机)之间的切换和优化
- 录音设备单元相关 AudioComponent,AudioUnit等相关输入输出设置
- 无限录制转换注意对内存进行控制
- 容易引发崩溃的点
二、出现的问题和对应分析解决:
- 
如果APP中集成了其他语音类SDK,在使用的时候会影响我方SDK,主要影响点: - 
AVAudioSession 相关设置,这个是全局设置。设置后也可能会影响app内其他语音类SDK(要想简单完美解决这种最好的方式当然是整个语音模块都自己实现,不过复杂平台app显然不可能) objectivec[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption error:&error];上面代码设置后,在使用activate 就会全局改变AVAudioSession的设置。可能对app内其他使用同样AVAudioSession的SDK造成影响。这里只记录其他语音SDK设置这个对我方造成的影响 1.情景一:前面有个语音SDK执行语音播报,播报完成后立马呼起我方语音SDK。此时无论对方AVAudioSession,只要调起我方SDK,我们直接按上面重新设置session。此时如果前面没有戴耳机是通过设备mic呼出,这时候启动我方语音SDK效果正常。这时对方一定也是设置了 AVAudioSessionCategoryOptionDefaultToSpeaker,我们这个也是defaultToSpeaker,系统没有发生设备切换。但是如果此时呼出后我们戴的是耳机就会出现另一种状况,会发现我方SDK启动较慢,而且1秒多后才能正常录音,这个原因就是我们这里发生了设备切换。而且代码中监控设备切换做了重新停止和再开启的原因。 2.情景二: 当前有个语音SDK正在进行长播报,此时我方要启动我方语音SDK。产品要求,不能影响当前播报,我方语音可以正常呼起对话框,然后正常说话讲语音转为文字。这个情况再调我方语音SDK上面同样操作也会出现问题,开启后因为重新设置和activate 会直接中断播报。 以上2种情况可以合并解决:1、首先判断AVAudioSession是否是激活状态。(并不能直接调用isActive,实际项目中根本没有这个方法)或者使用以下判断。 objectivec// 初始化时注册通知 - (void)setupAudioSessionObserver { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioInterruption:) name:AVAudioSessionInterruptionNotification object:nil]; } // 记录当前激活状态的变量 @property (nonatomic, assign) BOOL isAudioSessionActive; // 通知回调:处理激活/中断事件 - (void)handleAudioInterruption:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; AVAudioSessionInterruptionType type = [userInfo[AVAudioSessionInterruptionTypeKey] integerValue]; if (type == AVAudioSessionInterruptionTypeBegan) { // 会话被中断(变为未激活) self.isAudioSessionActive = NO; } else if (type == AVAudioSessionInterruptionTypeEnded) { // 中断结束(可能恢复激活) AVAudioSessionInterruptionOptions options = [userInfo[AVAudioSessionInterruptionOptionKey] integerValue]; if (options & AVAudioSessionInterruptionOptionShouldResume) { // 允许恢复激活 self.isAudioSessionActive = YES; } } }2、存储当前 session 的category 和 options。根据已有的的category,option 添加设置自己需要的 category,option(注意不要改原始的,自己重新定义一个,这个无论active和不是active 都会生效),如果要去不打断播报就设置mode为AVAudioSessionModeVoiceChat,同时注意 options 中要有mix。3、这里很重要,如果是active 就不要再设置 active 为YES。如果这样会直接中断当前其他语音SDK。 
- 
setMode 这个方法如果设置,会影响其他SDK,categoryOptions 会随之更改。实测如果设置 mode = VoiceChat/Measurement,categoryOptions 会变成1 。 解决这个问题就是结束自己语音的时候,把开始存储的对应session category和categoryOptions 重新设置为原始值以防止影响其他语音SDK。 
 
- 
- 
容易引起崩溃的点: - 快速点击/连续启动引起的崩溃:CMBAudioUnitRecorder *recorder = (__bridge CMBAudioUnitRecorder *)(inRefCon); 类似录音单元这里 inRefCon 有可能是空指针引起崩溃,特别是如果没有控制用户行为连续快速点击启动的时候
 解决这个问题的方法:在你开始设置callback时进行强引用,然后在结束的时间进行释放 开始时AURenderCallbackStruct inputCallBackStruce; inputCallBackStruce.inputProc = inputCallBackFun; self.inputProcRefCon = (__bridge_retained void *)self; inputCallBackStruce.inputProcRefCon = self.inputProcRefCon;结束时// 释放 retained 的 self if (self.inputProcRefCon) { CFRelease(self.inputProcRefCon); self.inputProcRefCon = NULL; }- 无网飞行模式下引起的崩溃:这个主要原因和上面 inRefCon 空指针类似。AudioUnit(特别是 RemoteIO)的输入回调是系统底层音频线程(AURemoteIO::IOThread)触发的。即使你在主线程调用[recorder stopRecord]或释放对象,只要没有正确 停止 AudioUnit 并移除回调 ,系统仍然会在底层线程调用inputCallBackFun(), 这时inRefCon就成了一个悬空指针(dangling pointer) ,转成(__bridge CMBAudioUnitRecorder *)时自然就是nil或无效内存。
 解决方案:结束记得回收资源 
            
            
              结束释放
              
              
            
          
                      CheckError(AudioOutputUnitStop(self->audioUnit),"AudioOutputUnitStop failed");
        AURenderCallbackStruct emptyCallback = {0};
        AudioUnitSetProperty(self->audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, kInputBus, &emptyCallback, sizeof(emptyCallback));
        // 释放 retained 的 self
        if (self.inputProcRefCon) {
            CFRelease(self.inputProcRefCon);
            self.inputProcRefCon = NULL;
        }
        
        self->_isRecording = NO;
        AudioUnitUninitialize(self->audioUnit);
        AudioComponentInstanceDispose(self->audioUnit);
        self->audioUnit = NULL;- 无限录制时造成内存泄露:inputCallBackFun设置回调方法时,会有持续数据流进入,这时要注意对内存进行管理

三、优化相关:
- 加快整个SDK启动速度及效率:
- 使用多线程,使用队列,单独维持一条线程进行语音SDK的整个录音启动流程
- 
session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil\];AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation opt 可以告诉系统不用等前面的 session 状态马上启动自己的session 
- 监控设备之间的切换,根据不同状态进行SDK重启相关操作
 
            
            
              注册通知
              
              
            
          
                  //注册通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:nil];
            
            
              相关方法
              
              
            
          
          - (void)handleInterruption:(NSNotification *)notification {
    AVAudioSessionInterruptionType type = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [self stop];
    }else if (type == AVAudioSessionInterruptionTypeEnded) {
        [self start];
    }
}- 注意整个工程的内存管理,特别是自己管理内存的相关地方(例如自己实现的 C/C++方法相关,及其他CF需要内存管理的地方)