Swift中如何播放Socket.io接收的实时INT32 PCM音频数据?
实现实时INT32 PCM音频播放(Swift)
1. 明确音频格式参数
先梳理你提供的音频规格,方便后续配置:
- 采样率:4000 Hz
- 位深:32-bit 有符号整数(INT32)
- 声道数:2(立体声)
- 单包数据大小:256字节 → 对应32个音频帧(每帧=2声道×4字节=8字节,256/8=32)
2. 实现线程安全的循环缓冲区
循环缓冲区用来缓存实时接收的音频数据,解决网络传输和播放速率不匹配的问题,避免卡顿或丢包:
import Foundation class AudioRingBuffer { private let buffer: UnsafeMutableRawPointer private let capacity: Int private var readIndex: Int = 0 private var writeIndex: Int = 0 private let lock = NSLock() init(capacity: Int) { self.capacity = capacity self.buffer = malloc(capacity)! } deinit { free(buffer) } // 写入数据到缓冲区 func write(_ data: Data) -> Int { lock.lock() defer { lock.unlock() } let bytesAvailable = availableWriteBytes() let bytesToWrite = min(data.count, bytesAvailable) guard bytesToWrite > 0 else { return 0 } let firstSegmentSize = min(bytesToWrite, capacity - writeIndex) data.copyBytes(to: buffer.advanced(by: writeIndex), count: firstSegmentSize) if bytesToWrite > firstSegmentSize { let secondSegmentSize = bytesToWrite - firstSegmentSize data.copyBytes(to: buffer, from: firstSegmentSize..<firstSegmentSize+secondSegmentSize) } writeIndex = (writeIndex + bytesToWrite) % capacity return bytesToWrite } // 从缓冲区读取数据 func read(_ buffer: UnsafeMutableRawPointer, count: Int) -> Int { lock.lock() defer { lock.unlock() } let bytesAvailable = availableReadBytes() let bytesToRead = min(count, bytesAvailable) guard bytesToRead > 0 else { return 0 } let firstSegmentSize = min(bytesToRead, capacity - readIndex) memcpy(buffer, self.buffer.advanced(by: readIndex), firstSegmentSize) if bytesToRead > firstSegmentSize { let secondSegmentSize = bytesToRead - firstSegmentSize memcpy(buffer.advanced(by: firstSegmentSize), self.buffer, secondSegmentSize) } readIndex = (readIndex + bytesToRead) % capacity return bytesToRead } // 获取可写入的字节数 private func availableWriteBytes() -> Int { let diff = writeIndex - readIndex return diff >= 0 ? capacity - diff : -diff } // 获取可读取的字节数 private func availableReadBytes() -> Int { let diff = writeIndex - readIndex return diff >= 0 ? diff : capacity + diff } // 清空缓冲区 func reset() { lock.lock() defer { lock.unlock() } readIndex = 0 writeIndex = 0 } }
3. 基于AVAudioEngine实现播放逻辑
AVAudioEngine是Apple推荐的现代音频API,结合循环缓冲区处理实时流:
import AVFoundation class AudioPlayerManager: NSObject { private let engine = AVAudioEngine() private let playerNode = AVAudioPlayerNode() private let ringBuffer: AudioRingBuffer private let audioFormat: AVAudioFormat override init() { // 配置对应INT32格式的音频描述 var formatDesc = AudioStreamBasicDescription( mSampleRate: 4000.0, mFormatID: kAudioFormatLinearPCM, mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, mBytesPerPacket: 8, mFramesPerPacket: 1, mBytesPerFrame: 8, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0 ) self.audioFormat = AVAudioFormat(streamDescription: &formatDesc)! // 初始化10秒容量的缓冲区(4000采样×2声道×4字节×10=320000字节) self.ringBuffer = AudioRingBuffer(capacity: 320000) super.init() setupAudioEngine() } private func setupAudioEngine() { engine.attach(playerNode) engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat) // 设置播放回调,持续从缓冲区取数据 playerNode.installTap(onBus: 0, bufferSize: 32, format: audioFormat) { [weak self] buffer, _ in guard let self = self else { return } let bytesNeeded = Int(buffer.frameLength) * Int(self.audioFormat.streamDescription.pointee.mBytesPerFrame) let bytesRead = self.ringBuffer.read(buffer.mutableAudioBufferList.pointee.mBuffers.mData!, count: bytesNeeded) // 数据不足时填充静音 if bytesRead < bytesNeeded { let silenceBytes = bytesNeeded - bytesRead memset(buffer.mutableAudioBufferList.pointee.mBuffers.mData!.advanced(by: bytesRead), 0, silenceBytes) } } do { try engine.start() } catch { print("音频引擎启动失败: \(error.localizedDescription)") } } // 启动播放 func startPlaying() { if !playerNode.isPlaying { playerNode.play() } } // 停止播放并清空缓冲区 func stopPlaying() { playerNode.stop() ringBuffer.reset() } // 处理Socket.io接收的音频数据 func processReceivedAudioData(_ data: Data) { let bytesWritten = ringBuffer.write(data) if bytesWritten != data.count { print("缓冲区已满,丢弃部分音频数据") } } }
4. 集成Socket.io数据接收
在你的Socket.io回调中直接调用音频处理方法:
// 假设已初始化Socket.io客户端 socket.on("audioData") { [weak self] dataArray, _ in guard let audioData = dataArray.first as? Data else { return } self?.audioPlayerManager.processReceivedAudioData(audioData) }
注意事项
- 权限申请:在
Info.plist中添加NSMicrophoneUsageDescription(iOS 16及以下)或NSAudioUsageDescription(iOS 17+),否则音频引擎无法启动 - 线程安全:Socket.io回调通常在后台线程,循环缓冲区已加锁保证线程安全
- 缓冲区调整:可根据网络稳定性调整缓冲区容量,平衡延迟和卡顿问题
- 错误处理:示例中错误处理较为基础,实际项目需补充引擎启动、播放状态的异常捕获
内容的提问来源于stack exchange,提问作者infamousbazooka
相关产品推荐
相关产品推荐

