STM32MP157-Linux音频应用编程-简易语音助手


前言

本篇分享:

Linux应用编程之音频编程,使用户用户可以使用语音控制开发板上的LED灯和蜂鸣器模块。

环境介绍:

系统:Linux
硬件:正点原子STM32MP157开发板
声卡:开发板自带


STM32MP157简易语音助手

实现目标 :用户可以使用语音控制开发板上的LED灯和蜂鸣器模块。
知识点 : C语言文件IOalsa-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 keySecret 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.crun_asr函数中(该函数我已修改,原本无result_voice参数,根据实际情况使用),需要修改的有:禁用SSL证书验证(在文件中查到下面这个函数名即可找到需要修改的位置)和 延长连接超时时间(源代码中设定时间为5s,在开发板上连接时间更长,需要改为10s,可能是由于硬件性能较弱或者网络环境不稳定等原因导致 )。

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);//禁用SSL证书验证
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文件

token.cspeech_get_token函数中:
和上面部分相同,禁用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进行识别,并向用户展示识别的结果以及实现对硬件的控制。之后用户可自行选择继续识别或退出程序。

注意事项

该程序在声卡不进行录音时是将声卡设备给停止工作了的,在停止声卡设备前需要加入一小段的延时等待,若不添加延时等待,可能会出现子线程使声卡设备停止的同时主线程在读取声卡设备,从而导致下图中出现的错误:
在这里插入图片描述


源代码(转载请注明出处)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Octopus1633/article/details/129348136
今日推荐