OPUS编解码学习资料Opus 音频编码学习指南从原理到项目实践一、 Opus 基础知识为什么选择 Opus二、 解析标准的 Opus 包结构 (TOC 字节)Opus 的 4 种基础包裹骨架三、 Opus 数据在当前代码项目中的流转分析1. 配置参数 (opus_config.h)2. 发送流程采集→ \rightarrow→编码→ \rightarrow→网络传输 (audio_input.c 和 demo.c)3. 接收流程网络接收→ \rightarrow→增加特殊包头→ \rightarrow→硬件解码 (websockets.c 和 audio_input.c)总结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即单声道。为什么选择 OpusOpus 是一个“混血儿”它内部其实包含了两种编码引擎并能动态切换SILK: 传承自 Skype特别擅长**人声语音**编码在低比特率下表现优异。CELT: 传承自 Xiph.Org 基金会擅长音乐编码在高频和高码率下表现出色。Hybrid: 混合模式低频用 SILK高频用 CELT。在我们的项目中OPUS_APPLICATION_VOIP16kHz 采样率主要用到的是SILK 模式和宽带WB - Wideband, 8kHz 带宽。二、 解析标准的 Opus 包结构 (TOC 字节)无论是发送还是接收Opus 在网络中传输的基本单位是“包 (Packet)”。每个 Opus 网络包必须要有一个“头”这个头被称为TOC (Table of Contents) 字节。TOC 字节占 1 个 byte8个 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 号包 (c0,00): 最简单的结构。一个包里只装一帧音频。[ TOC (1字节) ] [ Opus压缩数据 (N字节) ]1 号包 (c1,01):一个包里装 两帧大小完全一样 的音频。因为两帧一样大所以不需要记录它们的长度信息直接对半分就好。[ TOC (1字节) ] [ 帧1 数据 ] [ 帧2 数据 ]2 号包 (c2,10):一个包里装 两帧大小不一样 的音频。由于不一样大必须在 TOC 后面插入 1~2 字节来记录第一帧有多大(N1)。[ TOC ] [ 帧1长度(1-2字节) ] [ 帧1 数据 ] [ 帧2 数据(直接算到末尾即可) ]3 号包 (c3,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()中req.enc.formatopus;req.enc.bitrateOPUS_TARGET_BITRATE_BPS;// 16000req.enc.format_mode0;req.enc.frame_ms60;我们告诉底层的硬件编码服务开启 Opus 编码。应用层拉取数据:当麦克风采集音频并在底层压缩完成后会放在环形缓冲区pcm_cbuff_w中。tbz_demo.c通过调用_device_get_voice_data(audio_buf, OPUS_FRAME_SIZE)将定长为 120 字节的一帧压缩数据读取出来。注意这时候读出来的audio_buf前第一个字节一定是那一帧的 TOC 字节后续的字节是真正的压缩后频域数据。发送到服务器:send_hex_to_websocket(audio_buf,audio_read);应用原封不动地把这 120 字节[TOC] [压缩数据]通过 WebSocket 以 Binary format 发射出去。符合 Opus 传输的标准规范。3. 接收流程网络接收→ \rightarrow→增加特殊包头→ \rightarrow→硬件解码 (websockets.c和audio_input.c)这里展现了代码针对硬件特性的特殊处理设计这是本项目的核心差异点。接收到标准包:在websockets.c中if(type(u8)130buflen8){// 收到服务器发来的 Binary 数据_device_write_voice_data(buf,len);}此时传入_device_write_voice_data的buf是标准的 Opus 网络包由TOC开头。穿上 8 字节的私有马甲 (核心操作):如果将网络上接收的裸数据直接扔进底层的解码循环缓冲区(cbuf)解码硬件可能无法知道“这到底是不是完整的一包”。因为底层驱动读数据是没有网络“帧边界”概念的它只知道是一股水流。因此在audio_input.c的_device_write_voice_data中被动了手脚// 获取一个自增的包序号staticunsignedintframe_index0;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上行发向服务器麦克风采集 - 硬件芯片按TOCData压缩出一帧 - 应用层读出这段以 TOC 打头的二进制 - 顺原样传给 WebSocket。这部分完全符合标准。基本点2下行来自服务器WebSocket 收到服务器发过来的TOCData-应用层为了适配解码硬件的口味给包前强行穿了一件标明长度和序号的「8字节马甲」- 扔进硬件缓冲区 - 硬件喇叭出声。通过对比标准和我们项目私有添加的这 8 个字节就能彻底理解这个项目的编解码逻辑流转