Opus 音频编码学习指南:从原理到项目实践

本文档旨在结合 Opus 的标准协议和我们实际项目中的代码,为您梳理出一条清晰的 Opus 学习路径。Opus 是一种先进的、开源且免专利费的音频编码格式,在实时通讯(如 WebRTC)、流媒体等领域有着广泛的应用。

一、 Opus 基础知识

在深入编码细节前,先重温几个核心的音频概念:

  • 采样率 (Sample Rate): 一秒钟内对声音的采样次数(单位:Hz)。比如项目中常见的 16000 Hz (即 16kHz),常用于高质量的语音通话(宽带语音)。
  • 比特率/码率 (Bitrate): 一秒钟内传输的音频数据量(单位:bps)。比特率越高,音质越好,但占用的网络带宽也越大。项目中使用的是 16000 bps (16 kbps)。
  • 帧长 (Frame Duration): 音频数据被切割成一个个小段处理,每一段的时间长度。较短的帧长(如 2.5ms、10ms)延迟低,适合实时对讲;较长的帧(如 20ms、60ms)可以获得更高的压缩效率。项目中使用了 60ms 的长帧。
  • 单声道 (Channels): 声音的通道数。如果是 1,就是所有的声音信息混在一起;如果是 2,就是立体声。项目中是 1,即单声道。

为什么选择 Opus?

Opus 是一个“混血儿”,它内部其实包含了两种编码引擎,并能动态切换:

  1. SILK: 传承自 Skype,特别擅长**人声(语音)**编码,在低比特率下表现优异。
  2. CELT: 传承自 Xiph.Org 基金会,擅长音乐编码,在高频和高码率下表现出色。
  3. Hybrid: 混合模式,低频用 SILK,高频用 CELT。

在我们的项目中(OPUS_APPLICATION_VOIP,16kHz 采样率),主要用到的是 SILK 模式 和 宽带(WB - Wideband, 8kHz 带宽)。


二、 解析标准的 Opus 包结构 (TOC 字节)

无论是发送还是接收,Opus 在网络中传输的基本单位是“包 (Packet)”。每个 Opus 网络包必须要有一个“头”,这个头被称为 TOC (Table of Contents) 字节。

TOC 字节占 1 个 byte(8个 bit),类似于物流公司贴在包裹上的标签,它长这样:

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
| config  | s | c |
+-+-+-+-+-+-+-+-+
  • config (5 bits): 配置位。它决定了编码模式(SILK/CELT)、音频带宽(窄带/宽带等)以及每一帧的长度时长。Opus 定义了 32 种固定组合。
  • s (1 bit): 立体声标志位。0 代表单声道,1 代表立体声。
  • c (2 bits): 帧数标识位,即定义了你这一个包里面塞了多少帧(Frame)进去。它决定了包裹的结构(我们通常叫它 X 号包)。

Opus 的 4 种基础包裹骨架

根据 c (最后 2 个 bit)的值,Opus 包分为四种:

  1. 0 号包 (c=0, 00): 最简单的结构。一个包里只装一帧音频。
    [ TOC (1字节) ] + [ Opus压缩数据 (N字节) ]
    
  2. 1 号包 (c=1, 01): 一个包里装 两帧大小完全一样 的音频。因为两帧一样大,所以不需要记录它们的长度信息,直接对半分就好。
    [ TOC (1字节) ] + [ 帧1 数据 ] + [ 帧2 数据 ]
    
  3. 2 号包 (c=2, 10): 一个包里装 两帧大小不一样 的音频。由于不一样大,必须在 TOC 后面插入 1~2 字节来记录"第一帧有多大(N1)"。
    [ TOC ] + [ 帧1长度(1-2字节) ] + [ 帧1 数据 ] + [ 帧2 数据(直接算到末尾即可) ]
    
  4. 3 号包 (c=3, 11): 多帧包。这里可以放入大于等于 2 的任意多帧数据。它可以是固定比特率(CBR)的等长帧,也可以是可变比特率(VBR)的不等长帧,结构最复杂。包头还要包含一个字节 [ M ] 来标识具体有多少帧以及是否要填充。

三、 Opus 数据在当前代码项目中的流转分析

我们将文档中的理论放到我们项目代码中来看看具体的表现形式。

1. 配置参数 (opus_config.h)

这是项目定义的 Opus “身份卡”:

#define OPUS_SAMPLE_RATE 16000 /* 16kHz采样率 */
#define OPUS_CHANNELS 1        /* 单声道 */
#define OPUS_BIT_DEPTH 16      /* 16位采样深度 */
#define OPUS_BITRATE 16000     /* 16kbps比特率 */
#define OPUS_FRAME_MS 60       /* 60ms帧长 */
  • 推算包大小: 对于 16kbps 比特率,相当于每秒需要 16000 个 bits = 2000 个 bytes。一帧我们要 60ms,即 2000 * 0.06 = 120 bytes。
  • 代码中宏定义 OPUS_FRAME_SIZE 的计算正是如此。由于我们采用固定 60ms 一帧的处理方式,我们通常发向服务端的包都是 0 号包(单帧结构包)。

2. 发送流程:采集 → \rightarrow → 编码 → \rightarrow → 网络传输 (audio_input.c 和 demo.c)

  1. 配置发向硬件的编码环境:
    在 audio_input.c 的 audio_player_enc_init() 中:
    req.enc.format = "opus";
    req.enc.bitrate = OPUS_TARGET_BITRATE_BPS; // 16000
    req.enc.format_mode = 0;
    req.enc.frame_ms = 60;
    
    我们告诉底层的硬件编码服务开启 Opus 编码。
  2. 应用层拉取数据:
    当麦克风采集音频并在底层压缩完成后,会放在环形缓冲区 pcm_cbuff_w 中。tbz_demo.c 通过调用 _device_get_voice_data(audio_buf, OPUS_FRAME_SIZE) 将定长为 120 字节的一帧压缩数据读取出来。
    注意:这时候读出来的 audio_buf 前第一个字节一定是那一帧的 TOC 字节,后续的字节是真正的压缩后频域数据。
  3. 发送到服务器:
    send_hex_to_websocket(audio_buf, audio_read);
    
    应用原封不动地把这 120 字节([TOC] + [压缩数据])通过 WebSocket 以 Binary format 发射出去。符合 Opus 传输的标准规范。

3. 接收流程:网络接收 → \rightarrow → 增加特殊包头 → \rightarrow → 硬件解码 (websockets.c 和 audio_input.c)

这里展现了代码针对硬件特性的特殊处理设计,这是本项目的核心差异点。

  1. 接收到标准包:
    在 websockets.c 中:

    if (type == (u8)130 && buf && len > 8) { // 收到服务器发来的 Binary 数据
        _device_write_voice_data(buf, len);
    }
    

    此时传入 _device_write_voice_data 的 buf,是标准的 Opus 网络包(由 TOC 开头)。

  2. 穿上 8 字节的私有马甲 (核心操作):
    如果将网络上接收的裸数据直接扔进底层的解码循环缓冲区(cbuf),解码硬件可能无法知道“这到底是不是完整的一包”。因为底层驱动读数据是没有网络“帧边界”概念的,它只知道是一股水流。
    因此,在 audio_input.c 的 _device_write_voice_data 中被动了手脚:

    // 获取一个自增的包序号
    static unsigned int frame_index = 0;
    frame_index++;
    
    // 准备 8 个字节的头
    uint8_t packet_header[8] = {0};
    
    // 将 "该Opus帧的长度(len)" 和 "当前的序号(frame_index)" 以大端序编码到这 8 字节里
    uint32_to_big_endian_bytes(len, frame_index, packet_header);
    
    // 然后怎么写入?先写 8字节的马甲,再写真正的 Opus 网络数据
    cbuf_write(cbuf, packet_header, 8); // 写马甲
    cbuf_write(cbuf, data, len);        // 写本体
    
  3. 硬件如何识辨这个马甲:
    在初始化解码器 audio_player_dec_init 中,有一个非常重要的声明:

    req.dec.attr |= OPUS_DECODE_ATTR_WITH_8_BYTES; // 声明需要8字节包头的属性
    

    这个配置告知底层的解码固件:“兄弟,我给你的缓冲区数据不是普通的连续流,而是被我打包过的,每次你读数据前先解开那个 8 字节的头,里面明确告诉你接下来的这个 Opus 包包有多大”。

    当调用 dec_vfs_fread 读取数据给解码器时,其读取的数据块就遵循上述的拼接逻辑。


总结

学习本项目中的 Opus,可以归纳为一个中心,两个基本点:

  • 一个中心:Opus 是一个高效的音频压缩容器。它的核心是在包的开头有一个叫 TOC 的字节,通过这个字节记录声音如何被压缩、是不是立体声、包里面包含了多小帧。
  • 基本点1(上行,发向服务器):麦克风采集 -> 硬件芯片按 TOC+Data 压缩出一帧 -> 应用层读出这段以 TOC 打头的二进制 -> 顺原样传给 WebSocket。这部分完全符合标准。
  • 基本点2(下行,来自服务器):WebSocket 收到服务器发过来的 TOC+Data -> 应用层为了适配解码硬件的口味,给包前强行穿了一件标明长度和序号的「8字节马甲」 -> 扔进硬件缓冲区 -> 硬件喇叭出声。

通过对比标准和我们项目私有添加的这 8 个字节,就能彻底理解这个项目的编解码逻辑流转!

Logo

邀请您加入社区

更多推荐