iOS语音转换SDK相关记录

前言:

在开发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需要内存管理的地方)
相关推荐
往来凡尘1 天前
Flutter运行iOS26真机的两个问题
flutter·ios
普通网友1 天前
Objective-C 类的方法重载与重写:区别与正确使用场景
开发语言·ios·objective-c
denggun123451 天前
卡顿监测原理
macos·ios·xcode
@大迁世界1 天前
iOS 26.2 引入三种全新 iPhone 自定义方式
ios·iphone
Sheffi661 天前
iOS 触摸事件完整传递链路:Hit-Test 全流程深度解析
macos·ios·cocoa
Swift社区1 天前
用 Task Local Values 构建 Swift 里的依赖容器:一种更轻量的依赖注入思路
开发语言·ios·swift
2501_915909061 天前
苹果应用加密方案的一种方法,在没有源码的前提下,如何处理 IPA 的安全问题
android·安全·ios·小程序·uni-app·iphone·webview
TouchWorld1 天前
iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景
ios·swift
2501_915909061 天前
iOS 项目中常被忽略的 Bundle ID 管理问题
android·ios·小程序·https·uni-app·iphone·webview