音频播放¶
本文说明本章节功能的接入方式、关键源码和应用参考,帮助开发者快速完成集成。
1. 应用例程¶
场景 |
文件 |
调用接口 |
|---|---|---|
AI App 同声传译音频采集和播放 |
|
音频采集 / 播放回调 |
AI App 同声传译 WebSocket 音频服务 |
|
WebSocket 连接、音频数据收发回调 |
2. 使用前配置¶
音频功能的总开关为 AUDIO。其直接依赖 !SOC_SF32LB55X,默认关闭,因此 SF32LB55X 工程中不显示。SDK 音频配置由所有核的 SDK 配置树引入,完整路径为:menuconfig (Top) → SiFli SDK configuration → SiFli Built-in Components → Audio (AUDIO);HCPU 和 PC Simulator 工程还会由 Solution 框架引入同一符号的第二个入口:menuconfig (Top) → Audio config → Audio (AUDIO)。两个入口配置的是同一个 AUDIO 符号,修改任一入口均生效;LCPU 工程没有 Audio config 菜单,应使用前一条 SDK 路径。
打开 AUDIO 后,按实际业务选择音频路径和功能;不需要的功能不要打开。
使用场景 |
必需宏 |
配置路径与说明 |
|---|---|---|
PCM 播放、录音、基础扬声器/麦克风通路 |
|
HCPU/PC Simulator 路径为: |
使用 |
|
HCPU/PC Simulator 路径为: |
使用 |
|
路径为: |
MP3/WAV 边下载边播放 |
上一行全部宏,加 |
路径为: |
蓝牙音频 |
|
HCPU/PC Simulator 路径为: |
网络音频还需要按实际承载完成 Wi-Fi、4G 或 BT PAN 配置,详见网络访问与文件下载。
3. 音频来源与接口选择¶
网络音频不要只按“从网络来”分类,而要按“数据到达设备时的形态”选择接口。
音频数据形态 |
推荐接口 |
适用场景 |
|---|---|---|
文件系统中的 mp3/wav |
|
已下载完成的文件、本地音乐 |
内存中的 mp3/wav |
|
HTTP 下载完再播放的短音频 |
ringbuffer 中的 mp3/wav |
|
边下载边播放,需要 |
PCM 流 |
|
解码后的 PCM,或服务器直接返回 PCM |
Opus/AI TTS 流 |
Opus 解码 + |
小智 AI、大模型 TTS 流 |
系统默认支持 mp3、wav、aac、opus,其中 wav 只支持 16 位 PCM。mp3/wav 可由 audio_mp3ctrl 提供自动 demux 和播放;aac 与 opus 的格式识别、解码和缓存需要业务层按音频来源处理。
4. 播放本地音频文件¶
适合已经下载到文件系统中的 mp3/wav 文件。
#include <rtthread.h>
#include <rtdevice.h>
#include "audio_mp3ctrl.h"
static mp3ctrl_handle g_file_player;
int demo_play_local_file(const char *path)
{
if (!path)
{
return -1;
}
g_file_player = mp3ctrl_open(AUDIO_TYPE_LOCAL_MUSIC, path, NULL, NULL);
if (!g_file_player)
{
rt_kprintf("mp3ctrl_open failed: %s\n", path);
return -1;
}
mp3ctrl_play(g_file_player);
return 0;
}
void demo_stop_local_file(void)
{
if (g_file_player)
{
mp3ctrl_close(g_file_player);
g_file_player = NULL;
}
}
5. 播放内存中的音频¶
适合 HTTP 下载到内存后再播放的短音频。注意:播放是异步过程,data 在播放结束前不能释放;如果需要自动释放,建议在 mp3ctrl_open_buffer() 的播放结束 callback 中释放。
#include <rtthread.h>
#include "audio_mp3ctrl.h"
static mp3ctrl_handle g_buffer_player;
int demo_play_audio_buffer(uint8_t *data, uint32_t len)
{
if (!data || len == 0)
{
return -1;
}
g_buffer_player = mp3ctrl_open_buffer(
AUDIO_TYPE_LOCAL_MUSIC,
(const char *)data,
len,
NULL,
NULL
);
if (!g_buffer_player)
{
rt_kprintf("mp3ctrl_open_buffer failed\n");
return -1;
}
mp3ctrl_play(g_buffer_player);
return 0;
}
void demo_stop_audio_buffer(void)
{
if (g_buffer_player)
{
mp3ctrl_close(g_buffer_player);
g_buffer_player = NULL;
}
}
6. 下载完成后播放¶
6.1 下载到内存后播放短音频¶
可以复用 网络访问与文件下载 中的 demo_http_get_to_memory(),先下载到内存,再调用 demo_play_audio_buffer() 播放。
/* demo_mem_file_t 和 demo_http_get_to_memory() 来自网络访问与文件下载文档 */
static demo_mem_file_t g_voice_file;
int demo_download_voice_to_memory_and_play(const char *uri)
{
if (demo_http_get_to_memory(uri, &g_voice_file) != 0)
{
return -1;
}
return demo_play_audio_buffer(g_voice_file.data, g_voice_file.size);
}
播放结束后再释放 g_voice_file.data。如果需要自动释放,可以在 mp3ctrl_open_buffer() 的 callback 中处理。
6.2 下载到文件系统后播放大音频¶
大音频建议保存到文件系统,再通过文件路径播放。
#include <rtthread.h>
#include <webclient.h>
#include "audio_mp3ctrl.h"
int demo_download_voice_to_file_and_play(const char *uri, const char *path)
{
if (webclient_get_file(uri, path) != 0)
{
rt_kprintf("download failed: %s\n", uri);
return -1;
}
return demo_play_local_file(path);
}
7. MP3/WAV 边下载边播放¶
如果服务器返回 mp3/wav 连续数据,不想等整个 HTTP 文件下载完成后再播放,可以使用 ringbuffer 播放接口。需要打开 AUDIO、AUDIO_USING_AUDPROC、AUDIO_USING_MANAGER、AUDIO_LOCAL_MUSIC 和 AUDIO_MP3_RINGBUFF_SUPPORT;完整路径和依赖关系见使用前配置。mp3/aac/opus 等具体解码库同样依赖 AUDIO。
#include <rtthread.h>
#include <rtdevice.h>
#include "audio_mp3ctrl.h"
#define DEMO_AUDIO_RB_SIZE (64 * 1024)
static struct rt_ringbuffer *g_stream_rb;
static mp3ctrl_handle g_stream_player;
int demo_stream_player_start(uint32_t file_len)
{
#ifdef AUDIO_MP3_RINGBUFF_SUPPORT
g_stream_rb = rt_ringbuffer_create(DEMO_AUDIO_RB_SIZE);
if (!g_stream_rb)
{
rt_kprintf("create ringbuffer failed\n");
return -1;
}
g_stream_player = mp3ctrl_open_ringbuffer(
AUDIO_TYPE_LOCAL_MUSIC,
g_stream_rb,
file_len,
NULL,
NULL
);
if (!g_stream_player)
{
rt_ringbuffer_destroy(g_stream_rb);
g_stream_rb = RT_NULL;
return -1;
}
mp3ctrl_play(g_stream_player);
return 0;
#else
rt_kprintf("AUDIO_MP3_RINGBUFF_SUPPORT is not enabled\n");
return -1;
#endif
}
int demo_stream_player_feed(uint8_t *data, uint32_t len)
{
#ifdef AUDIO_MP3_RINGBUFF_SUPPORT
uint32_t offset = 0;
if (!g_stream_rb || !data || len == 0)
{
return -1;
}
while (offset < len)
{
uint32_t put_len = rt_ringbuffer_put(g_stream_rb, data + offset, len - offset);
if (put_len == 0)
{
rt_thread_mdelay(10);
continue;
}
offset += put_len;
}
return 0;
#else
return -1;
#endif
}
void demo_stream_player_stop(void)
{
#ifdef AUDIO_MP3_RINGBUFF_SUPPORT
if (g_stream_player)
{
mp3ctrl_close(g_stream_player);
g_stream_player = RT_NULL;
}
if (g_stream_rb)
{
rt_ringbuffer_destroy(g_stream_rb);
g_stream_rb = RT_NULL;
}
#endif
}
网络下载线程中可按下面方式投喂:
/* webclient_session_create() 和 webclient_get() 已成功 */
while (1)
{
int n = webclient_read(session, read_buf, read_buf_size);
if (n < 0)
{
break;
}
if (n == 0)
{
break;
}
demo_stream_player_feed(read_buf, n);
}
说明:如果 HTTP 响应没有 Content-Length,
file_len的取值需要结合当前mp3ctrlringbuffer 实现验证;无法确认总长度时,应在项目中实测 chunked 下载和播放结束条件。
8. PCM 流式播放¶
如果服务器直接返回 PCM,或者业务层已经把 mp3/aac/opus 解码成 PCM,可以直接通过 audio_write() 写入播放缓存。
#include <rtthread.h>
#include "audio_server.h"
static audio_client_t g_pcm_client;
int demo_pcm_stream_start(uint32_t sample_rate, uint8_t channels)
{
audio_parameter_t pa = {0};
pa.write_samplerate = sample_rate;
pa.write_channnel_num = channels;
pa.write_bits_per_sample = 16;
pa.write_cache_size = 4096;
g_pcm_client = audio_open(
AUDIO_TYPE_LOCAL_MUSIC,
AUDIO_TX,
&pa,
NULL,
NULL
);
if (!g_pcm_client)
{
rt_kprintf("audio_open failed\n");
return -1;
}
return 0;
}
int demo_pcm_stream_write(uint8_t *pcm, uint32_t len)
{
uint32_t offset = 0;
if (!g_pcm_client || !pcm || len == 0)
{
return -1;
}
while (offset < len)
{
int written = audio_write(g_pcm_client, pcm + offset, len - offset);
if (written < 0)
{
rt_kprintf("audio_write failed: %d\n", written);
return -1;
}
if (written == 0)
{
rt_thread_mdelay(10);
continue;
}
offset += written;
}
return 0;
}
void demo_pcm_stream_stop(void)
{
if (g_pcm_client)
{
audio_close(g_pcm_client);
g_pcm_client = RT_NULL;
}
}
audio_write() 的返回值是实际写入的字节数,可能小于请求写入长度,业务代码需要继续写剩余数据。返回 -1 表示当前音频服务优先级被音频打断,返回 -2 表示参数错误。
9. AI TTS / Opus 播放¶
小智 AI 组件已经提供了 Opus 流式音频链路,参考代码位于:
solution/components/xiaozhi/xiaozhi_audio.c
小智使用的音频参数为:
format: opus
sample_rate: 16000
channels: 1
frame_duration: 60 ms
典型播放流程为:
WebSocket/UDP 收到 Opus 数据
↓
Opus decoder 解码
↓
得到 16 kHz 单声道 PCM
↓
audio_write() 写入 speaker client
↓
扬声器播放
如果客户对接第三方大模型 TTS,也建议使用同样的分层:网络线程只负责接收压缩帧,解码线程负责 Opus/AAC/MP3 转 PCM,播放线程通过 audio_write() 连续写入。
10. 播放过程中的息屏和 AOD¶
播放音频时需要先明确产品策略:
场景 |
推荐方式 |
适用场景 |
|---|---|---|
允许音频继续播放但关闭屏幕 |
|
后台音乐、AI 语音、长音频 |
播放期间不允许系统进入睡眠 |
播放前申请 PM lock,播放结束释放 |
播放时需要保持 UI、网络或动画实时运行 |
息屏时仍需要显示 AOD |
进入 AOD 并注册 AOD 响应函数 |
表盘和固定提示类页面 |
10.1 只关闭屏幕但继续播放音频¶
SLEEP_CLOSE_DISPLAY_ONLY 的定义如下:
SLEEP_CLOSE_DISPLAY_ONLY = 0x08, /**< Close lcd */
在当前实现中会禁止系统进入真正睡眠,只关闭显示。该方式适合音频后台播放的场景。
#include "app_pm.h"
void demo_audio_allow_display_off(void)
{
app_gui_sleep_type_set(SLEEP_CLOSE_DISPLAY_ONLY);
}
10.2 播放期间禁止睡眠¶
如果播放时必须保持系统活跃,应在播放开始申请 PM lock,在播放结束释放。注意申请和释放必须成对调用。
#include <rtthread.h>
#include <rtdevice.h>
void demo_audio_pm_lock(void)
{
#ifdef BSP_USING_PM
rt_pm_request(PM_SLEEP_MODE_IDLE);
#endif
}
void demo_audio_pm_unlock(void)
{
#ifdef BSP_USING_PM
rt_pm_release(PM_SLEEP_MODE_IDLE);
#endif
}
结合 mp3ctrl 播放结束回调:
#include "audio_mp3ctrl.h"
static int demo_player_callback(audio_server_callback_cmt_t cmd,
void *callback_userdata,
uint32_t reserved)
{
if (cmd == as_callback_cmd_play_to_end)
{
demo_audio_pm_unlock();
}
return 0;
}
int demo_play_file_keep_awake(const char *path)
{
mp3ctrl_handle player;
demo_audio_pm_lock();
player = mp3ctrl_open(AUDIO_TYPE_LOCAL_MUSIC, path, demo_player_callback, NULL);
if (!player)
{
demo_audio_pm_unlock();
return -1;
}
mp3ctrl_play(player);
return 0;
}
10.3 息屏状态下唤醒 UI¶
网络线程、播放线程或 AI 线程需要在息屏时唤醒 UI,可发送带 NEED_WAKEUP_UI 的 GUI 消息。该机制用于明确需要点亮或唤醒界面的业务,例如 OTA、通话和重要提醒。
send_msg_to_gui_thread(data,
len,
process_in_gui_thread_cb,
msg_id,
NEED_WAKEUP_UI);
AOD 场景下不要滥用 NEED_WAKEUP_UI,因为唤醒 UI 会退出 AOD。AOD 框架中注册显示逻辑请参考 AOD。