在 AVSpeechUtterance 之后使用 SFSpeechRecognizer 时出现 AVAudioSession 问题

Posted

技术标签:

【中文标题】在 AVSpeechUtterance 之后使用 SFSpeechRecognizer 时出现 AVAudioSession 问题【英文标题】:AVAudioSession issue when using SFSpeechRecognizer after AVSpeechUtterance 【发布时间】:2017-04-17 18:19:25 【问题描述】:

在通过 AVSpeechUtterance 向用户说出欢迎消息后,我正在尝试使用 SFSpeechRecognizer 进行语音转文本。但是随机的,语音识别没有启动(在说出欢迎信息之后),它会抛出下面的错误信息。

[avas] 错误:AVAudiosession.mm:1049:-[AVAudioSession setActive:withOptions:error:]:停用正在运行 I/O 的音频会话。在停用音频会话之前,应停止或暂停所有 I/O。

它工作了几次。我不清楚为什么它不能始终如一地工作。

我尝试了其他 SO 帖子中提到的解决方案,其中提到检查是否有音频播放器正在运行。我将语音检查添加到代码的文本部分。它返回 false(即没有其他音频播放器正在运行)但是文本语音仍然没有开始收听用户语音。你能指导我怎么回事吗?

正在运行 iOS 10.3 的 iPhone 6 上进行测试

下面是使用的sn-ps代码:

TextToSpeech

- (void) speak:(NSString *) textToSpeak 
    [[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
      withOptions:AVAudioSessionCategoryOptionDuckOthers error:nil];

    [synthesizer stopSpeakingAtBoundary:AVSpeechBoundaryImmediate];

    AVSpeechUtterance* utterance = [[AVSpeechUtterance new] initWithString:textToSpeak];
    utterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:locale];
    utterance.rate = (AVSpeechUtteranceMinimumSpeechRate * 1.5 + AVSpeechUtteranceDefaultSpeechRate) / 2.5 * rate * rate;
    utterance.pitchMultiplier = 1.2;
    [synthesizer speakUtterance:utterance];


- (void)speechSynthesizer:(AVSpeechSynthesizer*)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance*)utterance 
    //Return success message back to caller

    [[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient
      withOptions: 0 error: nil];
    [[AVAudioSession sharedInstance] setActive:YES withOptions: 0 error:nil];

语音转文字

- (void) recordUserSpeech:(NSString *) lang 
    NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:lang];
    self.sfSpeechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale];
    [self.sfSpeechRecognizer setDelegate:self];

    NSLog(@"Step1: ");
    // Cancel the previous task if it's running.
    if ( self.recognitionTask ) 
        NSLog(@"Step2: ");
        [self.recognitionTask cancel];
        self.recognitionTask = nil;
    

    NSLog(@"Step3: ");
    [self initAudioSession];

    self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
    NSLog(@"Step4: ");

    if (!self.audioEngine.inputNode) 
        NSLog(@"Audio engine has no input node");
    

    if (!self.recognitionRequest) 
        NSLog(@"Unable to created a SFSpeechAudioBufferRecognitionRequest object");
    

    self.recognitionTask = [self.sfSpeechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult *result, NSError *error) 

        bool isFinal= false;

        if (error) 
            [self stopAndRelease];
            NSLog(@"In recognitionTaskWithRequest.. Error code ::: %ld, %@", (long)error.code, error.description);
            [self sendErrorWithMessage:error.localizedFailureReason andCode:error.code];
        

        if (result) 

            [self sendResults:result.bestTranscription.formattedString];
            isFinal = result.isFinal;
        

        if (isFinal) 
            NSLog(@"result.isFinal: ");
            [self stopAndRelease];
            //return control to caller
        
    ];

    NSLog(@"Step5: ");

    AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];

    [self.audioEngine.inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) 
        //NSLog(@"Installing Audio engine: ");
        [self.recognitionRequest appendAudioPCMBuffer:buffer];
    ];

    NSLog(@"Step6: ");

    [self.audioEngine prepare];
    NSLog(@"Step7: ");
    NSError *err;
    [self.audioEngine startAndReturnError:&err];

- (void) initAudioSession

    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory:AVAudioSessionCategoryRecord error:nil];
    [audioSession setMode:AVAudioSessionModeMeasurement error:nil];
    [audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];


-(void) stopAndRelease

    NSLog(@"Invoking SFSpeechRecognizer stopAndRelease: ");
    [self.audioEngine stop];
    [self.recognitionRequest endAudio];
    [self.audioEngine.inputNode removeTapOnBus:0];
    self.recognitionRequest = nil;
    [self.recognitionTask cancel];
    self.recognitionTask = nil;

关于添加的日志,可以看到所有日志,直到打印“Step7”。

在调试设备中的代码时,它始终在以下行触发 break(我设置了异常断点),但 continue 继续执行。然而,在少数成功的执行过程中也会发生同样的情况。

AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];

[self.audioEngine 准备];

【问题讨论】:

【参考方案1】:

原因是音频没有完全完成,当-speechSynthesizer:didFinishSpeechUtterance: 被调用时,因此你在尝试调用setActive:NO 时会遇到这种错误。您不能在 I/O 运行期间停用AudioSession 或更改任何设置。解决方法:等待几毫秒(多长时间阅读下文),然后执行AudioSession 停用等操作。

关于音频播放完成的几句话。

乍一看,这可能看起来很奇怪,但我已经花了很多时间来研究这个问题。当您将最后一个声音块放入设备输出时,您只有大概的实际完成时间。看AudioSession属性ioBufferDuration:

音频 I/O 缓冲持续时间是单次播放的秒数 音频输入/输出周期。例如,I/O 缓冲区持续时间为 0.005 s,每个音频 I/O 周期:

如果获得输入,您会收到 0.005 秒的音频。 如果提供输出,您必须提供 0.005 秒的音频。

典型的最大 I/O 缓冲持续时间为 0.93 s(对应 4096 个样本 帧以 44.1 kHz 的采样率)。最小 I/O 缓冲区持续时间 至少为 0.005 秒(256 帧),但可能会更低,具体取决于 正在使用的硬件。

因此,我们可以将此值解释为一个块的播放时间。但是您在此时间线和实际音频播放完成(硬件延迟)之间仍然有一个小的非计算持续时间。我想说你需要等待 ioBufferDuration * 1000 + delay 毫秒才能确保音频播放完成(ioBufferDuration * 1000 - 因为它是持续时间,以 为单位),其中 delay 是一个很小的值。

此外,似乎即使是 Apple 开发人员也不太确定音频完成时间。快速浏览一下新的音频类AVAudioPlayerNode 和func scheduleBuffer(_ buffer: AVAudioPCMBuffer, completionHandler: AVFoundation.AVAudioNodeCompletionHandler? = nil)

@param completionHandler 在缓冲区被消耗后调用 播放器或播放器停止。可能为零。

@discussion 安排要在任何先前安排的命令之后播放的缓冲区。可以调用completionHandler 在渲染开始之前或缓冲区完全播放之前

您可以在Understanding the Audio Unit Render Callback Function 中阅读有关音频处理的更多信息(AudioUnit 是提供快速访问 I/O 数据的低级 API)。

【讨论】:

感谢您的回复。我在识别用户语音(即语音到文本部分)时遇到问题。音频朗读效果很好。那么你的意思是说,在 SpeechToText 过程中的“initAudioSession”中添加延迟?此外,如前所述,我添加了检查以查看是否有音频播放器正在运行。我将语音检查添加到代码的文本部分。它返回 false(即没有其他音频播放器正在运行)但是文本语音仍然没有开始收听用户语音。另外,我在调试模式下运行我的代码,这自然也会增加几秒钟的延迟。 有没有办法确定语音话语何时真正结束?我假设'-speechSynthesizer:didFinishSpeechUtterance:'将被调用,当它语音话语实际完成时。不是这样吗? 这不是关于“其他音频正在运行”,而是您的音频仍在运行。是的,'-s​​peechSynthesizer: didFinishSpeechUtterance:' 在音频播放完毕时被调用,但不完全是。我试图在上面解释,有一个小的延迟,听不见,但它仍然存在。这是关于硬件实现的。问题不在于语音转文本部分,而在于 AudioSession 停用。尝试在 didFinishSpeechUtterance 完成回调中添加 dispatch after 并将 AudioSession 内容放入其中。 我试图把它放在一个 NStimer 中,在 2 秒后触发,但我仍然有同样的问题。它工作了一次,并在下一次运行中再次遇到了问题。我修改了 didFinishSpeechUtterance,并在 2S 之后将以下语句放在 NSTimer 中执行。 [[AVAudioSession sharedInstance] setActive:NO withOptions:0 error:nil]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient withOptions: 0 error: nil]; [[AVAudioSession sharedInstance] setActive:YES withOptions: 0 error:nil]; 语音转文本过程开始后,我看到“第 7 步日志”,然后看到错误 - [avas] ERROR: AVAudioSession.mm:1049: -[AVAudioSession setActive:withOptions:error :]: 停用正在运行 I/O 的音频会话。在停用音频会话之前,应停止或暂停所有 I/O。一段时间后,语音识别会自行停止,我也看到了以下错误。错误域=kAFAssistantErrorDomain 代码=203 "Corrupt" UserInfo=NSUnderlyingError=0x14651450 错误域=SiriSpeechErrorDomain Code=102 "(null)", NSLocalizedDescription=Corrupt

以上是关于在 AVSpeechUtterance 之后使用 SFSpeechRecognizer 时出现 AVAudioSession 问题的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发----文字转语音

iOS自带TTS技术的实现即语音播报

音频文件的 AVSpeechSynthesizer 话语

如何通过呼叫接收扬声器播放 AVSpeechSynthesizer?

iOS语音合成

[iOS]AVSpeechSynthesizer语音合成