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

SwiftUI中AVAudioEngine采集音频经GCDAsyncUdpSocket传输后杂音问题

音频传输杂音问题排查与修复

以下是导致接收端出现滋滋杂音的核心问题及对应修复方案:

1. 采样率不匹配(最关键原因)

发送端采集音频时使用44100Hz采样率,但接收端初始化的audioFormat是16000Hz,采样率不匹配会导致音频播放速度异常,直接产生严重杂音。

修复:统一采样率

在初始化和发送端使用相同的采样率,比如统一用16000Hz:

初始化代码修改:

func setupAudio () {
    let sampleRate = 16000
    self.audioEngine = AVAudioEngine()
    self.mixer = AVAudioMixerNode()
    self.mixer.volume = 0
    self.audioEngine.attach(mixer)
    
    self.socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
    // 明确指定Int16格式,和发送端保持一致
    audioFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: Double(sampleRate), channels: 1, interleaved: true)!
}

发送端format修改:

// 复用初始化时的统一格式,避免硬编码
let format = self.audioFormat

2. 音频Data转换长度错误

发送端将AVAudioPCMBuffer转Data时,计算的长度错误:

// 错误:只取了frameLength,没考虑每个Int16采样点占2字节
let data = Data(bytes: (buffer.int16ChannelData![0]), count: Int(buffer.frameLength))

正确长度应该是frameLength * 2,否则传输的数据只有一半,接收端解析会出现乱码杂音。

修复:修正Data长度计算

let frameCount = Int(buffer.frameLength)
let data = Data(bytes: buffer.int16ChannelData![0], count: frameCount * MemoryLayout<Int16>.stride)

3. UDP不可靠性导致的丢包/时序问题

UDP没有重传机制,丢包后直接播放会出现断层杂音;同时接收端直接调用scheduleBuffer,没有处理音频帧的时序对齐,也会导致播放卡顿或杂音。

修复:添加音频缓存队列

接收端维护一个线程安全的音频缓存队列,确保按顺序播放:

// 定义线程安全队列
private let audioQueue = DispatchQueue(label: "com.audio.queue")
private var bufferCache: [AVAudioPCMBuffer] = []

func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
    let buffer = getComingAudio(with: data)
    audioQueue.async { [weak self] in
        guard let self = self else { return }
        self.bufferCache.append(buffer)
        // 如果播放器处于空闲状态,开始播放缓存
        if !self.audioPlayer.isPlaying {
            self.playNextBuffer()
        }
    }
}

private func playNextBuffer() {
    audioQueue.async { [weak self] in
        guard let self = self, !self.bufferCache.isEmpty else { return }
        let buffer = self.bufferCache.removeFirst()
        DispatchQueue.main.async {
            self.audioPlayer.scheduleBuffer(buffer) { [weak self] in
                self?.playNextBuffer()
            }
            if !self.audioPlayer.isPlaying {
                try? self.audioPlayer.start()
            }
        }
    }
}

4. AVAudioSession设置不严谨

发送端直接使用try!强制解包,一旦权限不足或设备不支持会崩溃,同时没有设置合适的session选项,可能导致音频采集异常。

修复:完善Session设置

do {
    try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
    try AVAudioSession.sharedInstance().setActive(true)
} catch {
    print("Session setup error: \(error)")
}

5. 重复创建Mixer节点

初始化代码中重复创建了mixer节点,属于冗余代码,可能导致未知问题:

// 错误:重复创建
self.mixer = AVAudioMixerNode()
self.mixer = AVAudioMixerNode()

修复:移除重复创建

删除其中一行self.mixer = AVAudioMixerNode()即可。

最终接收端转换函数优化

确保接收端的格式和发送端完全一致,避免不必要的类型转换:

func getComingAudio(with data: Data) -> AVAudioPCMBuffer {
    let frameCapacity = UInt32(data.count) / UInt32(MemoryLayout<Int16>.stride)
    guard let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: frameCapacity) else {
        fatalError("Failed to create PCM buffer")
    }
    
    data.withUnsafeBytes { ptr in
        let int16Ptr = ptr.bindMemory(to: Int16.self).baseAddress!
        audioBuffer.int16ChannelData!.pointee.assign(from: int16Ptr, count: Int(frameCapacity))
    }
    audioBuffer.frameLength = frameCapacity
    return audioBuffer
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 01:11:04