科大訊飛語音無限制錄音、識別功能的實現:Android studio(一)
最近想要做一款語音聽寫APP,在網上搜索關於如何使用科大訊飛語音的Demo少之又少,又或者是隻是單純的按照文件來實現簡單的語音聽寫,遠遠不能滿足需求,看了幾天的文件和自己搜尋的一些資料,還有這幾天中遇到的一些問題,覺得有必要做一個筆記,能給初學者一些幫助,也順便理一下這些天的一些收穫,本人只是一個初學者,假如有寫得不對或者不好的地方,還望大家指出~~
1、首先當然是建立應用,我這裡只是使用了語音聽寫的功能,建立完成後下載SDK,開啟是這樣子的:
2、匯入SDK:
將開發工具包中libs目錄下的Msc.jar和Sunflower.jar複製到Android工程的libs目錄中,將online資料夾裡面的子檔案貼上到工程目錄src/main/jniLibs
3、新增許可權:
<!--連線網路許可權,用於執行雲端語音能力 --> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <!--獲取手機錄音機使用許可權,聽寫、識別、語義理解需要用到此許可權 --> <uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission> <!--讀取網路資訊狀態 --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission> <!--獲取當前wifi狀態 --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission> <!--允許程式改變網路連線狀態 --> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"></uses-permission> <!--讀取手機資訊許可權 --> <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission> <!--讀取聯絡人許可權,上傳聯絡人需要用到此許可權 --> <uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission> <!--假如我們要保存錄音,還需要以下許可權--> <!-- 在SDCard中建立與刪除檔案許可權 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission> <!-- SD卡許可權 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <!-- 允許程式讀取或寫入系統設定 --> <uses-permission android:name="android.permission.WRITE_SETTINGS"></uses-permission>
4、初始化:
//將“12345678”替換成您申請的APPID,申請地址:http://open.voicecloud.cn
SpeechUtility.createUtility(this, "appid=123456789");
資料收集介面:
@Override protected void onResume() { // 開放統計 移動資料統計分析 FlowerCollector.onResume(MainActivity.this); super.onResume(); } @Override protected void onPause() { // 開放統計 移動資料統計分析 FlowerCollector.onPause(MainActivity.this); super.onPause(); }
說明:
1.確保在所有的 activity 中都呼叫 FlowerCollector.onResume() 和 FlowerCollector.onPause()方法。這兩個呼叫將不會阻塞應用程式的主執行緒,也不會影響應用程式的效能。
2.注意,如果您的 Activity 之間有繼承或者控制關係請不要同時在父和子 Activity 中重複新增onPause 和 onResume 方法,否則會造成重複統計(eg:使用 TabHost、TabActivity、ActivityGroup 時)。3.一個應用程式在多個 activity 之間連續切換時,會被視為同一個 session(啟動)。4.當用戶兩次使用之間間隔超過 30 秒時,將被認為是兩個的獨立的 session(啟動)。例如:使用者回到home,或進入其他程式,經過一段時間後再返回之前的應用。
5.所有日誌收集工作均在 onResume 之後進行,在 onPause 之後結束。 (ps:其實我也不懂這是幹嘛的,猜想是為了收集資料的,還有一些引數設定我就不羅列了,有興趣的同學可以去看,因為我發現就算沒設定它們我也能進行語音聽寫,反正收集的資料我看不懂)
5、語音聽寫的使用(採取雲端聽寫的引擎模式,暫時未考慮本地整合)
注意導包:com.iflytek.cloud而不是android.speech.SpeechRecognizer
1)使用自帶UI語音對話方塊
優點:簡單方便、美觀
缺點:端點超時會自動停止識別(不理解的往下看設定引數說明)預設前段點:5000 後端點:1800
這是使用語音識別最簡單的方法,在點選事件上呼叫start()方法就好了
public void start() {
//1.建立SpeechRecognizer物件,第二個引數:本地聽寫時傳InitListener
iatDialog = new RecognizerDialog(this, initListener);
//2.設定聽寫引數
iatDialog.setParameter(SpeechConstant.DOMAIN, "iat");
iatDialog.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
iatDialog.setParameter(SpeechConstant.ACCENT, "mandarin ");
//3.設定回撥介面
iatDialog.setListener(new RecognizerDialogListener() {
@Override
public void onResult(RecognizerResult recognizerResult, boolean b) {
if (!b) {
String json = recognizerResult.getResultString();
String str = JsonParser.parseIatResult(json);
System.out.println("說話內容:"+str);
textView.setText(str);
}
}
@Override
public void onError(SpeechError speechError) {
Log.d("error", speechError.toString());
}
});
//4.開始聽寫
iatDialog.show();
}
增加if(!b)的判斷是因為每次說話結束之後,返回資料的最後一次是返回一個標點符號"。"或者"!"之類的,當然你也可以設定不返回標點符號。
//設定是否帶標點符號 0表示不帶標點,1則表示帶標點。
mIat.setParameter(SpeechConstant.ASR_PTT, "0");
2)不使用自帶UI對話方塊
優點:可以設定自己想要的引數
缺點:使用比較麻煩,對於初學者來講,根據講話音量大小自定義麥克風效果的View簡直是噩夢(我不會,哪位大神做出來了求分享~~)
簡單的用法:
1.建立物件:
<span style="font-size:18px;">//1.建立SpeechRecognizer物件,第二個引數:本地聽寫時傳InitListener
mIat = SpeechRecognizer.createRecognizer(this, null);</span>
2.設定引數(聽寫這三個引數是必須的,下面設定時不再提示):
mIat.setParameter(SpeechConstant.DOMAIN, "iat");
// 簡體中文:"zh_cn", 美式英文:"en_us"
mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
//普通話:mandarin(預設)
//粵 語:cantonese
//四川話:lmz
//河南話:henanese
mIat.setParameter(SpeechConstant.ACCENT, "mandarin ");
3.例項化監聽物件
private RecognizerListener recognizerListener = new RecognizerListener() {
@Override
public void onVolumeChanged(int i, byte[] bytes) {
}
@Override
public void onBeginOfSpeech() {
System.out.println("開始識別");
}
@Override
public void onEndOfSpeech() {
System.out.println("識別結束");
}
@Override
public void onResult(RecognizerResult recognizerResult, boolean b) {
String str=JsonParser.parseIatResult(recognizerResult.getResultString());
System.out.println("識別結果"+str);
}
@Override
public void onError(SpeechError speechError) {
System.out.println("識別出錯");
}
@Override
public void onEvent(int i, int i1, int i2, Bundle bundle) {
}
};
4.新增監聽
mIat.startListening(recognizerListener);
官方Demo給出的解析Json的類:
package com.hxl.voicetest1;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
* Json結果解析類
*/
public class JsonParser {
public static String parseIatResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray("ws");
for (int i = 0; i < words.length(); i++) {
// 轉寫結果詞,預設使用第一個結果
JSONArray items = words.getJSONObject(i).getJSONArray("cw");
JSONObject obj = items.getJSONObject(0);
ret.append(obj.getString("w"));
//如果需要多候選結果,解析陣列其他欄位
//for(int j = 0; j < items.length(); j++)
//{
//JSONObject obj = items.getJSONObject(j);
//ret.append(obj.getString("w"));
//}
}
} catch (Exception e) {
e.printStackTrace();
}
return ret.toString();
}
public static String parseGrammarResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray("ws");
for (int i = 0; i < words.length(); i++) {
JSONArray items = words.getJSONObject(i).getJSONArray("cw");
for (int j = 0; j < items.length(); j++) {
JSONObject obj = items.getJSONObject(j);
if (obj.getString("w").contains("nomatch")) {
ret.append("沒有匹配結果.");
return ret.toString();
}
ret.append("【結果】" + obj.getString("w"));
ret.append("【置信度】" + obj.getInt("sc"));
ret.append("n");
}
}
} catch (Exception e) {
e.printStackTrace();
ret.append("沒有匹配結果.");
}
return ret.toString();
}
}
到這裡,科大訊飛語音最簡單的聽寫Demo就算完成了。這時你們會想:WTF?就這些我還不如自個兒看文件.....
當然了,既然寫這篇部落格,肯定不僅僅是簡單介紹科大訊飛語音聽寫的使用而已。
我想要的效果是:無限制時間錄音,並且我想知道我在第幾秒說了什麼話,回放錄音的時候,播放到哪就顯示相應的文字,還可以對識別錯誤的欄位進行糾錯修改。
我們來看一下一些常用的設定引數(個人認為),我們可以根據我們的需求來設定相應的引數
// 清空引數
mIat.setParameter(SpeechConstant.PARAMS, null);
//簡訊和日常用語:iat (預設) 視訊:video 地圖:poi 音樂:music
mIat.setParameter(SpeechConstant.DOMAIN, "iat");
// 簡體中文:"zh_cn", 美式英文:"en_us"
mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
//普通話:mandarin(預設)
//粵 語:cantonese
//四川話:lmz
//河南話:henanese<span style="font-family: Menlo;"> </span>
mIat.setParameter(SpeechConstant.ACCENT, "mandarin ");
// 設定聽寫引擎 "cloud", "local","mixed" 線上 本地 混合
//本地的需要本地功能整合
mIat.setParameter(SpeechConstant.ENGINE_TYPE, "cloud");
// 設定返回結果格式 聽寫會話支援json和plain
mIat.setParameter(SpeechConstant.RESULT_TYPE, "json");
//設定是否帶標點符號 0表示不帶標點,1則表示帶標點。
mIat.setParameter(SpeechConstant.ASR_PTT, "0");
//只有設定這個屬性為1時,VAD_BOS VAD_EOS才會生效,且RecognizerListener.onVolumeChanged才有音量返回預設:1
mIat.setParameter(SpeechConstant.VAD_ENABLE,"1");
// 設定語音前端點:靜音超時時間,即使用者多長時間不說話則當做超時處理1000~10000
mIat.setParameter(SpeechConstant.VAD_BOS, "5000");
// 設定語音後端點:後端點靜音檢測時間,即使用者停止說話多長時間內即認為不再輸入, 自動停止錄音0~10000
mIat.setParameter(SpeechConstant.VAD_EOS, "1800");
// 設定音訊儲存路徑,儲存音訊格式支援pcm、wav,設定路徑為sd卡請注意WRITE_EXTERNAL_STORAGE許可權
// 注:AUDIO_FORMAT引數語記需要更新版本才能生效
mIat.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");
//設定識別會話被中斷時(如當前會話未結束就開啟了新會話等),
//是否通 過RecognizerListener.onError(com.iflytek.cloud.SpeechError)回撥ErrorCode.ERROR_INTERRUPT錯誤。
//預設false [null,true,false]
mIat.setParameter(SpeechConstant.ASR_INTERRUPT_ERROR,"false");
//音訊取樣率 8000~16000 預設:16000
mIat.setParameter(SpeechConstant.SAMPLE_RATE,"16000");
//預設:麥克風(1)(MediaRecorder.AudioSource.MIC)
//在寫音訊流方式(-1)下,應用層通過writeAudio函式送入音訊;
//在傳檔案路徑方式(-2)下,SDK通過應用層設定的ASR_SOURCE_PATH值, 直接讀取音訊檔案。目前僅在SpeechRecognizer中支援。
mIat.setParameter(SpeechConstant.AUDIO_SOURCE, "-1");
//儲存音訊檔案的路徑 僅支援pcm和wav
mIat.setParameter(SpeechConstant.ASR_SOURCE_PATH, Environment.getExternalStorageDirectory().getAbsolutePath() + "test.wav");
我首先想到當然就是設定音訊儲存路徑了啊,這還不簡單
mIat.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");
mIat.setParameter(SpeechConstant.ASR_SOURCE_PATH, "要儲存的路徑");
時間戳這個好辦,根據錄音開始時記錄當前時間startTime,在RecognizerListener的onResult方法獲取當前時間currentTime,然後用currentTime-startTime 就是第幾秒說的話了(由於雲端識別會有延遲,這個秒數其實是不正確的,這裡先忽略這個問題)。
結束了?
不是,我們再來看一下科大訊飛的說明文件中:
科大訊飛對語音聽寫做了限制,端點超時最大也只能設定10秒,超過這個時間識別自動終止,不再對後續的語音部分進行識別。假如我們要長時間錄音,不可能讓使用者每10秒鐘就要說一句話,而且還有一個是值得我們注意的:
也就是說,就算我們連續不停的講話,音訊錄製最多也就是60秒而已,怎麼辦?
這時候我就想,我用自己的方法錄音,用科大訊飛的去識別,這是個好方法,我立馬在百度輸入框敲上 ”Android錄音“
考慮到科大訊飛只支援wav、pcm檔案,我只是把AudioRecordFunc類copy過來,考慮到科大訊飛對音訊檔案識別的要求:
上傳音訊的取樣率與取樣精度:A:取樣率16KHZ或者8KHZ,單聲道,取樣精度16bit的PCM或者WAV格式的音訊
我將程式碼進行了部分修改。import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import android.media.AudioFormat;
import android.media.AudioRecord;
public class AudioRecordFunc {
// 緩衝區位元組大小
private int bufferSizeInBytes = 0;
//AudioName裸音訊資料檔案 ,麥克風
private String AudioName = "";
//NewAudioName可播放的音訊檔案
private String NewAudioName = "";
private AudioRecord audioRecord;
private boolean isRecord = false;// 設定正在錄製的狀態
private static AudioRecordFunc mInstance;
private AudioRecordFunc() {
}
public synchronized static AudioRecordFunc getInstance() {
if (mInstance == null)
mInstance = new AudioRecordFunc();
return mInstance;
}
public int startRecordAndFile() {
//判斷是否有外部儲存裝置sdcard
if (AudioFileFunc.isSdcardExit()) {
if (isRecord) {
return ErrorCode.E_STATE_RECODING;
} else {
if (audioRecord == null)
creatAudioRecord();
audioRecord.startRecording();
// 讓錄製狀態為true
isRecord = true;
// 開啟音訊檔案寫入執行緒
new Thread(new AudioRecordThread()).start();
return ErrorCode.SUCCESS;
}
} else {
return ErrorCode.E_NOSDCARD;
}
}
public void stopRecordAndFile() {
close();
}
public long getRecordFileSize() {
return AudioFileFunc.getFileSize(NewAudioName);
}
private void close() {
if (audioRecord != null) {
System.out.println("stopRecord");
isRecord = false;//停止檔案寫入
audioRecord.stop();
audioRecord.release();//釋放資源
audioRecord = null;
}
}
private void creatAudioRecord() {
// 獲取音訊檔案路徑
AudioName = AudioFileFunc.getRawFilePath();
NewAudioName = AudioFileFunc.getWavFilePath();
// 獲得緩衝區位元組大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(AudioFileFunc.AUDIO_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
// 建立AudioRecord物件(修改處)
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_ IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
}
class AudioRecordThread implements Runnable {
@Override
public void run() {
writeDateTOFile();//往檔案中寫入裸資料
copyWaveFile(AudioName, NewAudioName);//給裸資料加上標頭檔案
}
}
/**
* 這裡將資料寫入檔案,但是並不能播放,因為AudioRecord獲得的音訊是原始的裸音訊
* 如果需要播放就必須加入一些格式或者編碼的頭資訊。但是這樣的好處就是你可以對音訊的 裸資料進行處理
* 比如你要做一個愛說話的TOM貓在這裡就進行音訊的處理,然後重新封裝 所以說這樣得到的音訊比較容易做一些音訊的處理。
*/
private void writeDateTOFile() {
// new一個byte陣列用來存一些位元組資料,大小為緩衝區大小
byte[] audiodata = new byte[bufferSizeInBytes];
FileOutputStream fos = null;
int readsize = 0;
try {
File file = new File(AudioName);
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);
// 建立一個可存取位元組的檔案
} catch (Exception e) {
e.printStackTrace();
}
while (isRecord == true) {
readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
try {
fos.write(audiodata);
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
if (fos != null)
fos.close();// 關閉寫入流
} catch (IOException e) {
e.printStackTrace();
}
}
// 這裡得到可播放的音訊檔案
private void copyWaveFile(String inFilename, String outFilename) {
FileInputStream in = null;
FileOutputStream out = null;
long totalAudioLen = 0;
long totalDataLen = totalAudioLen + 36;
long longSampleRate = AudioFileFunc.AUDIO_SAMPLE_RATE;
int channels = 2;
long byteRate = 16 * AudioFileFunc.AUDIO_SAMPLE_RATE * channels / 8;
byte[] data = new byte[bufferSizeInBytes];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 這裡提供一個頭資訊。插入這些資訊就可以得到可以播放的檔案。
* 為我為啥插入這44個位元組,這個還真沒深入研究,不過你隨便開啟一個wav
* 音訊的檔案,可以發現前面的標頭檔案可以說基本一樣哦。每種格式的檔案都有自己特有的標頭檔案。
*/
private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF/WAVE header
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
header[20] = 1; // format = 1
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) (2 * 16 / 8); // block align
header[33] = 0;
header[34] = 16; // bits per sample
header[35] = 0;
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}
然後在錄音按鈕裡的點選事件裡寫了這麼兩行行程式碼:
//開始進行語音聽寫
mIat.startListening(mRecoListener);
//開始錄音並且保存錄音到sd卡
audioRecordFunc.startRecordAndFile();
這裡會發生一個很奇怪的現象,能聽寫的時候不能錄音,能錄音的時候不能聽寫,錄音的兩個類MediaRecorder、AudioRecord中MediaRecorder的start()方法中有這麼一句話:The apps should not start another recording session during recording.
(一個app不應該在錄音期間開啟另外一個錄音),原因我大致理解為麥克風被佔用了
既然不能一邊錄音一邊識別,那我們只好錄完音後再上傳去識別,嗯,回頭看了一下設定的引數
mIat.setParameter(SpeechConstant.AUDIO_SOURCE, "-2");
mIat.setParameter(SpeechConstant.ASR_SOURCE_PATH, ”要識別的音訊絕對路徑“);
設定好引數之後
mIat.startListening(mRecoListener);
這下可以了,儲存的音訊檔案可以識別,而且識別的速度和講話的時候是一樣的,也就是說,你在第5秒說的話,就是在第五秒開始識別(不考慮網路延遲的時候),這樣,就可以實現了我們的需求。然而,事情往往沒有那麼簡單,還是回到了原始的問題,也就是前後端點超時或者一分鐘過後的音訊不再識別!!!
有兩個解決方法:
1、自己重新寫一個類繼承科大訊飛裡面的類,重寫裡面的方法
2、通過流的方式去控制,假如檔案流讀取沒完成,音訊不再識別,那就重新啟用,直到檔案流讀取完畢
兩秒鐘之後我立馬放棄了第一種解決方案,原始碼是長這樣子的:那就只能採用第二種方案了,此時我們要進行相應的引數設定:
mIat.setParameter(SpeechConstant.AUDIO_SOURCE, "-1");
//開始進行語音聽寫
mIat.startListening(mRecoListener);
//然後在進行音訊流輸入
mIat.writeAudio(bytes, 0, bytes.length);
錄完音後,根據音訊檔案讀取流。程式碼我就不粘貼出來了,因為這個方法行不通,原因是因為流讀取的速度太快,幾分鐘的檔案一下子就讀取完了,端點超時問題還是會出現(ps:希望有個人告訴流能否控制其讀取速度,怎麼控制),而且就算能控制速度,假如使用者錄音一個小時的,你不可能又要花一個小時去識別音訊吧,這不現實!!
未完待續...