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

Swift下将音频原始数据写入AVAudioPCMBuffer生成AVAudioFile播放出现白噪问题

问题根因与修复方案

白噪的核心原因是原始PCM数据填充错误、格式参数多处不匹配,以下是具体修复点:

1. 修正AVAudioFile创建逻辑

你当前创建文件时的采样率和后续要写入的转换后格式采样率不统一,且文件格式与后缀不匹配:

func createAudioFile() {
    let fileMgr = FileManager.default
    let dirPaths = fileMgr.urls(for: .cachesDirectory, in: .userDomainMask)
    // 先获取主混音输出格式,和后续转换的输出格式保持一致
    let outputFormat = audioEngine.mainMixerNode.outputFormat(forBus: 0)
    var recordSettings: [String : Any] = [:]
    recordSettings[AVFormatIDKey] = kAudioFormatLinearPCM
    recordSettings[AVSampleRateKey] = outputFormat.sampleRate
    recordSettings[AVNumberOfChannelsKey] = outputFormat.channelCount
    // 后缀和格式对应,CAF格式用caf后缀
    self.soundFileUrl = dirPaths[0].appendingPathComponent("recording.caf")
    do {
        audioFile = try AVAudioFile(forWriting: soundFileUrl!, settings: recordSettings, commonFormat: .pcmFormatFloat32, interleaved: false)
    } catch let error as NSError {
        print("error:", error.localizedDescription)
    }
}

2. 修正回调中的原始数据填充与转换逻辑

核心错误点:

  • rawData.bufferLen是字节长度,16位单声道每个采样占2字节,你当前直接用bufferLen / 1算采样数,相当于把2字节拆成2个采样处理,数据完全错乱
  • 直接移动rawData.buffer指针逐字节取数转Int16,只取到了每个采样的低位字节,高位全为0
  • 转换输出buffer容量没有按采样率转换比例预留足够空间,导致转换截断
// 转换器全局创建一次即可,不要每次回调都创建
private var converter: AVAudioConverter?
private let inputSampleRate = 32000.0
private let inputChannels = 1

func onAudioRawDataReceived(_ rawData: AudioRawData) {
    if self.saveAudio == true {
        do {
            // 1. 计算正确的输入采样数:字节数 / 2(16位单声道每个采样2字节)
            let inputFrameCount = AVAudioFrameCount(rawData.bufferLen / 2)
            guard inputFrameCount > 0 else { return }
            
            // 2. 初始化输入格式和输入buffer
            let inputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: inputSampleRate, channels: AVAudioChannelCount(inputChannels), interleaved: true)!
            guard let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: inputFrameCount) else { return }
            inputBuffer.frameLength = inputFrameCount
            
            // 3. 直接把原始字节指针转成Int16指针批量复制,不要逐字节取值
            _ = inputBuffer.int16ChannelData!.pointee.withMemoryRebound(to: UInt8.self, capacity: rawData.bufferLen) { destPtr in
                memcpy(destPtr, rawData.buffer, rawData.bufferLen)
            }
            
            // 4. 初始化转换器(只创建一次)
            let outputFormat = audioEngine.mainMixerNode.outputFormat(forBus: 0)
            if converter == nil {
                converter = AVAudioConverter(from: inputFormat, to: outputFormat)
            }
            guard let converter = converter else { return }
            
            // 5. 计算输出需要的采样数,按采样率比例放大,预留足够空间
            let outputFrameCount = AVAudioFrameCount(Double(inputFrameCount) * outputFormat.sampleRate / inputSampleRate) + 10
            guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: outputFrameCount) else { return }
            
            // 6. 执行转换
            var error: NSError?
            let status = converter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
                outStatus.pointee = .haveData
                return inputBuffer
            }
            if status == .error, let error = error {
                print("转换失败: \(error.localizedDescription)")
                return
            }
            
            // 7. 写入文件
            try audioFile!.write(from: outputBuffer)
            isRecordingAudio = true
        }
        catch let error as NSError {
            print("处理音频失败: \(error.localizedDescription)")
        }
    } else {
        if isRecordingAudio == true {
            // 写入完成后要先关闭文件再播放
            audioFile = nil
            playPCM(soundFileUrl!.path)
            isRecordingAudio = false
        }
    }
}

3. 修正播放逻辑

本地文件路径不能用URL(string:)初始化,要用URL(fileURLWithPath:):

func playPCM(_ filefullpathstr: String) {
    do {
        // 本地文件用fileURLWithPath初始化
        let fileUrl = URL(fileURLWithPath: filefullpathstr)
        let audioFile = try AVAudioFile(forReading: fileUrl)
        let audioFormat = audioFile.processingFormat
        let audioFrameCount = UInt32(audioFile.length)
        guard let audioFileBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount) else { return }
        try audioFile.read(into: audioFileBuffer, frameCount: audioFrameCount)
        
        // 先停止之前的引擎实例,避免重复attach报错
        if audioEngine.isRunning {
            audioEngine.stop()
            audioEngine.reset()
        }
        let mainMixer = audioEngine.mainMixerNode
        audioEngine.attach(audioFilePlayer)
        audioEngine.connect(audioFilePlayer, to: mainMixer, format: audioFormat)
        try audioEngine.start()
        // 先scheduleBuffer再play,避免开头丢音
        audioFilePlayer.scheduleBuffer(audioFileBuffer, at: nil, options: []) {
            print("播放完成")
        }
        audioFilePlayer.play()
    } catch let error as NSError {
        print("播放失败: \(error.localizedDescription)")
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 17:36:02