Opus 音频编码学习基于杰理的开发
OPUS编解码学习资料
- Opus 音频编码学习指南:从原理到项目实践
- 一、 Opus 基础知识
- 为什么选择 Opus?
- 二、 解析标准的 Opus 包结构 (TOC 字节)
- Opus 的 4 种基础包裹骨架
- 三、 Opus 数据在当前代码项目中的流转分析
- 总结
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 是一个“混血儿”,它内部其实包含了两种编码引擎,并能动态切换:
- SILK: 传承自 Skype,特别擅长**人声(语音)**编码,在低比特率下表现优异。
- CELT: 传承自 Xiph.Org 基金会,擅长音乐编码,在高频和高码率下表现出色。
- 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 包分为四种:
- 0 号包 (
c=0,00): 最简单的结构。一个包里只装一帧音频。[ TOC (1字节) ] + [ Opus压缩数据 (N字节) ] - 1 号包 (
c=1,01):一个包里装 两帧大小完全一样 的音频。因为两帧一样大,所以不需要记录它们的长度信息,直接对半分就好。[ TOC (1字节) ] + [ 帧1 数据 ] + [ 帧2 数据 ] - 2 号包 (
c=2,10):一个包里装 两帧大小不一样 的音频。由于不一样大,必须在 TOC 后面插入 1~2 字节来记录"第一帧有多大(N1)"。[ TOC ] + [ 帧1长度(1-2字节) ] + [ 帧1 数据 ] + [ 帧2 数据(直接算到末尾即可) ] - 3 号包 (
c=3,11):多帧包。这里可以放入大于等于 2 的任意多帧数据。它可以是固定比特率(CBR)的等长帧,也可以是可变比特率(VBR)的不等长帧,结构最复杂。包头还要包含一个字节[ M ]来标识具体有多少帧以及是否要填充。
三、 Opus 数据在当前代码项目中的流转分析
我们将文档中的理论放到我们项目代码中来看看具体的表现形式。
1. 配置参数 (opus_config.h)
这是项目定义的 Opus “身份卡”:
#defineOPUS_SAMPLE_RATE16000/* 16kHz采样率 */#defineOPUS_CHANNELS1/* 单声道 */#defineOPUS_BIT_DEPTH16/* 16位采样深度 */#defineOPUS_BITRATE16000/* 16kbps比特率 */#defineOPUS_FRAME_MS60/* 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)
- 配置发向硬件的编码环境:
在audio_input.c的audio_player_enc_init()中:
我们告诉底层的硬件编码服务开启 Opus 编码。req.enc.format="opus";req.enc.bitrate=OPUS_TARGET_BITRATE_BPS;// 16000req.enc.format_mode=0;req.enc.frame_ms=60; - 应用层拉取数据:
当麦克风采集音频并在底层压缩完成后,会放在环形缓冲区pcm_cbuff_w中。tbz_demo.c通过调用_device_get_voice_data(audio_buf, OPUS_FRAME_SIZE)将定长为 120 字节的一帧压缩数据读取出来。
注意:这时候读出来的audio_buf前第一个字节一定是那一帧的 TOC 字节,后续的字节是真正的压缩后频域数据。 - 发送到服务器:
应用原封不动地把这 120 字节(send_hex_to_websocket(audio_buf,audio_read);[TOC] + [压缩数据])通过 WebSocket 以 Binary format 发射出去。符合 Opus 传输的标准规范。
3. 接收流程:网络接收→ \rightarrow→增加特殊包头→ \rightarrow→硬件解码 (websockets.c和audio_input.c)
这里展现了代码针对硬件特性的特殊处理设计,这是本项目的核心差异点。
接收到标准包:
在websockets.c中:if(type==(u8)130&&buf&&len>8){// 收到服务器发来的 Binary 数据_device_write_voice_data(buf,len);}此时传入
_device_write_voice_data的buf,是标准的 Opus 网络包(由TOC开头)。穿上 8 字节的私有马甲 (核心操作):
如果将网络上接收的裸数据直接扔进底层的解码循环缓冲区(cbuf),解码硬件可能无法知道“这到底是不是完整的一包”。因为底层驱动读数据是没有网络“帧边界”概念的,它只知道是一股水流。
因此,在audio_input.c的_device_write_voice_data中被动了手脚:// 获取一个自增的包序号staticunsignedintframe_index=0;frame_index++;// 准备 8 个字节的头uint8_tpacket_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);// 写本体硬件如何识辨这个马甲:
在初始化解码器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 个字节,就能彻底理解这个项目的编解码逻辑流转!
