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

iOS平台编码音频流为Opus时出现无效帧大小错误

Opus编码无效帧大小错误的解决方案

问题现象

在实现音频流编码为Opus格式并通过WebSocket发送的逻辑时,持续触发以下错误:

Invalid frame size: 4800. Must be one of [120, 240, 480, 960, 1920, 2880]

问题根源

你在setupAudioEngine中指定了opusCompatibleBufferSize = 960,但AVAudioEngine并不保证严格按照该值返回音频缓冲区——系统会根据硬件性能、当前负载等实际情况调整返回的帧长度,导致出现4800这类不符合Opus要求的帧大小。

解决方案

需要实现帧缓冲队列,将接收到的零散音频数据累积,直到凑够Opus要求的有效帧大小再进行编码。具体修改步骤如下:

1. 添加帧缓冲属性

在AudioManager类中新增两个属性,用于缓存未达到编码要求的音频数据:

private var pendingBuffer: AVAudioPCMBuffer? // 缓存未凑够大小的音频帧
private let targetFrameSize: AVAudioFrameCount = 960 // 目标Opus有效帧大小

2. 重写bufferAudioData方法

替换原有逻辑,实现帧的累积、拆分与编码:

private func bufferAudioData(_ buffer: AVAudioPCMBuffer) {
    guard let encoder = opusEncoder else {
        print("Opus编码器未初始化")
        return
    }

    // 验证音频格式是否符合要求
    if buffer.format.sampleRate != 48000 || buffer.format.channelCount != 1 {
        print("缓冲区格式不匹配:预期48000Hz单声道,实际\(buffer.format.sampleRate)Hz \(buffer.format.channelCount)声道")
        return
    }

    // 累积新接收的音频数据
    if let pending = pendingBuffer {
        let totalFrames = pending.frameLength + buffer.frameLength
        guard let combinedBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: totalFrames) else {
            print("无法创建合并缓冲区")
            pendingBuffer = nil
            return
        }
        pending.copy(to: combinedBuffer)
        buffer.copy(to: combinedBuffer, frameCount: buffer.frameLength, startingAt: pending.frameLength)
        pendingBuffer = combinedBuffer
    } else {
        pendingBuffer = buffer.copy() as? AVAudioPCMBuffer
    }

    guard let currentBuffer = pendingBuffer else { return }

    // 拆分出符合Opus要求的帧进行编码
    while currentBuffer.frameLength >= targetFrameSize {
        guard let targetBuffer = AVAudioPCMBuffer(pcmFormat: currentBuffer.format, frameCapacity: targetFrameSize) else {
            print("无法创建目标大小的缓冲区")
            pendingBuffer = nil
            return
        }
        currentBuffer.copy(to: targetBuffer, frameCount: targetFrameSize)
        
        // 编码并发送数据
        var opusData = Data()
        do {
            let bytesEncoded = try encoder.encode(targetBuffer, to: &opusData)
            print("编码成功,字节数:\(bytesEncoded)")
            
            if !opusData.isEmpty && canSendData {
                appManager?.webSocketManager.send(data: opusData) {
                    print("Opus编码数据已发送")
                }
            }
        } catch let error as Opus.Error {
            print("编码失败:Opus错误 \(error.rawValue) - \(interpretOpusError(error))")
        } catch {
            print("编码失败:\(error)")
        }

        // 更新剩余未处理的音频帧
        let remainingFrames = currentBuffer.frameLength - targetFrameSize
        if remainingFrames > 0 {
            guard let remainingBuffer = AVAudioPCMBuffer(pcmFormat: currentBuffer.format, frameCapacity: remainingFrames) else {
                pendingBuffer = nil
                return
            }
            currentBuffer.copy(to: remainingBuffer, frameCount: remainingFrames, startingAt: targetFrameSize)
            pendingBuffer = remainingBuffer
        } else {
            pendingBuffer = nil
        }
    }
}

3. 清理停止录音时的缓冲

在stopRecording方法中添加缓冲清理逻辑,避免残留数据干扰下次录音:

func stopRecording() {
    print("停止录音...")
    canSendData = false
    appManager?.webSocketManager.send(string: "{\"command\": \"aq_stop\"}") {
        print("已发送停止录音指令")
    }
    audioEngine?.stop()
    pendingBuffer = nil // 清理剩余未处理的缓冲数据
    print("录音已停止")
}

关键说明

  • Opus对输入帧大小有严格要求,48000Hz采样率下仅支持报错信息中列出的几个值
  • AVAudioEngine的Tap返回的缓冲区大小是建议值,系统可动态调整,因此必须自行处理帧的累积与拆分
  • 选择960作为目标帧大小是平衡延迟和编码效率的常用方案,你也可以根据需求换成其他有效值

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 05:34:52