ros小车(五)语音功能二(科大讯)
1.1 进入科大迅飞开放平台
1.2 创建一个应用,我的 是ros机器人123
1.3 下载SDK,选linux,彩色表示选中,语音听写有次数限制一天500次,而且在线,选离线命令识别,测试中
1.4 测试语音合成是否下载成功
先下载mplayer播放器
sudo apt-get install mplayer
到tts_offline_sample目录下(离线和在线名字不一样)
cd ~/SoftWare/samples/tts_offline_sample/
source 64bit_make.sh
make
将“64bit_make.sh”这个文件夹拷贝到bin目录下,我们后面需要使用到
cp 64bit_make.sh ../../bin/
回到bin目录下
cd ~/SoftWare/bin/
./tts_offline_sample
此时我们可以看到编译完成,而且bin目录下多了一个WAV文件
play tts_offline_sample.wav
此时,我们需要播m放WAV文件的内容,在此之前需要下在SOX,安装完后才能在可以用命令行来播放
sudo apt install sox
play tts_offline_sample.wav
在此,就能听到声音了,但是关掉命令窗口后重新执行tts_samples会发现libmsc.so找不到,主要是因为没有source,路径找不到
但是为以后不用每次都要source所以我们需要进行一下步骤
cd ~/SoftWare/libs/x64
sudo cp libmsc.so /usr/lib
这样以后就不用每次都source了,当我们在bin目录下再执行./tts_samples就不会出错了
同时我们需要在tts_offline_samples中修改Makefile文件,在文件中加入” $(DIR_BIN)/*.wav“使每次make clean都会删除之前的WAV文件
vim ~/SoftWare/samples/tts_offline_sample/Makefile
#common makefile header
DIR_INC = ../../include
DIR_BIN = ../../bin
DIR_LIB = ../../libs
TARGET = tts_sample
BIN_TARGET = $(DIR_BIN)/$(TARGET)
CROSS_COMPILE =
CFLAGS = -g -Wall -I$(DIR_INC)
LDFLAGS := -L$(DIR_LIB)/x64
LDFLAGS += -lmsc -lrt -ldl -lpthread -lstdc++
OBJECTS := $(patsubst %.c,%.o,$(wildcard *.c))
$(BIN_TARGET) : $(OBJECTS)
$(CROSS_COMPILE)gcc $(CFLAGS) $^ -o $@ $(LDFLAGS)
%.o : %.c
$(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@
clean:
@rm -f *.o $(BIN_TARGET) $(DIR_BIN)/*.wav
.PHONY:clean
#common makefile foot
现在再次运行就可以听到声音
yys@yys:~/SoftWare/bin$ ./tts_offline_sample ########################################################################### ## 语音合成(Text To Speech,TTS)技术能够自动将任意文字实时转换为连续的 ## ## 自然语音,是一种能够在任何时间、任何地点,向任何人提供语音信息服务的 ## ## 高效便捷手段,非常符合信息时代海量数据、动态更新和个性化查询的需求。 ## ########################################################################### 开始合成 ... 正在合成 ... 合成完毕 按任意键退出 ... yys@yys:~/SoftWare/bin$ play tts_sample.wav play WARN alsa: can't encode 0-bit Unknown or not applicable tts_sample.wav: File Size: 498k Bit Rate: 256k Encoding: Signed PCM Channels: 1 @ 16-bit Samplerate: 16000Hz Replaygain: off Duration: 00:00:15.57 In:100% 00:00:15.57 [00:00:00.00] Out:249k [ | ] Clip:0 Done.
cd catkin_ws/src/
catkin_create_pkg voice_system std_msgs rospy roscpp
把科大迅飞包里的代码复制到你创建的包里,并重命名为xf_tts.cpp
cp ~/SoftWare/samples/tts_offline_sample/tts_offline_sample.c ~/catkin_ws/src/voice_system/src/xf_tts.cpp
修改xf_tts.cpp文件(注意要把appid改为自己的appid,gtts.h,.jet,等路径要改为自己该文件的路径)
vim /home/yys/catkin_ws/src/voice_system/src/xf_tts.cpp
/*
* 语音合成(Text To Speech,TTS)技术能够自动将任意文字实时转换为连续的
* 自然语音,是一种能够在任何时间、任何地点,向任何人提供语音信息服务的
* 高效便捷手段,非常符合信息时代海量数据、动态更新和个性化查询的需求。
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <ros/ros.h>
#include <std_msgs/String.h>
#include "/home/yys/SoftWare/include/qtts.h"
#include "/home/yys/SoftWare/include/msp_cmn.h"
#include "/home/yys/SoftWare/include/msp_errors.h"
const char* fileName="/home/yys/voice.wav";
const char* playPath="play /home/yys/voice.wav";
typedef int SR_DWORD;
typedef short int SR_WORD ;
/* wav音频头部格式 */
typedef struct _wave_pcm_hdr
{
char riff[4]; // = "RIFF"
int size_8; // = FileSize - 8
char wave[4]; // = "WAVE"
char fmt[4]; // = "fmt "
int fmt_size; // = 下一个结构体的大小 : 16
short int format_tag; // = PCM : 1
short int channels; // = 通道数 : 1
int samples_per_sec; // = 采样率 : 8000 | 6000 | 11025 | 16000
int avg_bytes_per_sec; // = 每秒字节数 : samples_per_sec * bits_per_sample / 8
short int block_align; // = 每采样点字节数 : wBitsPerSample / 8
short int bits_per_sample; // = 量化比特数: 8 | 16
char data[4]; // = "data";
int data_size; // = 纯数据长度 : FileSize - 44
} wave_pcm_hdr;
/* 默认wav音频头部数据 */
wave_pcm_hdr default_wav_hdr =
{
{ 'R', 'I', 'F', 'F' },
0,
{'W', 'A', 'V', 'E'},
{'f', 'm', 't', ' '},
16,
1,
1,
16000,
32000,
2,
16,
{'d', 'a', 't', 'a'},
0
};
/* 文本合成 */
int text_to_speech(const char* src_text, const char* des_path, const char* params)
{
int ret = -1;
FILE* fp = NULL;
const char* sessionID = NULL;
unsigned int audio_len = 0;
wave_pcm_hdr wav_hdr = default_wav_hdr;
int synth_status = MSP_TTS_FLAG_STILL_HAVE_DATA;
if (NULL == src_text || NULL == des_path)
{
printf("params is error!\n");
return ret;
}
fp = fopen(des_path, "wb");
if (NULL == fp)
{
printf("open %s error.\n", des_path);
return ret;
}
/* 开始合成 */
sessionID = QTTSSessionBegin(params, &ret);
if (MSP_SUCCESS != ret)
{
printf("QTTSSessionBegin failed, error code: %d.\n", ret);
fclose(fp);
return ret;
}
ret = QTTSTextPut(sessionID, src_text, (unsigned int)strlen(src_text), NULL);
if (MSP_SUCCESS != ret)
{
printf("QTTSTextPut failed, error code: %d.\n",ret);
QTTSSessionEnd(sessionID, "TextPutError");
fclose(fp);
return ret;
}
printf("正在合成 ...\n");
fwrite(&wav_hdr, sizeof(wav_hdr) ,1, fp); //添加wav音频头,使用采样率为16000
while (1)
{
/* 获取合成音频 */
const void* data = QTTSAudioGet(sessionID, &audio_len, &synth_status, &ret);
if (MSP_SUCCESS != ret)
break;
if (NULL != data)
{
fwrite(data, audio_len, 1, fp);
wav_hdr.data_size += audio_len; //计算data_size大小
}
if (MSP_TTS_FLAG_DATA_END == synth_status)
break;
}
printf("\n");
if (MSP_SUCCESS != ret)
{
printf("QTTSAudioGet failed, error code: %d.\n",ret);
QTTSSessionEnd(sessionID, "AudioGetError");
fclose(fp);
return ret;
}
/* 修正wav文件头数据的大小 */
wav_hdr.size_8 += wav_hdr.data_size + (sizeof(wav_hdr) - 8);
/* 将修正过的数据写回文件头部,音频文件为wav格式 */
fseek(fp, 4, 0);
fwrite(&wav_hdr.size_8,sizeof(wav_hdr.size_8), 1, fp); //写入size_8的值
fseek(fp, 40, 0); //将文件指针偏移到存储data_size值的位置
fwrite(&wav_hdr.data_size,sizeof(wav_hdr.data_size), 1, fp); //写入data_size的值
fclose(fp);
fp = NULL;
/* 合成完毕 */
ret = QTTSSessionEnd(sessionID, "Normal");
if (MSP_SUCCESS != ret)
{
printf("QTTSSessionEnd failed, error code: %d.\n",ret);
}
return ret;
}
int makeTextToWav(const char* text, const char* filename){
int ret = MSP_SUCCESS;
const char* login_params = "appid = 5f6c1268, work_dir = .";//登录参数,appid与msc库绑定,请勿随意改动
/*
* rdn: 合成音频数字发音方式
* volume: 合成音频的音量
* pitch: 合成音频的音调
* speed: 合成音频对应的语速
* voice_name: 合成发音人
* sample_rate: 合成音频采样率
* text_encoding: 合成文本编码格式
*
*/
const char* session_begin_params = "engine_type = local,voice_name=xiaoyan, text_encoding = UTF8, tts_res_path = fo|/home/yys/SoftWare/bin/msc/res/tts/xiaoyan.jet;fo|/home/yys/SoftWare/bin/msc/res/tts/common.jet, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 0";
/* 用户登录 */
ret = MSPLogin(NULL, NULL, login_params); //第一个参数是用户名,第二个参数是密码,第三个参数是登录参数,用户名和密码可在http://www.xfyun.cn注册获取
if (MSP_SUCCESS != ret)
{
printf("MSPLogin failed, error code: %d.\n", ret);
}
else{
printf("开始合成 ...\n");
ret = text_to_speech(text,filename, session_begin_params);
if (MSP_SUCCESS != ret)
{
printf("text_to_speech failed, error code: %d.\n", ret);
}
printf("合成完毕\n");
}
MSPLogout();
return 0;
}
void playWav()
{
system(playPath);
}
void topicCallBack(const std_msgs::String::ConstPtr& msg)
{
std::cout<<"get topic text:" << msg->data.c_str();
makeTextToWav(msg->data.c_str(),fileName);
playWav();
}
int main(int argc, char* argv[]){
const char* start= "科大迅飞在线语音合成模块启动";
makeTextToWav(start,fileName);
playWav();
ros::init(argc,argv, "xf_tts_node");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("/voice/xf_tts_topic", 3,topicCallBack);
ros::spin();
return 0;
}
注意:科大迅飞接在ros上进行语音识别遇到的问题,科大迅飞10102错误
找到jet的路径 (换成绝对路径一般为/home/yys/SoftWare/bin/msc/res/tts)
修改如下:xiaoyan是选择发音的人
const char* session_begin_params = "engine_type = local,voice_name=xiaoyan, text_encoding = UTF8, tts_res_path = fo|/home/yys/SoftWare/bin/msc/res/tts/xiaoyan.jet;fo|/home/yys/SoftWare/bin/msc/res/tts/common.jet, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 0";
在CMakeList文件的“include_directories”后加入“include”,在文件末尾加入
vim /catkin_ws/src/voice_system/CMakeLists.txt
add_executable(xf_tts_node src/xf_tts.cpp)
target_link_libraries(xf_tts_node ${catkin_LIBRARIES} -lmsc -lrt -ldl -lpthread)
编译
cd ~/catkin_ws/
catkin_make
rosrun voice_system xf_tts_node
你能听到,“科大迅飞语音模块启动”的声音“
rostopic pub /voice/xf_tts_topic std_msgs/String 哈哈
会听到哈哈声音,语音合成成功
unzip -q Linux_aisound_exp1227_aitalk_exp1227_awaken_exp1227_5f6c1268.zip -d SoftWare/
cd SoftWare/
yys@yys:~/SoftWare$ tree -L 2 . ├── bin │ ├── asr_offline_record_sample │ ├── audio │ ├── call.bnf 巴科斯范式格式的语法文件 │ ├── control.bnf │ ├── msc │ └── wav ├── doc │ ├── BNF Grammar Development Manual.pdf 巴科斯范式格式的语法开发手册 │ └── readme.txt ├── include 离线命合词识别重要的头文件是msp_cmn.h和qisr.h │ ├── msp_cmn.h 通用接口(Mobile Speech Platform Common Interface Header File) │ ├── msp_errors.h │ ├── msp_types.h │ ├── qisr.h 语音识别(iFLY Speech Recognizer Header File) │ ├── qivw.h 语音唤醒(iFLY Speech Voice Wakeup Header File) │ └── qtts.h 语音合成(iFLY Speech Synthesizer Header Fil ├── libs │ ├── x64 │ └── x86 ├── README ├── release.txt ├── samples │ ├── asr_offline_record_sample 从麦克风识别离线命令 │ ├── asr_offline_sample 从语音文件识别离线命令 │ ├── awaken_offline_sample 唤醒 │ └── tts_offline_sample 离线语音合成 └── wordlist.txt 14 directories, 14 files
动态库移动到/usr/lib,占一个装机量(默认64位)
sudo mv ~/SoftWare/libs/x64/libmsc.so /usr/lib
cd ~/SoftWare/samples/asr_offline_record_sample
yys@yys:~/SoftWare/samples$ tree . ├── asr_offline_record_sample 从麦克风识别离线命令 │ ├── asr_offline_record_sample.c 离线命令识别入口代码 │ ├── asr_offline_record_sample.o │ ├── formats.h │ ├── linuxrec.c 录音文件 │ ├── linuxrec.h 录音文件 │ ├── linuxrec.o │ ├── Makefile │ ├── make.sh │ ├── speech_recognizer.c 进行识别代码 │ ├── speech_recognizer.h 进行识别代码 │ └── speech_recognizer.o ├── asr_offline_sample │ ├── 32bit_make.sh │ ├── 64bit_make.sh │ ├── asr_offline_sample.c │ └── Makefile ├── awaken_offline_sample │ ├── 32bit_make.sh │ ├── 64bit_make.sh │ ├── awaken_offline_sample.c │ └── Makefile └── tts_offline_sample ├── 32bit_make.sh ├── 64bit_make.sh ├── Makefile └── tts_offline_sample.c 4 directories, 23 files
source 64bit_make.sh
报错
linuxrec.c:12:10: fatal error: alsa/asoundlib.h: 没有那个文件或目录
#include <alsa/asoundlib.h>
^~~~~~~~~~~~~~~~~~
sudo apt install libasound2-dev
如果电脑第一次装科大迅飞要移动/libs/x64下面libmsc.so移动到/usr/lib下
sudo cp libmsc.so /usr/lib
但是在执行之前,我们需要认识一下这个巴科斯范式格式的语法文件,因为这个语法文件就是我们后面要离线识别的命令词了
cd ~/SoftWare/bin
cat call.bnf
#BNF+IAT 1.0 UTF-8;
!grammar call;
!slot <want>;
!slot <dialpre>;
!slot <dialsuf>;
!slot <contact>;
!start <callstart>;
<callstart>:[<want>]<dial>;
<want>:我想|我要|请|帮我|我想要|请帮我;
<dial>:<dialpre><contact>[<dialsuf>];
<dialpre>:打电话给!id(10001)|打给!id(10001)|拨打!id(10001)|拨打电话给!id(10001)|呼叫!id(10001)|
打一个电话给!id(10001)|打个电话给!id(10001)|给|拨通!id(10001)|
接通!id(10001)|呼叫!id(10001)|呼叫给!id(10001)|打!id(10001);
<dialsuf>:打电话!id(10001)|打个电话!id(10001)|打一个电话!id(10001)|
拨打电话!id(10001)|拨电话!id(10001)|拨个电话!id(10001)|呼个电话!id(10001)|
的电话!id(10001)|的号码!id(10001)|的手机!id(10001)|
的办公电话!id(10001)|的移动号码!id(10001)|的联通号码!id(10001)|
的电信号码!id(10001)|客服电话!id(10001);
<contact>:丁伟;
就是使用巴科斯范式实现的语法文件,最终上述语法文件将检测的是[<want>]<dial>这样的语音。也就是说<want>规则中的:我想、我要、请、帮我...,这些可以说也可以不说。但是<dial>这个规则必须要说,而<dial>规则是由<dialpre><contact>[<dialsuf>]组成的。这个dialpre是必须要说的,它将检测用户有没有说出来:打电话给、打给、拨打、呼叫....这些语音。后面的<contact>就是检测的联系人,这里的联系人只有一个"丁伟"。最后的这个<dialsuf>是可说可不说,这是因为我们平时要给某人打电话的语法就是:“打电话给丁伟”这样的语法,当然还可以换另外一种表达方式,那就是“拨打丁伟的电话”,所以这里是两种语法规则都支持的。
运行程序
./asr_offline_record_sample
在了解了语法文件后,我们还需要查看下示例源码,因为这里的语法文件中的规则,我们是可以动态的更新的。就是说我们不用重新启动程序,就可以实时的修改检测的语法规则,更新语法规则的API如下:
那接下来看一下在asr_offline_record_sample.c源码中是如何更新本次语法词典文件的,调用该API的代码如下:
int update_lex_cb(int ecode, const char *info, void *udata)
{
UserData *lex_data = (UserData *)udata;
if (NULL != lex_data) {
lex_data->update_fini = 1;
lex_data->errcode = ecode;
}
if (MSP_SUCCESS == ecode)
printf("更新词典成功!\n");
else
printf("更新词典失败!%d\n", ecode);
return 0;
}
int update_lexicon(UserData *udata)
{
const char *lex_content = "丁伟\n黄辣椒";
unsigned int lex_cnt_len = strlen(lex_content);
char update_lex_params[MAX_PARAMS_LEN] = {NULL};
snprintf(update_lex_params, MAX_PARAMS_LEN - 1,
"engine_type = local, text_encoding = UTF-8, \
asr_res_path = %s, sample_rate = %d, \
grm_build_path = %s, grammar_list = %s, ",
ASR_RES_PATH,
SAMPLE_RATE_16K,
GRM_BUILD_PATH,
udata->grammar_id);
return QISRUpdateLexicon(LEX_NAME, lex_content, lex_cnt_len, update_lex_params, update_lex_cb, udata);
}
可以看到在update_lexicon()函数中,将lex_content变量赋值为“丁伟\n黄辣椒”,那就是将在call.bnf语法文件中的<contact>规则,从“丁伟”更新到了“丁伟\n黄辣椒”。意思是说,我们在第一次识别的时候只能识别到“打电话给丁伟”这样的句子,如果没有更新参数之前说“打电话给黄辣椒”是无法识别的,因为默认的call.bnf语法文件中只有“丁伟”。但是当我们在update_lexicon()函数中调用了QISRUpdateLexicon()函数后,我们就可以将<contact>变量更新为“丁伟\n黄辣椒”了。
0x04 实现自定义的命令词识别
#BNF+IAT 1.0;
!grammar call;
!slot <name>;
!start <commands>;
<commands>:(找一下|打电话给) <name>;
<name>: 张三|李四;
#BNF+IAT 1.0 UTF-8;
!grammar control;
!slot <devices>;
!slot <operate>;
!start <cmdstart>;
<cmdstart>:<operate><devices>;
<operate>:停止!id(0)|前进!id(1)|后退!id(2)|左拐!id(3)|右拐!id(4);
<devices>:小车!id(10);
vim ~/SoftWare/samples/asr_offline_record_sample/asr_offline_record_sample.c
不用每次选0和1
printf("从MIC说话\n");
demo_mic(asr_params);
return 0;
在更新离线语法词典完成,开始识别...\n加入while(1)可以无限循环
printf("更新离线语法词典完成,开始识别...\n");
while(1){
ret = run_asr(&asr_data);
if (MSP_SUCCESS != ret) {
printf("离线语法识别出错: %d \n", ret);
goto exit;
}
}
每次说话后显示最后结果
static void show_result(char *string, char is_over)
{
printf("\rResult: [ %s ]", string);
if(is_over)
putchar('\n');
}
暂时先安装一个能说话的,
sudo apt install espeak
修改后
/*
* 语音听写(iFly Auto Transform)技术能够实时地将语音转换成对应的文字。
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "../../include/qisr.h"
#include "../../include/msp_cmn.h"
#include "../../include/msp_errors.h"
#include "speech_recognizer.h"
#define FRAME_LEN 640
#define BUFFER_SIZE 4096
#define SAMPLE_RATE_16K (16000)
#define SAMPLE_RATE_8K (8000)
#define MAX_GRAMMARID_LEN (32)
#define MAX_PARAMS_LEN (1024)
const char * ASR_RES_PATH = "fo|res/asr/common.jet"; //离线语法识别资源路径
const char * GRM_BUILD_PATH = "res/asr/GrmBuilld"; //构建离线语法识别网络生成数据保存路径
const char * GRM_FILE = "/home/yys/xf/bin/call.bnf"; //构建离线识别语法网络所用的语法文件
const char * LEX_NAME = "contact"; //更新离线识别语法的contact槽(语法文件为此示例中使用的call.bnf)
const char * stop = "停止";
const char * go = "前进";
const char * back = "后退";
const char * turnleft = "左转";
const char * turnright = "右转";
typedef struct _UserData {
int build_fini; //标识语法构建是否完成
int update_fini; //标识更新词典是否完成
int errcode; //记录语法构建或更新词典回调错误码
char grammar_id[MAX_GRAMMARID_LEN]; //保存语法构建返回的语法ID
}UserData;
int build_grammar(UserData *udata); //构建离线识别语法网络
int update_lexicon(UserData *udata); //更新离线识别语法词典
int run_asr(UserData *udata); //进行离线语法识别
int build_grm_cb(int ecode, const char *info, void *udata)
{
UserData *grm_data = (UserData *)udata;
if (NULL != grm_data) {
grm_data->build_fini = 1;
grm_data->errcode = ecode;
}
if (MSP_SUCCESS == ecode && NULL != info) {
printf("构建语法成功! 语法ID:%s\n", info);
if (NULL != grm_data)
snprintf(grm_data->grammar_id, MAX_GRAMMARID_LEN - 1, info);
}
else
printf("构建语法失败!%d\n", ecode);
return 0;
}
int build_grammar(UserData *udata)
{
FILE *grm_file = NULL;
char *grm_content = NULL;
unsigned int grm_cnt_len = 0;
char grm_build_params[MAX_PARAMS_LEN] = {NULL};
int ret = 0;
grm_file = fopen(GRM_FILE, "rb");
if(NULL == grm_file) {
printf("打开\"%s\"文件失败![%s]\n", GRM_FILE, strerror(errno));
return -1;
}
fseek(grm_file, 0, SEEK_END);
grm_cnt_len = ftell(grm_file);
fseek(grm_file, 0, SEEK_SET);
grm_content = (char *)malloc(grm_cnt_len + 1);
if (NULL == grm_content)
{
printf("内存分配失败!\n");
fclose(grm_file);
grm_file = NULL;
return -1;
}
fread((void*)grm_content, 1, grm_cnt_len, grm_file);
grm_content[grm_cnt_len] = '\0';
fclose(grm_file);
grm_file = NULL;
snprintf(grm_build_params, MAX_PARAMS_LEN - 1,
"engine_type = local, \
asr_res_path = %s, sample_rate = %d, \
grm_build_path = %s, ",
ASR_RES_PATH,
SAMPLE_RATE_16K,
GRM_BUILD_PATH
);
ret = QISRBuildGrammar("bnf", grm_content, grm_cnt_len, grm_build_params, build_grm_cb, udata);
free(grm_content);
grm_content = NULL;
return ret;
}
int update_lex_cb(int ecode, const char *info, void *udata)
{
UserData *lex_data = (UserData *)udata;
if (NULL != lex_data) {
lex_data->update_fini = 1;
lex_data->errcode = ecode;
}
if (MSP_SUCCESS == ecode)
printf("更新词典成功!\n");
else
printf("更新词典失败!%d\n", ecode);
return 0;
}
int update_lexicon(UserData *udata)
{
const char *lex_content = "丁伟\n小车";
unsigned int lex_cnt_len = strlen(lex_content);
char update_lex_params[MAX_PARAMS_LEN] = {NULL};
snprintf(update_lex_params, MAX_PARAMS_LEN - 1,
"engine_type = local, text_encoding = UTF-8, \
asr_res_path = %s, sample_rate = %d, \
grm_build_path = %s, grammar_list = %s, ",
ASR_RES_PATH,
SAMPLE_RATE_16K,
GRM_BUILD_PATH,
udata->grammar_id);
return QISRUpdateLexicon(LEX_NAME, lex_content, lex_cnt_len, update_lex_params, update_lex_cb, udata);
}
static void show_result(char *string, char is_over)
{
char check = NULL;
printf("\r结果:\n [ %s ]", string);
if(is_over)
putchar('\n');
check = strstr(string, stop);
if(check != NULL)
{
printf("停止");
system("python3 /home/yys/car.py '0'");
exit(0);
}
check = strstr(string, go);
if(check != NULL)
{
printf("为前进");
system("python3 /home/yys/car.py '1'");
exit(0);
}
check = strstr(string, back);
if(check != NULL)
{
printf("为后退");
system("python3 /home/yys/car.py '2'");
exit(0);
}
check = strstr(string, turnleft);
if(check != NULL)
{
printf("为左转");
system("python3 /home/yys/car.py '3'");
exit(0);
}
check = strstr(string, turnright);
if(check != NULL)
{
printf("为右转");
system("python3 /home/yys/car.py '4'");
exit(0);
}
}
static char *g_result = NULL;
static unsigned int g_buffersize = BUFFER_SIZE;
void on_result(const char *result, char is_last)
{
if (result) {
size_t left = g_buffersize - 1 - strlen(g_result);
size_t size = strlen(result);
if (left < size) {
g_result = (char*)realloc(g_result, g_buffersize + BUFFER_SIZE);
if (g_result)
g_buffersize += BUFFER_SIZE;
else {
printf("mem alloc failed\n");
return;
}
}
strncat(g_result, result, size);
show_result(g_result, is_last);
}
}
void on_speech_begin()
{
if (g_result)
{
free(g_result);
}
g_result = (char*)malloc(BUFFER_SIZE);
g_buffersize = BUFFER_SIZE;
memset(g_result, 0, g_buffersize);
printf("Start Listening...\n");
}
void on_speech_end(int reason)
{
if (reason == END_REASON_VAD_DETECT)
printf("\nSpeaking done \n");
else
printf("\nRecognizer error %d\n", reason);
}
/* demo send audio data from a file */
static void demo_file(const char* audio_file, const char* session_begin_params)
{
int errcode = 0;
FILE* f_pcm = NULL;
char* p_pcm = NULL;
unsigned long pcm_count = 0;
unsigned long pcm_size = 0;
unsigned long read_size = 0;
struct speech_rec iat;
struct speech_rec_notifier recnotifier = {
on_result,
on_speech_begin,
on_speech_end
};
if (NULL == audio_file)
goto iat_exit;
f_pcm = fopen(audio_file, "rb");
if (NULL == f_pcm)
{
printf("\nopen [%s] failed! \n", audio_file);
goto iat_exit;
}
fseek(f_pcm, 0, SEEK_END);
pcm_size = ftell(f_pcm);
fseek(f_pcm, 0, SEEK_SET);
p_pcm = (char *)malloc(pcm_size);
if (NULL == p_pcm)
{
printf("\nout of memory! \n");
goto iat_exit;
}
read_size = fread((void *)p_pcm, 1, pcm_size, f_pcm);
if (read_size != pcm_size)
{
printf("\nread [%s] error!\n", audio_file);
goto iat_exit;
}
errcode = sr_init(&iat, session_begin_params, SR_USER, &recnotifier);
if (errcode) {
printf("speech recognizer init failed : %d\n", errcode);
goto iat_exit;
}
errcode = sr_start_listening(&iat);
if (errcode) {
printf("\nsr_start_listening failed! error code:%d\n", errcode);
goto iat_exit;
}
while (1)
{
unsigned int len = 10 * FRAME_LEN; /* 200ms audio */
int ret = 0;
if (pcm_size < 2 * len)
len = pcm_size;
if (len <= 0)
break;
ret = sr_write_audio_data(&iat, &p_pcm[pcm_count], len);
if (0 != ret)
{
printf("\nwrite audio data failed! error code:%d\n", ret);
goto iat_exit;
}
pcm_count += (long)len;
pcm_size -= (long)len;
}
errcode = sr_stop_listening(&iat);
if (errcode) {
printf("\nsr_stop_listening failed! error code:%d \n", errcode);
goto iat_exit;
}
iat_exit:
if (NULL != f_pcm)
{
fclose(f_pcm);
f_pcm = NULL;
}
if (NULL != p_pcm)
{
free(p_pcm);
p_pcm = NULL;
}
sr_stop_listening(&iat);
sr_uninit(&iat);
}
/* demo recognize the audio from microphone */
static void demo_mic(const char* session_begin_params)
{
int errcode;
int i = 0;
struct speech_rec iat;
struct speech_rec_notifier recnotifier = {
on_result,
on_speech_begin,
on_speech_end
};
errcode = sr_init(&iat, session_begin_params, SR_MIC, &recnotifier);
if (errcode) {
printf("speech recognizer init failed\n");
return;
}
errcode = sr_start_listening(&iat);
if (errcode) {
printf("start listen failed %d\n", errcode);
}
/* demo 15 seconds recording */
while(i++ < 15)
sleep(1);
errcode = sr_stop_listening(&iat);
if (errcode) {
printf("stop listening failed %d\n", errcode);
}
sr_uninit(&iat);
}
int run_asr(UserData *udata)
{
char asr_params[MAX_PARAMS_LEN] = {NULL};
const char *rec_rslt = NULL;
const char *session_id = NULL;
const char *asr_audiof = NULL;
FILE *f_pcm = NULL;
char *pcm_data = NULL;
long pcm_count = 0;
long pcm_size = 0;
int last_audio = 0;
int aud_stat = MSP_AUDIO_SAMPLE_CONTINUE;
int ep_status = MSP_EP_LOOKING_FOR_SPEECH;
int rec_status = MSP_REC_STATUS_INCOMPLETE;
int rss_status = MSP_REC_STATUS_INCOMPLETE;
int errcode = -1;
int aud_src = 0;
//离线语法识别参数设置
snprintf(asr_params, MAX_PARAMS_LEN - 1,
"engine_type = local, \
asr_res_path = %s, sample_rate = %d, \
grm_build_path = %s, local_grammar = %s, \
result_type = xml, result_encoding = UTF-8, ",
ASR_RES_PATH,
SAMPLE_RATE_16K,
GRM_BUILD_PATH,
udata->grammar_id
);
printf("从MIC说话\n");
//第一次监听说话
system("espeak -v zh '开始说话'");
demo_mic(asr_params);
return 0;
}
int main(int argc, char* argv[])
{
const char *login_config = "appid = 5f6c1268"; //登录参数
UserData asr_data;
int ret = 0 ;
char c;
ret = MSPLogin(NULL, NULL, login_config); //第一个参数为用户名,第二个参数为密码,传NULL即可,第三个参数是登录参数
if (MSP_SUCCESS != ret) {
printf("登录失败:%d\n", ret);
goto exit;
}
memset(&asr_data, 0, sizeof(UserData));
printf("构建离线识别语法网络...\n");
ret = build_grammar(&asr_data); //第一次使用某语法进行识别,需要先构建语法网络,获取语法ID,之后使用此语法进行识别,无需再次构建
if (MSP_SUCCESS != ret) {
printf("构建语法调用失败!\n");
goto exit;
}
while (1 != asr_data.build_fini)
usleep(300 * 1000);
if (MSP_SUCCESS != asr_data.errcode)
goto exit;
printf("离线识别语法网络构建完成,开始识别...\n");
ret = run_asr(&asr_data);
if (MSP_SUCCESS != ret) {
printf("离线语法识别出错: %d \n", ret);
goto exit;
}
printf("更新离线语法词典...\n");
ret = update_lexicon(&asr_data); //当语法词典槽中的词条需要更新时,调用QISRUpdateLexicon接口完成更新
if (MSP_SUCCESS != ret) {
printf("更新词典调用失败!\n");
goto exit;
}
while (1 != asr_data.update_fini)
usleep(300 * 1000);
if (MSP_SUCCESS != asr_data.errcode)
goto exit;
printf("更新离线语法词典完成,开始识别...\n");
while(1){
ret = run_asr(&asr_data);
if (MSP_SUCCESS != ret) {
printf("离线语法识别出错: %d \n", ret);
goto exit;
}
}
exit:
MSPLogout();
printf("请按任意键退出...\n");
getchar();
return 0;
}
0x01 下载语音唤醒SDK
我们在使用讯飞的唤醒引擎服务之前,需要前往讯飞开放平台注册一个账户。然后登录账户后就可以前往自己的控制台,创建一个应用。创建这个应用以后,我们就可以得到一个调用科大讯飞各项语音服务的AppID。这个AppID是很重要的,它会跟我们后面下载的各项语音服务是打包在一起使用的。也就是说你下载的语音SDK必须要搭配着你的AppID来使用,否则可能调用就会出错。那注册科大讯飞开放平台账户是比较简单的,这里不再做介绍,大家可以直接去下面的讯飞开放平台上注册即可:
那现在注册账户的话,还是会有各种免费的资源服务可以使用,官网页面的可以免费使用的服务如下:
那注册账户后,我们就可以登录自己的控制台。这里要做的第一件事就是“创建新应用”,然后就可以准备使用“语音唤醒”服务了:
0x02 测试语音唤醒效果
当我们下载好自己设置的唤醒词SDK后,我们就可以将该SDK发送到树莓派板上。然后就可以解压,查看源码了。我们可以在本地电脑上使用如下命令,将下载的SDK发送到树莓派上:
scp Linux_awaken1226_5d5b9efd.zip corvin@192.168.*.*:~/
这里需要注意的是后面的IP地址,大家需要根据自己树莓派的IP地址来修改这条命令就可以了。执行完这条命令就可以将文件传输到树莓派的home目录下,注意我这里的树莓派系统用户名是corvin。这里需要注意的就是解压zip文件的命令,完整解压命令如下:
unzip -q Linux_awaken1226_5d5b9efd.zip -d xf_awaken/
接下来最重要的一件事就是来替换默认代码中提供的唤醒库了,因为默认提供的都是x86版本的,都是在我们的台式机这样电脑上使用的。
开始编译
source 64bit_make.sh
进入编译后bin文件夹
cd ~/SoftWare/bin
./awaken_offline_sample
###############################################################################################################
## 请注意,唤醒语音需要根据唤醒词内容自行录制并重命名为宏IVW_AUDIO_FILE_NAME所指定名称,存放在bin/audio文件里##
###############################################################################################################
QIVWSessionBegin failed! error code:25000
这里我们可以从源码中找到我们需要录制的文件格式和文件名,这里打开awaken_offline_sample.c文件可以找到,:
#define IVW AUDIO FILE NAME "audio/awake. pcm" //这是唤醒程序需要读取的pcm格式语音文件,
cd ~/SoftWare/bin/audio
这里我们可以在audio目录中,使用如下命令来录制pcm格式的语料,然后我们就可以来运行程序来检测是否包含有唤醒词了,录制语料的命令如下:
arecord -d 3 -r 16000 -c 1 -t wav -f S16_LE awake.pcm
yys@yys:~/SoftWare/bin/audio$ ls
awake.pcm //录制好文件
yys@yys:~/SoftWare/bin/audio$ file awake.pcm //查看文件格式
awake.pcm: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz
yys@yys:~/SoftWare/bin/audio$ play awake.pcm //听录音
play WARN alsa: can't encode 0-bit Unknown or not applicable
awake.pcm:
File Size: 96.0k Bit Rate: 256k
Encoding: Signed PCM
Channels: 1 @ 16-bit
Samplerate: 16000Hz
Replaygain: off
Duration: 00:00:03.00
In:100% 00:00:03.00 [00:00:00.00] Out:48.0k [ | ] Hd:0.0 Clip:0
Done.
这里需要注意的是arecord命令后面的-d参数是表示录制3秒钟,所以我们执行完这条命令后需要立刻说出需要检测的唤醒词,到达3秒后,录音就自动结束了。
当录制好测试语料后,我们就可以来运行唤醒测试程序了。当出现下图所示的日志,就说明唤醒程序已经检测到唤醒词了:
这里需要注意的是唤醒结果中提示的各字段值,各字段值的意义如下图所示:
0x03 修改唤醒程序(失败)
通过上面的测试唤醒过程,大家就可以知道这个demo程序有点不够完善。它无法实现实时的检测唤醒词功能,每次都是要录制好测试语料,然后再运行唤醒检测程序。这样就很不方便了,那我们这里就来修改一下,使该demo程序可以像snowboy测试程序那样,可以实时的检测唤醒词。首先我们来看下原始的唤醒程序的源码:
linux.c
0x01 下载snowboy代码
我们这款语音板现在就可以使用snowboy这个唤醒引擎,首先来下载snowboy的源码,然后我们就可以在本地进行测试了,下载snowboy的github仓库命令如下:
git clone https://github.com/Kitt-AI/snowboy.git
如果慢可以.我的gitee
git clone https://gitee.com/yang_yongsheng/snowboy.git
下载好代码后,就可以来安装下运行需要的软件了。后面我会把这些缺少的软件都提前安装好在下一版的ROS镜像中,把语音开发环境都配置好,这样后面的同学再使用语音板的时候,就不用这么麻烦来安装这些软件包了,直接就能用snowboy唤醒。这里的安装命令如下:没装python2的
sudo apt-get install python3-pyaudio
如果报错,再进行下面两步
sudo apt-get --fix-broken install
sudo apt-get install python-pyaudio python3-pyaudio swig
接下来就是安装python版本的PortAudio,安装命令如下:
pip3 install pyaudio
上述操作过程可以参考如下的截图:
在安装好这些软件后,接下来就可以来验证下是否可以正常录音了,只要语音板麦克风可以正常工作,可以录音了,我们才可以进行后面的操作。完整的测试录音命令如下:
rec test.wav
当在终端中输入完该命令后,就会自动开启录音,我们就可以对着麦克风说几句话。最终会保存到test.wav文件中,最后我们播放出来test.wav看看该文件是否正常,如果正常的话,那我们语音板就是进入正常工作状态了。要想播放一下wav文件,命令也很简单:
play test.wav
0x02 编译唤醒引擎动态库
在这里我们在使用唤醒引擎前,需要编译对应编程语言版本所使用的动态库。由于snowboy的唤醒引擎可以在多种语言环境中使用,我们这里选择python版本,也就是python2.7的版本。当然也可以编译python3的版本,这里编译对应语言版本动态库用到的工具就是swig,这个swig是由Simple Wrapper and Interface Generator的首字母组成,根据这个英文也大概了解swig是干嘛的了。简单来说,swig就是为了将我们使用C/C++编写的代码封装起来,然后可以给其他语言使用的一个工具。这里的snowboy唤醒引擎是使用C/C++开发的,所以python要想使用就需要做下转换了。
这里编译python版本的引擎库也很简单,整个操作过程如下图所示:
如果make不能用安装swig
安裝swig (編譯所需)
乌班图20.04
sudo apt install swig
对于乌班图18.04
wget http://hahack-1253537070.file.myqcloud.com/misc/swig-3.0.10.tar.gz
tar xvf swig-3.0.10.tar.gz
cd swig-3.0.10
./configure --prefix=/usr --without-clisp --without-maximum-compile-warnings
make
sudo make install
sudo install -v -m755 -d /usr/share/doc/swig-3.0.10
sudo cp -v -R Doc/* /usr/share/doc/swig-3.0.10
安装snowboy
cd ~/snowboy/swig/Python3
sudo apt-get install g++
这时候使用make会报错下面错,就执行下面命令
g++ -I../../ -O3 -fPIC -D_GLIBCXX_USE_CXX11_ABI=0 -std=c++0x -shared snowboy-detect-swig.o \
../..//lib/ubuntu64/libsnowboy-detect.a -L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl -lutil -lm -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions -lm -ldl -lf77blas -lcblas -llapack -latlas -o _snowboydetect.so
/usr/bin/ld: 找不到 -lf77blas
/usr/bin/ld: 找不到 -lcblas
/usr/bin/ld: 找不到 -latlas
collect2: error: ld returned 1 exit status
Makefile:73: recipe for target '_snowboydetect.so' failed
make: *** [_snowboydetect.so] Error 1
进入snowboy/examples/Python3文件夹
sudo apt-get install libatlas-base-dev
make
生成 _snowboydetect.so
然后进入/snowboy/examples/Python3文件夹
cd ~/snowboy/examples/Python3
运性 python3 demo.py ./resources/models/snowboy.umdl会报错
Traceback (most recent call last):
File "demo.py", line 1, in
import snowboydecoder
File "/home/corvin/snowboy/examples/Python3/snowboydecoder.py", line 5, in
from . import snowboydetect
ImportError: attempted relative import with no known parent package
这里的修改也很简单,我们就是要把snowboy/examples/Python3/snowboydecoder.py中第5行中的from . import snowboydetect修改为import snowboydetect就可以了
vim snowboydecoder.py
python3 demo.py ./resources/models/snowboy.umdl
运行成功如下
Listening... Press Ctrl+C to exit
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
最后训练
自己訓練則到 https://snowboy.kitt.ai/dashboard 下載 xxx.pmdl 檔案
yang.pmdl 自己训练的,复制到snowboy/examples/Python3
python3 demo.py yang.pmdl
在snowboydecoder.py 210多行可以调用条件
#small state machine to handle recording of phrase after keyword
¦ ¦ if state == "PASSIVE":
¦ ¦ ¦ if status > 0: #key word found
os.system("/home/yys/xf/bin/asr_offline_record_sample")
可以在科大迅飞bin文件夹下运行命令
一个是demo文件,一个是训练的文件
python3 /home/yys/snowboy/examples/Python3/demo.py /home/yys/snowboy/examples/Python3/yang.pmdl
本文作者: 永生
本文链接: https://yys.zone/detail/?id=140
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
评论列表 (0 条评论)