最近在做Android語音播報功能(TTS),現總結如下:(ps:demo代碼地址:https://github.com/giserlong/TTS_DEMO)
一.Android原生接口
用Android原生接口TextToSpeech,簡單易用,但是一般情況下不支持中文,需自己下載訊飛語音+ 等中文引擎,並設置為系統默認tts,方可正常播報中文,關鍵代碼如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_native); //初始化TTS tts = new TextToSpeech(this, this); //獲取控件 speechText = (EditText)findViewById(R.id.speechTextView); speechButton = (Button)findViewById(R.id.speechButton); //為button添加監聽 speechButton.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v){ // TODO Auto-generated method stub tts.speak(speechText.getText().toString(), TextToSpeech.QUEUE_FLUSH, null); } }); } @Override public void onInit(int status){ // 判斷是否轉化成功 if (status == TextToSpeech.SUCCESS){ //tts.getCurrentEngine(); //默認設定語言為中文,原生的android貌似不支持中文。 int result = tts.setLanguage(Locale.CHINA); if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED){ tts.setLanguage(Locale.US); Toast.makeText(this,"不支持中文,已自動設置為英文",Toast.LENGTH_SHORT).show(); Log.d("ss",""); }else{ Toast.makeText(this,"已自動設置為中文",Toast.LENGTH_SHORT).show(); Log.d("ss",""); } } }
二.百度離在線融合SDK
注冊百度智能雲開發者賬號后,添加語音合成應用,填寫包名等相關信息后,生成key及APPID等信息:
激活SDK需此關鍵信息,還需下載對應SDK,並添加至項目中,引用相關jar包,添加對應so庫至asset
關鍵代碼如下:
package com.yupont.www.myapplication; import android.content.Context; import android.os.Environment; import com.baidu.tts.client.SpeechSynthesizer; import com.baidu.tts.client.SpeechSynthesizerListener; import com.baidu.tts.client.TtsMode; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; /** * <p>文件描述:<p> * <p>作者:Mark<p> * <p>創建時間:2019/5/23<p> * <p>更改時間:2019/5/23<p> * <p>版本號:1<p> */ public class BaiDuSpeechUtil { private final String TAG = this.getClass().getSimpleName(); private SpeechSynthesizer mSpeechSynthesizer; private String mSampleDirPath; private static final String SAMPLE_DIR_NAME = "baiduTTS"; //-------以下全是在assets下的文件,使用離線時必須全部copy到手機中方可使用----start-- private static final String SPEECH_FEMALE_MODEL_NAME = "bd_etts_common_speech_f7_mand_eng_high_am-mix_v3.0.0_20170512.dat"; private static final String SPEECH_MALE_MODEL_NAME = "bd_etts_common_speech_m15_mand_eng_high_am-mix_v3.0.0_20170505.dat"; private static final String TEXT_MODEL_NAME = "bd_etts_text.dat"; private static final String ENGLISH_SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female_en.dat"; private static final String ENGLISH_SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male_en.dat"; private static final String ENGLISH_TEXT_MODEL_NAME = "bd_etts_text_en.dat"; //--------end------------------------------------------------------------- private static BaiDuSpeechUtil baiDuSpeechUtil = null; public static BaiDuSpeechUtil getInstance(){ if(baiDuSpeechUtil == null) { synchronized (BaiDuSpeechUtil.class) { if(baiDuSpeechUtil == null) { baiDuSpeechUtil = new BaiDuSpeechUtil(); } } } return baiDuSpeechUtil; } /** * 初始化百度語音資源 * @param context */ public void setInitialEnv(Context context) { initialEnv(context); } /** * 初始化百度語音播報相關 * @param context */ public void setInitialTts(Context context, SpeechSynthesizerListener speechSynthesizerListener){ initialTts(context,speechSynthesizerListener); } private void initialEnv(Context context) { // long start_time= System.currentTimeMillis(); if (mSampleDirPath == null) { String sdcardPath = Environment.getExternalStorageDirectory().toString(); mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME; } makeDir(mSampleDirPath); copyFromAssetsToSdcard(context,false, SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME); copyFromAssetsToSdcard(context,false, SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_MALE_MODEL_NAME); copyFromAssetsToSdcard(context,false, TEXT_MODEL_NAME, mSampleDirPath + "/" + TEXT_MODEL_NAME); copyFromAssetsToSdcard(context,false, "english/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME); copyFromAssetsToSdcard(context,false, "english/" + ENGLISH_SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + ENGLISH_SPEECH_MALE_MODEL_NAME); copyFromAssetsToSdcard(context,false, "english/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME); // Log.d(TAG,"initialEnv cost:"+ (System.currentTimeMillis()-start_time)); } private void makeDir(String dirPath) { File file = new File(dirPath); if (!file.exists()) { file.mkdirs(); } } /** * 將sample工程需要的資源文件拷貝到SD卡中使用(授權文件為臨時授權文件,請注冊正式授權) * 主要是在離線時候用到,只需執行一次即可,這里寫的不嚴謹,應該去判斷一下離線用的那些文件,sd卡是否存在,如果不存在,則copy,如果存在則無需在copy,可在子線程操作 * @param isCover 是否覆蓋已存在的目標文件 * @param source * @param dest */ private void copyFromAssetsToSdcard(Context context, boolean isCover, String source, String dest) { File file = new File(dest); if (isCover || (!isCover && !file.exists())) { InputStream is = null; FileOutputStream fos = null; try { is = context.getAssets().open(source); String path = dest; fos = new FileOutputStream(path); byte[] buffer = new byte[1024]; int size = 0; while ((size = is.read(buffer, 0, 1024)) != -1) { fos.write(buffer, 0, size); } fos.flush(); } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (Exception e) { e.printStackTrace(); } } if (fos != null) { try { is.close(); } catch (Exception e) { e.printStackTrace(); } } } } } //此方法可在子線程中操作,由於這個初始化過程比較費時,大概在1s左右,看項目需求而定,如果是進入界面就必須播放(伴隨UI改變的)的,在UI線程,如無其他特殊要求,放在子線程中即可 private void initialTts(Context context,SpeechSynthesizerListener speechSynthesizerListener) { // long start_time= System.currentTimeMillis(); mSpeechSynthesizer = SpeechSynthesizer.getInstance(); mSpeechSynthesizer.setContext(context); mSpeechSynthesizer.setSpeechSynthesizerListener(speechSynthesizerListener); mSpeechSynthesizer.setApiKey(Config.appKey_baidu, Config.secret_baidu); mSpeechSynthesizer.setAppId(Config.appID_baidu); // 文本模型文件路徑 (離線引擎使用) mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/" + TEXT_MODEL_NAME); mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME); // 本地授權文件路徑,如未設置將使用默認路徑.設置臨時授權文件路徑,LICENCE_FILE_NAME請替換成臨時授權文件的實際路徑,僅在使用臨時license文件時需要進行設置,如果在[應用管理]中開通了正式離線授權,不需要設置該參數,建議將該行代碼刪除(離線引擎) // 如果合成結果出現臨時授權文件將要到期的提示,說明使用了臨時授權文件,請刪除臨時授權即可。 // 發音人(在線引擎),可用參數為0,1,2,3。。。(服務器端會動態增加,各值含義參考文檔,以文檔說明為准。0--普通女聲,1--普通男聲,2--特別男聲,3--情感男聲。。。) mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0"); // 設置Mix模式的合成策略, //mix模式下,wifi使用在線合成,非wifi使用離線合成) mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_HIGH_SPEED_SYNTHESIZE_WIFI); // if(SystemUtil.isNetWorkConnected(getCurrentActivity())) { // // AuthInfo接口用於測試開發者是否成功申請了在線或者離線授權,如果測試授權成功了,可以刪除AuthInfo部分的代碼(該接口首次驗證時比較耗時),不會影響正常使用(合成使用時 // AuthInfo authInfo=this.mSpeechSynthesizer.auth(TtsMode.MIX); // // if (authInfo.isSuccess()){ // toPrint("auth success"); // }else{ // String errorMsg=authInfo.getTtsError().getDetailMessage(); // toPrint("auth failed errorMsg=" + errorMsg); // } // } // 初始化tts mSpeechSynthesizer.initTts(TtsMode.MIX); // 加載離線英文資源(提供離線英文合成功能) //int result = mSpeechSynthesizer.loadEnglishModel(mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + mSampleDirPath + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME); // Log.d(TAG,"initialTts cost:"+ (System.currentTimeMillis()-start_time)); int result = mSpeechSynthesizer.loadModel(mSampleDirPath + "/" + TEXT_MODEL_NAME, mSampleDirPath + mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME); if(result<0){ result++; } } /** * 播報的文字 * @param content */ public void speakText(String content) { try{ if(mSpeechSynthesizer != null) { int result = mSpeechSynthesizer.speak(content); if (result < 0) { // Log.d(TAG,"error,please look up error code in doc or URL:http://yuyin.baidu.com/docs/tts/122 "); } } }catch (Exception e) { e.printStackTrace(); } } /** * 暫停 */ public void pauseSpeechSynthesizer(){ if(mSpeechSynthesizer != null) { mSpeechSynthesizer.pause(); } } /** * 停止播放 */ public void stopSpeechSynthesizer(){ if(mSpeechSynthesizer != null) { mSpeechSynthesizer.stop(); } } /** * 接着停止后的地方播放 */ public void resumeSpeechSynthesizer(){ if(mSpeechSynthesizer != null) { mSpeechSynthesizer.resume(); } } /** * 釋放mSpeechSynthesizer,在使用完之后必須調用,確保下個界面使用的時候資源已經釋放掉了,否則下個界面將無法正常播放 */ public void releaseSpeechSynthesizer(){ if(mSpeechSynthesizer != null) { mSpeechSynthesizer.release(); } } public void setSpeechSynthesizerNull(){ if(mSpeechSynthesizer != null) { mSpeechSynthesizer = null; } } public void endSpeechSynthesizer(){ pauseSpeechSynthesizer(); stopSpeechSynthesizer(); releaseSpeechSynthesizer(); setSpeechSynthesizerNull(); } }
首次需聯網,自動下載授權文件,以后離線也能用
三.雲知聲離線SDK
同百度,注冊賬號后,下載sdk,做好引用與配置
gradle中配置:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
關鍵代碼:
package com.yupont.www.myapplication; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.unisound.client.SpeechConstants; import com.unisound.client.SpeechSynthesizer; import com.unisound.client.SpeechSynthesizerListener; public class yzsTTSOfflineActivity extends Activity { private static boolean TTS_PLAY_FLAGE = false; private EditText mTTSText; private TextView mTextViewTip; private TextView mTextViewStatus; private Button mTTSPlayBtn; private SpeechSynthesizer mTTSPlayer; private final String mFrontendModel= Environment.getExternalStorageDirectory().toString()+"/Yupont/UAV/OfflineTTSModels/frontend_model"; private final String mBackendModel = Environment.getExternalStorageDirectory().toString()+"/Yupont/UAV/OfflineTTSModels/backend_lzl"; // private final String mFrontendModel= getClass().getClassLoader().getResource("assets/OfflineTTSModels/frontend_model").getPath().substring(5); // private final String mBackendModel = getClass().getClassLoader().getResource("assets/OfflineTTSModels/backend_lzl").getPath(); // @Override public void onCreate(Bundle savedInstanceState) { // requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); super.onCreate(savedInstanceState); setContentView(R.layout.activity_yzs_offline_tts); copyFilesFassets(this,"OfflineTTSModels", Environment.getExternalStorageDirectory().toString()+"/Yupont/UAV/OfflineTTSModels"); //getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.status_bar_main); mTTSText = (EditText) findViewById(R.id.textViewResult); //mTextViewStatus = (TextView) findViewById(R.id.textViewStatus); //mTextViewTip = (TextView) findViewById(R.id.textViewTip); mTTSPlayBtn = (Button) findViewById(R.id.recognizer_btn); mTTSPlayBtn.setEnabled(false); mTTSPlayBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { TTSPlay(); } }); // 初始化本地TTS播報 initTts(); } /** * 從assets目錄中復制整個文件夾內容 * @param context Context 使用CopyFiles類的Activity * @param oldPath String 原文件路徑 如:/aa * @param newPath String 復制后路徑 如:xx:/bb/cc */ public void copyFilesFassets(Context context, String oldPath, String newPath) { try { String fileNames[] = context.getAssets().list(oldPath);//獲取assets目錄下的所有文件及目錄名 if (fileNames.length > 0) {//如果是目錄 File file = new File(newPath); file.mkdirs();//如果文件夾不存在,則遞歸 for (String fileName : fileNames) { copyFilesFassets(context,oldPath + "/" + fileName,newPath+"/"+fileName); } } else {//如果是文件 if(new File(newPath).exists()){ return; } InputStream is = context.getAssets().open(oldPath); FileOutputStream fos = new FileOutputStream(new File(newPath)); byte[] buffer = new byte[1024]; int byteCount=0; while((byteCount=is.read(buffer))!=-1) {//循環從輸入流讀取 buffer字節 fos.write(buffer, 0, byteCount);//將讀取的輸入流寫入到輸出流 } fos.flush();//刷新緩沖區 is.close(); fos.close(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); //如果捕捉到錯誤則通知UI線程 //MainActivity.handler.sendEmptyMessage(COPY_FALSE); } } /** * 初始化本地離線TTS */ private void initTts() { // 初始化語音合成對象 try { mTTSPlayer = new SpeechSynthesizer(this, Config.appKey, Config.secret); // 設置本地合成 mTTSPlayer.setOption(SpeechConstants.TTS_SERVICE_MODE, SpeechConstants.TTS_SERVICE_MODE_LOCAL); File _FrontendModelFile = new File(mFrontendModel); if (!_FrontendModelFile.exists()) { toastMessage("文件:" + mFrontendModel + "不存在,請將assets下相關文件拷貝到SD卡指定目錄!"); } File _BackendModelFile = new File(mBackendModel); if (!_BackendModelFile.exists()) { toastMessage("文件:" + mBackendModel + "不存在,請將assets下相關文件拷貝到SD卡指定目錄!"); } // 設置前端模型 mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONTEND_MODEL_PATH, mFrontendModel); // 設置后端模型 mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACKEND_MODEL_PATH, mBackendModel); // 設置回調監聽 mTTSPlayer.setTTSListener(new SpeechSynthesizerListener() { @Override public void onEvent(int type) { switch (type) { case SpeechConstants.TTS_EVENT_INIT: // 初始化成功回調 log_i("onInitFinish"); mTTSPlayBtn.setEnabled(true); break; case SpeechConstants.TTS_EVENT_SYNTHESIZER_START: // 開始合成回調 log_i("beginSynthesizer"); break; case SpeechConstants.TTS_EVENT_SYNTHESIZER_END: // 合成結束回調 log_i("endSynthesizer"); break; case SpeechConstants.TTS_EVENT_BUFFER_BEGIN: // 開始緩存回調 log_i("beginBuffer"); break; case SpeechConstants.TTS_EVENT_BUFFER_READY: // 緩存完畢回調 log_i("bufferReady"); break; case SpeechConstants.TTS_EVENT_PLAYING_START: // 開始播放回調 log_i("onPlayBegin"); break; case SpeechConstants.TTS_EVENT_PLAYING_END: // 播放完成回調 log_i("onPlayEnd"); setTTSButtonReady(); break; case SpeechConstants.TTS_EVENT_PAUSE: // 暫停回調 log_i("pause"); break; case SpeechConstants.TTS_EVENT_RESUME: // 恢復回調 log_i("resume"); break; case SpeechConstants.TTS_EVENT_STOP: // 停止回調 log_i("stop"); break; case SpeechConstants.TTS_EVENT_RELEASE: // 釋放資源回調 log_i("release"); break; default: break; } } @Override public void onError(int type, String errorMSG) { // 語音合成錯誤回調 log_i("onError"); toastMessage(errorMSG); setTTSButtonReady(); } }); // 初始化合成引擎 mTTSPlayer.init(""); } catch (Exception e) { e.printStackTrace(); } } private void TTSPlay() { if (!TTS_PLAY_FLAGE) { mTTSPlayer.playText(mTTSText.getText().toString()); setTTSButtonStop(); } else { mTTSPlayer.stop(); setTTSButtonReady(); } } private void setTTSButtonStop() { TTS_PLAY_FLAGE = true; mTTSPlayBtn.setText(R.string.stop_tts); } private void setTTSButtonReady() { mTTSPlayBtn.setText(R.string.start_tts); TTS_PLAY_FLAGE = false; } protected void setTipText(String tip) { mTextViewTip.setText(tip); } protected void setStatusText(String status) { mTextViewStatus.setText(getString(R.string.lable_status) + "(" + status + ")"); } @Override public void onPause() { super.onPause(); // 主動停止識別 if (mTTSPlayer != null) { mTTSPlayer.stop(); } } private void log_i(String log) { Log.i("demo", log); } @Override protected void onDestroy() { // 主動釋放離線引擎 if (mTTSPlayer != null) { mTTSPlayer.release(SpeechConstants.TTS_RELEASE_ENGINE, null); } super.onDestroy(); } private void toastMessage(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } }