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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:52:40