馮揚(yáng),駱德漢
(廣東工業(yè)大學(xué)信息工程學(xué)院,廣州510006)
語(yǔ)音助手是人工智能應(yīng)用的重要形式。Siri(蘋果智能語(yǔ)音助手)帶動(dòng)了業(yè)內(nèi)人工智能語(yǔ)音的發(fā)展。從載體來看,目前語(yǔ)音助手主要分為三大類:以Siri為代表的手機(jī)輔助功能,以天貓精靈、小度音箱為代表的智能音箱以及以榮耀智慧屏為代表的大屏語(yǔ)音交互,其中手機(jī)是智能語(yǔ)音助手的主要戰(zhàn)場(chǎng)。
首先,語(yǔ)音交互取消了屏幕的限制,除了作為家庭的控制中樞,語(yǔ)音交互在可穿戴設(shè)備以及輔助駕駛領(lǐng)域也有著十分可觀的應(yīng)用前景。就輔助駕駛來說,由于駕駛員需要注意力的高度集中,需要在不干擾視野的情況下僅通過聲音就可以滿足交互的需求,例如搜索詢問道路、編寫短信、撥打電話、尋找食譜或查看天氣等。
本文旨在分析語(yǔ)音助手設(shè)計(jì)開發(fā)所用到的關(guān)鍵技術(shù),自設(shè)計(jì)一款安卓語(yǔ)音助手App,實(shí)現(xiàn)的基礎(chǔ)功能包括打電話、發(fā)短信、播放音樂/視頻、打開應(yīng)用等,還添加了個(gè)性化的互聯(lián)網(wǎng)服務(wù)功能包括中英翻譯、電影搜索等。在設(shè)計(jì)開發(fā)過程中,集成使用了科大訊飛SDK(語(yǔ)音聽寫和語(yǔ)音合成)、百度地圖定位SDK,以及主流網(wǎng)絡(luò)請(qǐng)求框架Retrofit和JSON數(shù)據(jù)解析等關(guān)鍵技術(shù)。整個(gè)使用流程如圖1所示。
圖1 語(yǔ)音助手流程圖
實(shí)現(xiàn)語(yǔ)音交互功能需要用到第三方工具庫(kù),該流程如下所示:
(1)下載科大訊飛語(yǔ)音開發(fā)SDK(含語(yǔ)音聽寫和語(yǔ)音合成功能),分別將libmsc.so、Msc.jar和Sunflower.jar以及assets文件夾等導(dǎo)入到Android項(xiàng)目中,并在An?droidManifest.xml文件中申請(qǐng)相關(guān)權(quán)限。
(2)使用注冊(cè)時(shí)申請(qǐng)的APPID初始化語(yǔ)音配置對(duì)象。代碼如下所示:
SpeechUtility.createUtility(context,SpeechConstant.AP?PID+"=1234ABCD");
(3)分別創(chuàng)建語(yǔ)音聽寫和語(yǔ)音合成的監(jiān)聽器對(duì)象。代碼如下所示:
SpeechRecognizer mSRec=SpeechRecognizer.creat?eRecognizer(context,mInitListener);
SpeechSynthesizer mTts=SpeechSynthesizer.create?Synthesizer(context,TtsInitListener);
(4)監(jiān)聽“開始講話”按鈕是否按下,獲取音頻數(shù)據(jù)返回SDK進(jìn)行處理,進(jìn)行音頻轉(zhuǎn)文字過程(即語(yǔ)音聽寫)。代碼如下所示:
@Override
public void onClick(Viewview){
if(null==mSRec||null==mTts){ //創(chuàng)建對(duì)象失敗后的操作
return;
}
switch(view.getId()){
case R.id.mSRec_start:{
int ret=mSRec.startListening(mRecognizerListener);
if(ret!=ErrorCode.SUCCESS){//聽寫失敗后進(jìn)行的操作 }
else{ //聽寫成功后進(jìn)行的操作 }
}break;
}
}
首先判斷語(yǔ)音聽寫對(duì)象mSRec和語(yǔ)音合成對(duì)象mTts是否成功創(chuàng)建,然后通過調(diào)用mSRec.startListen?ing()方法,監(jiān)聽語(yǔ)音聽寫過程中的狀態(tài)返回碼來進(jìn)行判斷是否存在異常。
(5)RecognizerListener接口實(shí)現(xiàn)了語(yǔ)音聽寫結(jié)果的數(shù)據(jù)解析及可視化,存儲(chǔ)到字符串keywords中。代碼如下所示:
RecognizerListener mRecognizerListener=new RecognizerLis?tener(){
@Override
public void onBeginOfSpeech(){//語(yǔ)音開始后的操作}
@Override public void onError(SpeechError error){//語(yǔ)音出錯(cuò)后的操作}
@Override public void onEndOfSpeech(){//語(yǔ)音結(jié)束后的操作}
@Override
public void onResult(RecognizerResult results,boolean is?Last){
//解析語(yǔ)音聽寫結(jié)果數(shù)據(jù)
}
}
JSON的全稱是JavaScript Object Notation,它是一種代碼輕量級(jí)的數(shù)據(jù)交互格式,方便人們閱讀和編寫以及機(jī)器進(jìn)行生成和解析。在實(shí)際應(yīng)用中,JSON主要有兩種表示結(jié)構(gòu):對(duì)象和數(shù)組[1]。這里以JSON對(duì)象為例,結(jié)合音頻數(shù)據(jù)處理結(jié)果數(shù)據(jù)JSON格式的解析代碼,分析JSON數(shù)據(jù)解析方法。代碼如下所示:
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)創(chuàng)建一個(gè)JSONObject對(duì)象,第四行使用optString(String key)方法獲取該對(duì)象鍵值對(duì)“key:value”中的String類型value值。同理可得,獲取其他基本數(shù)據(jù)類型鍵值對(duì)的方法也與之相似,如optInt(Stringkey)、opt?Long(String key)分別返回long型和int型的值[1]。
結(jié)合科大訊飛SDK實(shí)現(xiàn)語(yǔ)音合成功能,按1.1小節(jié)中的步驟配置好資源文件后,使用setParam()方法設(shè)定默認(rèn)發(fā)音人、音量、語(yǔ)調(diào)、語(yǔ)速等參數(shù),然后使用mTts.startSpeaking(strings,mTtsListener)就 可 以 將strings源字符串以音頻形式輸出,同時(shí)返回code狀態(tài)碼判斷是否存在異常。
代碼如下所示:
public static void setParam(){
mTts.setParameter(SpeechConstant.SPEED,mSharedPrefer?ences.getString("speed_preference","50"));
…
}int code=mTts.startSpeaking("歡迎使用車載語(yǔ)音助手",mTtsListener);
實(shí)現(xiàn)定位導(dǎo)航功能需要用到第三方工具庫(kù),該流程如下所示:
(1)下載百度地圖Android SDK(含基礎(chǔ)定位和導(dǎo)航功能),分別將libs文件夾、assets文件夾和Baid?uLBS_Android.jar導(dǎo)入到項(xiàng)目中,申請(qǐng)定位相關(guān)權(quán)限。
(2)獲取系統(tǒng)定位服務(wù),注冊(cè)監(jiān)聽并開啟定位服務(wù)。代碼如下所示:
LocationService locService=getApplication().locationService;
locService.registerListener(mListener);
locService.start();
(3)其中,服務(wù)監(jiān)聽器mListener的定義代碼如下所示:
BDAbstractLocationListener mListener=new BDAbstractLoca?tionListener(){
@Override
public void onReceiveLocation(BDLocation location){
if(null!=location&&location.getLocType()!=Location.
TypeServerError){//獲取相關(guān)地理信息
}}};
抽象監(jiān)聽接口BDAbstractLocationListener主要用于定位,通過各種getXX()方法來獲取地理位置信息。這里首先判斷BDLocation對(duì)象及LocationService服務(wù)是否正常,然后分別使用location.getAddrStr()、getLati?tude()等方法獲取地址信息、經(jīng)緯度信息等。
在結(jié)合語(yǔ)音SDK和地圖SDK的基礎(chǔ)上,嵌入使用Retrofit框架和JSON解析等方法,來設(shè)計(jì)實(shí)現(xiàn)一款語(yǔ)音助手軟件,剖析其開發(fā)的過程。使用過程中主要是根據(jù)語(yǔ)音聽寫結(jié)果中的命令關(guān)鍵詞進(jìn)行判斷,實(shí)現(xiàn)不同事件的跳轉(zhuǎn)處理,以及使用語(yǔ)音合成來完成人機(jī)之間的語(yǔ)音交互功能。
先檢查讀取手機(jī)應(yīng)用信息的權(quán)限,使用queryIn?tentActivities()方法遍歷已安裝的所有應(yīng)用,同時(shí)分別通過ResolveInfo.activityInfo.packageName獲取應(yīng)用包名稱、ResolveInfo.activityInfo.loadLabel(getPackageMan?ager())獲取應(yīng)用名稱。結(jié)合startActivity(package?Name)方法就可以實(shí)現(xiàn)打開應(yīng)用的操作。代碼如下所示:
(1)讀取所有應(yīng)用信息,用List列表存儲(chǔ)
List<ResolveInfo>applist=getPackageManager()
.queryIntentActivities(queryIntent,0);
(2)判斷聽寫結(jié)果中如有“打開XX應(yīng)用”指令,則打開該應(yīng)用
if(keywords.contains(appName)&&keywords.contains(“打開應(yīng)用”))
startActivity(packageManager.getLaunchIntentForPackage(packageName));
先檢查讀取聯(lián)系人、打電話的權(quán)限,分別使用兩組ArrayList來存儲(chǔ)聯(lián)系人名稱和聯(lián)系人號(hào)碼。根據(jù)語(yǔ)音聽寫結(jié)果的指令判斷,撥打特定聯(lián)系人的號(hào)碼。其中設(shè)定android.intent.action.CALL進(jìn)行撥打電話的Activi?ty模式,呼叫指定號(hào)碼的數(shù)據(jù)格式為:tel:+phone num?ber。代碼如下所示:
(1)讀取聯(lián)系人信息
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()){//分別讀取名字和號(hào)碼存儲(chǔ)到對(duì)應(yīng)的List}cursor.close();}
(2)根據(jù)語(yǔ)音指令,給特定聯(lián)系人撥打電話
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);
}
先檢查發(fā)送短信的權(quán)限,而獲取聯(lián)系人信息的過程與2.2小節(jié)的第(1)點(diǎn)相同。根據(jù)語(yǔ)音聽寫結(jié)果的指令判斷,給特定聯(lián)系人發(fā)送短信,并指定輸入框的語(yǔ)音短信內(nèi)容。代碼如下所示:
for(int j=0;j<namelist.size();j++){
String s=namelist.get(j);
if(keywords.contains(s)&&keywords.contains("發(fā) 短 信
")){
Intent sendIntent=new Intent(Intent.ACTION_SENDTO);
sendIntent.setData(Uri.parse("smsto:"+numList.get(j)));
sendIntent.putExtra("sms_body","【語(yǔ)音短信】:"+key?
words);
startActivity(sendIntent);
}
}
這里使用網(wǎng)絡(luò)請(qǐng)求框架Retrofit,將語(yǔ)音聽寫結(jié)果中需要翻譯的部分內(nèi)容上傳到有道翻譯服務(wù)器進(jìn)行翻譯以后,根據(jù)有道翻譯API的返回?cái)?shù)據(jù)格式,輸出翻譯結(jié)果。具體流程如下所示:
(1)創(chuàng)建一個(gè)Retrofit對(duì)象,并實(shí)例化。核心代碼:
Retrofit retrofit=new Retrofit.Builder().baseUrl("https://fanyi.youdao.com/")
.addConverterFactory(GsonConverterFactory.create()).build();
(2)定義接口trans_Interface,使用post請(qǐng)求方式,定義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創(chuàng)建接口實(shí)例trans_Interface,并調(diào)用接口方法進(jìn)行網(wǎng)絡(luò)請(qǐng)求。核心代碼:
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){//打印翻譯結(jié)果
Log.d(response.body().getTranslateResult().get(0).get(0) .getTgt());}
@Override
public void onFailure(Call<Translation1>call,Throwable throwable){
//翻譯出錯(cuò)后的處理
}
});
依然使用網(wǎng)絡(luò)請(qǐng)求框架Retrofit,判斷電影搜索的語(yǔ)音命令,訪問豆瓣電影服務(wù)器,根據(jù)豆瓣電影API返回Top30的電影數(shù)據(jù),然后解析JSON格式數(shù)據(jù)進(jìn)行顯示得以實(shí)現(xiàn)。具體流程如下所示:
(1)創(chuàng)建Retrofit對(duì)象,并實(shí)例化。核心代碼:
Retrofit mRetrofit=new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.douban.com/v2/movie/").build();
(2)定義接口MovieService,使用get請(qǐng)求方式,定義url尾址。核心代碼:
public interface MovieService{
@GET
("top30?apikey=0b2bdeda43b5688921839c8ecb20399b")
Observable<MovieSubject>getTop30(
@Query("start")int start,
@Query("count")int count);
}
(3)用mRetrofit創(chuàng)建接口實(shí)例MovieService,并調(diào)用接口方法進(jìn)行網(wǎng)絡(luò)請(qǐng)求。核心代碼:
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響應(yīng)對(duì)象中的top30電影數(shù)據(jù)(中英文名稱
、主演、導(dǎo)演、年份等)并顯示
}
@Override
public void onFailure(Call<MovieSubject>call,Throwable t){
//電影數(shù)據(jù)獲取失敗后的操作
}});
(1)視頻播放
Android在播放音頻和視頻方面做了不錯(cuò)的支持,它提供了一套較為完整的API,使得開發(fā)者可以輕松編寫出一個(gè)簡(jiǎn)易的音頻或視頻播放器[2]。這里額外加入了語(yǔ)音識(shí)別控制功能,通過語(yǔ)音輸入來實(shí)現(xiàn)本地媒體(音樂和視頻)的播放、暫停、停止、切換以及音量大小調(diào)節(jié)等操作。這里以本地存儲(chǔ)5首音頻和2個(gè)視頻為例,初始化VideoPlayer(),核心代碼:
①播放視頻文件主要通過VideoView類來實(shí)現(xiàn),這個(gè)類集視頻的顯示和控制于一身[2]。調(diào)用initVideo?Player()來設(shè)置視頻文件在SD卡中的存儲(chǔ)路徑。進(jìn)一步在setVideoURI(Uri.parse(path))解析為通用資源標(biāo)志符Uri并獲取數(shù)據(jù)。其實(shí)際背后是使用MediaPlayer工具類來對(duì)視頻文件進(jìn)行操作的。
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)然后根據(jù)語(yǔ)音聽寫命令來進(jìn)行視頻文件的播放、暫停、切換等操作。核心代碼:
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("下一個(gè)")){
if(videoView.isPlaying())
videoView.stopPlayback();
initVideoPlayer();
videoView.start();
}
}
(2)音樂播放
同理,創(chuàng)建一個(gè)MediaPlayer對(duì)象,調(diào)用setData?Source()方法來設(shè)置音頻文件的路徑,再調(diào)用prepare()方法進(jìn)入到準(zhǔn)備狀態(tài)[2]。最后通過語(yǔ)音輸入指令,調(diào)用start()方法就可以進(jìn)行播放等操作。核心代碼如下:
(1)通過獲取path轉(zhuǎn)換Uri并獲取數(shù)據(jù)。
privatevoid initMediaPlayer(){
if(musicNum>5) musicNum=0;
File file=new File(Environment.getExternalStorageDirectory(),musics[musicNum++]);
mediaPlayer.setDataSource(file.getPath());
mediaPlayer.prepare();
}
(2)根據(jù)語(yǔ)音聽寫命令來進(jìn)行音樂文件的播放、暫停等操作。核心代碼:
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)音量調(diào)節(jié)
通過AudioManager類來控制手機(jī)音頻調(diào)節(jié),首先獲取當(dāng)前媒體音量大小,然后根據(jù)語(yǔ)音指令進(jìn)行音量增減調(diào)節(jié)的操作。具體代碼如下所示:
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("調(diào)大音量")){
if(currentVolume<maxVolume)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,currentVolume++,Au?
dioManager.FLAG_PLAY_SOUND);
}else showTip("音量已經(jīng)調(diào)到最大");
if(keywords.contains("調(diào)小音量")){
if(currentVolume>minVolume)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,currentVolume--,Au?
dioManager.FLAG_PLAY_SOUND);
}else showTip("已經(jīng)調(diào)到靜音");
整個(gè)程序的運(yùn)行結(jié)果如圖2所示。
圖2 程序運(yùn)行結(jié)果
經(jīng)過測(cè)試和程序運(yùn)行結(jié)果可以看出,整個(gè)實(shí)現(xiàn)方案是完全可行的。上述的這種語(yǔ)音助手實(shí)現(xiàn)方案,不僅能夠應(yīng)用在可穿戴設(shè)備方面,還能拓展到輔助駕駛領(lǐng)域。其中Retrofit框架作為當(dāng)前Android網(wǎng)絡(luò)請(qǐng)求非常流行的方式,結(jié)合JSON數(shù)據(jù)解析能夠進(jìn)一步實(shí)現(xiàn)查詢天氣、搜索附近景點(diǎn)/美食等操作,具有很快速實(shí)現(xiàn)、簡(jiǎn)化代碼的優(yōu)點(diǎn)??偟膩碚f,這種語(yǔ)音助手實(shí)現(xiàn)方案還是具有卓越的可擴(kuò)展性的。