第06章 opus编解码器
约 2576 字大约 9 分钟
2026-03-23
1. ESP_AUDIO_CODEC 组件
espressif/esp_audio_codec是 乐鑫(Espressif Systems)官方开发的音频编解码组件,专为 ESP 系列 SoC(如 ESP32、ESP32-S3、ESP32-C3 等)设计,用于在资源受限的嵌入式设备上高效地进行 音频编码(压缩)和解码(播放)。
提供统一、轻量、高性能的音频编解码能力,支持多种主流音频格式,适用于语音通信、音乐播放、录音上传、蓝牙音频等场景。
1.1. 音频编码器(Encoder)
支持格式:AAC、AMR-NB/WB、ADPCM、G711A/U、PCM、OPUS、ALAC、SBC、LC3 等。
特点:
- 可通过统一接口调用不同编码器(
esp_audio_enc.h)。 - 支持多实例并发编码(例如同时录两路语音)。
- 可注册自定义编码器或覆盖默认实现。
- 高度可配置(比特率、采样率、声道数、帧长等)。
- 可通过统一接口调用不同编码器(
典型用途:
- 将麦克风采集的 PCM 数据压缩为 AAC 或 OPUS 后上传到服务器。
- 蓝牙耳机中将语音编码为 SBC/msbc 发送给手机
1.2. 音频解码器(Decoder)
支持格式:AAC、MP3、AMR-NB/WB、ADPCM、G711A/U、OPUS、VORBIS、FLAC、ALAC、SBC、LC3 等。
特点:
- 统一接口管理所有解码器(
esp_audio_dec.h)。 - 要求输入数据是“完整音频帧”(frame-aligned)。
- 支持多实例并发解码。
- 统一接口管理所有解码器(
注意:
- 解码器本身 不负责解析文件容器(如 MP3 文件头、WAV 头),只处理原始帧数据。
1.3 安装
官方组件库中搜索 esp_audio_codec,获取下载指令:
idf.py add-dependency "espressif/esp_audio_codec^2.4.1"2. 编码器代码实现
2.1 创建编码器对象和添加输入输出缓冲区
audio_encoder.h
#ifndef __AUDIO_ENCODER_H__
#define __AUDIO_ENCODER_H__
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "unity.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_audio_enc_default.h"
#include "esp_audio_enc_reg.h"
#include "esp_audio_enc.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "freertos/ringbuf.h"
// 定义编码器对象类型的别名
typedef struct audio_encoder_struct audio_encoder_t;
/**
* @brief 创建编码器对象
*
* @return audio_encoder_t* 编码器对象
*/
audio_encoder_t *audio_encoder_create(void);
/**
* @brief 注册输入缓冲区
*
* @param audio_encoder 编码器对象
* @param input_buffer 输入缓冲区
*/
void audio_encoder_register_input_buffer(audio_encoder_t *audio_encoder, RingbufHandle_t input_buffer);
/**
* @brief 注册输出缓冲区
*
* @param audio_encoder 编码器对象
* @param output_buffer 输出缓冲区
*/
void audio_encoder_register_output_buffer(audio_encoder_t *audio_encoder, RingbufHandle_t output_buffer);
#endif /* __AUDIO_ENCODER_H__ */audio_encoder.c
#include "audio_encoder.h"
// 定义编码器对象的结构体类型
struct audio_encoder_struct
{
// 输入缓冲区
RingbufHandle_t input_buffer;
// 输出缓冲区
RingbufHandle_t output_buffer;
};
/**
* @brief 创建编码器对象
*
* @return audio_encoder_t* 编码器对象
*/
audio_encoder_t *audio_encoder_create(void)
{
// 在PSRAM中分配内存,用于存储编码器对象
return (audio_encoder_t *)heap_caps_malloc(sizeof(audio_encoder_t), MALLOC_CAP_SPIRAM);
}
/**
* @brief 注册输入缓冲区
*
* @param audio_encoder 编码器对象
* @param input_buffer 输入缓冲区
*/
void audio_encoder_register_input_buffer(audio_encoder_t *audio_encoder, RingbufHandle_t input_buffer)
{
audio_encoder->input_buffer = input_buffer;
}
/**
* @brief 注册输出缓冲区
*
* @param audio_encoder 编码器对象
* @param output_buffer 输出缓冲区
*/
void audio_encoder_register_output_buffer(audio_encoder_t *audio_encoder, RingbufHandle_t output_buffer)
{
audio_encoder->output_buffer = output_buffer;
}2.2 启动编码器进行编码
audio_encoder.h
// 代码省略 ...
/**
* @brief 启动编码器
*/
void audio_encode_start(audio_encode_t *audio_encode);
// 代码省略 ...audio_encoder.c
// 代码省略 ...
struct audio_encoder_struct
{
// 添加成员 ....
// 编码器句柄
esp_audio_enc_handle_t encoder;
};
// 代码省略 ...
// 静态函数:编码任务的任务函数
void audio_encoder_task_func(void *arg)
{
// 获取参数
audio_encoder_t *audio_encoder = (audio_encoder_t *)arg;
// 1. 注册 OPUS 编码器
esp_opus_enc_register();
// 2. 配置 OPUS 编码器
esp_opus_enc_config_t opus_cfg = {
.sample_rate = ESP_AUDIO_SAMPLE_RATE_16K, // 采样率是16K
.channel = ESP_AUDIO_MONO, // 单声道
.bits_per_sample = ESP_AUDIO_BIT16, // 位深
.bitrate = 90000, // 比特率
.frame_duration = ESP_OPUS_ENC_FRAME_DURATION_60_MS, // 帧长是60ms(小智服务器要求)
.application_mode = ESP_OPUS_ENC_APPLICATION_AUDIO, // 通用音频(音乐)优化
.complexity = 5, // 编码复杂度,5表示中等复杂度
.enable_fec = false,
.enable_dtx = false,
.enable_vbr = false,
};
esp_audio_enc_config_t enc_cfg = {
.type = ESP_AUDIO_TYPE_OPUS,
.cfg = &opus_cfg,
.cfg_sz = sizeof(opus_cfg)};
// 3. 打开编码器
esp_audio_enc_open(&enc_cfg, &audio_encoder->encoder);
// 4. 获取输入数据的长度和输出数据的长度
int pcm_size = 0, raw_size = 0;
esp_audio_enc_get_frame_size(audio_encoder->encoder, &pcm_size, &raw_size);
// 5. 根据输入数据长度和输出数据长度创建两个缓冲区
uint8_t *pcm_data = heap_caps_malloc(pcm_size, MALLOC_CAP_SPIRAM); // 输入缓冲区
uint8_t *raw_data = heap_caps_malloc(raw_size, MALLOC_CAP_SPIRAM); // 输出缓冲区
// 6. 创建输入数据帧和输出数据帧
esp_audio_enc_in_frame_t in_frame = {
.buffer = pcm_data,
.len = pcm_size,
};
esp_audio_enc_out_frame_t out_frame = {
.buffer = raw_data,
.len = raw_size,
};
// 7. 循环进行编码
while (1)
{
// 7.1 从编码对象的输入缓冲区(input_buffer)中取出数据,给到 pcm_data, 如果取不出数据一直阻塞
size_t len = 0;
uint8_t *data = (uint8_t *)xRingbufferReceive(audio_encoder->input_buffer, &len, portMAX_DELAY);
memcpy(pcm_data, data, len);
vRingbufferReturnItem(audio_encoder->input_buffer, data); // 释放环形缓冲区中被取出的数据
// 7.2 对取出的数据进行编码
esp_audio_enc_process(audio_encoder->encoder, &in_frame, &out_frame);
// 7.3 将编码好的数据放入编码器对象的输出缓冲区(output_buffer); 注意 out_frame.encoded_bytes 才是实际的编码后的数据长度
xRingbufferSend(audio_encoder->output_buffer, out_frame.buffer, out_frame.encoded_bytes, portMAX_DELAY);
}
}
// 代码省略 ...
/**
* @brief 启动编码器
*/
void audio_encode_start(audio_encode_t *audio_encode)
{
// 创建编码任务
xTaskCreateWithCaps(audio_encoder_task_func, "audio_encoder_task", 32 * 1024, audio_encoder, 10, NULL, MALLOC_CAP_SPIRAM);
}
// 代码省略 ...2.3 编码器获取定长数据
audio_encoder.c
// 静态函数:从编码器对象的输入缓冲区中取出指定长度的数据
// input_buffer:输入缓冲区
// pcm_data: 将取到的数据存入该地址
// pcm_size: 目标长度
static void audio_encoder_get_fix_len_data(RingbufHandle_t input_buffer, uint8_t *pcm_data, int pcm_size)
{
// 定义指针,存储单次从输入缓冲区取出的数据
uint8_t *data = NULL;
// 定义变了,保存单词从输入缓冲区取出的数据长度
size_t len = 0;
// 知道取到目标长度的数据,才停止
while (pcm_size > 0)
{
// 先从输入缓冲区取出1次数据, xRingbufferReceiveUpTo 单次最大长度不超过 pcm_size
// data = (uint8_t *)xRingbufferReceive(input_buffer, &len, portMAX_DELAY);
data = (uint8_t *)xRingbufferReceiveUpTo(input_buffer, &len, portMAX_DELAY, pcm_size);
// 将取出的数据放入pcm_data
memcpy(pcm_data, data, len);
// 释放本次输入缓冲区中被取出的数据
vRingbufferReturnItem(input_buffer, data);
// 目标长度减去len
pcm_size -= len;
// pcm_data指针后移
pcm_data += len;
}
}
// 代码省略 ...
/**
* @brief 编码器任务函数
*/
static void audio_encode_task(void *arg)
{
// 代码省略 ...
// 7. 持续编码
while (1)
{
// 7.1 从编码对象的输入缓冲区(input_buffer)中取出数据, 需要满足 pcm_size 的长度要求
audio_encoder_get_fix_len_data(audio_encoder->input_buffer, pcm_data, pcm_size);
// 代码省略 ...
}
}
// 代码省略 ...3. 解码器代码实现
3.1 创建解码器对象和添加输入输出缓冲区
audio_decoder.h
#ifndef __AUDIO_DECODER_H__
#define __AUDIO_DECODER_H__
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "unity.h"
#include "esp_err.h"
#include "esp_system.h"
#include "esp_audio_dec_default.h"
#include "esp_audio_dec.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "freertos/ringbuf.h"
// 定义解码器对象结构体类型别名
typedef struct audio_decoder_struct audio_decoder_t;
/**
* @brief 创建解码器对象
*/
audio_decoder_t *audio_decoder_create();
/**
* @brief 注册输入缓冲区函数
*/
void audio_decoder_register_input_ringbuf(audio_decoder_t *audio_decoder, RingbufHandle_t ringbuf_in);
/**
* @brief 注册输出缓冲区函数
*/
void audio_decoder_register_output_ringbuf(audio_decoder_t *audio_decoder, RingbufHandle_t ringbuf_out);
#endif /* __AUDIO_DECODER_H__ */audio_decoder.c
#include "audio_decoder.h"
// 定义解码器对象结构体
struct audio_decoder_struct
{
// 解码器的输入缓冲区
RingbufHandle_t input_buffer;
// 解码器的输出缓冲区
RingbufHandle_t output_buffer;
};
/**
* @brief 创建解码器对象
*/
audio_decoder_t *audio_decoder_create()
{
audio_decoder_t *audio_decoder = (audio_decoder_t *)heap_caps_malloc(sizeof(audio_decoder_t), MALLOC_CAP_SPIRAM);
return audio_decoder;
}
/**
* @brief 注册输入缓冲区函数
*/
void audio_decoder_register_input_ringbuf(audio_decoder_t *audio_decoder, RingbufHandle_t ringbuf_in)
{
audio_decoder->input_buffer = ringbuf_in;
}
/**
* @brief 注册输出缓冲区函数
*/
void audio_decoder_register_output_ringbuf(audio_decoder_t *audio_decoder, RingbufHandle_t ringbuf_out)
{
audio_decoder->output_buffer = ringbuf_out;
}3.2 启动解码器进行解码
audio_decoder.h
// 省略代码 ...
/**
* @brief 启动解码器
*/
void audio_decoder_start(audio_decoder_t *audio_decoder);
// 省略代码 ...audio_decoder.c
// 定义解码器对象结构体
struct audio_decoder_struct
{
// 解码器的输入缓冲区
RingbufHandle_t input_buffer;
// 解码器的输出缓冲区
RingbufHandle_t output_buffer;
// 解码器的句柄
esp_audio_dec_handle_t decoder;
};
// 静态函数:解码任务的任务函数
void audio_decoder_task_func(void *arg)
{
// 获取参数
audio_decoder_t *audio_decoder = (audio_decoder_t *)arg;
// 1. 注册 OPUS 解码器
esp_opus_dec_register();
// 2. 配置 OPUS 解码器
esp_opus_dec_cfg_t opus_cfg = {
.sample_rate = 16000, // 采样率
.channel = 1, // 单声道
.frame_duration = ESP_OPUS_DEC_FRAME_DURATION_60_MS, // 帧时长
};
esp_audio_dec_cfg_t dec_cfg = {
.type = ESP_AUDIO_TYPE_OPUS,
.cfg = &opus_cfg,
.cfg_sz = sizeof(opus_cfg),
};
// 3. 打开解码器
esp_audio_dec_open(&dec_cfg, &audio_decoder->decoder);
// 4. 创建两个数据帧
// 4.1 输入数据帧
esp_audio_dec_in_raw_t in_raw = {
.buffer = NULL,
.len = 0,
};
// 4.2 输出数据帧
esp_audio_dec_out_frame_t out_frame = {
.buffer = (uint8_t *)heap_caps_malloc(1920, MALLOC_CAP_SPIRAM),
.len = 1920,
};
// 5. 循环解码
while (1)
{
// 5.1 将编码器对象中输入缓冲区的数据给到in_raw
in_raw.buffer = xRingbufferReceive(audio_decoder->input_buffer, &in_raw.len, portMAX_DELAY);
// 5.2 解码
esp_audio_dec_process(audio_decoder->decoder, &in_raw, &out_frame);
// 5.3 把解码后的数据 交给输出缓冲区
xRingbufferSend(audio_decoder->output_buffer, out_frame.buffer, out_frame.decoded_size, portMAX_DELAY);
// 5.4 释放输入缓冲区数据
vRingbufferReturnItem(audio_decoder->input_buffer, in_raw.buffer);
}
}
// 代码省略 ...
/**
* @brief 启动解码器
*/
void audio_decoder_start(audio_decoder_t *audio_decoder)
{
// 创建解码任务,
xTaskCreateWithCaps(audio_decoder_task_func, "audio_decoder_task", 32 * 1024, audio_decoder, 10, NULL, MALLOC_CAP_SPIRAM);
}
// 代码省略 ...4 测试
main.c
#include <stdio.h>
#include "esp_task.h"
#include "com_debug.h"
#include "int_es8311.h"
#include "audio_sr.h"
#include "audio_encoder.h"
#include "audio_decoder.h"
// 定义语音活动检测的回调函数
void app_main_vad_cb(vad_state_t vad_state)
{
if (vad_state == VAD_SPEECH)
{
MY_LOGI("检测到声音!");
}
else
{
MY_LOGI("检测到静音!");
}
}
// 定义检测到唤醒词次回调函数
void app_main_wakeup_cb(void)
{
MY_LOGI("AI小智被唤醒!");
}
void app_main(void)
{
MY_LOGI("Opus 编解码器功能测试:");
// 1. 初始化 ES8311 音频编解码器
int_es8311_init();
// 2. SR对象
// 2.1创建SR对象
audio_sr_t *audio_sr = audio_sr_create();
// 2.2注册回调函数
audio_sr_register_vad_cb(audio_sr, app_main_vad_cb);
audio_sr_register_wakeup_cb(audio_sr, app_main_wakeup_cb);
// 2.3创建环形缓冲区(放入PSARM)并添加, 必须选择RINGBUF_TYPE_BYTEBUF
RingbufHandle_t sr_out_buffer = xRingbufferCreateWithCaps(64 * 1024, RINGBUF_TYPE_BYTEBUF, MALLOC_CAP_SPIRAM);
audio_sr_register_ringbuf(audio_sr, sr_out_buffer);
// 3. OPUS编码器对象
// 3.1 创建编码器对象
audio_encoder_t *audio_encoder = audio_encoder_create();
// 3.2 将SR的输出缓冲区连接到编码器的输入缓冲区
audio_encoder_register_input_buffer(audio_encoder, sr_out_buffer);
// 3.3 创建环形缓冲区作为编码器的输出缓冲区
RingbufHandle_t encoder_out_buffer = xRingbufferCreateWithCaps(64 * 1024, RINGBUF_TYPE_NOSPLIT, MALLOC_CAP_SPIRAM);
audio_encoder_register_output_buffer(audio_encoder, encoder_out_buffer);
// 4. OPUS解码器对象
// 4.1 创建解码器对象
audio_decoder_t *audio_decoder = audio_decoder_create();
// 4.2 将编码器的输出缓冲区连接到解码器的输入缓冲区
audio_decoder_register_input_ringbuf(audio_decoder, encoder_out_buffer);
// 4.3 创建环形缓冲区作为解码器的输出缓冲区
RingbufHandle_t decoder_out_buffer = xRingbufferCreateWithCaps(64 * 1024, RINGBUF_TYPE_NOSPLIT, MALLOC_CAP_SPIRAM);
audio_decoder_register_output_ringbuf(audio_decoder, decoder_out_buffer);
// 5. 启动
audio_decoder_start(audio_decoder);
audio_encoder_start(audio_encoder);
audio_sr_start(audio_sr);
// 定义变量,保存从环形缓冲区获取到的数据长度
size_t len;
// 定义指针,用于指向从环形缓冲区获取到的数据
void *data;
while (1)
{
// 解码器的输出缓冲区获取数据,获取不到一直阻塞
data = xRingbufferReceive(decoder_out_buffer, &len, portMAX_DELAY);
// 将获取到的数据写入扬声器
int_es8311_write_speaker(data, len);
// 释放环形缓冲区的资源
vRingbufferReturnItem(decoder_out_buffer, data);
}
}注意:
为SR对象创建环形缓冲区的时候,类型必须是
RINGBUF_TYPE_BYTEBUF应为SR对象的环形缓冲区要作为编码器对象的输入缓冲区,编码器在从输入缓冲区取数据的时候使用了
xRingbufferReceiveUpTo()函数,该函数要求被读取的环形缓冲区类型必须是RINGBUF_TYPE_BYTEBUF
