将语音保存到文件 - 缓冲错误 - Swift

Sha*_*DES 0 avfoundation ios avspeechsynthesizer swift swiftui

去年我问了一个关于如何将语音保存到文件的问题。 堆栈溢出问题 - 将语音合成录制到保存的文件中

感谢 kakaiikaka 的回答。虽然它确实有效,但缓冲时出现了一些错误。以下代码隔离了该问题。在 iOS 16 中,尽管存在错误,但它确实可以按预期工作。我按预期打印的完成处理程序。以下错误打印了 20 次左右。

2023-06-17 15:35:33.811838-0400 RecordSpeechFix [3899:1958883] [AXTTSCommon] TTSPlaybackEnqueueFullAudioQueueBuffer:错误-66686排队缓冲区

iOS 17(第一个测试版)存在一个更具描述性的错误,并且它不起作用。完成处理程序不打印。以下错误打印了 20 次左右。

输入数据过程返回不一致的 512 个数据包(2,048 字节);按每个数据包 2 字节计算,实际上是 1,024 个数据包

我假设这是同一个问题。修复 iOS16 的错误也将修复 iOS17 的错误。我的这个假设可能是错误的。

//
//  ContentView.swift
//  RecordSpeechFix
//
//  Created by Dennis Sargent on 6/16/23.
//

import AVFoundation
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Record Speech")
        }
        .padding()
        .onTapGesture {
            saveSpeechUtteranceToFile(phrase: "This produces warnings.") {
                print("In iOS 16, this will print.  In iOS17, this will not.")
            }
        }
    }
    
    func documentsDirectory(fileName: String, ext: String) -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectoryURL = paths[0]
    
        return documentsDirectoryURL.appendingPathComponent("\(fileName).\(ext)")
    }
    
    let synthesizer = AVSpeechSynthesizer()
    
    func saveSpeechUtteranceToFile(phrase: String, completionHandler: @escaping () -> ()) {
        
        let fileURL = documentsDirectory(fileName: "Test", ext: ".caf")
        
        let utterance = AVSpeechUtterance(string: phrase)
        utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
        utterance.rate = 0.50
        utterance.volume = 1.0
    
        var output: AVAudioFile?
        
        synthesizer.write(utterance) {(buffer: AVAudioBuffer) in
            guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
                fatalError("unknown buffer type: \(buffer)")
            }
            
            
            if pcmBuffer.frameLength == 0 {
                // Done
                completionHandler()
            } else {
                do{
                    if output == nil {
                        try  output = AVAudioFile(
                            forWriting: fileURL,
                            settings: pcmBuffer.format.settings,
                            commonFormat: .pcmFormatInt16,
                            interleaved: false)
                    }
                    try output?.write(from: pcmBuffer)
                }catch {
                    print("Buffer has an error")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我对一般录音不太熟悉。似乎存在某种缓冲问题。您知道需要什么设置来清除这些错误吗?

小智 8

原来,iOS 17 将 AVAudioPCMBuffer 格式从 Int16 更改为 Float32。只有苹果知道这是有意还是无意。这破坏了每个使用/sf/answers/4068300841/中的代码片段的应用程序

解决方法有两个:

  1. 将 AVAudioFile 创建从固定的 .pcmFormatInt16 更改为与 AVAudioPCMBuffer 格式匹配的格式。
  2. 对于 Float32 格式缓冲区,将完成的测试从 bufferLength 0 更改为 <= 1。他们显然吐出了长度为 1 的最终缓冲区(内容 00)。

这是我在 iOS 17 beta 4 和 iOS 16.6 上运行的代码:

func saveAVSpeechUtteranceToFile(utterance: AVSpeechUtterance, fileURL: URL) throws {
    // init
    totalBufferLength = 0
    bufferWriteCount = 0
    output = nil
    
    // delete file if it already exists
    try? FileManager.default.removeItem(at: fileURL)
    
    // synthesize to buffers
    synthesizer.write(utterance) { [self] buffer in
        guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
            return
        }
        
        // float32 buffers spit out a final buffer of length 1, contents 00
        let doneLength: Int
        if pcmBuffer.format.commonFormat == .pcmFormatInt16 || pcmBuffer.format.commonFormat == .pcmFormatInt32 {
            doneLength = 0
        } else {
            doneLength = 1
        }
        
        if pcmBuffer.frameLength <= doneLength {
            // done
            playAudioFile(url: fileURL)  // or whatever
            
        } else {
            totalBufferLength += pcmBuffer.frameLength
            bufferWriteCount += 1
            
            if output == nil {
                do {
                    output = try AVAudioFile(forWriting: fileURL, settings: pcmBuffer.format.settings, commonFormat: pcmBuffer.format.commonFormat, interleaved: false)
                } catch {
                    print("create AVAudioFile error: \(error.localizedDescription)")
                }
            }
            
            do {
                try output!.write(from: pcmBuffer)
            } catch {
                print("output!.write failed, error: \(error.localizedDescription)")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)