1. 从标题拆解:Hi3519A 上播放 AAC 到底在做什么
第一次看到“Hi3519A播放ACC理解”这个标题,我估计不少人会愣一下——ACC 大概率是 AAC 的笔误,也就是 Advanced Audio Coding,一种非常常见的有损音频压缩格式。而 Hi3519A 是一颗面向视频监控、智能视觉、边缘计算场景的嵌入式 SoC,主打 H.264/H.265 编解码和多路视频处理。把这两个词放在一起,核心问题就变成了:在 Hi3519A 这类嵌入式平台上,怎么把 AAC 音频数据播放出来,整个链路涉及哪些环节,坑通常出在哪里。
这个题目看起来小,实际上牵扯的面很广。它不是一个“调个 API 就完事”的问题,而是从音频文件封装格式解析、AAC 解码、PCM 输出、时钟同步、硬件接口配置到系统资源调度的一整条链路。任何一个环节没对齐,结果就是没声音、断断续续、杂音、音画不同步,甚至系统直接卡死。
我写这篇东西的目标很明确:把 Hi3519A 平台上播放 AAC 的完整思路、关键细节、实操步骤和常见问题排查方法讲透。适合正在做嵌入式音视频开发的工程师、刚接触海思平台的新手,以及需要把音频播放集成到现有项目里的开发者参考。即使你之前没碰过海思的 SDK,跟着思路走也能理解整个链路是怎么串起来的。
需要提前说明的是,下面涉及的具体接口名称、参数值、配置方式,一部分来自海思 SDK 的通用设计惯例,一部分来自实际项目中的常见做法。不同 SDK 版本之间会有差异,具体以你手上的文档和头文件为准。但底层的原理和排查思路是通用的。
2. 整体设计思路:为什么 AAC 播放不是“解码完丢给喇叭”这么简单
2.1 先搞清楚数据从哪来、到哪去
在嵌入式平台上做音频播放,第一件事不是写代码,而是把数据流画清楚。AAC 数据通常不会裸奔,它一般封装在某种容器里,比如 ADTS 流、MP4 文件、TS 流,或者跟着视频一起打包在自定义格式中。Hi3519A 的音频子系统并不直接认识“MP4 文件”这种东西,它需要的是已经分离好的 AAC 裸流或 ADTS 帧。
所以整个链路大致是这样的:
- 输入层:AAC 文件或流,可能带容器封装
- 解封装层:把 AAC 数据从容器里提取出来,得到 ADTS 帧或裸 AAC 帧
- 解码层:调用 AAC 解码器,把压缩数据变成 PCM 采样数据
- 输出层:把 PCM 数据通过 I2S 或其他音频接口送到 Codec 芯片,最终驱动扬声器或耳机
- 控制层:管理播放状态、音量、时钟同步、缓冲区
这五层里,任何一层出问题都会导致“没声音”。而实际项目中最容易出问题的,往往是解封装和时钟同步这两个环节,而不是解码本身。
2.2 为什么选择在 Hi3519A 上做软解码而不是硬解码
Hi3519A 的音频子系统通常提供的是音频输入输出接口和基础编解码能力,但对 AAC 的支持情况取决于具体 SDK 配置和授权。很多场景下,AAC 解码是通过软件解码器完成的,比如用 Helix AAC、FAAD2 或者芯片厂商提供的软解库。
选择软解码的理由很直接:AAC 的专利授权和硬件解码器配置相对复杂,而软解码在 Hi3519A 的 ARM 核上跑单路音频绰绰有余。实测下来,一颗主频 1GHz 左右的 Cortex-A 核,解码 44.1kHz 立体声 AAC 的 CPU 占用大概在 5% 到 15% 之间,完全在可接受范围内。
但软解码也有代价:它会占用 CPU 资源,如果同时还在跑视频编码、AI 推理等任务,就需要考虑优先级和核间分配。我的建议是,如果系统负载已经很高,把音频解码线程绑定到单独的 CPU 核上,避免和视频任务抢资源。
2.3 输出接口的选择:I2S 还是内置 Codec
Hi3519A 通常提供 I2S 接口用于连接外部音频 Codec,部分型号也可能有内置的音频 DAC。实际项目中,用外部 Codec 的情况更多,因为音质和驱动能力更可控。
I2S 配置里有几个关键参数必须和 Codec 侧完全一致,否则要么没声音,要么全是噪声:
- 采样率:AAC 解码后通常是 44.1kHz 或 48kHz
- 位宽:16bit 是最常见的,也有 24bit 的配置
- 声道数:单声道或立体声
- 主从模式:Hi3519A 通常作为 I2S 主机,输出 BCLK 和 LRCLK
- 数据格式:I2S 标准、左对齐、右对齐,必须和 Codec 匹配
这些参数不是随便填的,后面我会详细讲怎么计算和验证。
3. 核心细节解析:AAC 解码与 PCM 输出的关键环节
3.1 AAC 格式的几种变体与识别方法
AAC 不是一个单一格式,它有几个常见变体,处理方式略有不同:
| 格式类型 | 特点 | 常见场景 | 处理方式 |
|---|---|---|---|
| ADTS | 每帧带同步头和头信息 | 流媒体、TS 流 | 可直接逐帧送入解码器 |
| RAW AAC | 无头信息,需要额外配置 | MP4 文件 | 需要从容器获取 AudioSpecificConfig |
| LATM | 带更复杂的封装 | 广播流 | 需要 LATM 解析器 |
| HE-AAC | 带 SBR 参数 | 低码率流媒体 | 解码器需支持 SBR |
实际项目中最常见的是 ADTS 和 MP4 里的 AAC。ADTS 的好处是每帧自带同步信息,你可以直接按帧读取然后送解码器。MP4 里的 AAC 则需要先解析 moov box,拿到采样率、声道数、AudioSpecificConfig 等信息,再初始化解码器。
注意:如果你拿到的是 MP4 文件,不要试图直接把文件数据丢给 AAC 解码器,它不认识 MP4 的盒子结构。必须先解封装。
3.2 AAC 解码器的初始化和输入输出配置
以常见的软解库为例,初始化流程大致如下:
/* 伪代码,展示初始化逻辑 */ aac_decoder_handle *handle = aac_decoder_open(); /* 设置解码参数 */ aac_decoder_set_param(handle, AAC_PARAM_SAMPLE_RATE, 44100); aac_decoder_set_param(handle, AAC_PARAM_CHANNELS, 2); aac_decoder_set_param(handle, AAC_PARAM_BIT_DEPTH, 16); /* 如果是 RAW AAC,需要传入 AudioSpecificConfig */ aac_decoder_set_config(handle, asc_data, asc_len); /* 分配输出 PCM 缓冲区 */ int pcm_buf_size = 4096 * 2 * 2; /* 4096 samples * 2 channels * 2 bytes */ short *pcm_buf = malloc(pcm_buf_size);这里有几个细节值得展开:
采样率的处理:AAC 支持 96000、88200、64000、48000、44100、32000、24000、22050、16000、12000、11025、8000 等采样率。解码器输出的 PCM 采样率必须和 I2S 配置一致。如果 AAC 文件是 44.1kHz,而 I2S 配成了 48kHz,播放速度就会不对,音调也会变。
声道数的处理:AAC 支持单声道、立体声、多声道。Hi3519A 的音频输出通常只支持单声道或立体声,如果遇到 5.1 声道的 AAC,需要做下混处理,否则要么报错,要么只输出部分声道。
位宽的处理:AAC 解码输出通常是 16bit PCM,也有 24bit 或浮点的配置。I2S 配置的位宽必须和解码输出匹配,否则会出现数据错位,听起来就是噪声。
3.3 PCM 缓冲区的管理与播放节奏控制
解码出来的 PCM 数据不能直接往 I2S 里塞,因为解码速度和播放速度不一致。你需要一个缓冲区来平滑这个差异,通常叫音频环形缓冲区。
缓冲区的设计有几个关键点:
- 缓冲区大小:太小会导致欠载,声音断断续续;太大会导致延迟高,音画不同步。一般建议缓冲 200ms 到 500ms 的音频数据。
- 读写指针管理:写指针由解码线程推进,读指针由 I2S 中断或 DMA 回调推进。
- 同步机制:用互斥锁或信号量保护读写指针,避免竞态条件。
以 44.1kHz、16bit、立体声为例,每秒的数据量是:
44100 samples/s * 2 channels * 2 bytes = 176400 bytes/s如果缓冲 300ms,缓冲区大小就是:
176400 * 0.3 = 52920 bytes实际分配时可以取整到 64KB 或 128KB,方便内存对齐。
提示:缓冲区不是越大越好。在嵌入式系统里,内存是宝贵资源,而且大缓冲区会引入更大的播放延迟。如果应用场景对实时性有要求,比如语音对讲,缓冲区要控制在 100ms 以内。
3.4 I2S 接口配置与 Codec 寄存器初始化
I2S 配置是硬件层面的,参数必须和 Codec 侧严格匹配。以 44.1kHz、16bit、立体声、I2S 标准格式为例,典型配置如下:
/* I2S 配置示例 */ i2s_config_t i2s_cfg; i2s_cfg.sample_rate = 44100; i2s_cfg.bit_width = 16; i2s_cfg.channels = 2; i2s_cfg.mode = I2S_MODE_MASTER; i2s_cfg.format = I2S_FORMAT_STANDARD; i2s_cfg.bclk_polarity = I2S_BCLK_POLARITY_NORMAL; i2s_cfg.lrclk_polarity = I2S_LRCLK_POLARITY_NORMAL; i2s_init(&i2s_cfg);BCLK 的频率计算方式是:
BCLK = sample_rate * channels * bit_width = 44100 * 2 * 16 = 1411200 Hz这个频率必须和 Codec 的 BCLK 输入要求一致。如果 Codec 需要 BCLK 是 64fs 或 32fs,你需要确认 I2S 控制器输出的 BCLK 分频比是否正确。
Codec 侧的初始化通常通过 I2C 写寄存器完成,包括:
- 上电顺序:先给 Codec 供电,再配置寄存器,最后使能输出
- 时钟配置:选择 MCLK 来源和分频比
- 输入通道选择:选择 I2S 输入还是其他输入
- 音量设置:初始音量不要设太大,避免爆音
- 输出使能:最后打开输出
注意:Codec 的上电顺序很重要。如果先使能输出再配置寄存器,可能会听到明显的爆音或噪声。正确的顺序是:供电 -> 复位 -> 配置寄存器 -> 使能输出。
4. 实操过程:从零搭建一个 AAC 播放链路
4.1 环境准备与依赖确认
在开始写代码之前,先确认几件事:
- Hi3519A 的 SDK 是否包含音频驱动和 I2S 接口支持
- 是否有可用的 AAC 解码库,比如芯片厂商提供的软解库或开源库
- Codec 芯片的驱动是否已经移植好,I2C 通信是否正常
- 系统是否有足够的 CPU 和内存资源
我一般会先用一个简单的测试程序验证 I2S 输出是否正常,比如输出一段正弦波 PCM 数据,用示波器或耳机确认有声音。这一步能排除硬件和驱动层面的问题,避免后面调试时把软件问题和硬件问题混在一起。
4.2 音频文件的解封装与 AAC 帧提取
假设你拿到的是一个 MP4 文件,需要先把 AAC 数据提取出来。可以用 FFmpeg 在 PC 上先做一次转换,把 AAC 提取成 ADTS 格式:
ffmpeg -i input.mp4 -vn -acodec copy -f adts output.aac这样得到的 output.aac 就是 ADTS 流,每帧带同步头,可以直接送解码器。
如果你需要在嵌入式系统里实时解封装,那就需要移植一个轻量的 MP4 解析库,或者自己写一个简单的 box 解析器。MP4 的 moov box 里包含了音频轨道的采样率、声道数、AudioSpecificConfig 等信息,这些是初始化解码器必需的。
提示:在嵌入式系统里做 MP4 解封装,建议只解析需要的 box,不要引入完整的 FFmpeg,否则代码体积和内存占用都会很大。
4.3 AAC 解码线程的实现
解码线程的核心逻辑是一个循环:
- 从输入队列取一帧 AAC 数据
- 送入解码器
- 获取解码后的 PCM 数据
- 写入环形缓冲区
- 如果缓冲区满,等待;如果输入队列空,等待
void *aac_decode_thread(void *arg) { while (running) { /* 从队列取 AAC 帧 */ aac_frame_t *frame = queue_pop(&aac_queue); if (!frame) { usleep(1000); continue; } /* 解码 */ int pcm_len = aac_decode(handle, frame->data, frame->len, pcm_buf, sizeof(pcm_buf)); /* 写入环形缓冲区 */ if (pcm_len > 0) { ring_buffer_write(&pcm_ring, pcm_buf, pcm_len); } free(frame->data); free(frame); } return NULL; }这个线程的优先级建议设高一些,避免被其他任务抢占导致解码不及时。
4.4 I2S 输出线程与 DMA 配置
I2S 输出通常用 DMA 方式,减少 CPU 占用。配置 DMA 时需要注意:
- DMA 缓冲区大小要和 I2S FIFO 深度匹配
- DMA 中断频率不要太高,否则 CPU 会被频繁打断
- 双缓冲或多缓冲模式可以避免播放断点
/* DMA 配置示例 */ dma_config_t dma_cfg; dma_cfg.src_addr = (uint32_t)pcm_ring_buffer; dma_cfg.dst_addr = I2S_TX_FIFO_ADDR; dma_cfg.transfer_size = DMA_BUF_SIZE; dma_cfg.mode = DMA_MODE_CIRCULAR; dma_cfg.callback = i2s_dma_callback; dma_init(&dma_cfg);DMA 回调里要做的事情是从环形缓冲区读取下一段 PCM 数据,填充到 DMA 缓冲区。这个回调的执行时间要尽量短,否则会影响音频播放的连续性。
4.5 时钟同步与音画同步的处理
如果只是播放音频,时钟同步相对简单:以 I2S 的采样时钟为准,解码线程根据缓冲区的空闲空间调整解码速度。
但如果是音视频同时播放,就需要考虑音视频同步。常见的做法是:
- 以音频时钟为主时钟,视频根据音频时间戳调整播放速度
- 或者以系统时钟为主时钟,音频和视频都向系统时钟对齐
在 Hi3519A 上,如果视频是硬件解码,音频是软解码,建议以音频为主时钟,因为音频对时间更敏感,视频可以通过丢帧或重复帧来调整。
注意:音视频同步不是一劳永逸的,需要在播放过程中持续调整。如果发现音频比视频快,可以适当降低音频解码速度或增加缓冲区;如果音频比视频慢,则相反。
5. 常见问题与排查技巧实录
5.1 没声音:从后往前排查
没声音是最常见的问题,排查顺序建议从后往前:
| 排查步骤 | 检查内容 | 可能问题 |
|---|---|---|
| 1 | 扬声器/耳机是否正常 | 硬件故障 |
| 2 | Codec 是否上电、寄存器是否配置 | I2C 通信失败 |
| 3 | I2S 是否有 BCLK 和 LRCLK 输出 | I2S 未使能或配置错误 |
| 4 | DMA 是否在搬运数据 | DMA 配置错误 |
| 5 | 环形缓冲区是否有数据 | 解码线程未运行 |
| 6 | AAC 解码器是否初始化成功 | 参数配置错误 |
| 7 | 输入数据是否是有效 AAC 帧 | 解封装错误 |
这个顺序的核心逻辑是:先确认硬件通路没问题,再确认软件数据流没问题。很多新手一上来就查解码代码,结果发现是 Codec 没上电。
5.2 声音断断续续:缓冲区与 CPU 占用分析
断断续续通常有两个原因:缓冲区欠载或 CPU 占用过高。
缓冲区欠载的表现是声音每隔一段时间就断一下,间隔比较规律。解决办法是增大缓冲区,或者提高解码线程的优先级。
CPU 占用过高的表现是声音卡顿不规律,可能伴随视频卡顿。可以用 top 或 ps 命令查看 CPU 占用,如果解码线程占用超过 30%,就需要考虑优化解码器或降低采样率。
提示:在 Hi3519A 上,如果同时跑视频编码和音频解码,建议把音频解码线程绑定到单独的 CPU 核上,用 taskset 或 sched_setaffinity 设置。
5.3 杂音或噪声:时钟与格式匹配检查
杂音通常是因为 I2S 配置和 Codec 不匹配,或者 PCM 数据格式不对。检查以下几点:
- 采样率是否一致:AAC 解码输出 44.1kHz,I2S 也必须是 44.1kHz
- 位宽是否一致:16bit PCM 对应 16bit I2S 配置
- 声道数是否一致:立体声对应 2 声道
- 数据格式是否一致:I2S 标准、左对齐、右对齐要匹配
- BCLK 极性是否正确:有些 Codec 对时钟极性有要求
如果以上都确认无误,但还有杂音,可以检查 PCM 数据是否有溢出或截断。用 Audacity 等工具把 PCM 数据导出成 WAV 文件,在 PC 上播放,如果 PC 上正常,说明问题在 I2S 或 Codec 侧。
5.4 音画不同步:时间戳与时钟源的选择
音画不同步的表现是声音比画面快或慢。排查思路:
- 确认音频和视频的时间戳是否来自同一个时钟源
- 检查音频缓冲区的延迟是否过大
- 检查视频解码和渲染的延迟是否稳定
如果音频比视频快,可以增大音频缓冲区,或者让视频渲染等待音频。如果音频比视频慢,可以减少音频缓冲区,或者丢帧。
注意:音画同步的调整幅度不要太大,否则会引入新的问题。一般每次调整 10ms 到 20ms,观察效果后再继续调整。
5.5 高频踩坑点速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 完全没声音 | Codec 未上电 | 检查供电和 I2C 通信 |
| 只有一边有声音 | 声道配置错误 | 检查 I2S 声道数和 Codec 寄存器 |
| 声音速度不对 | 采样率不匹配 | 统一 AAC 解码和 I2S 采样率 |
| 播放几秒后停止 | 缓冲区溢出或欠载 | 调整缓冲区大小和线程优先级 |
| 爆音 | Codec 上电顺序错误 | 按正确顺序初始化 Codec |
| 噪声 | 位宽或格式不匹配 | 检查 I2S 配置和 PCM 格式 |
| CPU 占用高 | 解码器效率低 | 优化解码器或绑定 CPU 核 |
6. 性能优化与资源调度的一些经验
6.1 解码线程的优先级与 CPU 绑定
在 Hi3519A 这种多核平台上,线程调度对音频播放的稳定性影响很大。我的做法是:
- 把 AAC 解码线程的优先级设为比普通线程高,但不要设为实时优先级,避免影响系统其他关键任务
- 用 sched_setaffinity 把解码线程绑定到某个 CPU 核上,减少核间迁移带来的缓存失效
- 如果系统支持,可以用 CPU 隔离,把某个核专门留给音频处理
/* 绑定线程到 CPU 核 1 */ cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(1, &cpuset); pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);6.2 内存使用的优化
嵌入式系统的内存有限,音频缓冲区和解码器的内存占用需要控制。几个建议:
- 环形缓冲区用静态分配,避免动态内存分配带来的碎片
- 解码器的输出缓冲区复用,不要每帧都 malloc/free
- 如果支持,用 DMA 一致性内存,避免 cache 同步的开销
6.3 低延迟场景的特殊处理
如果是语音对讲、游戏音频等低延迟场景,缓冲区要控制在 50ms 以内,甚至更低。这时候需要:
- 减小 DMA 缓冲区大小,提高中断频率
- 解码线程用实时优先级
- 避免在音频路径上做任何阻塞操作
但低延迟的代价是抗抖动能力差,如果系统负载波动大,容易出现断音。需要在延迟和稳定性之间做权衡。
7. 后续扩展的一些方向
这套 AAC 播放链路搭好之后,可以往几个方向扩展。一个是支持更多音频格式,比如 MP3、Opus、G.711,只需要替换解码器,链路其他部分基本不用动。另一个是加入音频效果处理,比如均衡器、混响、回声消除,在 PCM 数据写入缓冲区之前做处理。还有就是多路音频混合,比如同时播放背景音乐和提示音,需要在缓冲区层面做混音。
我在实际项目里遇到过一个需求,是在视频监控的同时播放告警音频,要求音频延迟低于 200ms。当时的做法是把音频解码和输出放在一个独立线程里,用实时优先级,缓冲区控制在 100ms,实测下来延迟在 150ms 左右,满足要求。但这个方案对 CPU 占用比较敏感,如果视频编码负载突然升高,音频可能会出现轻微卡顿。后来通过 CPU 绑定和优先级调整,基本解决了这个问题。
如果你也在做类似的项目,建议先把最简单的链路跑通,再逐步优化。不要一开始就追求低延迟和高音质,先把声音放出来,再调参数。很多问题在链路跑通之后会自然暴露出来,这时候再针对性解决,效率会高很多。