测试一下一种低成本的音频播放方案 ...... 矜辰所致
前言
这不是最近有语音方案需求嘛,就去了解了一下嵌入式语音开发应用相关的基础知识,也就是上一篇博文,然后又找沁恒的 FAE 要了一份基于 CH585 的低成本的 SBC 解码播放的示例 (博主提供了公司以及项目名称),然后把需要用到的硬件准备好了(除了CH585 开发板,就是额外的一个 H 桥 + 一个喇叭),这两天抽空来看看怎么个事。
本文内容主要就是记录测试一下 CH585 SBC 音频解码以及双 PWM 驱动喇叭的播放效果 。
相关博文:
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 基础说明](#一、 基础说明)
-
- [1.1 硬件连接说明](#1.1 硬件连接说明)
- [1.2 示例说明](#1.2 示例说明)
-
- [1.2.1 音频文件 Flash 的分配](#1.2.1 音频文件 Flash 的分配)
- [1.2.2 采样率](#1.2.2 采样率)
- [1.2.3 播放顺序](#1.2.3 播放顺序)
- [二、 音频文件生成](#二、 音频文件生成)
-
- [2.1 音频文件转 SBC](#2.1 音频文件转 SBC)
- [2.2 APP Flash 分配](#2.2 APP Flash 分配)
- [2.3 SBC 转成 HEX](#2.3 SBC 转成 HEX)
- 三、测试修改
-
- [3.1 基础测试](#3.1 基础测试)
- [3.2 指定播放](#3.2 指定播放)
- 结语
一、 基础说明
1.1 硬件连接说明
示例采用的音频播放为 双 PWM 驱动方式,只需要外接一个 H 桥( CH271, 价格不到 1 元),就可以直接驱动喇叭播放音频,连接方式如下图:

1.2 示例说明
注意,本示例并不是沁恒官方对外提供的标准示例,仅供参考。
本文的目的是测试一下 解码以及播放效果,只做必要记录说明,并不会对示例代码深入解析,但是在后面应用过程,有应用需求的时候,博主也会来补充说明,比如博主已经想好了,后面要把 裸 PCM 和 ADPCM 的播放都实现了,到时候有些修改说明还是会有的 。
在工程根目录下面有一份 readme 说明文档,大家如果也有项目需求可以自己要了示例查看,我也只是简单了看了下,然后就直接看了下应用层,这里可以给大家看一下示例应用层的代码 main.c 内容(从应用层代码可以看到基本的测试逻辑,然后可以看到给音频文件分配的 Flash 存放位置和大小):
c
#include "CH58x_common.h"
#include <stdbool.h>
#include "ch5xx_audio_dma.h"
#include "sbc_decoder.h"
//#define SBC_IN_DATAFLASH
#ifdef SBC_IN_DATAFLASH
#define SBC_START_ADDR (0u)
#else
#define SBC_START_ADDR (1024u*32)
#endif
sbc_t sbc;
volatile uint32_t read_addr;
volatile uint32_t read_index;
volatile uint32_t read_all_length;
volatile uint8_t play_on;
volatile uint8_t read_data_sta = 0;
volatile uint16_t ramp_sample = 0;
typedef enum {
SAMPLE_U8 = 1,
SAMPLE_I16 =2
} sample_width_t;
typedef struct{
uint32_t start_addr;
uint32_t length;
sample_width_t width;
}sample_t;
sample_t sample;
void sbc_decoder_init(void) {
PRINT("sbc_decode version:%s\r\n",sbc_decode_version());
sbc_decode_init(&sbc);
sbc.endian = SBC_LE;
}
void DebugInit(void)
{
GPIOA_SetBits(GPIO_Pin_9);
GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeIN_PU);
GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeOut_PP_5mA);
UART1_DefInit();
UART1_BaudRateCfg(460800);
}
uint8_t sbc_buf[64];
int16_t pcm_buf[128];
__HIGH_CODE
static void audio_handler(ch5xx_audio_event_t * p_event){
GPIOA_SetBits(GPIO_Pin_4);
switch(p_event->type){
case CH5XX_PWM_DMA_GET:
{
uint32_t *p_data = p_event->p_data;
uint32_t *p_data2 = p_event->p_data2;
uint32_t length = p_event->len;
uint8_t *p_src = (uint8_t *)(read_addr+read_index);
uint32_t target_sample;
switch(read_data_sta){
case 0:
if(read_index>=sample.length) {
read_data_sta = 1;
}else {
__wrap_memcpy(sbc_buf,(uint8_t*)sample.start_addr+read_index,64);
int32_t decoded_len;
unsigned int out_len;
decoded_len = sbc_decode(&sbc, sbc_buf ,\
64, pcm_buf,256, &out_len);
if(decoded_len > 0) {
read_index += decoded_len;
for(uint32_t i=0;i<(out_len>>1);i++) {
if(pcm_buf[i] >0) {
*p_data = pcm_buf[i]/8;
*p_data2 = 0;
}else {
*p_data2 = 0-pcm_buf[i]/8;
*p_data = 0;
}
p_data ++;
p_data2 ++;
}
if(read_index >= sample.length) {
PRINT("stop\r\n");
ch5xx_audio_stop();
}
}else {
PRINT("decode error\r\n");
ch5xx_audio_stop();
}
}
break;
case 1:
ch5xx_audio_stop();
break;
default:
break;
}
}
break;
case CH5XX_PWM_ALL_END:
PRINT("ALL END\r\n");
play_on = 0;
read_data_sta = 0;
break;
default:
break;
}
GPIOA_ResetBits(GPIO_Pin_4);
}
typedef struct {
uint32_t addr;
uint32_t length:24; //[23: 0]
uint32_t res0:8; //[31:24]
} audio_record_t;
audio_record_t music_record[] = {
{ 0x000000, 51200 },
{ 0x00c800, 51200 },//0x14800
{ 0x019000, 51200 },//0x21000
{ 0x025800, 10240 },//0x2D800
{ 0x028000, 10240 },//0x30000,
};
__HIGH_CODE
void main_loop(void) {
uint8_t index = 0;
while(1) {
read_index = 0;
play_on = 1;
read_data_sta = 0;
ramp_sample = 0;
sample.length = music_record[index].length;
sample.start_addr = 32768+music_record[index].addr;
PRINT("ADDR:%4x,len:%u\r\n",music_record[index].addr,sample.length);
ch5xx_audio_start();
while(play_on);
index ++;
if(index >= 5) {
index = 0;
}
DelayMs(1000);
}
}
/*********************************************************************
* @fn main
*
* @brief 主函数
*
* @return none
*/
int main()
{
uint8_t i;
SetSysClock(CLK_SOURCE_HSE_PLL_78MHz);
DebugInit();
PRINT("Start @ChipID=%02X\n", R8_CHIP_ID);
sbc_decoder_init();
{
}
sample.start_addr = (SBC_START_ADDR);
sample.width = 1;
sample.length = 0XFFFFFFFF;
PRINT("16K\r\n");
ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_16K,audio_handler);
main_loop();
}
1.2.1 音频文件 Flash 的分配
本次测试我们把音频文件放在 codeflash 区域,在程序中与音频文件存放位置有关的有两个地方,第一个是开头的一个宏定义:
c
#define SBC_START_ADDR (1024u*32)
这个宏定义代表第一个音频文件的存放地址,上面这个起始地址为 flash 32 K 位置,在后面我们生成音频文件 Hex 的时候需要用到这个地址 。
除了音频文件的起始地址,还有一个数组music_record 代表每个音频文件的大小和存放位置,如下:
c
audio_record_t music_record[] = {
{ 0x000000, 51200 },
{ 0x00c800, 51200 },//0x14800
{ 0x019000, 51200 },//0x21000
{ 0x025800, 10240 },//0x2D800
{ 0x028000, 10240 },//0x30000,
};
上面 Flash 分配解释如下,所有数组成员的第一个成员的地址都是相对起始地址的偏移量,第二个个成员就是本段 Flash 的区域分配的大小。
c
//这里解释就不用注释符号了,
//加了注释符号看不清楚
第一段:相对起始地址偏移 0, 占用 50K,
绝对起始地址就是: 32768 (0x8000)
制作音频文件需要用到绝对起始地址
第 1 块区域:{ 0x000000, 51200 },
第二段:相对起始地址偏移 0x00c800(50K), 占用 50K,
绝对起始地址就是: 起始地址+ 50K = 82K (0x14800)
第 2 块区域:{ 0x00c800, 51200 },//0x14800
第三段:相对起始地址偏移 0x019000 ,占用50K
绝对起始地址就是: 82K+50K = 0x019000
第 3 块区域:{ 0x019000, 51200 },//0x21000
下面按照同样方式计算,
主要是需要计算绝对起始地址,
生成的音频文件 hex 需要用到
4 { 0x025800, 10240 },//0x2D800
5 { 0x028000, 10240 },//0x30000,
1.2.2 采样率
我们之前说过采样率,本示例可以通过下面函数第一个参数设定采样率:
c
ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_16K,audio_handler);
可配置为 16K 和 8K,博主手头这个示例好像 8K 采样率有点小问题,但是很好修改,这个后面我会说明 ,测试我们保持 16K 不变,测试 16K 采样率的音频文件就行。
1.2.3 播放顺序
这并不是本文重点,只是指定播放 是实际应用中常见的需求,所以有必要说明一下。
示例是一直循环播放几段音频,通过下面逻辑控制:
c
index ++;
if(index >= 5) {
index = 0;
}
在实际应用中,我们可以通过一定的逻辑控制播放第几段,比如一个简单的测试框架如下(下文我们也会简单测试实现一下):
c
switch(key_val)
{
case 1:
play_audio_by_index(0);
break;
case 2:
play_audio_by_index(1);
break;
case 3:
play_audio_by_index(2);
break;
default: break;
}
二、 音频文件生成
2.1 音频文件转 SBC
音频文件生成就需要用到我们上一篇文章讲到的FFmpeg 工具了,我们可以准备好需要播放的音频源文件(理论上,只要 基本上电脑能播放的格式,无论是 .wav、.ogg、.flac、.aac,还是视频格式 .mp4、.avi、.mkv ,都可以转成 sbc )。
我们本文使用 mp3 举例说明(其他的格式指令结构类似,指令说明可参考博主上一篇文章 《嵌入式语音开发应用基础说明》或者自己网上搜索一下),直接使用命令可以把音频文件转成 sbc 文件:
bash
ffmpeg -i 004.mp3 -acodec sbc -ab 64k -ar 16000 -ac 1 004s.sbc
ffmpeg -i 005.mp3 -acodec sbc -ab 64k -ar 16000 -ac 1 005s.sbc
ffmpeg -i 006.mp3 -acodec sbc -ab 64k -ar 16000 -ac 1 006s.sbc
我们这里先直接转 3 段语音,转成 sbc 格式。
2.2 APP Flash 分配
接下来要考虑到程序中分配多大的 flash ,所以需要知道一下这 3 段语音多大。
我们通过下面命令可以直接查看:
bash
dir 004s.sbc
如下图:

本次测试生成的 3 段 sbc 大小都是 40 KB 左右,所以我们之前每段分 50 KB 是完全够用的,一般来说预留点余量,所以我们的数组定义如下:
c
// 改这里,可以改长度和地址
audio_record_t music_record[] = {
{ 0x000000, 51200 }, //32768
{ 0x00c800, 51200 },//0x14800
{ 0x019000, 51200 },//0x21000
};
2.3 SBC 转成 HEX
我们需要使用专门的工具 把 SBC 转成 HEX,这里可以使用后免费开源的 SRecord 或者 bin2hex 。
先说 SRecord :
官方下载地址:https://sourceforge.net/projects/srecord/files/srecord-win32/
需要科学上网
在上面页面进去以后点击版本,建议选择压缩包(.exe 安装博主是装了没有成功,可能需要重启,反正自己解压添加环境变量就可以直接使用),点击然后等一段时间就会自动下载:

压缩包解压出来,要添加环境变量,这个应该难不倒搞开发的大家,之前文章也说过很多次环境变量如何添加:

我们安装好了 SRecord 之后,就可以使用命令把音频文件转成 Hex 了,这里就要对应上我们前面数组分配的地址了:
bash
-offset 后面是存放地址,
对应代码中程序分配的地址
使用的是绝对地址
srec_cat.exe 004s.sbc -binary -offset 32768 -o 004s.hex -intel
#{ 0x00c800, 51200 },//0x14800
# 起始地址 32K 第二段相对起始偏移 0x00c800(50K)
# 计算 :32K+50K = 82K (0x14800)
# 所以第二段音频 hex 地址为 0x14800
srec_cat.exe 005s.sbc -binary -offset 0x14800 -o 005s.hex -intel
srec_cat.exe 006s.sbc -binary -offset 0x21000 -o 006s.hex -intel
如果使用 bin2hex ,这里也有个连接:
有个大佬的仓库有:
https://gitee.com/iot-fan/iot-fan_at_cnblogs/tree/master/Tools/hex_tools
下载以后也要添加环境变量:

转换命令如下:
bash
bin2hex.exe --offset=32768 004s.sbc 004s.hex
三、测试修改
完成上面的步骤,就可以直接烧录查看效果了。
3.1 基础测试
原示例只需要把 music_record[] 修改一下,根据自己要放的音频文件大小,和音频文件数量修改。
当然测试需要注意测试芯片整体 Flash 的大小,够不够放得下。
烧录的时候,APP 和音频文件 HEX 一起烧录:

烧录完成以后,CH585 PA9 PA10 连接 H 桥,然后接上喇叭,就可以听到循环语音播放。
这里不太好放录音,博主只能说,效果很清晰,很不错!
3.2 指定播放
示例终归是个测试,最终的应用肯定是触发播放,所以我们要简单修改一下,这个处理就是纯粹的应用层逻辑而已,这里直接上一下修改测试的代码:
c
#define SBC_START_ADDR (1024u*32)
void play_audio_by_index(uint8_t index) {
uint32_t record_cnt = sizeof(music_record)/sizeof(audio_record_t);
// 限制索引范围,防止数组越界
if(index >= record_cnt) {
PRINT("audio index out of range:%d\r\n", index);
return;
}
// 如果当前正在播放,先停止再播放新音频
if(play_on) {
PRINT("stop old audio, switch to index:%d\r\n", index);
ch5xx_audio_stop();
play_on = 0;
read_data_sta = 0; // 同步复位解码状态标志,防止中断残留
DelayMs(10); // 稍作延时等待硬件停止
}
read_index = 0;
play_on = 1;
read_data_sta = 0;
ramp_sample = 0;
sample.length = music_record[index].length;
sample.start_addr = SBC_START_ADDR + music_record[index].addr;
PRINT("Play Key:%d, ADDR:%4x, len:%u\r\n", index + 1, music_record[index].addr, sample.length);
ch5xx_audio_start();
}
在实际应用中,如果需要播放,直接调用play_audio_by_index 即可:
c
play_audio_by_index(0); //播放第一段音频
play_audio_by_index(1); //播放第二段音频
比如,我测试了一下使用蓝牙控制播放音乐 :

可以实现功能,但是语音播放和蓝牙一起还需要考虑蓝牙连接稳定的问题,这个虽然测试时候可以通过修改连接参数的方式,但是实际应用中还是要处理好解码、播放与蓝牙稳定的事情 。
结语
本文做的示例测试,主要是体验了一下在 CH585 上做 SBC 解码,同时低成本的 双 PWM 播放音频的效果。
声音效果听起来还是很不错的,在实际应用中,可以根据自己的应用需求进行一定的修改,博主后面还计划自己修改测试一下 ADPCM 解码播放 。
好了,本文就到这里。谢谢大家!