本期分享来自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所有的设备框架的对接,基本上都是两大步骤:
准备好相应的设备驱动,实现对应框架的ops函数
进行设备注册
想要弄懂这两步,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协议,可免费在商业产品中使用,不需要公布源码,无潜在商业风险。
长按二维码,关注我们
点击“阅读原文”报名开发者大会