文章目录
- 前言
- STM32MP157简易语音助手
- alsa-lib简介:
- 移植alsa-lib库:
- libcurl库简介:
- 移植libcurl库:
- API调用
- 修改asrmain.c文件
- 修改token.c文件
- 录音
- 文件IO
- 打开音频文件
- 硬件控制
- sysfs文件系统
- 数据解析和控制
- 多线程
- 主循环
- 实现效果及注意事项
- 实现效果
- 注意事项
- 源代码(转载请注明出处)
前言
本篇分享:
Linux应用编程之音频编程,使用户用户可以使用语音控制开发板上的LED灯和蜂鸣器模块。
环境介绍:
系统:Linux
硬件:正点原子STM32MP157开发板
声卡:开发板自带
STM32MP157简易语音助手
实现目标 :用户可以使用语音控制开发板上的LED灯和蜂鸣器模块。
知识点 : C语言、文件IO、alsa-lib 库的使用、libcurl库、API调用、字符串解析、多线程。
在上一篇STM32MP157语音识别项目中,由于之前使用的交叉编译器无法正常编译使用了libcurl库的程序,导致不得不使用execl函数调用CURL指令实现(可能是由于交叉编译器的c库版本和libcurl库的c库版本不同导致)。这样的程序不够灵活(依赖操作系统提供的指令和参数格式)且性能低(调用指令时需要花费额外的系统开销和时间,包括切换上下文、启动子进程、进行系统调用等操作,而使用c库函数则避免了这些额外的开销 )。所以,这次继续沿用Linux语音识别项目中用到的libcurl库来实现。
alsa-lib简介:
alsa-lib 是一套 Linux 应用层的 C 语言函数库,为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套 API 即可完成对底层声卡设备的操控,譬如播放与录音。
用户空间的 alsa-lib 对应用程序提供了统一的API 接口,这样可以隐藏驱动层的实现细节,简化了应用 程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以,主要就是学习 alsa-lib 库函数的使用、如何基于 alsa-lib 库函数开发音频应用程序。
alsa-lib官方说明文档:https://www.alsa-project.org/alsa-doc/alsa-lib/
移植alsa-lib库:
正点STM32MP157开发板出厂已移植(非广告!),需要请参考其他教程。
要在嵌入式linux系统上运行使用alsa-lib库的程序,需要移植alsa-lib库,可以参考网上移植alsa-lib库的方法,或自行下载alsa-lib资源包,自行编译移植。
开源ALSA架构的官网地址:https://www.alsa-project.org/wiki/Main_Page
libcurl库简介:
libcurl是一个跨平台的网络协议库,支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议。libcurl同样支持HTTPS证书授权,HTTP POST, HTTP PUT, FTP 上传, HTTP基本表单上传,代理,cookies,和用户认证。
官网地址:http://curl.haxx.se/
移植libcurl库:
正点STM32MP157开发板出厂已移植(非广告!),需要请参考其他教程。
注意:curl
指令和libcurl
是两个不同的东西,虽然它们都用于处理HTTP请求,但是有以下区别:
curl
指令是一个命令行工具,而libcurl
是一个C语言的库,可以通过函数调用使用。curl
指令可以直接在终端中运行,而libcurl
需要在编程时使用。curl
指令的功能相对简单,主要用于从终端中发送HTTP请求并获取响应,而libcurl
功能更为强大,可以通过编程实现更多复杂的HTTP请求和响应处理操作。curl
指令可以在不同的操作系统和终端上运行,而libcurl
需要在特定的平台上进行编译和部署。
总之,curl
指令是一个简单、方便的工具,可以帮助开发人员快速进行HTTP请求和响应测试。而libcurl
是一个功能更为强大的库,适合于在编程时进行HTTP请求和响应处理,但需要进行编译和部署。
API调用
该程序使用的是百度语音识别API
注册后领取免费额度及创建中文普通话应用(创建前先领取免费额度(180 天免费额度,可调用约 5 万次左右) )
创建好应用后,可以得到API key和Secret Key(填写到程序中的相应位置)
调用API相关说明,Demo代码中有多种语言的调用示例可以参考,使用c语言的话也可以直接在本程序上面再次更改:
API相关c文件中 需要修改的有asrmain.c、token.c和相应的头文件:
修改asrmain.c文件
asrmain.c的fill_config函数中(该函数我已修改,原本无file参数,根据实际情况使用),需要修改的有:音频文件格式,API Key以及Secret Key:
RETURN_CODE fill_config(struct asr_config *config,char *file) {
// 填写网页上申请的appkey 如 g_api_key="g8eBUMSokVB1BHGmgxxxxxx"
char api_key[] = "填写网页上申请的API key";
// 填写网页上申请的APP SECRET 如 $secretKey="94dc99566550d87f8fa8ece112xxxxx"
char secret_key[] = "填写网页上申请的Secret Key";
// 需要识别的文件
char *filename = NULL;
filename = file;
// 文件后缀仅支持 pcm/wav/amr 格式,极速版额外支持m4a 格式
char format[] = "pcm";
char *url = "http://vop.baidu.com/server_api"; // 可改为https
// 1537 表示识别普通话,使用输入法模型。其它语种参见文档
int dev_pid = 1537;
char *scope = "audio_voice_assistant_get"; // # 有此scope表示有asr能力,没有请在网页里勾选,非常旧的应用可能没有
…………
结合音频录制的程序使用的话,还需要删除示例中的main函数,run函数中的相关初始化以及API调用函数需要根据实际情况重新调整调用位置。本项目总体按照:获取token(在程序开始时获取一次即可,根据官网可知获取的token有效期为30天,重新获取token则之前获取的token失效)->调用API->得到返回结果->解析结果->对硬件控制。
asrmain.c的run_asr函数中(该函数我已修改,原本无result_voice参数,根据实际情况使用),需要修改的有:禁用SSL证书验证(在文件中查到下面这个函数名即可找到需要修改的位置)和 延长连接超时时间(源代码中设定时间为5s,在开发板上连接时间更长,需要改为10s,可能是由于硬件性能较弱或者网络环境不稳定等原因导致 )。
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0)
是一个CURL库的选项设置,用于控制CURL库对SSL证书的验证行为。默认情况下,CURL库会验证SSL证书的有效性,以确保请求的安全性。如果SSL证书验证失败,CURL库将阻止请求的进一步处理并返回一个错误。
将CURLOPT_SSL_VERIFYPEER
选项设置为0将禁用SSL证书验证,从而允许不受信任的证书通过。这个设置通常用于调试和测试目的,不建议在生产环境中使用,因为它会降低请求的安全性。如果需要在生产环境中使用不受信任的证书,建议使用自签名证书或受信任的CA签名证书,并通过其他手段验证证书的有效性,而不是禁用SSL证书验证。
修改token.c文件
和上面部分相同,禁用SSL证书验证。
录音
查看Linux语音识别中的录音内容
文件IO
我们需要将录制的音频文件保存到本地,就需要用到文件IO相关知识,打开音频文件以及向音频文件写数据。
打开音频文件
函数:
函数原型:
FILE *fopen(const char *filename, const char *mode)
参数:
filename -- 字符串,表示要打开的文件名称。
mode -- 字符串,表示文件的访问模式。
作用:
以指定的方式打开文件。
代码:
/*创建一个保存PCM数据的文件*/
if ((pcm_data_file = fopen(argv[1], "wb")) == NULL)
{
printf("无法创建%s音频文件.\n", argv[1]);
exit(1);
}
printf("用于录制的音频文件已打开.\n");
参数:
argv[1]:程序执行时传递的参数,例./voice record.cpm,则该参数为"record.cpm"
"wb":只写打开或新建一个二进制文件,只允许写数据。
硬件控制
sysfs文件系统
在 Linux 系统下,一切皆文件!应用层如何操控底层硬件,同样也是通过文件 I/O 的方式来实现。本项目的硬件控制都是通过sysfs文件系统实现对LED和蜂鸣器的控制。
在嵌入式Linux开发中,sysfs文件系统通常被用来访问硬件资源,例如GPIO、I2C、SPI等外设,可以通过sysfs文件系统的接口来控制和读取硬件设备的状态信息。因此,sysfs文件系统对于嵌入式Linux开发非常重要,几乎所有的嵌入式Linux系统都会支持sysfs文件系统。
sysfs 文件系统挂载在/sys 目录下,启动开发板后可以到/sys目录查看。/sys下不同的子目录。
/sys
目录下包含了很多子目录,每个子目录都代表一个系统设备或内核模块,其中常见的子目录包括:
- block:块设备相关的信息,例如硬盘、光驱等。
- bus:系统总线相关的信息,例如USB、PCI、I2C等。
- class:设备类型相关的信息,例如输入设备、网络设备等。
- dev:与设备文件相关的信息,例如设备号、设备名称等。
- firmware:硬件固件相关的信息,例如BIOS、驱动程序等。
- fs:文件系统相关的信息,例如文件系统挂载状态等。
- kernel:内核相关的信息,例如内核版本、内核命令行参数等。
- module:内核模块相关的信息,例如已加载的内核模块等。
- power:电源管理相关的信息,例如电量、电源状态等。
- sys:系统信息相关的信息,例如CPU信息、内存信息等。
这些子目录下包含了许多虚拟文件和目录,通过这些文件和目录可以方便地访问内核数据结构和设备信息,从而实现对设备的控制和监控。
在sysfs文件系统中,一个硬件设备为一个目录,设备的属性则为文件。
在正点原子STM32MP157中,LED和蜂鸣器对应的设备目录均为/sys/class/leds/。
LED的触发方式控制文件为/sys/class/leds/user-led/trigger,LED亮度控制文件为/sys/class/leds/user-led/brightness。
蜂鸣器的触发方式控制未见为/sys/class/leds/beep/trigger,蜂鸣器开关控制文件为/sys/class/leds/beep/brightness。
数据解析和控制
本项目使用strstr函数对识别的结果进行判断,判断识别结果中是否包含"灯"、“蜂鸣器"字符串,有的话再判断结果中包含"开"还是"关”。
如识别的结果为"灯打开"或"开灯",程序将修改向LED灯的触发方式文件写入none(无触发),向LED亮度控制文件写入"1",这样就实现了LED灯的点亮
函数:
函数原型:
char *strstr(const char *haystack, const char *needle)
haystack -- 要被检索的 C 字符串.
needle -- 在 haystack 字符串内要搜索的子字符串。
作用:
查看原字符串中是否存在子字符串,不存在返回NULL
部分代码:
void Voice_Controll(char result[])
{
/*检测语音和灯的控制有关*/
if(strstr(result,"灯")!=NULL)
{
if(strstr(result,"开")!=NULL && strstr(result,"关")!=NULL)
return;
else if(strstr(result,"开")!=NULL)
Led_Controll(1);
else if(strstr(result,"关")!=NULL)
Led_Controll(0);
}
/*检测语音和蜂鸣器的控制有关*/
......
}
void Led_Controll(int ONOFF)
{
int fd1,fd2;
/*打开LED触发文件*/
fd1 = open(LED_TRIGGER, O_WRONLY);
if (0 > fd1) {
perror("open error");
exit(-1);
}
/*打开LED开关文件*/
fd2 = open(LED_BRIGHTNESS, O_WRONLY);
if (0 > fd2) {
perror("open error");
exit(-1);
}
/*根据传递参数控制LED*/
if(ONOFF == 1)
{
write(fd1, "none", 4); //先将触发模式设置为 none
write(fd2, "1", 1); //点亮 LED
printf("LED已打开!\n");
}
else
{
write(fd1, "none", 4); //先将触发模式设置为 none
write(fd2, "0", 1); //熄灭 LED
printf("LED已关闭!\n");
}
/*关闭文件*/
close(fd1);
close(fd2);
}
多线程
函数:
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
thread -- 传出参数,保存系统为我们分配好的线程 ID
attr -- 通常传 NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
start_routine -- 函数指针,指向线程主函数,该函数运行结束,则线程结束。
arg -- 线程主函数执行期间所使用的参数。
作用:
创建一个新线程。
函数原型:
int int truncate(const char *path,off_t length);
参数:
path -- 文件路径名。
length -- 截断长度,若文件大小>length大小,额外的数据丢失。若文件大小<length大小,那么,这个文件将会被扩展,扩展的部分将补以null,也就是‘\0’。
作用:
截断或扩展文件。
代码:
/*创建子线程检测按键是否按下*/
pthread_t tid;
ret = pthread_create(&tid, NULL, button_tfn, NULL);
if (ret != 0) perror("pthread_create failed");
void *button_tfn(void *arg)
{
struct input_event in_ev = {0};
int fd;
int value = -1;
/*打开按键事件对应的文件*/
if (0 > (fd = open("/dev/input/event1", O_RDONLY)))
{
perror("open error");
exit(-1);
}
while(1)
{
/*循环读取数据*/
if (sizeof(struct input_event) != read(fd, &in_ev, sizeof(struct input_event)))
{
perror("read error");
exit(-1);
}
if (EV_KEY == in_ev.type && in_ev.code == 114)//114为KEY0
{
/*按键事件*/
switch (in_ev.value)
{
/*KEY0松开*/
case 0:
/**
* 1.更新按键状态为松开
* 2.延时等待主循环判断,否则可能出现主循环先判断标志位为1而出现PCM设备停止还在继续读数据
* 3.停止PCM设备
*/
key_flag_now = 0;
sleep(1);
snd_pcm_drop(capture_handle);
break;
/*KEY0按下*/
case 1:
/**
* 1.清空文件,使文件从头开始写,等于重新录制音频
* 2.同样注意顺序,先使设备恢复进入准备状态,避免出现主循环先检测到标志位为1而读取声卡设备
* 3.更新按键状态为按下
*/
truncate(pcm_file_name,1);
snd_pcm_prepare(capture_handle);
key_flag_now = 1;
break;
}
}
else if(EV_KEY == in_ev.type && in_ev.code == 115)//115为KEY1
{
/*按键事件*/
switch (in_ev.value)
{
/*KEY1按下*/
case 1:
/*退出程序*/
exit_program();
break;
}
}
}
}
主循环
主循环内判断声卡设备状态是否改变(按键状态决定),若当前声卡为运行状态则进行音频采集,若当前声卡为停止状态则调用API进行识别。
while (1)
{
/*判断按键状态是否更新*/
if(key_flag_now != key_flag_old)
{
/*视当前状态为旧状态*/
key_flag_old = key_flag_now;
/*若按键按下*/
if(key_flag_now == 1)
printf("开始采集音频数据...\n");
/*若按键松开*/
else
{
printf("采集结束!\n");
/*调用API进行识别*/
run_asr(&config, token,result);
/*对识别的结果进行处理*/
Voice_Controll(result);
printf("请长按KEY0按键开始采集音频数据!单击KEY1退出程序!\n");
}
}
/*若按键按下*/
if(key_flag_now == 1)
{
/*从声卡设备读取一帧音频数据:2048字节*/
ret = snd_pcm_readi(capture_handle, buffer, buffer_frames);
if(0 > ret)
{
printf("从音频接口读取失败(%s)\n", snd_strerror(ret));
exit(1);
}
/*写数据到文件: 音频的每帧数据样本大小是16位=2个字节*/
fwrite(buffer, (ret * AUDIO_CHANNEL_SET), frame_byte, pcm_data_file);
}
}
实现效果及注意事项
实现效果
如图所示长按KEY0按键开始音频录制,松开即音频录制结束,再调用百度语言API进行识别,并向用户展示识别的结果以及实现对硬件的控制。之后用户可自行选择继续识别或退出程序。
注意事项
该程序在声卡不进行录音时是将声卡设备给停止工作了的,在停止声卡设备前需要加入一小段的延时等待,若不添加延时等待,可能会出现子线程使声卡设备停止的同时主线程在读取声卡设备,从而导致下图中出现的错误: