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

Swift中播放Base64音频块出现噪音问题排查

WebSocket音频播放噪音问题排查与修复

核心问题分析与修复方案

1. 输入音频格式必须完全匹配

你硬编码的inputFormat参数(pcmFormatInt16、44100采样率、双声道交错)必须和WebSocket传输的音频参数完全一致。如果后端实际传输的是单声道、不同采样率或非交错格式,直接解析必然产生噪音。

修复:
优先从后端获取明确的音频参数(比如握手阶段协商),或硬编码为后端实际使用的配置。例如后端是单声道时:

inputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 44100, channels: AVAudioChannelCount(1), interleaved: true)!

2. 修正FrameCount计算与内存拷贝逻辑

当前计算frameCount时未处理数据长度与帧大小的非整数倍情况,会导致帧长度计算错误;内存拷贝的可选值处理也存在潜在风险。

修复:

let bytesPerFrame = inputFormat.streamDescription.pointee.mBytesPerFrame
// 确保数据长度与帧大小对齐,避免截断或错误
guard data.count % Int(bytesPerFrame) == 0 else {
    NSLog("Audio data length is not aligned with frame size")
    return nil
}
let frameCount = UInt32(data.count) / bytesPerFrame

内存拷贝部分优化指针处理,确保数据完整拷贝:

data.withUnsafeBytes { bufferPointer in
    guard let sourcePtr = bufferPointer.baseAddress?.assumingMemoryBound(to: Int16.self),
          let destPtr = inputBuffer.int16ChannelData?.pointee else {
        NSLog("Failed to get audio buffer pointers")
        return
    }
    let totalSamples = Int(frameCount) * Int(inputFormat.channelCount)
    destPtr.assign(from: sourcePtr, count: totalSamples)
}

3. 修复音频转换器的错误使用

当前转换器的inputBlock会重复返回同一个buffer,且错误地将转换后buffer的frameLength设为frameCapacity,导致填充无效数据,引发噪音。

修复:

// 初始化转换后buffer,不预先设置frameLength,由转换器自动填充
guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: converterFrameCapacity) else {
    NSLog("Error creating converted buffer")
    return nil
}

var error: NSError?
var processedFrames: AVAudioFrameCount = 0
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
    let remainingFrames = inputBuffer.frameLength - processedFrames
    if remainingFrames == 0 {
        outStatus.pointee = .endOfStream
        return nil
    }
    let framesToProcess = min(AVAudioFrameCount(inNumPackets), remainingFrames)
    
    // 创建子buffer返回当前需要处理的音频段,避免重复输出
    guard let subBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: framesToProcess) else {
        outStatus.pointee = .noDataNow
        return nil
    }
    subBuffer.frameLength = framesToProcess
    
    if let sourcePtr = inputBuffer.int16ChannelData?.pointee.advanced(by: Int(processedFrames) * Int(inputFormat.channelCount)),
       let destPtr = subBuffer.int16ChannelData?.pointee {
        destPtr.assign(from: sourcePtr, count: Int(framesToProcess) * Int(inputFormat.channelCount))
    }
    
    processedFrames += framesToProcess
    outStatus.pointee = .haveData
    return subBuffer
}

// 执行转换,转换器会自动设置convertedBuffer的实际帧长度
let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputBlock)
if status != .success || error != nil {
    NSLog("Error during conversion: \(error?.localizedDescription ?? "Unknown error")")
    return nil
}

4. 处理字节序不匹配问题

如果后端传输的音频是大端字节序(Big-Endian),而iOS默认使用小端(Little-Endian),解析出的PCM数据会完全错误,产生噪音。

修复:
在内存拷贝时转换字节序(根据后端实际情况调整):

data.withUnsafeBytes { bufferPointer in
    guard let sourcePtr = bufferPointer.baseAddress?.assumingMemoryBound(to: Int16.self),
          let destPtr = inputBuffer.int16ChannelData?.pointee else {
        NSLog("Failed to get audio buffer pointers")
        return
    }
    let totalSamples = Int(frameCount) * Int(inputFormat.channelCount)
    for i in 0..<totalSamples {
        destPtr[i] = sourcePtr[i].bigEndian
    }
}

5. 其他潜在优化点

  • 将音频处理逻辑从主线程移至后台队列,避免阻塞UI,同时保证音频线程优先级;
  • 对buffersInQueue的计数加锁或使用原子操作,确保线程安全(因为scheduleBuffer的回调在音频线程触发)。

完整修复后的decodeAudioData函数

private func decodeAudioData(_ base64String: String) -> AVAudioPCMBuffer? {
    guard let data = Data(base64Encoded: base64String) else {
        NSLog("Error decoding base64 data")
        return nil
    }
    
    guard let inputFormat = inputFormat else {
        NSLog("Error: Audio format is nil")
        return nil
    }
    
    let bytesPerFrame = inputFormat.streamDescription.pointee.mBytesPerFrame
    guard data.count % Int(bytesPerFrame) == 0 else {
        NSLog("Audio data length is not aligned with frame size")
        return nil
    }
    let frameCount = UInt32(data.count) / bytesPerFrame
    
    guard let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: frameCount) else {
        NSLog("Error creating AVAudioPCMBuffer")
        return nil
    }
    inputBuffer.frameLength = frameCount
    
    // 处理字节序并拷贝数据
    data.withUnsafeBytes { bufferPointer in
        guard let sourcePtr = bufferPointer.baseAddress?.assumingMemoryBound(to: Int16.self),
              let destPtr = inputBuffer.int16ChannelData?.pointee else {
            NSLog("Failed to get audio buffer pointers")
            return
        }
        let totalSamples = Int(frameCount) * Int(inputFormat.channelCount)
        // 假设后端为大端字节序,根据实际情况调整
        for i in 0..<totalSamples {
            destPtr[i] = sourcePtr[i].bigEndian
        }
    }
    
    guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
        NSLog("Error creating audio converter")
        return nil
    }
    
    let converterFrameCapacity = AVAudioFrameCount(outputFormat.sampleRate / inputFormat.sampleRate * Double(inputBuffer.frameCapacity))
    guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: converterFrameCapacity) else {
        NSLog("Error creating converted buffer")
        return nil
    }
    
    var error: NSError?
    var processedFrames: AVAudioFrameCount = 0
    let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
        let remainingFrames = inputBuffer.frameLength - processedFrames
        if remainingFrames == 0 {
            outStatus.pointee = .endOfStream
            return nil
        }
        let framesToProcess = min(AVAudioFrameCount(inNumPackets), remainingFrames)
        
        guard let subBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: framesToProcess) else {
            outStatus.pointee = .noDataNow
            return nil
        }
        subBuffer.frameLength = framesToProcess
        
        if let sourcePtr = inputBuffer.int16ChannelData?.pointee.advanced(by: Int(processedFrames) * Int(inputFormat.channelCount)),
           let destPtr = subBuffer.int16ChannelData?.pointee {
            destPtr.assign(from: sourcePtr, count: Int(framesToProcess) * Int(inputFormat.channelCount))
        }
        
        processedFrames += framesToProcess
        outStatus.pointee = .haveData
        return subBuffer
    }
    
    let status = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputBlock)
    if status != .success || error != nil {
        NSLog("Error during conversion: \(error?.localizedDescription ?? "Unknown error")")
        return nil
    }
    
    print("Converted Buffer Frame Count: \(convertedBuffer.frameLength)")
    return convertedBuffer
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 12:22:04