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

如何录制应用输出的全部音频?AVSpeechSynthesizer声波生成需求

嘿,这个需求我刚好踩过坑!AVSpeechSynthesizer确实没有直接导出音频的API,但咱们完全不用录制整个应用的声音——用AVAudioEngine就能精准捕获它的合成输出,不管是存成文件还是直接拿数据生成波形都没问题。下面是具体的实现思路和代码:

核心方案:用AVAudioEngine拦截合成音频

AVSpeechSynthesizer的语音输出会经过系统音频管线,我们可以通过AVAudioEngine的节点来“截胡”这段音频,既高效又不会混入其他应用的声音。

步骤1:实现音频捕获与保存

下面是Swift的完整实现代码,包含了从启动合成、捕获音频到保存文件的全流程:

import AVFoundation

class SpeechAudioCapture {
    private let audioEngine = AVAudioEngine()
    private let synthesizer = AVSpeechSynthesizer()
    private var outputAudioFile: AVAudioFile?
    
    // 开始捕获并合成语音
    func startCapturingSpeech(text: String) {
        // 定义音频格式(这里用16位PCM、44.1kHz采样率、单声道,适合后续处理)
        guard let audioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
                                             sampleRate: 44100,
                                             channels: 1,
                                             interleaved: false) else {
            print("音频格式初始化失败")
            return
        }
        
        // 创建保存音频的文件路径(存在Documents目录)
        let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let audioSaveURL = documentsDir.appendingPathComponent("synthesized_speech.wav")
        
        // 初始化音频文件用于写入
        do {
            outputAudioFile = try AVAudioFile(forWriting: audioSaveURL,
                                             settings: audioFormat.settings,
                                             commonFormat: audioFormat.commonFormat,
                                             interleaved: audioFormat.isInterleaved)
        } catch {
            print("创建音频文件失败:\(error.localizedDescription)")
            return
        }
        
        // 获取音频引擎的输入节点,用来接收合成器的输出
        let inputNode = audioEngine.inputNode
        let bus = 0
        
        // 安装Tap,实时捕获音频数据
        inputNode.installTap(onBus: bus, bufferSize: 1024, format: audioFormat) { [weak self] buffer, time in
            guard let self = self, let audioFile = self.outputAudioFile else { return }
            do {
                try audioFile.write(from: buffer)
            } catch {
                print("写入音频数据失败:\(error.localizedDescription)")
            }
        }
        
        // 启动音频引擎
        do {
            try audioEngine.start()
        } catch {
            print("启动音频引擎失败:\(error.localizedDescription)")
            return
        }
        
        // 开始语音合成
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN") // 可根据需求切换语言
        synthesizer.speak(utterance)
    }
    
    // 停止捕获与合成
    func stopCapturing() {
        synthesizer.stopSpeaking(at: .immediate)
        audioEngine.stop()
        audioEngine.inputNode.removeTap(onBus: 0)
        outputAudioFile = nil
        print("音频捕获完成,文件已保存到Documents目录")
    }
}

步骤2:权限与注意事项

  • 必须在Info.plist中添加麦克风权限描述NSMicrophoneUsageDescription——虽然我们不是录制麦克风,但AVAudioEngine的inputNode会默认请求麦克风权限,这是系统的硬性要求。
  • 要确保在语音合成之前启动音频引擎,否则可能会丢失开头的部分音频数据。
  • 如果需要生成声波,可以读取保存的音频文件,通过AVAudioPCMBuffer提取PCM数据,计算每个片段的振幅值,再绘制到UI上。

进阶:直接获取数据生成波形(无需保存文件)

如果不需要保存音频文件,只想实时获取数据来生成波形,可以在Tap的回调中直接处理PCM数据,省去写入文件的步骤:

inputNode.installTap(onBus: bus, bufferSize: 1024, format: audioFormat) { buffer, time in
    guard let pcmBuffer = buffer as? AVAudioPCMBuffer,
          let channelData = pcmBuffer.int16ChannelData else { return }
    
    let channelDataArray = Array(UnsafeBufferPointer(start: channelData.pointee, count: Int(pcmBuffer.frameLength)))
    // 计算当前片段的平均振幅(取绝对值的平均值,适合作为波形的一个点)
    let averageAmplitude = channelDataArray.reduce(0) { $0 + abs($1) } / channelDataArray.count
    
    // 回到主线程更新波形UI
    DispatchQueue.main.async {
        // 这里把averageAmplitude传给你的波形绘制逻辑
        // 比如更新自定义View的波形数据
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:10:48