You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在SwiftUI中如何将文本转语音(TTS)输出写入音频文件?

问题根因

你定义的AVSpeechSynthesizer实例是generateSpeech函数内的局部变量,函数执行完成后该实例会被系统自动回收,而synth.write是异步操作,还没等到回调触发,实例就已经被释放了,所以回调永远不会执行。
原来在Storyboard项目中你大概率是把合成器实例定义成了ViewController的全局属性,实例生命周期和ViewController绑定,不会被提前释放,所以可以正常运行。

解决步骤

  1. 把AVSpeechSynthesizer实例从函数内部移到ContentView的属性中,保证它的生命周期和当前页面绑定
  2. 新增异步完成回调,在语音写入操作全部结束后再判断文件是否存在,原来的逻辑是函数刚返回就判断,此时异步写入还没完成,自然读不到文件

修改后的完整可运行代码如下:

import SwiftUI
import AVFoundation

struct ContentView: View {
    // 把合成器定义为页面属性,避免被提前释放
    private let synth = AVSpeechSynthesizer()
    
    var body: some View {
        Button(action: runSpeech) {
            Text("Button")
        }
    }
    
    func runSpeech() {
        generateSpeech("hello world", 1.0) { fileURL in
            guard let fileURL = fileURL else { return }
            DispatchQueue.main.async {
                if FileManager.default.fileExists(atPath: fileURL.path) {
                    print("🔴 Speech file exists at \(fileURL)")
                } else {
                    print("🔴 no file exists at \(fileURL)")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

// 增加完成回调,异步通知写操作结束
func generateSpeech(_ speech: String, _ speechRate: Float, completion: @escaping (URL?) -> Void) {
    let myUtterance = AVSpeechUtterance(string: speech)
    myUtterance.rate = speechRate
    myUtterance.voice = AVSpeechSynthesisVoice(language: "en")!
    myUtterance.postUtteranceDelay = 2

    guard let dataPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
        completion(nil)
        return
    }

    do {
        try FileManager.default.createDirectory(atPath: dataPath.path, withIntermediateDirectories: true)
    } catch {
        print("🔵Error creating directory: \(error.localizedDescription)")
        completion(nil)
        return
    }
    let fileName = "myspeech.caf"
    
    let fileURL = dataPath.appendingPathComponent(fileName)

    if FileManager.default.fileExists(atPath: fileURL.path) {
        print("🔵Speech file exists at \(fileURL)")
        completion(fileURL)
        return
    }
    
    print("🔵Speech file does not exist at \(fileURL)")
   
    var output: AVAudioFile?
    // 这里用外部传入的synth实例,不是局部变量
    synth.write(myUtterance) { buffer in
        print("🔵callback")
        guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
            fatalError("unknown buffer type: \(buffer)")
        }
        if pcmBuffer.frameLength == 0 {
            // 写入完成,回调通知
            completion(fileURL)
        } 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("🔵error: \(error.localizedDescription)")
                completion(nil)
            }
        }
    }
    print("🔵return")
}

内容的提问来源于stack exchange,提问作者Makoto Niijima

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 12:36:03