一、音频AAC编码
1.1音频编码的原理
音频为什么要进行编码压缩?我们就以PCM原始数据为例,假设这个PCM数据采样率为:48000、采样深度:16bit、声道数:2。对应的码率是:48000 * 16bit * 2 = 1536000bps ~=1.46M **,**若传输一分钟那就是1.46M * 60S~ = 87.6M。这个数据量是非常大,若在网络传输上这个音频的数据量很容易造成网络的负载压力。所以此时我们就需要对音频进行编码压缩,音频编码压缩格式分很多种,比方说:MP3、AAC、OGG格式。我们的课程重点来说AAC编码格式,因为AAC编码在网络传输中质量最好,并且AAC的压缩比高达1:18,是所有音频编码技术中压缩比最高。
1.2 AAC 编码的特点
每个AAC音频帧包含了多个音频采样的压缩数据,AAC的一个音频帧包含1024个采样值。由于原始数据块它是以帧的形式存在,我们称之为原始帧。在AAC中一般有两种方式来封装,一种是ADIF,另外一种是ADTS。
1.2.1. ADIF 格式
音频数据交换格式,这种格式必须在定义的音频数据流进行处理,基本上用于存储磁盘文件中
1.2.2. ADTS 格式
音频数据传输流,这种格式是最常用的格式。它的特点是会同步字的比特流,并且允许在音频数据流任意帧解码。换言之,就是它的每一帧都有信息头,一个是AAC原始数据长度是可变,对原始帧加上ADTS头进行封装就生成ADTS帧。AAC的每一帧数据由ADTS Header和AAC Audio Data组成,其中ADTS Header占有7个字节-9个字节,ADTS Header由两部分组成分别是:固定头部信息 (adts_fixed_header)和可变头信息(adts_variable_header),固定头信息指的是数据每一帧都是相同的,它主要定义了音频的采样率、声道数、帧长度等信息;可变头信息则主要描述帧和帧之间的可变。下面是adts帧的结构

1. adts_fixed_header 的参数:

2. 采样率下标和采样率关系

3. adts_variable_header 的参数:

二、AENC模块
2.1 RV1126 的 AENC 模块的介绍
RV1126的AENC模块是音频编码模块,主要是对AI模块进来的数据进行音频编码压缩处理,并输出对应的音频压缩码流,下面是AENC模块在RV1126里面和AI模块的关系。

2.2 RV1126 的 AENC 模块参数
设置AENC模块的是AENC_CHN_ATTR_S 结构体,下面我们重点看看这个结构体属性的具体定义:

2.2.1. enCodecType :
音频编码协议类型,下面是AENC支持的音频编码格式

2.2.2. u32Bitrate :
音频编码比特率,音频编码每秒传输的数据量。AAC 编码协议推荐使用 64kpbs(64000bps) ; G711A 、 G711U 编码协议推荐使用 64kps(64000bps) ; G726 推荐使用 32kpbs(32000bps) 。
2.2.3. u32Quality:
音频编码质量,默认是1
2.2.4.stAencAAC 、 stAencMp2 、 stAencG711A 、 stAencG711U 、 stAencG726 :
这几个结构体是不同的音频编码器的专门协议属性结构体
1 .stAencAAC**:它是** AAC 编码协议属性

u32Channels **:**编码通道数
u32SampleRate **:**音频采样率,AAC的采样率范围是7350-96000
2 .stAencMp2**:** 它是MP2编码协议属性

u32Channels **:**编码通道数
u32SampleRate **:**音频采样率,MP2的采样率范围是0-48000
3 .stAencG711A**:** 它是G711A编码协议属性

u32Channels **:**编码通道数
u32SampleRate **:**音频采样率,G711A的采样率是8KHZ
u32NbSamples **:**采样个数,默认的采样个数是320
4 .stAencG711U**:** 它是G711U编码协议属性

u32Channels **:**编码通道数
u32SampleRate **:**音频采样率,G711U的采样率是8KHZ
u32NbSamples **:**采样个数,默认的采样个数是320
2.3 设置 AENC 的 API 属性

**第一个参数:**AENC的通道号,取值范围是0,16
**第二个参数:**AENC_CHN_ATTR的结构体指针
三、获取AAC码流
3.1 RV1126 多线程获取音频编码 AAC 码流的流程

上面是RV1126多线程获取AAC码流的流程,分为六步:AI模块的初始化并使能、AENC模块的初始化、绑定AI模块和AENC模块、创建多线程获取AAC码流、向每个 AAC 码流添加 ADTSHeader 头部 ( 这个是重点 )、写入具体每一帧AAC的ES码流。
3.1.1.AI 模块的初始化并使能
AI模块的初始化实际上就是对AI_CHN_ATTR_S 的参数进行设置、然后调用RK_MPI_AI_SetChnAttr 设置AI模块并使能RK_MPI_AI_EnableChn **,**伪代码如下:
AI_CHN_ATTR_S ai_chn_s;
ai_chn_s.pcAudioNode = AUDIO_PATH;
ai_chn_s.u32Channels = 2;
ai_chn_s.u32NbSamples = 1024;
ai_chn_s.u32SampleRate = 48000;
ai_chn_s.enAiLayout = AI_LAYOUT_NORMAL;
ai_chn_s.enSampleFormat = RK_SAMPLE_FMT_S16;
ret = RK_MPI_AI_SetChnAttr(AI_CHN, &ai_chn_s);
if(ret)
{
printf("RK_MPI_AI_SetChnAttr Failed...\n");
}
ret = RK_MPI_AI_EnableChn(AI_CHN);
if(ret)
{
printf("RK_MPI_AI_EnableChn Failed...\n");
}
3.1.2.AENC 模块的初始化
AENC模块的初始化实际上就是对AI_CHN_ATTR_S 的参数进行设置、然后调用RK_MPI_AENC_CreateChn设置AENC模块伪代码如下:
AENC_CHN_ATTR_S aenc_attr;
aenc_attr.enCodecType = RK_CODEC_TYPE_AAC;
aenc_attr.u32Bitrate = 64000;
aenc_attr.u32Quality = 1;
aenc_attr.stAencAAC.u32Channels = 2;
aenc_attr.stAencAAC.u32SampleRate = 48000;
ret = RK_MPI_AENC_CreateChn(AENC_CHN, &aenc_attr);
if (ret)
{
printf("Create AENC[0] failed! ret=%d\n", ret);
return -1;
}
else
{
printf("Create AENC[0] success!\n");
}
3.1.3.绑定 AI 模块和 AENC 模块
分别创建两个MPP_CHN_S结构体,分别是AI模块的MPP_CHN_S和AENC模块的MPP_CHN_S,创建完成之后则用RK_MPI_SYS_Bind对两个模块进行绑定
MPP_CHN_S ai_mpp_chn_s;
ai_mpp_chn_s.enModId = RK_ID_AI;
ai_mpp_chn_s.s32ChnId = 0;
MPP_CHN_S aenc_mpp_chn_s;
aenc_mpp_chn_s.enModId = RK_ID_AENC;
aenc_mpp_chn_s.s32ChnId = 0;
ret = RK_MPI_SYS_Bind(&ai_mpp_chn_s, &aenc_mpp_chn_s);
if (ret)
{
printf("RK_MPI_SYS_Bind Failed....\n");
}
else
{
printf("RK_MPI_SYS_Bind Success....\n");
}
3.1.4.多线程获取每一帧 AAC 码流
开启一个线程去采集每一帧AENC模块的数据,使用的API是RK_MPI_SYS_GetMediaBuffer , 模块ID是RK_ID_AENC,通道号ID是AENC创建的通道ID号**。这里需要注意的是,要进行两层写入。第一层要进行** adts header 头部的写入,第二层则需要进行 adts es 流的写入
....................................................
While(1)
{
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_AENC, AENC_CHN, -1);
if (!mb)
{
printf("Get Aenc_Buffer break...\n");
break;
}
...................................
fwrite(aac_header, 1, 7, aac_file); //对每一帧AAC码流写入adts_header头部
fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), aac_file); //写入每一帧AAC的ES码流
}
.......................................................
3.1.5.每个 AAC 码流添加 ADTSHeader 头部
在写入AACES码流前需要对其进行ADTS Header的封装,adts header头部分为:adts_fixed_header和adts_variable_header。在对其写入的时候需要把这两个头部都一并写到码流上面。具体的写入格式以下面两张表(adts_fixed_header和adts_variable_header)作为参考

(adts_fixed_header) (adts_variable_header)
根据上面两张表的信息,RK官方封装了一套写入AAC的方法,下面就是封装的具体方法。
typedef struct AacFreqIdx_
{
RK_S32 u32SmpRate;
RK_U8 u8FreqIdx;
} AacFreqIdx;
AacFreqIdx AacFreqIdxTbl[13] = {{96000, 0}, {88200, 1}, {64000, 2}, {48000, 3}, {44100, 4}, {32000, 5}, {24000, 6}, {22050, 7}, {16000, 8}, {12000, 9}, {11025, 10}, {8000, 11}, {7350, 12}};
static void GetAdtsHeader(RK_U8 *pu8AdtsHdr, RK_S32 u32SmpleRate, RK_U8 u8Channel,
RK_U32 u32DataLen)
{
RK_U8 u8FreqIdx = 0;
for (int i = 0; i < 13; i++)
{
if (u32SmlpRate == AacFreqIdxTbl[i].u32SmpRate)
{
u8FreqIdx = AacFreqIdxTbl[i].u8FreqIdx;
break;
}
}
RK_U32 u32PacketLen = u32DataLen + 7;
pu8AdtsHdr[0] = 0xFF; //主要是写入syncword同步字节的前8位
pu8AdtsHdr[1] = 0xF1; //主要是写入syncword同步字节的后4位,并且设置ID号、layer、protection_absent
pu8AdtsHdr[2] = ((AAC_PROFILE_LOW) << 6) + (u8FreqIdx << 2) + (u8Channel >> 2); //设置音频profile、sample_rate_index、声道数
pu8AdtsHdr[3] = (((u8Channel & 3) << 6) + (u32PacketLen >> 11)); //设置声道数,original_copy,home,copyright_identification_bit、copyright_identification_start、aac_frame_length
pu8AdtsHdr[4] = ((u32PacketLen & 0x7FF) >> 3); //设置aac_frame_length+adts_buffer_fullness
pu8AdtsHdr[5] = (((u32PacketLen & 7) << 5) + 0x1F);//设置adts_buffer_fullness + number_of_raw_data_blocks_in_frame
pu8AdtsHdr[6] = 0xFC; //设置 number_of_raw_data_blocks_in_frame
}
上面这个方法需要传入四个参数,分别是:
第一个参数pu8AdtsHdr **:**需要处理输出的字符串
第二个参数u32SampleRate : 音频的采样率(根据音频采样率去查找对应的采样率索引,这里的索引是AacFreqIdx)
第三个参数u8Channel **:**音频通道数
第四个参数u32DataLen **:**每一帧aac码流的长度(这里需要注意的是,在写入AAC码流的时候需要把每个AAC长度+7,因为头部是adts的头部是7个字节)
设置完成之后,就要对每个码流进行7个字节头部的写入。
fwrite(aac_header, 1, 7, aac_file); //对每一帧AAC码流写入adts_header头部
3.1.6.写入具体每一帧 AAC 的 ES 码流
在写入AAC头部之后,就可以写入每一帧具体的AAC的ES码流
..................................................
fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), aac_file); //写入每一帧AAC的ES码流
....................................................
3.2 代码
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
// RKMedia SDK头文件,多媒体音视频基础API
#include "include/rkmedia/rkmedia_api.h"
// RKMedia缓冲区操作相关API
#include "include/rkmedia/rkmedia_buffer.h"
// RKMedia通用枚举、结构体定义
#include "include/rkmedia/rkmedia_common.h"
// 去除重复包含的rkmedia_api.h,避免冗余警告
// 音频采集硬件设备节点,hw:0,0对应板载ES8311声卡,替换default提升兼容性
#define AUDIO_PATH "hw:0,0"
// AI音频采集通道号,RV1126单麦场景固定使用通道0
#define AI_CHN 0
// AAC硬件编码通道号,独立编码通道0
#define AENC_CHN 0
// AAC-LC低复杂度编码标准,日常录音通用profile
#define AAC_PROFILE_LOW 1
// 全局退出标记,用于Ctrl+C优雅停止采集、释放资源
static int g_exit_flag = 0;
/**
* @brief AAC采样率与ADTS头部freq_idx映射结构体
* @u32SampleRate 音频采样率数值
* @u8FreqIdx ADTS头部需要填入的采样率索引值
*/
typedef struct AacFreqIdx_
{
RK_S32 u32SampleRate;
RK_U8 u8FreqIdx;
} AacFreqIdx;
/**
* @brief 13档标准AAC采样率查表数组,ADTS头生成时匹配索引
*/
AacFreqIdx AacFreqIdxTbl[13] = {
{96000, 0}, {88200, 1}, {64000, 2}, {48000, 3},
{44100, 4}, {32000, 5}, {24000, 6}, {22050, 7},
{16000, 8}, {12000, 9}, {11025, 10}, {8000, 11}, {7350, 12}
};
/**
* @brief Ctrl+C中断信号捕获回调函数
* @param sig 触发的信号编号,SIGINT对应键盘Ctrl+C
* @desc 收到中断后置全局退出标记,线程与主线程检测后有序释放资源
*/
void sigint_handler(int sig)
{
if (sig == SIGINT)
{
printf("\n[INFO] Receive Ctrl+C signal, stop recording and release resource...\n");
g_exit_flag = 1;
}
}
/**
* @brief 生成AAC标准7字节ADTS头部
* @param pu8AdtsHdr [OUT] 输出7字节ADTS头缓冲区
* @param u32SampleRate [IN] 当前音频采样率
* @param u8Channel [IN] 声道数量(1单声道/2双声道)
* @param u32DataLen [IN] 单帧裸AAC码流数据长度
* @desc 纯AAC裸码流无文件头,播放器无法识别,每帧必须拼接ADTS头才能正常播放
*/
static void GetAdtsHeader(RK_U8 *pu8AdtsHdr, RK_S32 u32SampleRate, RK_U8 u8Channel,
RK_U32 u32DataLen)
{
RK_U8 u8FreqIdx = 0;
// 遍历查表,匹配当前采样率对应的ADTS索引
for (int i = 0; i < 13; i++)
{
if (u32SampleRate == AacFreqIdxTbl[i].u32SampleRate)
{
u8FreqIdx = AacFreqIdxTbl[i].u8FreqIdx;
break;
}
}
// ADTS单帧总长度 = 7字节头部 + AAC裸数据长度
RK_U32 u32PacketLen = u32DataLen + 7;
pu8AdtsHdr[0] = 0xFF; // ADTS同步字高8bit,播放器识别帧起始标志
pu8AdtsHdr[1] = 0xF1; // 同步字低4bit + 编码层标识、无校验位
pu8AdtsHdr[2] = ((AAC_PROFILE_LOW) << 6) + (u8FreqIdx << 2) + (u8Channel >> 2); // AAC编码规格、采样率索引、声道高2位
pu8AdtsHdr[3] = (((u8Channel & 3) << 6) + (u32PacketLen >> 11)); // 声道低2位 + 帧总长度高2bit
pu8AdtsHdr[4] = ((u32PacketLen & 0x7FF) >> 3); // 帧总长度中间8bit
pu8AdtsHdr[5] = (((u32PacketLen & 7) << 5) + 0x1F); // 帧总长度低3bit + 缓冲区满值占位
pu8AdtsHdr[6] = 0xFC; // 单帧原始数据块数量,固定1块
}
/**
* @brief AAC码流读取子线程
* @param args 线程入参,本程序未使用
* @return NULL 线程正常退出返回空指针,消除C++无返回值编译警告
* @desc 后台循环读取硬件AAC编码输出码流,拼接ADTS头部写入本地.aac文件
*/
void *get_audio_aenc_thread(void *args)
{
// 设置分离线程:线程退出后系统自动回收栈资源,主线程无需pthread_join等待
pthread_detach(pthread_self());
// 二进制读写模式打开AAC文件,不存在则创建,存在则覆盖
FILE *aac_file = fopen("test_capture.aac", "w+");
// 文件打开合法性校验,无写入权限/磁盘满时直接退出线程,避免空指针崩溃
if (NULL == aac_file)
{
printf("[ERROR] Open test_capture.aac failed! No write permission?\n");
return NULL;
}
MEDIA_BUFFER mb; // RKMedia媒体缓冲区,承载一帧AAC编码码流
RK_U8 aac_header[7]; // 存储单帧7字节ADTS头部
// 用全局退出标记替代永久死循环,收到Ctrl+C后自动跳出循环
while (0 == g_exit_flag)
{
// 阻塞获取AENC编码通道缓冲区,-1代表永久阻塞,无码流时线程休眠不占用CPU
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_AENC, AENC_CHN, -1);
if (!mb)
{
printf("[ERROR] RK_MPI_SYS_GetMediaBuffer break....\n");
break;
}
printf("[INFO] Get AAC_Buffer success...\n");
// 根据当前音频参数生成对应ADTS头部
GetAdtsHeader(aac_header, 48000, 2, RK_MPI_MB_GetSize(mb));
// 先写入7字节ADTS帧头
fwrite(aac_header, 1, 7, aac_file);
// 再写入裸AAC二进制码流
fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), aac_file);
// 【强制必须调用】释放媒体缓冲区,SDK缓冲池有数量上限,不释放会卡死采集、内存泄漏
RK_MPI_MB_ReleaseBuffer(mb);
}
// 循环退出,刷新文件缓存并关闭文件,防止音频数据残留在内存丢失
fclose(aac_file);
printf("[INFO] AAC file write flush complete.\n");
// 线程函数标准返回,彻底消除no return编译警告
return NULL;
}
/**
* @brief 程序主函数,RKMedia音频采集+硬件AAC编码主流程
* @param argc 命令行参数个数
* @param argv 命令行参数数组
* @return int 程序退出码,0正常退出,负数初始化失败
* @flow RKMedia初始化 → AI声卡采集配置 → AAC编码器创建 → AI绑定AENC编码通道 → 启动码流读取线程 → 等待退出信号 → 有序释放全部媒体资源
*/
int main(int argc, char *argv[])
{
int ret; // 存储所有RKMedia API返回值,0代表执行成功,非0为错误码
// 注册Ctrl+C中断信号处理函数,捕获键盘退出指令
signal(SIGINT, sigint_handler);
// ========== 步骤1:RKMedia多媒体框架全局初始化【所有音视频API调用前置必备】 ==========
ret = RK_MPI_SYS_Init();
if (ret)
{
printf("[ERROR] RK_MPI_SYS_Init failed, ret=%d\n", ret);
return -1;
}
printf("[INFO] RKMedia system init success...\n");
// ========== 步骤2:AI音频采集通道参数配置 ==========
AI_CHN_ATTR_S ai_chn_s;
// 清空结构体内存,消除栈内随机垃圾参数导致声卡配置异常
memset(&ai_chn_s, 0, sizeof(AI_CHN_ATTR_S));
// 指定ALSA声卡硬件节点,强转const字符串消除C++常量指针转换警告
ai_chn_s.pcAudioNode = (char *)AUDIO_PATH;
ai_chn_s.u32SampleRate = 48000; // 麦克风硬件采样率48kHz,匹配ES8311声卡
ai_chn_s.u32NbSamples = 1024; // 单次DMA传输采样点数,硬件DMA缓冲区大小
ai_chn_s.u32Channels = 2; // 双声道立体声采集
ai_chn_s.enSampleFormat = RK_SAMPLE_FMT_S16; // 音频格式:16bit有符号小端PCM,对应ffplay s16le
ai_chn_s.enAiLayout = AI_LAYOUT_NORMAL; // 标准录音布局,无特殊混音处理
// 下发采集参数到底层声卡驱动
ret = RK_MPI_AI_SetChnAttr(AI_CHN, &ai_chn_s);
if (ret)
{
printf("[ERROR] RK_MPI_AI_SetChnAttr failed, ret=%d\n", ret);
goto RES_RELEASE; // 初始化失败直接跳转到资源释放流程
}
printf("[INFO] RK_MPI_AI_SetChnAttr success...\n");
// 使能AI音频硬件通道,初始化ALSA采集流
ret = RK_MPI_AI_EnableChn(AI_CHN);
if (ret)
{
printf("[ERROR] RK_MPI_AI_EnableChn failed, ret=%d\n", ret);
goto RES_RELEASE;
}
printf("[INFO] RK_MPI_AI_EnableChn success...\n");
// ========== 步骤3:AAC硬件编码通道创建与参数配置 ==========
AENC_CHN_ATTR_S aenc_chn_attrs;
// 清空编码器结构体,避免垃圾参数导致编码启动失败
memset(&aenc_chn_attrs, 0, sizeof(AENC_CHN_ATTR_S));
aenc_chn_attrs.enCodecType = RK_CODEC_TYPE_AAC; // 编码类型:AAC硬件编码器
aenc_chn_attrs.u32Bitrate = 64000; // 双声道AAC码率64kbps,人声录音清晰度平衡
aenc_chn_attrs.u32Quality = 1; // 编码质量档位,1为平衡模式
aenc_chn_attrs.stAencAAC.u32Channels = 2; // 编码声道与采集声道统一双声道
aenc_chn_attrs.stAencAAC.u32SampleRate = 48000; // 编码采样率与声卡采集48kHz对齐
// 创建AAC硬件编码通道
ret = RK_MPI_AENC_CreateChn(AENC_CHN, &aenc_chn_attrs);
if (ret)
{
printf("[ERROR] RK_MPI_AENC_CreateChn failed, ret=%d\n", ret);
goto RES_RELEASE;
}
printf("[INFO] RK_MPI_AENC_CreateChn success...\n");
// ========== 步骤4:媒体通道绑定 AI采集 → AENC编码器 ==========
// 定义AI采集通道媒体句柄
MPP_CHN_S ai_mpp_chn_s;
ai_mpp_chn_s.enModId = RK_ID_AI;
ai_mpp_chn_s.s32ChnId = AI_CHN;
// 定义AAC编码通道媒体句柄
MPP_CHN_S aenc_mpp_chn_s;
aenc_mpp_chn_s.enModId = RK_ID_AENC;
aenc_mpp_chn_s.s32ChnId = AENC_CHN;
// RKMedia内部数据流绑定:采集到的PCM数据自动转发给编码器,无需软件拷贝中转,低CPU占用
ret = RK_MPI_SYS_Bind(&ai_mpp_chn_s, &aenc_mpp_chn_s);
if (ret)
{
printf("[ERROR] RK_MPI_SYS_Bind failed, ret=%d\n", ret);
goto RES_RELEASE;
}
printf("[INFO] RK_MPI_SYS_Bind success...\n");
// ========== 步骤5:启动AAC码流读取后台线程 ==========
pthread_t pid;
pthread_create(&pid, NULL, get_audio_aenc_thread, NULL);
// 主线程阻塞休眠循环,检测全局退出标记,不占用CPU资源
while (0 == g_exit_flag)
{
sleep(2);
}
// ========== 统一资源释放入口,正常退出/初始化失败都会执行 ==========
RES_RELEASE:
printf("\n[INFO] Start release all media resource...\n");
// 解绑AI采集与AAC编码通道数据流
RK_MPI_SYS_UnBind(&ai_mpp_chn_s, &aenc_mpp_chn_s);
// 销毁AAC硬件编码通道,释放编码硬件资源
RK_MPI_AENC_DestroyChn(AENC_CHN);
// 关闭AI声卡采集硬件通道,释放声卡占用
RK_MPI_AI_DisableChn(AI_CHN);
// 销毁RKMedia全局框架,释放SDK全部内存、句柄资源
RK_MPI_SYS_Exit();
printf("[INFO] All resource release done, program exit.\n");
return 0;
}
很多在用 RV1126 做音频采集时,会踩一个大坑:直接输出的 AAC 裸码流播放器打不开,必须加上 ADTS 帧头才行。下面我把完整代码拆成 4 大块,用大白话讲清楚每一段到底在干嘛,零基础也能看懂。
3.2.1.采样率对照表 AacFreqIdx 结构体 + AacFreqIdxTbl 数组
完整代码片段
// 定义采样率和ADTS头部索引的对应结构
typedef struct AacFreqIdx_
{
RK_S32 u32SampleRate;
RK_U8 u8FreqIdx;
} AacFreqIdx;
// 行业标准13档AAC采样率映射表
AacFreqIdx AacFreqIdxTbl[13] = {
{96000, 0}, {88200, 1}, {64000, 2}, {48000, 3},
{44100, 4}, {32000, 5}, {24000, 6}, {22050, 7},
{16000, 8}, {12000, 9}, {11025, 10}, {8000, 11}, {7350, 12}
};
1.先说结构体 AacFreqIdx_ 这个结构体只有两个成员,就是做 "配对记录" 用的:
u32SampleRate:我们录音用的采样率,比如咱们板子用的 48000;u8FreqIdx:ADTS 头部规定的索引编号,不同采样率对应固定数字。 举个例子:48000 采样率,对应的索引数字就是 3。
2.数组 AacFreqIdxTbl[13] AAC 标准一共规定了 13 种合法采样率,这个数组就是一张对照表,把每一个采样率和它专属的索引一一存起来。 后面生成 ADTS 头部的时候,程序会拿着我们设置的 48000,遍历这张表找到数字 3,填进头部固定位置。 要是没有这张表,程序根本不知道 48kHz 该填什么数字,播放器读到错误索引就直接无法解码音频。
3.2.2.生成 AAC 播放头函数 static void GetAdtsHeader
完整代码片段
static void GetAdtsHeader(RK_U8 *pu8AdtsHdr, RK_S32 u32SampleRate, RK_U8 u8Channel,
RK_U32 u32DataLen)
{
RK_U8 u8FreqIdx = 0;
// 循环查表,拿到当前采样率对应的索引
for (int i = 0; i < 13; i++)
{
if (u32SampleRate == AacFreqIdxTbl[i].u32SampleRate)
{
u8FreqIdx = AacFreqIdxTbl[i].u8FreqIdx;
break;
}
}
// 一整帧AAC总长度 = 7字节头部 + 音频数据长度
RK_U32 u32PacketLen = u32DataLen + 7;
pu8AdtsHdr[0] = 0xFF;
pu8AdtsHdr[1] = 0xF1;
pu8AdtsHdr[2] = ((AAC_PROFILE_LOW) << 6) + (u8FreqIdx << 2) + (u8Channel >> 2);
pu8AdtsHdr[3] = (((u8Channel & 3) << 6) + (u32PacketLen >> 11));
pu8AdtsHdr[4] = ((u32PacketLen & 0x7FF) >> 3);
pu8AdtsHdr[5] = (((u32PacketLen & 7) << 5) + 0x1F);
pu8AdtsHdr[6] = 0xFC;
}
-
这个函数是整个代码里最关键的工具函数 ,解决一个核心问题:RKMedia 硬件编码器输出的是「裸 AAC 码流」,电脑播放器、手机都识别不了,必须给每一帧音频前面拼 7 个字节的 ADTS 头部,才能变成通用
.aac文件。 -
入参分别是什么
pu8AdtsHdr:输出缓冲区,我们生成好的 7 字节头部会存在这里;u32SampleRate:当前录音采样率(我们固定 48000);u8Channel:声道数,双声道填 2;u32DataLen:硬件编码输出的这一段 AAC 数据有多少字节。
-
内部执行流程 第一步循环遍历上面的采样率对照表,拿到 48000 对应的索引 3; 第二步计算完整帧长度:7 字节头部 + 音频数据长度; 后面 7 行分别填充 ADTS 头部每一位规范字段:同步标记、编码规格、采样率索引、声道、单帧总长度。 不用深究每一位二进制运算,只要记住:每读取一帧 AAC 码流,必须先调用这个函数生成头,先写头再写音频数据,文件才能正常播放。
3.2.3.AAC 码流读取线程 get_audio_aenc_thread
完整代码片段
void *get_audio_aenc_thread(void *args)
{
// 分离线程,主线程不用等待回收
pthread_detach(pthread_self());
// 新建aac文件,不存在自动创建,存在覆盖旧内容
FILE *aac_file = fopen("test_capture.aac", "w+");
// 判断文件是否打开成功,避免无权限崩溃
if (NULL == aac_file)
{
printf("[错误] 创建音频文件失败,检查目录写入权限\n");
return NULL;
}
MEDIA_BUFFER mb;
RK_U8 aac_header[7];
// 全局标记控制循环,按下Ctrl+C就停止采集
while (0 == g_exit_flag)
{
// 阻塞等待硬件编码输出AAC数据,没数据就休眠不占CPU
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_AENC, AENC_CHN, -1);
if (!mb)
{
printf("[提示] 编码缓冲区读取中断\n");
break;
}
printf("[正常] 获取到一帧AAC音频数据\n");
// 生成7字节ADTS播放头
GetAdtsHeader(aac_header, 48000, 2, RK_MPI_MB_GetSize(mb));
// 先写入头部,再写入AAC裸数据
fwrite(aac_header, 1, 7, aac_file);
fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), aac_file);
// 重点!缓冲区必须释放,不释放会卡死采集
RK_MPI_MB_ReleaseBuffer(mb);
}
// 循环退出,把缓存数据全部写入磁盘,关闭文件
fclose(aac_file);
printf("[完成] 音频文件保存完毕\n");
return NULL;
}
-
为什么要单独开一个线程? 主线程要负责初始化硬件、等待退出指令,如果把读取音频的循环放在主线程,程序会卡死在录音循环里,无法响应 Ctrl+C 停止信号。单独开线程专门做「读码流、写文件」,分工更清晰。
-
线程开头
pthread_detach(pthread_self());分离线程设置,意思是这个线程结束后,系统自动清理它占用的内存,主线程不用额外写代码等待线程结束,简化代码。 -
文件操作逻辑 用
w+模式打开test_capture.aac,板子/tmp目录有写入权限,运行后就能在同目录生成音频文件;增加判空是防止目录只读、磁盘满导致程序直接崩溃。 -
核心采集循环
RK_MPI_SYS_GetMediaBuffer:阻塞等待硬件 AAC 编码器输出数据,没有音频时线程休眠,不会疯狂占用 CPU;- 拿到一帧数据后,先调用上面讲的
GetAdtsHeader生成播放头; - 先写 7 字节头,再写硬件输出的 AAC 数据;
RK_MPI_MB_ReleaseBuffer(mb)这一行绝对不能删!RKMedia 缓冲区数量有限,不释放几秒钟就会耗尽缓冲区,录音直接卡死。
-
退出收尾 按下 Ctrl+C 后全局标记
g_exit_flag变成 1,循环跳出,执行fclose把内存里缓存的音频全部写到磁盘,防止录音文件残缺损坏。最后 return NULL 消除编译器警告。
3.2.4.主函数 main 整体流程拆解
完整代码片段
int main(int argc, char *argv[])
{
int ret;
// 捕获Ctrl+C停止信号
signal(SIGINT, sigint_handler);
// 1. 初始化RKMedia整个多媒体框架,所有音视频接口前置操作
ret = RK_MPI_SYS_Init();
if (ret)
{
printf("[错误] RKMedia框架初始化失败, 错误码=%d\n", ret);
return -1;
}
printf("[正常] RKMedia初始化完成\n");
// 2. 配置麦克风AI采集通道
AI_CHN_ATTR_S ai_chn_s;
memset(&ai_chn_s, 0, sizeof(AI_CHN_ATTR_S));
ai_chn_s.pcAudioNode = (char *)AUDIO_PATH;
ai_chn_s.u32SampleRate = 48000;
ai_chn_s.u32NbSamples = 1024;
ai_chn_s.u32Channels = 2;
ai_chn_s.enSampleFormat = RK_SAMPLE_FMT_S16;
ai_chn_s.enAiLayout = AI_LAYOUT_NORMAL;
ret = RK_MPI_AI_SetChnAttr(AI_CHN, &ai_chn_s);
if (ret)
{
printf("[错误] AI通道参数配置失败, 错误码=%d\n", ret);
goto RES_RELEASE;
}
printf("[正常] AI采集参数配置完成\n");
ret = RK_MPI_AI_EnableChn(AI_CHN);
if (ret)
{
printf("[错误] AI通道使能失败, 错误码=%d\n", ret);
goto RES_RELEASE;
}
printf("[正常] AI麦克风通道开启成功\n");
// 3. 配置硬件AAC编码通道
AENC_CHN_ATTR_S aenc_chn_attrs;
memset(&aenc_chn_attrs, 0, sizeof(AENC_CHN_ATTR_S));
aenc_chn_attrs.enCodecType = RK_CODEC_TYPE_AAC;
aenc_chn_attrs.u32Bitrate = 64000;
aenc_chn_attrs.u32Quality = 1;
aenc_chn_attrs.stAencAAC.u32Channels = 2;
aenc_chn_attrs.stAencAAC.u32SampleRate = 48000;
ret = RK_MPI_AENC_CreateChn(AENC_CHN, &aenc_chn_attrs);
if (ret)
{
printf("[错误] AAC编码器创建失败, 错误码=%d\n", ret);
goto RES_RELEASE;
}
printf("[正常] AAC硬件编码器创建完成\n");
// 4. 把麦克风采集通道和AAC编码器绑定
MPP_CHN_S ai_mpp_chn_s;
ai_mpp_chn_s.enModId = RK_ID_AI;
ai_mpp_chn_s.s32ChnId = AI_CHN;
MPP_CHN_S aenc_mpp_chn_s;
aenc_mpp_chn_s.enModId = RK_ID_AENC;
aenc_mpp_chn_s.s32ChnId = AENC_CHN;
ret = RK_MPI_SYS_Bind(&ai_mpp_chn_s, &aenc_mpp_chn_s);
if (ret)
{
printf("[错误] 采集与编码通道绑定失败, 错误码=%d\n", ret);
goto RES_RELEASE;
}
printf("[正常] AI采集通道绑定AAC编码器成功\n");
// 5. 启动读取AAC码流的子线程
pthread_t pid;
pthread_create(&pid, NULL, get_audio_aenc_thread, NULL);
// 主线程休眠循环,等待Ctrl+C信号
while (0 == g_exit_flag)
{
sleep(2);
}
// 统一释放所有硬件、SDK资源
RES_RELEASE:
printf("\n[提示] 开始释放所有音视频资源\n");
RK_MPI_SYS_UnBind(&ai_mpp_chn_s, &aenc_mpp_chn_s);
RK_MPI_AENC_DestroyChn(AENC_CHN);
RK_MPI_AI_DisableChn(AI_CHN);
RK_MPI_SYS_Exit();
printf("[完成] 资源全部释放,程序退出\n");
return 0;
}
整个 main 函数就是一套固定流水线:初始化框架 → 打开麦克风 → 创建 AAC 编码器 → 把采集和编码连起来 → 启动写文件线程 → 等待停止信号 → 关闭所有硬件释放资源。
-
开头信号注册
signal(SIGINT, sigint_handler);捕获键盘Ctrl+C,按下后修改全局g_exit_flag标记,线程和主线程都会收到停止指令,不会暴力终止程序导致音频文件损坏、声卡一直被占用。 -
第一步:RK_MPI_SYS_Init () 必须放在所有 RKMedia 接口最前面,相当于打开多媒体 SDK 的总开关,不初始化后面所有采集、编码接口全部失效,拿不到音频数据。
-
第二步:AI 麦克风采集通道配置 这一段用来告诉板子声卡参数:48000 采样率、双声道、16 位音频格式、声卡硬件节点
hw:0,0。memset清空结构体:防止栈内存里的随机垃圾参数干扰声卡配置;RK_MPI_AI_SetChnAttr:把我们写好的采样参数下发给声卡驱动;RK_MPI_AI_EnableChn:真正打开麦克风硬件,开始接收外界声音。
-
第三步:AAC 硬件编码器创建 RV1126 自带硬件 AAC 编码器,不用 CPU 软编码,占用资源极低。这里配置编码规格:64kbps 码率、双声道 48kHz,和麦克风采集参数必须完全对应,不然编码会报错。
RK_MPI_AENC_CreateChn执行后,硬件编码器就准备就绪。 -
第四步:通道绑定 RK_MPI_SYS_Bind 这里是 RKMedia 的数据流核心:把麦克风(AI)输出的原始 PCM 声音,直接内部转发给 AAC 编码器(AENC),不需要我们在代码里拷贝音频数据,性能拉满。 简单理解:麦克风采集的数据自动流入编码器,全程不用软件中转。
-
第五步:创建子线程 调用
pthread_create启动前面第三部分讲的码流读取线程,后台自动保存 AAC 文件。 -
主线程循环等待 主线程每 2 秒休眠一次,不占用 CPU,静静等待用户按下 Ctrl+C。
-
末尾统一资源释放标签 RES_RELEASE 不管是初始化中途报错,还是正常按 Ctrl+C 退出,都会跳转到这里执行释放操作: 解绑采集编码通道 → 销毁 AAC 编码器 → 关闭麦克风声卡 → 关闭 RKMedia 总框架。 如果不做这一步,声卡会一直被程序占用,下次重新运行代码会打开麦克风失败,同时造成内存泄漏。
3.3 编译和执行
编译和执行的具体步骤在前面的博客里面都有提到

执行成功了,获取了aac文件
通过ffplay进行播放就可以
ffplay.exe aac文件名