简析Android语音助手开发的一种实现方案
2020-04-01冯扬骆德汉
冯扬,骆德汉
(广东工业大学信息工程学院,广州510006)
0 引言
语音助手是人工智能应用的重要形式。Siri(苹果智能语音助手)带动了业内人工智能语音的发展。从载体来看,目前语音助手主要分为三大类:以Siri为代表的手机辅助功能,以天猫精灵、小度音箱为代表的智能音箱以及以荣耀智慧屏为代表的大屏语音交互,其中手机是智能语音助手的主要战场。
首先,语音交互取消了屏幕的限制,除了作为家庭的控制中枢,语音交互在可穿戴设备以及辅助驾驶领域也有着十分可观的应用前景。就辅助驾驶来说,由于驾驶员需要注意力的高度集中,需要在不干扰视野的情况下仅通过声音就可以满足交互的需求,例如搜索询问道路、编写短信、拨打电话、寻找食谱或查看天气等。
1 关键技术
本文旨在分析语音助手设计开发所用到的关键技术,自设计一款安卓语音助手App,实现的基础功能包括打电话、发短信、播放音乐/视频、打开应用等,还添加了个性化的互联网服务功能包括中英翻译、电影搜索等。在设计开发过程中,集成使用了科大讯飞SDK(语音听写和语音合成)、百度地图定位SDK,以及主流网络请求框架Retrofit和JSON数据解析等关键技术。整个使用流程如图1所示。
图1 语音助手流程图
1.1 语音交互
实现语音交互功能需要用到第三方工具库,该流程如下所示:
(1)下载科大讯飞语音开发SDK(含语音听写和语音合成功能),分别将libmsc.so、Msc.jar和Sunflower.jar以及assets文件夹等导入到Android项目中,并在An⁃droidManifest.xml文件中申请相关权限。
(2)使用注册时申请的APPID初始化语音配置对象。代码如下所示:
SpeechUtility.createUtility(context,SpeechConstant.AP⁃PID+"=1234ABCD");
(3)分别创建语音听写和语音合成的监听器对象。代码如下所示:
SpeechRecognizer mSRec=SpeechRecognizer.creat⁃eRecognizer(context,mInitListener);
SpeechSynthesizer mTts=SpeechSynthesizer.create⁃Synthesizer(context,TtsInitListener);
(4)监听“开始讲话”按钮是否按下,获取音频数据返回SDK进行处理,进行音频转文字过程(即语音听写)。代码如下所示:
@Override
public void onClick(Viewview){
if(null==mSRec||null==mTts){ //创建对象失败后的操作
return;
}
switch(view.getId()){
case R.id.mSRec_start:{
int ret=mSRec.startListening(mRecognizerListener);
if(ret!=ErrorCode.SUCCESS){//听写失败后进行的操作 }
else{ //听写成功后进行的操作 }
}break;
}
}
首先判断语音听写对象mSRec和语音合成对象mTts是否成功创建,然后通过调用mSRec.startListen⁃ing()方法,监听语音听写过程中的状态返回码来进行判断是否存在异常。
(5)RecognizerListener接口实现了语音听写结果的数据解析及可视化,存储到字符串keywords中。代码如下所示:
RecognizerListener mRecognizerListener=new RecognizerLis⁃tener(){
@Override
public void onBeginOfSpeech(){//语音开始后的操作}
@Override public void onError(SpeechError error){//语音出错后的操作}
@Override public void onEndOfSpeech(){//语音结束后的操作}
@Override
public void onResult(RecognizerResult results,boolean is⁃Last){
//解析语音听写结果数据
}
}
1.2 JSON数据解析
JSON的全称是JavaScript Object Notation,它是一种代码轻量级的数据交互格式,方便人们阅读和编写以及机器进行生成和解析。在实际应用中,JSON主要有两种表示结构:对象和数组[1]。这里以JSON对象为例,结合音频数据处理结果数据JSON格式的解析代码,分析JSON数据解析方法。代码如下所示:
HashMap<String,String>map=new LinkedHashMap<>();
String text=JsonParser.parseIatResult(results.getResultString
());
JSONObject resultJson=new JSONObject(results.getResult⁃
String());
Stringsn=resultJson.optString("sn");
map.put(sn,text);
StringBuffer resultBuffer=new StringBuffer();
for(Stringkey:map.keySet()){
resultBuffer.append(map.get(key));
}
Stringkeywords=resultBuffer.toString();
mSRecResults.setText(keywords);
其中第三行代码通过new JSONObject(Stringjson)创建一个JSONObject对象,第四行使用optString(String key)方法获取该对象键值对“key:value”中的String类型value值。同理可得,获取其他基本数据类型键值对的方法也与之相似,如optInt(Stringkey)、opt⁃Long(String key)分别返回long型和int型的值[1]。
1.3 语音合成
结合科大讯飞SDK实现语音合成功能,按1.1小节中的步骤配置好资源文件后,使用setParam()方法设定默认发音人、音量、语调、语速等参数,然后使用mTts.startSpeaking(strings,mTtsListener)就 可 以 将strings源字符串以音频形式输出,同时返回code状态码判断是否存在异常。
代码如下所示:
public static void setParam(){
mTts.setParameter(SpeechConstant.SPEED,mSharedPrefer⁃ences.getString("speed_preference","50"));
…
}int code=mTts.startSpeaking("欢迎使用车载语音助手",mTtsListener);
1.4 地图定位
实现定位导航功能需要用到第三方工具库,该流程如下所示:
(1)下载百度地图Android SDK(含基础定位和导航功能),分别将libs文件夹、assets文件夹和Baid⁃uLBS_Android.jar导入到项目中,申请定位相关权限。
(2)获取系统定位服务,注册监听并开启定位服务。代码如下所示:
LocationService locService=getApplication().locationService;
locService.registerListener(mListener);
locService.start();
(3)其中,服务监听器mListener的定义代码如下所示:
BDAbstractLocationListener mListener=new BDAbstractLoca⁃tionListener(){
@Override
public void onReceiveLocation(BDLocation location){
if(null!=location&&location.getLocType()!=Location.
TypeServerError){//获取相关地理信息
}}};
抽象监听接口BDAbstractLocationListener主要用于定位,通过各种getXX()方法来获取地理位置信息。这里首先判断BDLocation对象及LocationService服务是否正常,然后分别使用location.getAddrStr()、getLati⁃tude()等方法获取地址信息、经纬度信息等。
2 实例
在结合语音SDK和地图SDK的基础上,嵌入使用Retrofit框架和JSON解析等方法,来设计实现一款语音助手软件,剖析其开发的过程。使用过程中主要是根据语音听写结果中的命令关键词进行判断,实现不同事件的跳转处理,以及使用语音合成来完成人机之间的语音交互功能。
2.1 指令判断:打开应用
先检查读取手机应用信息的权限,使用queryIn⁃tentActivities()方法遍历已安装的所有应用,同时分别通过ResolveInfo.activityInfo.packageName获取应用包名称、ResolveInfo.activityInfo.loadLabel(getPackageMan⁃ager())获取应用名称。结合startActivity(package⁃Name)方法就可以实现打开应用的操作。代码如下所示:
(1)读取所有应用信息,用List列表存储
List<ResolveInfo>applist=getPackageManager()
.queryIntentActivities(queryIntent,0);
(2)判断听写结果中如有“打开XX应用”指令,则打开该应用
if(keywords.contains(appName)&&keywords.contains(“打开应用”))
startActivity(packageManager.getLaunchIntentForPackage(packageName));
2.2 指令判断:打电话
先检查读取联系人、打电话的权限,分别使用两组ArrayList来存储联系人名称和联系人号码。根据语音听写结果的指令判断,拨打特定联系人的号码。其中设定android.intent.action.CALL进行拨打电话的Activi⁃ty模式,呼叫指定号码的数据格式为:tel:+phone num⁃ber。代码如下所示:
(1)读取联系人信息
List<String>namelist=new ArrayList<>();
List<String>numList=new ArrayList<>();
Cursor cursor=getContentResolver().query(ContactsContract
.CommonDataKinds.Phone.CONTENT_URI,null,null,null,
null);
if(cursor!=null){
while(cursor.moveToNext()){//分别读取名字和号码存储到对应的List}cursor.close();}
(2)根据语音指令,给特定联系人拨打电话
for(int j=0;j<namelist.size();j++){
String s=namelist.get(j);if(keywords.contains(s)&&keywords.contains("打电话")){
Intent intent=new Intent(Intent.ACTION_CALL);
Uridata=Uri.parse("tel:"+numList.get(j));
intent.setData(data);
startActivity(intent);
}
2.3 指令判断:发短信
先检查发送短信的权限,而获取联系人信息的过程与2.2小节的第(1)点相同。根据语音听写结果的指令判断,给特定联系人发送短信,并指定输入框的语音短信内容。代码如下所示:
for(int j=0;j<namelist.size();j++){
String s=namelist.get(j);
if(keywords.contains(s)&&keywords.contains("发 短 信
")){
Intent sendIntent=new Intent(Intent.ACTION_SENDTO);
sendIntent.setData(Uri.parse("smsto:"+numList.get(j)));
sendIntent.putExtra("sms_body","【语音短信】:"+key⁃
words);
startActivity(sendIntent);
}
}
2.4 指令判断:中英翻译
这里使用网络请求框架Retrofit,将语音听写结果中需要翻译的部分内容上传到有道翻译服务器进行翻译以后,根据有道翻译API的返回数据格式,输出翻译结果。具体流程如下所示:
(1)创建一个Retrofit对象,并实例化。核心代码:
Retrofit retrofit=new Retrofit.Builder().baseUrl("https://fanyi.youdao.com/")
.addConverterFactory(GsonConverterFactory.create()).build();
(2)定义接口trans_Interface,使用post请求方式,定义url尾址,完整地址就是baseUrl+尾址。核心代码:
public interface trans_Interface{
@POST("translate?doctype=json&jsonversion=&type=&key⁃from=&model=&mid=&imei=&vendor=&screen=&ssid=&net ⁃work=&abtest=")
@FormUrlEncoded
Call<Translation1>getCall(@Field("i")String targetSen⁃tence);
}
(3)用retrofit创建接口实例trans_Interface,并调用接口方法进行网络请求。核心代码:
trans_Interface request=retrofit.create(trans_Interface.clas);
Call<Translation1>call= request.getCall(keywords);
call.enqueue(new Callback<Translation1>(){
@Override
public void onResponse(Call<Translation1>call,Response
<Translation1>response){//打印翻译结果
Log.d(response.body().getTranslateResult().get(0).get(0) .getTgt());}
@Override
public void onFailure(Call<Translation1>call,Throwable throwable){
//翻译出错后的处理
}
});
2.5 指令判断:搜索电影
依然使用网络请求框架Retrofit,判断电影搜索的语音命令,访问豆瓣电影服务器,根据豆瓣电影API返回Top30的电影数据,然后解析JSON格式数据进行显示得以实现。具体流程如下所示:
(1)创建Retrofit对象,并实例化。核心代码:
Retrofit mRetrofit=new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.douban.com/v2/movie/").build();
(2)定义接口MovieService,使用get请求方式,定义url尾址。核心代码:
public interface MovieService{
@GET
("top30?apikey=0b2bdeda43b5688921839c8ecb20399b")
Observable<MovieSubject>getTop30(
@Query("start")int start,
@Query("count")int count);
}
(3)用mRetrofit创建接口实例MovieService,并调用接口方法进行网络请求。核心代码:
MovieService movieService=mRetrofit.create(MovieService.
class);
Call<MovieSubject>call=movieService.getTop30(0,30);
call.enqueue(new Callback<MovieSubject>(){
@Override
public void onResponse(Call<MovieSubject>call,Response<MovieSubject>response){
//获取response响应对象中的top30电影数据(中英文名称
、主演、导演、年份等)并显示
}
@Override
public void onFailure(Call<MovieSubject>call,Throwable t){
//电影数据获取失败后的操作
}});
2.6 指令判断:媒体播放
(1)视频播放
Android在播放音频和视频方面做了不错的支持,它提供了一套较为完整的API,使得开发者可以轻松编写出一个简易的音频或视频播放器[2]。这里额外加入了语音识别控制功能,通过语音输入来实现本地媒体(音乐和视频)的播放、暂停、停止、切换以及音量大小调节等操作。这里以本地存储5首音频和2个视频为例,初始化VideoPlayer(),核心代码:
①播放视频文件主要通过VideoView类来实现,这个类集视频的显示和控制于一身[2]。调用initVideo⁃Player()来设置视频文件在SD卡中的存储路径。进一步在setVideoURI(Uri.parse(path))解析为通用资源标志符Uri并获取数据。其实际背后是使用MediaPlayer工具类来对视频文件进行操作的。
private void initVideoPlayer(){
if(videoNum>2) videoNum=0;
VideoView videoView= findViewById (R.id.vid⁃eoView);
File file=new File(Environment.getExternalStorageDirec⁃tory(),
videos[videoNum++]);
videoView.setVideoPath(file.getPath());
videoView.setMediaController(new android.widget.Media⁃Controller(this));}
(2)然后根据语音听写命令来进行视频文件的播放、暂停、切换等操作。核心代码:
if(keywords.contains("视频")){
videoView.setVisibility(View.VISIBLE);
if(keywords.contains("播放")){
if(!videoView.isPlaying())
videoView.start();
}else if(keywords.contains("暂停")){
if(videoView.isPlaying())
videoView.pause();
}else if(keywords.contains("重播")){
if(videoView.isPlaying()) videoView.resume();
}else if(keywords.contains("下一个")){
if(videoView.isPlaying())
videoView.stopPlayback();
initVideoPlayer();
videoView.start();
}
}
(2)音乐播放
同理,创建一个MediaPlayer对象,调用setData⁃Source()方法来设置音频文件的路径,再调用prepare()方法进入到准备状态[2]。最后通过语音输入指令,调用start()方法就可以进行播放等操作。核心代码如下:
(1)通过获取path转换Uri并获取数据。
privatevoid initMediaPlayer(){
if(musicNum>5) musicNum=0;
File file=new File(Environment.getExternalStorageDirectory(),musics[musicNum++]);
mediaPlayer.setDataSource(file.getPath());
mediaPlayer.prepare();
}
(2)根据语音听写命令来进行音乐文件的播放、暂停等操作。核心代码:
if(keywords.contains("音乐")){
if(keywords.contains("播放")){
if(!mediaPlayer.isPlaying())
mediaPlayer.start();
}elseif(keywords.contains("暂停")){
if(mediaPlayer.isPlaying())
mediaPlayer.pause();
}elseif(keywords.contains("停止")){
if(mediaPlayer.isPlaying()){
mediaPlayer.reset();
mediaPlayer.release();
}
}else if(keywords.contains("下一首")){
if(videoView.isPlaying()){
mediaPlayer.reset();
mediaPlayer.release();
}initMediaPlayer();
mediaPlayer.start();
}
}
(3)音量调节
通过AudioManager类来控制手机音频调节,首先获取当前媒体音量大小,然后根据语音指令进行音量增减调节的操作。具体代码如下所示:
AudioManager audioManager=getApplicationContext()
.getSystemService(Context.AUDIO_SERVICE);
int currentVolume =audioManager.getStreamVolume(Audio⁃
Manager.STREAM_MUSIC);
int maxVolume=audioManager.getStreamMaxVolume(Audio⁃
Manager.STREAM_MUSIC);
int minVolume=audioManager.getStreamMinVolume(Audio⁃
Manager.STREAM_MUSIC);
if(keywords.contains("调大音量")){
if(currentVolume<maxVolume)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,currentVolume++,Au⁃
dioManager.FLAG_PLAY_SOUND);
}else showTip("音量已经调到最大");
if(keywords.contains("调小音量")){
if(currentVolume>minVolume)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,currentVolume--,Au⁃
dioManager.FLAG_PLAY_SOUND);
}else showTip("已经调到静音");
2.7 结果显示
整个程序的运行结果如图2所示。
图2 程序运行结果
3 结语
经过测试和程序运行结果可以看出,整个实现方案是完全可行的。上述的这种语音助手实现方案,不仅能够应用在可穿戴设备方面,还能拓展到辅助驾驶领域。其中Retrofit框架作为当前Android网络请求非常流行的方式,结合JSON数据解析能够进一步实现查询天气、搜索附近景点/美食等操作,具有很快速实现、简化代码的优点。总的来说,这种语音助手实现方案还是具有卓越的可扩展性的。