开源啦!基于RT-Thread的百度语音识别——录音功能的实现(三)

  • Post author:
  • Post category:其他


本期分享来自RT-Thread的社区小伙伴霹雳大乌龙,如果你也有文章愿意分享/希望获得官方的写作指导,可以发送文章/联系方式邮件至邮箱:xuqianqian@rt-thread.com


回顾往期:



软件包应用分享|基于RT-Thread的百度语音识别(一)


软件包应用分享|基于RT-Thread的百度语音识别(二)

一、前言


项目地址:https://github.com/lxzzzzzxl/Baidu_Speech_base_on_RT-Thread

(请复制至外部浏览器打开)

在前面的2篇连载中我们已经讲解了百度语音识别的流程,如何使用

webclient软件包

进行语音识别,如何使用

CJson软件包

进行数据解析,如何在LCD上显示识别结果,如何通过语音识别控制外设。这一切的一切的首要前提,就是语音,那我们前面使用的是事先录制好的音频,而本次连载,我们终于要来实现录音功能了,有了录音,你想怎么识别就可以怎么识别,是不是很棒。

我将采用RT-Thread的Audio设备框架,下面我会简单介绍该框架。但在那之前,我建议你先去看看正点原子教程的 “音乐播放器” 及 “录音机” 两个例程,确保你对WAVE文件,音频编解码芯片,SAI等知识点有一定的了解。

二、 Audio设备框架


Audio(音频)设备是嵌入式系统中非常重要的一个组成部分,负责音频数据的采样和输出。如下图所示:

RT-Thread的Audio设备驱动框架为我们提供了标准 device 接口(open/close/read/control),只要我们对接好设备框架,就可以在我们的应用代码里直接使用这些标准接口,对设备进行操作。(RT-Thread其他设备框架实现原理都是如此)详细介绍见RT-Thread文档中心:https://www.rt-thread.org/document/site/

本篇我们不具体讲解Audio设备框架的对接,因为我使用的潘多拉开发板是官方支持的板子,所以底层驱动,框架对接这部分已经有相应的支持了。这里只简单提一下设备框架的对接方法,RT-Thread所有的设备框架的对接,基本上都是两大步骤:

  1. 准备好相应的设备驱动,实现对应框架的ops函数

  2. 进行设备注册

想要弄懂这两步,RT-Thread的文档中心需要多看,还有就是去看框架源码。

三、动手实践

1.打开Audio设备(使能录音功能)

2.录音功能实现
  1/* wav_record.c */
  2
  3#include <rtthread.h>
  4#include <rtdevice.h>
  5#include <dfs_posix.h>
  6
  7#define RECORD_TIME_MS      5000
  8#define RECORD_SAMPLERATE   8000
  9#define RECORD_CHANNEL      2
 10#define RECORD_CHUNK_SZ     ((RECORD_SAMPLERATE * RECORD_CHANNEL * 2) * 20 / 1000)
 11
 12#define SOUND_DEVICE_NAME    "mic0"      /* Audio 设备名称 */
 13static rt_device_t mic_dev;              /* Audio 设备句柄 */
 14
 15struct wav_header
 16{
 17    char  riff_id[4];              /* "RIFF" */
 18    int   riff_datasize;           /* RIFF chunk data size,exclude riff_id[4] and riff_datasize,total - 8 */
 19    char  riff_type[4];            /* "WAVE" */
 20    char  fmt_id[4];               /* "fmt " */
 21    int   fmt_datasize;            /* fmt chunk data size,16 for pcm */
 22    short fmt_compression_code;    /* 1 for PCM */
 23    short fmt_channels;            /* 1(mono) or 2(stereo) */
 24    int   fmt_sample_rate;         /* samples per second */
 25    int   fmt_avg_bytes_per_sec;   /* sample_rate * channels * bit_per_sample / 8 */
 26    short fmt_block_align;         /* number bytes per sample, bit_per_sample * channels / 8 */
 27    short fmt_bit_per_sample;      /* bits of each sample(8,16,32). */
 28    char  data_id[4];              /* "data" */
 29    int   data_datasize;           /* data chunk size,pcm_size - 44 */
 30};
 31
 32static void wavheader_init(struct wav_header *header, int sample_rate, int channels, int datasize)
 33{
 34    memcpy(header->riff_id, "RIFF", 4);
 35    header->riff_datasize = datasize + 44 - 8;
 36    memcpy(header->riff_type, "WAVE", 4);
 37    memcpy(header->fmt_id, "fmt ", 4);
 38    header->fmt_datasize = 16;
 39    header->fmt_compression_code = 1;
 40    header->fmt_channels = channels;
 41    header->fmt_sample_rate = sample_rate;
 42    header->fmt_bit_per_sample = 16;
 43    header->fmt_avg_bytes_per_sec = header->fmt_sample_rate * header->fmt_channels * header->fmt_bit_per_sample / 8;
 44    header->fmt_block_align = header->fmt_bit_per_sample * header->fmt_channels / 8;
 45    memcpy(header->data_id, "data", 4);
 46    header->data_datasize = datasize;
 47}
 48
 49int wavrecord_sample(int argc, char **argv)
 50{
 51    int fd = -1;
 52    uint8_t *buffer = NULL;
 53    struct wav_header header;
 54    struct rt_audio_caps caps = {0};
 55    int length, total_length = 0;
 56
 57    if (argc != 2)
 58    {
 59        rt_kprintf("Usage:\n");
 60        rt_kprintf("wavrecord_sample file.wav\n");
 61        return -1;
 62    }
 63
 64    fd = open(argv[1], O_WRONLY | O_CREAT);
 65    if (fd < 0)
 66    {
 67        rt_kprintf("open file for recording failed!\n");
 68        return -1;
 69    }
 70    write(fd, &header, sizeof(struct wav_header));
 71
 72    buffer = rt_malloc(RECORD_CHUNK_SZ);
 73    if (buffer == RT_NULL)
 74        goto __exit;
 75
 76    /* 根据设备名称查找 Audio 设备,获取设备句柄 */
 77    mic_dev = rt_device_find(SOUND_DEVICE_NAME);
 78    if (mic_dev == RT_NULL)
 79        goto __exit;
 80
 81    /* 以只读方式打开 Audio 录音设备 */
 82    rt_device_open(mic_dev, RT_DEVICE_OFLAG_RDONLY);
 83
 84    /* 设置采样率、通道、采样位数等音频参数信息 */
 85    caps.main_type               = AUDIO_TYPE_INPUT;                            /* 输入类型(录音设备 )*/
 86    caps.sub_type                = AUDIO_DSP_PARAM;                             /* 设置所有音频参数信息 */
 87    caps.udata.config.samplerate = RECORD_SAMPLERATE;                           /* 采样率 */
 88    caps.udata.config.channels   = RECORD_CHANNEL;                              /* 采样通道 */
 89    caps.udata.config.samplebits = 16;                                          /* 采样位数 */
 90    rt_device_control(mic_dev, AUDIO_CTL_CONFIGURE, &caps);
 91
 92    while (1)
 93    {
 94        /* 从 Audio 设备中,读取 20ms 的音频数据  */
 95        length = rt_device_read(mic_dev, 0, buffer, RECORD_CHUNK_SZ);
 96
 97        if (length)
 98        {
 99            /* 写入音频数据到到文件系统 */
100            write(fd, buffer, length);
101            total_length += length;
102        }
103
104        if ((total_length / RECORD_CHUNK_SZ) >  (RECORD_TIME_MS / 20))
105            break;
106    }
107
108    /* 重新写入 wav 文件的头 */
109    wavheader_init(&header, RECORD_SAMPLERATE, RECORD_CHANNEL, total_length);
110    lseek(fd, 0, SEEK_SET);
111    write(fd, &header, sizeof(struct wav_header));
112    close(fd);
113
114    /* 关闭 Audio 设备 */
115    rt_device_close(mic_dev);
116
117__exit:
118    if (fd >= 0)
119        close(fd);
120
121    if (buffer)
122        rt_free(buffer);
123
124    return 0;
125}
126MSH_CMD_EXPORT(wavrecord_sample, record voice to a wav file);

以上代码其实就是文档中心的示例程序,搬过来就能直接使用,很方便~

注意这里:

1#define RECORD_TIME_MS      5000
2#define RECORD_SAMPLERATE   8000
3#define RECORD_CHANNEL      2

录音时间固定为每次5s,音频采样率设置为8000,通道数设置为2,因为百度语音要求的是16000(8000*2)的采样率。

同样的,这里导出了wavrecord_sample这个命令,使用方式:在finsh控制台输入:

1wavrecord_sample bd.wav

录音功能便开启了,程序里设置的录音时间是5s,音频将存放于bd.wav中。再用之前实现的bd命令进行语音识别,发现效果还是非常不错的。

至此,我们的录音功能就算实现了。说实话,别看上面配置很简单,但其实Audio设备还是挺复杂的,需要反复学习,所以我可能讲的不好(好吧,我基本没讲),需要大家多去看看框架的源码。

3.IPC使用

整个项目的各部分功能我们都已经实现了,接下来就要用IPC将它们串接起来,形成一个完整的项目,设计效果是这样的:

按下按键,开始录音,录音结束后自动将音频发送到百度服务器端,返回识别结果,进行数据解析,显示结果(控制外设)。

那么我们大致可以将以上功能划分为三个线程,分别是:按键线程,录音线程以及识别线程。具体怎么做呢?前面我已经把各部分功能拆分成了多个源文件,这样使整个工程看起来更加简洁,下面我们简单理一下:


  • main.c

    —> 功能初始化,创建线程…


  • bd_speech_rcg.c

    —> 语音识别


  • wav_record.c

    —> 录音

  • 按键我这里只是简单的读IO状态,就放在main.c就好了

bd_speech_rcg.c和wav_record.c里都是前面已经实现的功能函数,这里我就不做讲解,主要来看看main.c:

  1/* main.c */
  2
  3#include <rtthread.h>
  4#include <rtdevice.h>
  5#include <board.h>
  6#include <dfs_posix.h>
  7#include <string.h>
  8#include <fal.h>
  9#include <drv_lcd.h>
 10#include <cn_font.h>
 11
 12/* 函数声明 */    
 13extern int wavrecord_sample();
 14extern void bd();
 15
 16/* 线程参数 */
 17#define THREAD_PRIORITY            25      //优先级
 18#define THREAD_STACK_SIZE        1024    //线程栈大小
 19#define THREAD_TIMESLICE        10      //时间片
 20
 21/* 线程句柄 */
 22static rt_thread_t tid1 = RT_NULL;
 23static rt_thread_t tid2 = RT_NULL;
 24static rt_thread_t tid3 = RT_NULL;
 25
 26/* 指向信号量的指针 */
 27static rt_sem_t dynamic_sem = RT_NULL;
 28
 29/* 邮箱控制块 */
 30static struct rt_mailbox mb;
 31/* 用于放邮件的内存池 */
 32static char mb_pool[128];
 33
 34/* 录音线程 tid1 入口函数 */
 35static void thread1_entry(void *parameter)
 36{
 37    static rt_err_t result;
 38    while(1)
 39    {
 40        result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
 41        if (result != RT_EOK)
 42        {
 43            rt_kprintf("take a dynamic semaphore, failed.\n");
 44            rt_sem_delete(dynamic_sem);
 45            return;
 46        }
 47        else
 48        {
 49            rt_kprintf("take a dynamic semaphore, success.\n");
 50            wavrecord_sample();         //获取到信号量,开始录音
 51            rt_mb_send(&mb, NULL);      //录音结束,发送邮件
 52        }
 53        rt_thread_mdelay(100);
 54    }        
 55}
 56
 57/* 语音识别线程 tid2 入口函数 */
 58static void thread2_entry(void *parameter)
 59{
 60    while (1)
 61    {
 62        rt_kprintf("try to recv a mail\n");
 63        /* 从邮箱中收取邮件 */
 64        if (rt_mb_recv(&mb, NULL, RT_WAITING_FOREVER) == RT_EOK)
 65        {
 66            show_str(20, 40, 200, 32, (rt_uint8_t *)"百度语音识别", 32);
 67            show_str(20, 100, 200, 32, (rt_uint8_t *)"识别结果:", 32);
 68            rt_kprintf("get a mail from mailbox!");
 69            bd();       //收到邮件,进行语音识别
 70            rt_thread_mdelay(100);
 71        }
 72    }
 73    /* 执行邮箱对象脱离 */
 74    rt_mb_detach(&mb);
 75}
 76
 77/* 按键线程 tid3 入口函数 */
 78static void thread3_entry(void *parameter)
 79{
 80        unsigned int count = 1;
 81        while(count > 0)
 82        {
 83            if(rt_pin_read(KEY0) == 0)
 84            {
 85                rt_kprintf("release a dynamic semaphore.\n");
 86                rt_sem_release(dynamic_sem);        //当按键被按下,释放一个信号量
 87            }
 88        rt_thread_mdelay(100);
 89        }
 90}
 91
 92int main(void)
 93{
 94
 95        fal_init();
 96        rt_pin_mode(KEY0, PIN_MODE_INPUT);
 97        rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);
 98        rt_pin_mode(PIN_LED_G, PIN_MODE_OUTPUT);
 99        rt_pin_mode(PIN_LED_B, PIN_MODE_OUTPUT);
100        rt_pin_write(PIN_LED_R,1);
101        rt_pin_write(PIN_LED_G,1);
102        rt_pin_write(PIN_LED_B,1);
103
104    /* 清屏 */
105    lcd_clear(WHITE);
106
107    /* 设置背景色和前景色 */
108    lcd_set_color(WHITE,BLACK);
109
110    /* 在LCD 上显示字符 */
111    lcd_show_string(55, 5, 24, "RT-Thread");
112
113    show_str(120, 220, 200, 16, (rt_uint8_t *)"By 霹雳大乌龙", 16);
114
115    /* 创建一个动态信号量,初始值是 0 */
116    dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_FIFO);
117    if (dynamic_sem == RT_NULL)
118    {
119        rt_kprintf("create dynamic semaphore failed.\n");
120        return -1;
121    }
122    else
123    {
124        rt_kprintf("create done. dynamic semaphore value = 0.\n");
125    }
126
127    rt_err_t result;
128
129    /* 初始化一个 mailbox */
130    result = rt_mb_init(&mb,
131                        "mbt",                      /* 名称是 mbt */
132                        &mb_pool[0],                /* 邮箱用到的内存池是 mb_pool */
133                        sizeof(mb_pool) / 4,        /* 邮箱中的邮件数目,因为一封邮件占 4 字节 */
134                        RT_IPC_FLAG_FIFO);          /* 采用 FIFO 方式进行线程等待 */
135    if (result != RT_EOK)
136    {
137        rt_kprintf("init mailbox failed.\n");
138        return -1;
139    }
140
141    /* 创建线程 */    
142        tid1 = rt_thread_create("thread1",
143                            thread1_entry, RT_NULL,
144                            THREAD_STACK_SIZE,
145                            THREAD_PRIORITY, THREAD_TIMESLICE);
146    if (tid1 != RT_NULL)
147        rt_thread_startup(tid1);
148
149
150        tid2 = rt_thread_create("thread2",
151                            thread2_entry, RT_NULL,
152                            THREAD_STACK_SIZE,
153                            THREAD_PRIORITY, THREAD_TIMESLICE);
154    if (tid2 != RT_NULL)
155        rt_thread_startup(tid2);
156
157        tid3 = rt_thread_create("thread3",
158                            thread3_entry, RT_NULL,
159                            THREAD_STACK_SIZE,
160                            THREAD_PRIORITY, THREAD_TIMESLICE);
161    if (tid3 != RT_NULL)
162        rt_thread_startup(tid3);
163
164    return 0;
165}

通过上面的源码可以看到,我采用了信号量+邮箱的通讯机制;按键线程不断读取IO状态,当按键被按下时,释放一个信号量;录音线程处于永久等待信号量的状态,当接收到一个信号量时开始录音,录音结束后发送一封邮件到邮箱中;识别线程不停尝试获取邮件,当接收到邮件时,进行语音识别,后续的解析,显示。

4、总结



视频演示:

受在线识别及串口wifi传输速率等限制,该视频识别速率不佳,仅作功能演示。

自此整个百度语音识别项目就完整结束了,这个作品还不是很完美,希望大家批评指正,但大致流程应该是没问题的,效果亲测也OK。我会把完整工程放到我的GitHub上,后续随着我对RT-Thread的进一步学习,我会对项目进行优化,实现功能拓展,以下是本项目在GitHub上的地址:https://github.com/lxzzzzzxl/Baidu_Speech_base_on_RT-Thread  感谢大家支持!


欢迎大家留言讲讲:有哪些部分希望作者可以展开讲解或者期待作者可以实现哪些功能~


RT-Thread线上/下活动


1、






RT-Thread开发者大会报名



深圳站即将截止报名!除了RT-Thread在中高端智能领域的应用、RT-Thread Studio、打造IoT极速开发模式等精彩内容呈现,还长还有各种开发板、智能焊台、无限调试器、四轴等礼品,期待您的参与!


立即报名


#题外话#

喜欢RT-Thread不要忘了在GitHub上留下你的

STAR

哦,你的star对我们来说非常重要!链接地址:https://github.com/RT-Thread/rt-thread

你可以添加微信17775982065为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群


RT-Thread


让物联网终端的开发变得简单、快速,芯片的价值得到最大化发挥。Apache2.0协议,可免费在商业产品中使用,不需要公布源码,无潜在商业风险。

长按二维码,关注我们


点击“阅读原文”报名开发者大会



版权声明:本文为FILLMOREad原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。