ROS进阶(四):机器语音项目实战

声明:ROS进阶教程参考了《ROS机器人开发实践》,由胡春旭老师所著。此系列教程唯一目的是学习与分享,与大家共勉。若感兴趣,请大家购买原版书籍进行学习,侵权删。

本文在Ubuntu16.04 + ROS Kinetic环境下完成

项目github链接:

一、项目介绍

1、项目内容

机器语音是让机器人能够和人通过语音进行沟通,以便更好地服务人类。在机器人系统上增加语音接口,用语音代替键盘输入并进行人机对话。机器语音的关键是语音识别,识别流程如下图所示。

第一步是“学习”。根据识别系统的类型选择能够满足要求的一种识别方法,并分析出这种识别所需的语音特征参数,作为标准模式存储起来。

第二步是“识别”。根据实际需要选择语音特征参数,将这些参数的时间序列构成测试模板,再将其与已存在的参考模板逐一比较,并进行测度估计,最后经由专家知识库判决,最佳匹配的参考模板即为识别结果。

2、预期目标

本章将走进机器语音的世界,实现机器人“听”和“说”两大功能:

  • 英文语音识别:基于创建的语音库,ROS中的pocketsphinx功能包可以实现机器人的语音识别功能。
  • 英文语音播放:ROS中的元功能包audio-common提供了文本转语音的功能包sound_play,可以实现机器人的英文语音播放功能。
  • 智能语音应答:结合人工智能标记语言AIML,机器人可以从语料库中智能匹配交流的输出语句,从而实现智能交流应用。
  • 中文语音的识别与合成:在ROS中集成科大讯飞的语音处理SDK,让机器人懂中文。

二、让机器人听懂语言

ROS中集成了CMU Sphinx和Festival开源项目中的代码,发布了独立的语音识别功能包——pocketsphinx,可以帮助我们的机器人实现语音识别功能。

1、pocketsphinx功能包

首先安装依赖的功能包和第三方库:

$ sudo apt-get install ros-kinetic-audio-common
$ sudo apt-get install libasound2
$ sudo apt-get install gstreamer0.10-*

接下来下载4个deb依赖库的安装文件,4个依赖库的安装文件在robot_perception/
pocketsphinx/lib中可以找到。

使用以下命令依次安装下载完成的依赖库:

$ sudo dpkg -i libsphinxbase1_0.8-6_amd64.deb
$ sudo dpkg -i libpocketsphinx1_0.8-5_amd64.deb
$ sudo dpkg -i libgstreamer-plugins-base0.10-0_0.10.36-2ubuntu0.1_amd64.deb
$ sudo dpkg -i gstreamer0.10-pocketsphinx_0.8-5_amd64.deb

依赖库安装完成后,使用如下命令从github上下载pocketsphinx功能包的源码:

$ git clone https://github.com/mikeferguson/pocketsphinx.git

下载完成后就可以在工作空间下使用cakin_make命令编译功能包了。

在运行语音识别之前,先来了解一下pocketsphinx功能包的用户接口。

  • 话题和服务
  • 参数

pocketsphinx功能包的核心节点是recognizer.py文件。这个文件通过麦克风收集语音信息,然后调用语音识别库进行识别并生成文本信息,通过/recognizer/output消息进行发布,其他节点可以通过订阅该消息获取识别结果,并进行相应处理。

2、语音识别测试

首先,插入麦克风设备,并在系统设置里测试麦克风是否有语音输入。如下图所示,输入音量不能太小,也不能太大。

然后,运行pocketsphinx包中的测试程序:

$ roslaunch pocketsphinx robocup.launch
已经帮你下好了多种语音引擎,在robot_perception/robot_voice/config文件夹下可以直接链接使用。

为了重新链接自定义的语音引擎,需要对recognizer.py和robocup.launch文件稍作修改。首先在recognizer.py中加入语音模型hmm参数的加载配置。

然后在robocup.launch文件中设置lm、dic、hmm参数的具体链接路径。

<launch>

  <node name="recognizer" pkg="pocketsphinx" type="recognizer.py" output="screen">
    <param name="lm" value="$(find robot_voice)/config/pocketsphinx-en/model/lm/en/tidigits.DMP"/>
    <param name="dict" value="$(find robot_voice)/config/pocketsphinx-en/model/lm/en/tidigits.dic"/>
    <param name="hmm" value="$(find robot_voice)/config/pocketsphinx-en/model/hmm/en/tidigits"/>
  </node>

</launch>

修改到此结束,重新运行robocup.launch启动语音识别,在终端中会看到很多参数输出,现在就可以开始说话了。

以上链接的语音模型相对简单,仅支持简单数字的识别,可以用记事本打开语音参数配置文件tidigits.dic,其中列出了该语音识别模型仅支持识别的文本。

后续内容会使用更加复杂的语音模型,目前先测试这里的数字。识别后的消息会通过/recognizer/output话题发布,使用以下命令在终端中打印语音识别的结果。

$ rostopic echo /recognizer/output

pocketsphinx功能包提供一种离线的语音识别模型,默认支持的模型有限,在下一节我们将学习如何添加自己的语音模型。

3、创建语音库

语音库中的可识别信息使用txt文档存储。在功能包robot_voice中创建一个文件夹config,用来存储语音库的相关文件。然后在该文件夹下创建一个commands.txt文件,并输入希望识别的指令,如下图所示。

将该文件在线生成语音信息和模板文件,这一步需要登录以下网站操作:

http://www.speech.cs.cmu.edu/tools/lmtool-new.html

根据网站提示,点击“选择文件”按钮,上传刚刚创建的command.txt文件,再点击“COMPILE KNWLEDGE BASE”按钮进行编译。

编译完后,下载“COMPRESSED TARBALL”压缩文件,解压至robot_voice功能包的config文件夹下,这些解压出来的.dic、.lm文件就是根据我们设计的语音识别指令生成的语音模板库。将这些文件全部都重命名为commands。

4、创建launch文件

接下来创建一个launch文件,启动语音识别节点,并设置语音模板库的位置。robot_voice/
launch/voice_commands.launch文件内容如下:

<launch>

    <node name="recognizer" pkg="pocketsphinx" type="recognizer.py" output="screen">
        <param name="lm" value="$(find robot_voice)/config/commands.lm"/>
        <param name="dict" value="$(find robot_voice)/config/commands.dic"/>
        <param name="hmm" value="$(find robot_voice)/config/pocketsphinx-en/model/hmm/en/hub4wsj_sc_8k"/>
    </node>
  
</launch>

从以上代码中可以看到,launch文件在运行recognizer.py节点的时候使用了之前生成的语音识别库和文件参数,这样就可以使用自己的语音库来进行语音识别了,此外,这里的hmm语音引擎参数发生了变化,改为一款支持更多语音模型的引擎。

5、语音指令识别

通过以下命令测试语音识别的效果如何,尝试能否成功识别出command.txt中设置的语音指令。

$ roslaunch robot_voice voice_commands.launch
$ rostopic echo /recognizer/output

6、中文语音识别

pocketsphinx功能包不仅可以识别英文语音,通过使用中文语音引擎和模型,也可以识别中文。robot_voice/config文件夹下已经包含中文语音识别的所有配置文件,使用robot_voice/
launch/chinese_recognizer.launch即可启动recognizer节点,并且链接所需要的配置文件。launch文件内容如下:

<launch>

    <node name="recognizer" pkg="pocketsphinx" type="recognizer.py" output="screen">
        <param name="lm" value="$(find robot_voice)/config/pocketsphinx-cn/model/lm/zh_CN/gigatdt.5000.DMP"/>
        <param name="dict" value="$(find robot_voice)/config/pocketsphinx-cn/model/lm/zh_CN/mandarin_notone.dic"/>
        <param name="hmm" value="$(find robot_voice)/config/pocketsphinx-cn/model/hmm/zh/tdt_sc_8k"/>
    </node>
  
</launch>

使用以下命令运行该文件,即可开始中文识别:

$ roslaunch robot_voice chinese_recognizer.launch

中文语音模型支持的识别文本可以参考pocketsphinx-cn/model/lm/zh_CN/
mandarin_notone.dic文件中的内容,里面有近十万条识别文本,几乎包含所有常用的中文词汇,所以我们可以随便说中文,通过打印出的识别结果来测试中文识别的效果:

$ rostopic echo /recognizer/output

中文虽然可以识别,但是识别效果不佳,后面会使用科大讯飞的中文语音识别引擎实现更加准确的识别效果。

三、通过语音控制机器人

上一节实现了语音识别的基本功能,可以成功将英文语音指令识别生成对应的字符串,本节基于以上功能实现一个语音控制机器人的小应用,机器人就使用仿真环境中的小乌龟。

1、编写语音控制节点

该应用需要编写一个语音控制的节点,订阅语音识别发布的“/recognizer/output”消息,然后根据消息中的具体指令发布速度控制指令。

该节点实现在robot_vision/script/voice_teleop.py中,代码内容如下:

import rospy
from geometry_msgs.msg import Twist
from std_msgs.msg import String

# 初始化ROS节点,声明一个发布速度控制的Publisher
rospy.init_node('voice_teleop')
pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)
r = rospy.Rate(10)

# 接收到语音命令后发布速度指令
def get_voice(data):
    voice_text=data.data
    rospy.loginfo("I said:: %s",voice_text)
    twist = Twist()
    
    if voice_text == "go":
        twist.linear.x = 2 
    elif voice_text == "back":
        twist.linear.x = -2 
    elif voice_text == "left":
        twist.angular.z = 2 
    elif voice_text == "right":
        twist.angular.z = -2 
        
    pub.publish(twist)

# 订阅pocketsphinx语音识别的输出字符
def teleop():
    rospy.loginfo("Starting voice Teleop")
    rospy.Subscriber("/recognizer/output", String, get_voice)
    rospy.spin()

while not rospy.is_shutdown():
    teleop()

以上代码的实现较为简单,通过一个Subscriber订阅/recognizer/output话题;接受到语音识别的结果后进入回调函数,简单处理后通过Publisher发布控制小乌龟运动的速度控制指令。

2、语音控制小乌龟运动

接下来就可以运行这个语音控制的例程,通过以下命令启动所有节点:

$ roslaunch robot_voice voice_commands.launch
$ rosrun robot_voice voice_teleop.py
$ rosrun turtlesim turtlesim_node

所有终端中的命令成功执行后,就可以打开小乌龟的仿真界面。然后通过语音“go”“back”“left”“right”等命令控制小乌龟的运动。

四、让机器人说话

现在机器人已经可以听懂我们所说的话了,但是还没办法通过语音回应。接下来,我们将使机器人学会说话。

1、sound_play功能包

ROS中的元功能包audio-common提供了文本转语音的功能包sound_play。使用以下命令安装:

$ sudo apt-get install ros-kinetic-audio-common
$ sudo apt-get install libasound2
$ sudo apt-get install mplayer

2、语音播放测试

通过以下命令运行sound_play主节点,并进行测试:

$ roscore
$ rosrun sound_play soundplay_node.py

在另一终端中输入需要转化成语音的文本信息:

$ rosrun sound_play say.py "Greetings Humans. Take me to your leader."

sound_play识别输入的文本,并使用语音读了出来,发出这个声音的人叫kal_diphone,你也可以换成另外的人:

$ sudo apt-get install festvox-don
$ rosrun sound_play say.py "Welcome to the future" voice_don_diphone

机器人只能“说出”指定的文本,智能程度有限,接下来玩点更高级的,综合使用之前学习的pocketsphinx和sound_play功能包,再加入一点简单的人工智能,让机器人具有自然语言理解能力,能够和我们进行简单的交流,就像苹果手机上的Siri助手一样。不过,我们需要先了解一些语音智能方面的基础知识。

五、人工智能标记语言

人工标记语言(Artificial Intelligence Markup Language,AIML)是一种创建自然语言处理软件代理的XML语言。AIML主要用于实现机器人的语言交流功能,用户可以与机器人说话,而机器人可以通过一个自然语言的软件代理,也可以给出一个聪明的回答。

1、AIML中的标签

AIML文件包含一系列已定义的标签,可以通过以下简单案例了解AIML语法规则:

<aiml version="1.0.1" encoding="UTF-8">
    <category>
        <pattern> HOW ARE YOU </pattern>
        <template> I AM FINE </template>
    </category>
</aiml>
  • <aiml>标签

所有的AIML代码都要介于<aiml>和</aiml>之间。

  • <category>标签

<category>标签表示一个基本的知识块,包含一条输入语句和一条输出语句,用来匹配机器人和人交流过程中的一问一答或一问多答,但不允许多问一答或多问多答的匹配。

  • <pattern>标签

<pattern>标签表示用户输入语句的匹配,在上边的例子中,用户一旦输入“How are you”,机器人就会找到这个匹配。

<pattern>标签内的语句必须用大写。
  • <template>标签

<template>标签表示机器人应答语句,机器人找到相应的输入匹配语句之后,会输出该应答语句。

有了这几个元素,理论上可以写出任意匹配模式,但在实际应用中,往往还不够,我们再通过另一个示例演示:

<aiml version="1.0.1" encoding="UTF-8">
    <category>
        <pattern> WHAT IS A ROBOT? </pattern>
        <template>
             A ROBOT IS A MACHINE  MAINLY DESIGNED FOR EXECUTING REPEATED TASK WITH SPEED AND PRECISION.
        </template>
    </category>
    <category>
        <pattern> DO YOU KNOW WHAT A * IS ? </pattern>
        <template>
             <srai> WHAT IS A <star/></srai>
        </template>
    </category>
</aiml>
  • <star/>标签

<star/>标签表示*号,这里pattern元素里的匹配模式用*表示任意匹配,但在其他元素里不能用*号,而用<star/>标签来表示。在该示例中,当用户输入“Do you know what a robot is?”后,机器人就会使用*匹配输入的“robot”,然后将<star/>替换成“robot”。

  • </srai>标签

</srai>标签表示<srai>里面的语句会被当作用户输入,重新查找匹配模式,直到找到非<srai>定义的回复。例如用户输入“Do you know what a robot is?”后,机器人会把“what is a robot”作为用户输入,然后查找匹配的输出是“A ROBOT IS A MACHINE MAINLY DESIGNED FOR EXECUTING REPEATED TASK WITH SPEED AND PRECISION.”

当然,AIML支持的标签和用法远不止这些,如果想深入学习,请访问网站:alicebot.org/aiml.html

2、Python中的AIML解析器

Python有针对AIML的开源解析模块——PyAIML,该模块可以通过扫描AIML文件建立一颗定向模式树,然后通过深度搜索来匹配用户的输入。首先对该模块进行简单介绍。

$ sudo apt-get install python-aiml

检查是否安装成功:

$ cd /path_to/robot_voice/data
$ python
>>> import aiml

若无报错,则说明安装成功。

aiml模块中最重要的类是Kernel(),必须创建一个aiml.Kernel()对象,才能实现对AIML文件的操作。

>>> mybot = aiml.Kernel()

接下来加载一个AIML文件:

>>> mybot.learn('sample.aiml')

如果需要加载多个AIML文件,则可以使用以下命令:

>>> mybot.learn('startup.xml')

startup.xml文件的内容如下:

<aiml version="1.0">
    <category>
        <pattern>LOAD AIML B</pattern>
        <template>
        <!-- Load standard AIML set -->
            <learn>*.aiml</learn>
        </template>
    </category>
</aiml>

然后还要发出一条指令,加载当前路径下的所有AIML文件,并生成模式匹配树:

>>> mybot.respond("load aiml b")

系统已经记住了所有的匹配语句,可以尝试输入一条语句进行测试:

>>> while True: print mybot.respond(raw_input("> "))

现在应该可以看到机器人匹配到了输入语句,并输出了应答语句。

六、与机器人对话

我们已经学习了相关的背景知识,重新整理一下思路,准备动手实现一个语音对话的机器人应用。

如下图所示,可以将语音对话实现的整个过程分为三个节点。

  • 语音识别节点:将用户语音转换成字符串。
  • 智能匹配应答节点:在数据库中匹配应答字符串。
  • 文本转语音节点:将应答字符串转换成语音播放。

1、语音识别

语音识别节点基于pocketsphinx功能包。按上述方法生成一个语音库,包括以下常用的交流语句,也可以添加更多自己需要的语句。

hello
how are you
what is your name
what can you do
who is Nameless
what time is it
go away
can you help me
what is history
what is your age
thank you
googbye

语音库生成后,将所有库文件命名为chat,并与之前的commands文件放置到同一路径下。再创建一个launch文件robot_voice/launch/chat_recognizer.launch,运行pocketsphinx语音识别节点,并设置语音库的路径,代码如下:

<launch>

    <node name="recognizer" pkg="pocketsphinx" type="recognizer.py" output="screen">
        <param name="lm" value="$(find robot_voice)/config/chat.lm"/>
        <param name="dict" value="$(find robot_voice)/config/chat.dic"/>
        <param name="hmm" value="$(find robot_voice)/config/pocketsphinx-en/model/hmm/en/hub4wsj_sc_8k"/>
    </node>
  
</launch>

pocketsphinx功能包将识别得到的文本使用/recognizer/output话题进行发布,所以再创建一个话题转换的节点/recognizer/srcipts/aiml_voice_recognizer.py,将语音识别结果发送到voiceWords话题。语音文本通过String类型发布,该节点的详细代码如下:

import rospy
from std_msgs.msg import String

# 初始化ROS节点,声明一个发布语音字符的Publisher
rospy.init_node('aiml_voice_recognizer')
pub = rospy.Publisher('voiceWords', String, queue_size=10)
r = rospy.Rate(1)

# 将pocketsphinx功能包识别输出的字符转换成aiml_voice_server需要的输入字符
def get_voice(data):
    voice_text=data.data
    rospy.loginfo("I said:: %s",voice_text)
    pub.publish(voice_text) 

# 订阅pocketsphinx语音识别的输出字符
def listener():
    rospy.loginfo("Starting voice recognizer")
    rospy.Subscriber("/recognizer/output", String, get_voice)
    rospy.spin()

while not rospy.is_shutdown():
    listener()

现在就可以测试语音识别部分的功能了,在终端输入以下命令:

$ roslaunch robot_voice chat_recognizer.launch
$ rosrun robot_voice aiml_voice_recognizer.py
$ rostopic echo /voiceWords

对着麦克风说话,可以在终端看到识别到的语音字符串。

2、智能匹配应答

语音已经可以被识别成字符串了,接下来基于AIML实现应答文本的匹配。实现节点robot_voice/scripts/aiml_voice_server.py的代码如下:

import rospy
import aiml
import os
import sys
from std_msgs.msg import String

# 初始化ROS节点,创建aiml.Kernel()对象
rospy.init_node('aiml_voice_server')
mybot = aiml.Kernel()
response_publisher = rospy.Publisher('response',String,queue_size=10)

# 加载aiml文件数据
def load_aiml(xml_file):
    data_path = rospy.get_param("aiml_path")
    print data_path
    os.chdir(data_path)
    if os.path.isfile("standard.brn"):
        mybot.bootstrap(brainFile = "standard.brn")
    else:
        mybot.bootstrap(learnFiles = xml_file, commands = "load aiml b")
        mybot.saveBrain("standard.brn")

# 解析输入字符串,匹配并发布应答字符串
def callback(data):
    input = data.data
    response = mybot.respond(input)
    
    rospy.loginfo("I heard:: %s",data.data)
    rospy.loginfo("I spoke:: %s",response)
    response_publisher.publish(response)

# 订阅用于语音识别的语音字符
def listener():
    rospy.loginfo("Starting ROS AIML voice Server")
    rospy.Subscriber("voiceWords", String, callback)
    rospy.spin()

if __name__ == '__main__':  
    load_aiml('startup.xml')
    listener()

该节点在运行过程中需要加载AIML数据库的路径,创建robot_voice/launch/
start_aiml_server.launch进行参数加载:

<launch>

    <param name="aiml_path" value="$(find robot_voice)/data" />
    <node name="aiml_voice_server" pkg="robot_voice" type="aiml_voice_server.py" output="screen" />
    
</launch>

在终端运行如下命令进行测试:

$ roslaunch robot_voice start_aiml_server.launch
$ rostopic echo /response
$ rostopic pub /voiceWords std_msgs/String "data: 'what is your name'"

可以在终端看到匹配的应答信息。

3、文本转语音

现在我们已经可以匹配得到应答的文本了,使用前面学习的sound_play功能包就可以把文本转换成语音播放出来。文本转语音节点的实现在robot_voice/scripts/aiml_tts.py中完成,内容如下:

import rospy, os, sys
from sound_play.msg import SoundRequest
from sound_play.libsoundplay import SoundClient
from std_msgs.msg import String

# 初始化ROS节点以及sound工具
rospy.init_node('aiml_tts', anonymous = True)

soundhandle = SoundClient()
rospy.sleep(1)
soundhandle.stopAll()
print 'Starting TTS'

# 获取应答字符,并且通过语音输出
def get_response(data):
    response = data.data
    rospy.loginfo("Response ::%s",response)
    soundhandle.say(response)

# 订阅语音识别后的应答字符
def listener():
    rospy.loginfo("Starting listening to response")
    rospy.Subscriber("response",String, get_response,queue_size=10)
    rospy.spin()

if __name__ == '__main__':
    listener()

在终端使用以下命令进行测试:

$ roscore
$ rosrun sound_play soundplay_node.py
$ rosrun robot_voice aiml_tts.py
$ rostopic pub /response std_msgs/String "data: 'what is your name'"

运行成功后,很快就可以听到“what is your name”这段文本的语音了。

4、智能对话

我们将上述三个过程集成到一起,就制作成一个完整的智能语音对话应用了。

创建robot_voice/launch/start_chat.launch文件启动以上所有节点:

<launch>

    <param name="aiml_path" value="$(find robot_voice)/data" />
    <node name="aiml_voice_server" pkg="robot_voice" type="aiml_voice_server.py" output="screen" />

    <include file="$(find sound_play)/soundplay_node.launch"></include>
    <node name="aiml_tts" pkg="robot_voice" type="aiml_tts.py" output="screen" />

    <node name="aiml_voice_recognizer" pkg="robot_voice" type="aiml_voice_recognizer.py" output="screen" />
    
</launch>

通过以下命令启动语音识别、智能匹配应答、文本转语音等关键节点:

$ roslaunch robot_voice chat_recognizer.launch
$ roslaunch robot_voice start_chat.launch

启动成功后,就可以开始和这款机器人进行对话了。

七、让机器人听懂中文

前面的内容中,我们已经创建了一个类似于Siri的语音对话机器人,如果我们想使用中文与机器人进行交流,我们就要用到科大讯飞的语音识别SDK。

1、下载科大讯飞SDK

  • 首先登陆科大讯飞开放平台的官网(xfyun.cn/),使用个人信息注册一个账户。
  • 登录账户后,创建一个新应用,这里为应用取名为your_name_ros_voice,如下图所示。
  • 创建完成后,在“我的应用”可以看到刚刚创建完成的语音应用,暂时还没有开通任何语音服务,如下图所示。
  • 点击pan_ros_voice,会出现下图所示的界面,然后在列表中选中“语音听写”。
  • 接着界面会跳转到应用语音听写的数据统计界面,在界面有“SDK下载”选项,点击它,随后出现下载选项界面,默认已经根据应用的属性进行了配置,直接点击页面下方的“下载SDK”即可,如下图所示。

2、tianjia SDK

接下来将科大讯飞SDK的库文件拷贝到系统目录下,在后续的编译过程中才可以链接到库文件。进入SDK根目录下的libs文件夹,选择相应的平台架构,64位系统是“x64”,32位系统是“x86”,进入相应文件夹后,使用如下命令完成拷贝:

$ sudo cp libmsc.so /usr/lib/libmsc.so
科大讯飞的SDK带有ID号,每个人每次下载后的ID都不同,更换SDK后需要修改代码中的APPID,你也可以使用rrobot_voice/libs/x86/libmsc.so文件。

3、语音听写

第一步,要让机器人听懂我们说的中文,这个功能节点基于科大讯飞SDK中的“iat_online_record_sample”例程。将该例程中的代码拷贝到功能包robot_voice中,针对主要代码文件iat_online_record_sample进行修改,添加需要的ROS接口,修改完成后重命名文件为robot_voice/src/iat_publish.cpp。其中主要修改以下代码:

int main(int argc, char* argv[])
{
    // 初始化ROS
    ros::init(argc, argv, "voiceRecognition");
    ros::NodeHandle n;
    ros::Rate loop_rate(10);

    // 声明Publisher和Subscriber
    // 订阅唤醒语音识别的信号
    ros::Subscriber wakeUpSub = n.subscribe("voiceWakeup", 1000, WakeUp);   
    // 订阅唤醒语音识别的信号    
    ros::Publisher voiceWordsPub = n.advertise<std_msgs::String>("voiceWords", 1000);  

    ROS_INFO("Sleeping...");
    int count=0;
    while(ros::ok())
    {
        // 语音识别唤醒
        if (wakeupFlag){
            ROS_INFO("Wakeup...");
            int ret = MSP_SUCCESS;
            const char* login_params = "appid = 593ff61d, work_dir = .";

            const char* session_begin_params =
                "sub = iat, domain = iat, language = zh_cn, "
                "accent = mandarin, sample_rate = 16000, "
                "result_type = plain, result_encoding = utf8";

            ret = MSPLogin(NULL, NULL, login_params);
            if(MSP_SUCCESS != ret){
                MSPLogout();
                printf("MSPLogin failed , Error code %d.\n",ret);
            }

            printf("Demo recognizing the speech from microphone\n");
            printf("Speak in 10 seconds\n");

            demo_mic(session_begin_params);

            printf("10 sec passed\n");
        
            wakeupFlag=0;
            MSPLogout();
        }

        // 语音识别完成
        if(resultFlag){
            resultFlag=0;
            std_msgs::String msg;
            msg.data = g_result;
            voiceWordsPub.publish(msg);
        }

        ros::spinOnce();
        loop_rate.sleep();
        count++;
    }

exit:
    MSPLogout(); // Logout...

    return 0;
}

以上代码中加入了一个Publisher和一个Subscriber。Subscriber用来接收语音唤醒信号,接收到唤醒信号后,会将wakeupFlag变量置位,然后在主循环中调用SDK的语音听写功能,识别成功后置位resultFlag变量,通过Publisher将识别出来的字符串进行发布。

在CMakeLists.txt中加入编译规则:

add_executable(iat_publish 
  src/iat_publish.cpp 
  src/speech_recognizer.c 
  src/linuxrec.c)
target_link_libraries(
   iat_publish
   ${catkin_LIBRARIES} 
   libmsc.so -ldl -lpthread -lm -lrt -lasound
 )
编译过程中需要链接SDK中的libmsc.so库,此前已经将此库拷贝到系统路径下,所以此处无需添加完整路径。

编译完成后,使用以下命令进行测试:

$ roscore
$ rosrun robot_voice iat_publish
$ rostopic echo /voiceWords
$ rostopic pub /voiceWakeup std_msgs/String "data: any 'string'"

发布唤醒信号(任意字符串)后,可以看到“Start Listening...”的提示,然后就可以对着麦克风说话了,联网完成在线识别后会将结果进行发布。

4、语音合成

机器人已经具备基础听的能力,接下来让机器人具备说的功能。该功能模块基于科大讯飞SDK中的tts_sample例程,同样把示例所需的代码拷贝到功能包中,然后修改主代码文件,添加ROS接口,并重命名为robot_voice/src/tts_subscribe.cpp,其中的核心代码如下:

void voiceWordsCallback(const std_msgs::String::ConstPtr& msg)
{
    char cmd[2000];
    const char* text;
    int         ret                  = MSP_SUCCESS;
    const char* session_begin_params = "voice_name = xiaoyan, text_encoding = utf8, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 2";
    const char* filename             = "tts_sample.wav"; //合成的语音文件名称


    std::cout<<"I heard :"<<msg->data.c_str()<<std::endl;
    text = msg->data.c_str(); 

    /* 文本合成 */
    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");
    ......
}
int main(int argc, char* argv[])
{
    ......
    ros::init(argc,argv,"TextToSpeech");
    ros::NodeHandle n;
    ros::Subscriber sub =n.subscribe("voiceWords", 1000,voiceWordsCallback);
    ros::spin();
    ......
    return 0;
}

main()函数中声明了一个订阅voiceWords话题的Subscriber,接受输入的语音字符串,接收成功后,在回调函数voiceWordsCallback()中使用SDK接口将字符串转换成中文语音。

然后再CMakeLists.txt中添加编译规则:

add_executable(tts_subscribe src/tts_subscribe.cpp)
target_link_libraries(
   tts_subscribe
   ${catkin_LIBRARIES} 
   libmsc.so -ldl -pthread
 )

编译完成后,使用以下命令进行测试:

$ roscore
$ rosrun robot_voice tts_subscribe
$ rostopic pub /voiceWords std_msgs/String "data: '你好,我是机器人'"

如果语音合成成功,终端会显示如下图所示信息。

机器人用标准的普通话说出了“你好,我是机器人”这句话。这里可能会出现错误提示,但不影响语音输出效果。

这是由于mplayer配置导致的问题,解决方法是再/etc/mplayer/mplayer.conf文件中添加如下设置:

lirc=no

5、智能语音助手

现在机器人已经会听说了,但还没有智能化的数据处理能力,下面我们可以在以上代码的基础上添加一些数据处理,赋予机器人简单的智能,让机器人可以进行简单的中文对话。

我们可以在tts_subscribe.cpp代码的基础上进行修改。在voiceWordsCallback()回调函数中添加一些功能代码,并命名为robot_voice/src/voice_assistant.cpp。添加的代码如下:

void voiceWordsCallback(const std_msgs::String::ConstPtr& msg)
{
    ......
    std::cout<<"I heard :"<<msg->data.c_str()<<std::endl;

    std::string dataString = msg->data;
    if(dataString.compare("你是谁?") == 0)
    {
        char nameString[40] = "我是你的语音小助手";
        text = nameString;
        std::cout<<text<<std::endl;
    }
    else if(dataString.compare("你可以做什么?") == 0)
    {
        char helpString[40] = "你可以问我现在时间";
        text = helpString;
        std::cout<<text<<std::endl;
    }
    else if(dataString.compare("现在时间。") == 0)
    {
        //获取当前时间
        struct tm *ptm; 
        long ts; 

        ts = time(NULL); 
        ptm = localtime(&ts); 
        std::string string = "现在时间" + to_string(ptm-> tm_hour) + "点" + to_string(ptm-> tm_min) + "分";

        char timeString[40];
        string.copy(timeString, sizeof(string), 0);
        text = timeString;
        std::cout<<text<<std::endl;
    }
    else
    {
        text = msg->data.c_str();
    }

    /* 文本合成 */
    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");
    ......
}

在以上代码中,添加了一系列if、else语句来判断中文语音输入的含义,当我们说出“你是谁”“你可以做什么”“现在时间”等问题时,机器人可以获取系统当前时间,并且回答我们的问题。

然后再CMakeLists.txt中添加编译规则:

add_executable(voice_assistant src/voice_assistant.cpp)
target_link_libraries(
   voice_assistant
   ${catkin_LIBRARIES} 
   libmsc.so -ldl -pthread
 )

编译完成后,使用如下命令进行测试:

$ roscore
$ rosrun robot_voice iat_publish
$ rosrun robot_voice voice_assistant
$ rostopic pub /voiceWakeup std_msgs/String "data: 'any string'"

语音唤醒后,我们就可以向机器人发问了。

这里仅以中文语音交互为例实现了一个非常简单的机器人语音应用,重点是学习科大讯飞SDK与ROS系统的集成,以辅助我们实现更为复杂的机器语音功能。

八、本章小节

通过本章学习,我们了解了以下内容:

  • pocketsphinx功能包:用于实现英文语音的识别。
  • sound_play功能包:用于实现英文字符语音的功能。
  • 人工智能标记语言(AIML):用于实现机器人的语言交流功能。
  • 科大讯飞SDK:中文语音识别、合成的重要开发工具。