CH585 SBC 音频解码播放测试说明

复制代码
测试一下一种低成本的音频播放方案    ...... 矜辰所致

前言

这不是最近有语音方案需求嘛,就去了解了一下嵌入式语音开发应用相关的基础知识,也就是上一篇博文,然后又找沁恒的 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 解码播放 。

好了,本文就到这里。谢谢大家!