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
相关产品推荐
相关产品推荐

