最近無意中發(fā)現(xiàn)有需要做語音播報插件的需求棍掐,因?yàn)榭紤]到語音助手集成會使得原應(yīng)用程序包體積變大粱侣,所以采取插件的形式去從外部下載券坞,然后安裝這樣一個插件即可完成語音合成(文字轉(zhuǎn)成語音)琅锻。主要用到的技術(shù)就是集成 百度語音合成SDK有序,AIDL跨進(jìn)程通信。 github下載地址
前戲
無圖無真相晕鹊,關(guān)鍵這并沒有什么絢麗的特效松却,哈哈 ~ 主要功能就是使用插件apk去朗讀主apk輸入的文字暴浦,兩個apk用AIDL跨進(jìn)程通信溅话。
集成百度語音合成SDK
這個集成也沒啥難度,大家可以取參考上面鏈接對應(yīng)的開發(fā)文檔去看一下歌焦。簡單說下幾個重要步驟:
1.下載語音合成SDK飞几,包含Sample里面,但是是Eclipse的工程独撇,所以這里還需要自己新建一個AS工程屑墨。
2.拷貝資源文件到自己項(xiàng)目里面,再把SO文件拷貝到libs目錄下纷铣,并把jar包add到項(xiàng)目里面卵史,還需要在gradle文件里面去把libs目錄加到項(xiàng)目里面,如下圖所示:
3.在AndroidManifest.xml文件配置需要的權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
官網(wǎng)上是吧Appid,Key都放到AndroidManifest.xml里面搜立,我這里想在代碼里面設(shè)置,等下看下代碼就知道以躯。
編寫AIDL通信
首先在main目錄下新建aidl目錄,并且新建一個aidl類啄踊,如圖所示:aidl類很簡單忧设,就是一個接口,因?yàn)槲覀兪且邮瘴淖秩缓笞x取的所以定義了一個方法:void speak(String text);
這個時候可以rebuild一把這個插件module畸冲,這樣會生成該接口對應(yīng)的java文件妆档,這個文件里面雖然不用我們自己去寫憎瘸,自動生成,但是我們要知道里面有哪些主要的方法谨垃。
好了 ,現(xiàn)在我們需要一個Service在后臺默默的等待著主app去調(diào)用它硼控,所以這個Service應(yīng)該具有可讀取傳過來文字的功能乘客,我們來看一下這個Service代碼:
import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.baidu.tts.auth.AuthInfo;
import com.baidu.tts.client.SpeechError;
import com.baidu.tts.client.SpeechSynthesizer;
import com.baidu.tts.client.SpeechSynthesizerListener;
import com.baidu.tts.client.TtsMode;
import com.wzh.aidl.IVoiceInterface;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* author:Administrator on 2017/3/10 16:49
* description:文件說明
* version:版本
*/
public class VoiceService extends Service implements SpeechSynthesizerListener {
private SpeechSynthesizer mSpeechSynthesizer;
private static final String TAG = "AIDLService";
private static final String SAMPLE_DIR_NAME = "baiduTTS";
private static final String SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female.dat";
private static final String SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male.dat";
private static final String TEXT_MODEL_NAME = "bd_etts_text.dat";
private static final String LICENSE_FILE_NAME = "temp_license";
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";
private String mSampleDirPath;
private static final int PRINT = 0;
private static final int UI_CHANGE_INPUT_TEXT_SELECTION = 1;
private static final int UI_CHANGE_SYNTHES_TEXT_SELECTION = 2;
@Override
public void onCreate() {
super.onCreate();
initialEnv();
initialTts();
}
private void initialEnv() {
if (mSampleDirPath == null) {
String sdcardPath = Environment.getExternalStorageDirectory().toString();
mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME;
}
makeDir(mSampleDirPath);
copyFromAssetsToSdcard(false, SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME);
copyFromAssetsToSdcard(false, SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_MALE_MODEL_NAME);
copyFromAssetsToSdcard(false, TEXT_MODEL_NAME, mSampleDirPath + "/" + TEXT_MODEL_NAME);
copyFromAssetsToSdcard(false, LICENSE_FILE_NAME, mSampleDirPath + "/" + LICENSE_FILE_NAME);
copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/"
+ ENGLISH_SPEECH_FEMALE_MODEL_NAME);
copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/"
+ ENGLISH_SPEECH_MALE_MODEL_NAME);
copyFromAssetsToSdcard(false, "english/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/"
+ ENGLISH_TEXT_MODEL_NAME);
}
private void makeDir(String dirPath) {
File file = new File(dirPath);
if (!file.exists()) {
file.mkdirs();
}
}
/**
* 將sample工程需要的資源文件拷貝到SD卡中使用(授權(quán)文件為臨時授權(quán)文件,請注冊正式授權(quán))
*
* @param isCover 是否覆蓋已存在的目標(biāo)文件
* @param source
* @param dest
*/
private void copyFromAssetsToSdcard(boolean isCover, String source, String dest) {
File file = new File(dest);
if (isCover || (!isCover && !file.exists())) {
InputStream is = null;
FileOutputStream fos = null;
try {
is = getResources().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)) >= 0) {
fos.write(buffer, 0, size);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void initialTts() {
this.mSpeechSynthesizer = SpeechSynthesizer.getInstance();
this.mSpeechSynthesizer.setContext(this);
this.mSpeechSynthesizer.setSpeechSynthesizerListener(this);
// 文本模型文件路徑 (離線引擎使用)
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/"
+ TEXT_MODEL_NAME);
// 聲學(xué)模型文件路徑 (離線引擎使用)
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/"
+ SPEECH_FEMALE_MODEL_NAME);
// 本地授權(quán)文件路徑,如未設(shè)置將使用默認(rèn)路徑.設(shè)置臨時授權(quán)文件路徑淀歇,LICENCE_FILE_NAME請?zhí)鎿Q成臨時授權(quán)文件的實(shí)際路徑易核,僅在使用臨時license文件時需要進(jìn)行設(shè)置,如果在[應(yīng)用管理]中開通了正式離線授權(quán)浪默,不需要設(shè)置該參數(shù)牡直,建議將該行代碼刪除(離線引擎)
// 如果合成結(jié)果出現(xiàn)臨時授權(quán)文件將要到期的提示缀匕,說明使用了臨時授權(quán)文件,請刪除臨時授權(quán)即可碰逸。
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_LICENCE_FILE, mSampleDirPath + "/"
+ LICENSE_FILE_NAME);
// 請?zhí)鎿Q為語音開發(fā)者平臺上注冊應(yīng)用得到的App ID (離線授權(quán))
this.mSpeechSynthesizer.setAppId("9378271"/*這里只是為了讓Demo運(yùn)行使用的APPID,請?zhí)鎿Q成自己的id乡小。*/);
// 請?zhí)鎿Q為語音開發(fā)者平臺注冊應(yīng)用得到的apikey和secretkey (在線授權(quán))
this.mSpeechSynthesizer.setApiKey("RC33pEHG3Kd3DOOrI2GMhHMC",
"e8d7636d58dc96322ab704aec5999744"/*這里只是為了讓Demo正常運(yùn)行使用APIKey,請?zhí)鎿Q成自己的APIKey*/);
// 發(fā)音人(在線引擎),可用參數(shù)為0,1,2,3饵史。满钟。。(服務(wù)器端會動態(tài)增加胳喷,各值含義參考文檔湃番,以文檔說明為準(zhǔn)。0--普通女聲吭露,1--普通男聲吠撮,2--特別男聲,3--情感男聲讲竿。泥兰。。)
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "6");
// 設(shè)置Mix模式的合成策略
this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_DEFAULT);
// 授權(quán)檢測接口(只是通過AuthInfo進(jìn)行檢驗(yàn)授權(quán)是否成功题禀。)
// AuthInfo接口用于測試開發(fā)者是否成功申請了在線或者離線授權(quán)鞋诗,如果測試授權(quán)成功了,可以刪除AuthInfo部分的代碼(該接口首次驗(yàn)證時比較耗時)迈嘹,不會影響正常使用(合成使用時SDK內(nèi)部會自動驗(yàn)證授權(quán))
AuthInfo authInfo = this.mSpeechSynthesizer.auth(TtsMode.MIX);
if (authInfo.isSuccess()) {
} else {
String errorMsg = authInfo.getTtsError().getDetailMessage();
}
// 初始化tts
mSpeechSynthesizer.initTts(TtsMode.MIX);
// 加載離線英文資源(提供離線英文合成功能)
int result =
mSpeechSynthesizer.loadEnglishModel(mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath
+ "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
}
IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
@Override
public void speak(String text) throws RemoteException {
int result = mSpeechSynthesizer.speak(text);
if (result < 0) {
Log.i(TAG, "failed");
}
}
};
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind() called");
return stub;
}
@Override
public boolean onUnbind(Intent intent) {
Log.i(TAG, "onUnbind() called");
this.mSpeechSynthesizer.release();
return true;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy() called");
}
/*
* @param arg0
*/
@Override
public void onSynthesizeStart(String utteranceId) {
}
/**
* 合成數(shù)據(jù)和進(jìn)度的回調(diào)接口削彬,分多次回調(diào)
*
* @param utteranceId
* @param data 合成的音頻數(shù)據(jù)。該音頻數(shù)據(jù)是采樣率為16K江锨,2字節(jié)精度吃警,單聲道的pcm數(shù)據(jù)。
* @param progress 文本按字符劃分的進(jìn)度啄育,比如:你好啊 進(jìn)度是0-3
*/
@Override
public void onSynthesizeDataArrived(String utteranceId, byte[] data, int progress) {
}
/**
* 合成正常結(jié)束酌心,每句合成正常結(jié)束都會回調(diào),如果過程中出錯挑豌,則回調(diào)onError安券,不再回調(diào)此接口
*
* @param utteranceId
*/
@Override
public void onSynthesizeFinish(String utteranceId) {
}
/**
* 播放開始,每句播放開始都會回調(diào)
*
* @param utteranceId
*/
@Override
public void onSpeechStart(String utteranceId) {
}
/**
* 播放進(jìn)度回調(diào)接口氓英,分多次回調(diào)
*
* @param utteranceId
* @param progress 文本按字符劃分的進(jìn)度侯勉,比如:你好啊 進(jìn)度是0-3
*/
@Override
public void onSpeechProgressChanged(String utteranceId, int progress) {
}
/**
* 播放正常結(jié)束,每句播放正常結(jié)束都會回調(diào)铝阐,如果過程中出錯址貌,則回調(diào)onError,不再回調(diào)此接口
*
* @param utteranceId
*/
@Override
public void onSpeechFinish(String utteranceId) {
}
/**
* 當(dāng)合成或者播放過程中出錯時回調(diào)此接口
*
* @param utteranceId
* @param error 包含錯誤碼和錯誤信息
*/
@Override
public void onError(String utteranceId, SpeechError error) {
}
}
這里部分代碼可以參考百度語音助手文檔,我們來看主要的一個地方:
IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
@Override
public void speak(String text) throws RemoteException {
int result = mSpeechSynthesizer.speak(text);
if (result < 0) {
Log.i(TAG, "failed");
}
}
};
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind() called");
return stub;
}
這里聲明的stub對象我們可以看到,在這個方法里面是實(shí)現(xiàn)了讀取文字的功能练对,因?yàn)樗羌蒊Binder 的遍蟋,所以可以直接返回。
好了螟凭,到現(xiàn)在基本這個插件已經(jīng)具備語音合成的功能了虚青,但是我不想要圖標(biāo)顯示在桌面,所以這里去掉圖標(biāo)螺男,并且注冊Service,這里的action我們要記住棒厘,因?yàn)榭蛻舳艘残枰玫剑?/p>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service android:name=".VoiceService" android:process=":remote">
<intent-filter>
<action android:name="android.intent.action.VoiceService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
</application>
到這里插件基本可以使用了,現(xiàn)在來新建一個客戶端程序下隧。
拷貝aidl包及aidl文件奢人,要保持和插件的aidl包名及文件一致。
新建一個布局一個按鈕一個EditText汪拥,我們要實(shí)現(xiàn)的就是輸入文字达传,插件去郎讀篙耗。
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import com.wzh.aidl.IVoiceInterface;
public class MainActivity extends AppCompatActivity {
private EditText input ;
private Button button ;
/**
* 創(chuàng)建遠(yuǎn)程服務(wù)
*/
private IVoiceInterface iVoiceInterface;
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iVoiceInterface = IVoiceInterface.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
iVoiceInterface = null ;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 綁定迫筑、啟動所謂的服務(wù)端 服務(wù)
*/
final Intent intent = new Intent();
intent.setAction("android.intent.action.VoiceService");
intent.setPackage("com.wzh.baiduvoice");
bindService(intent,conn, Service.BIND_AUTO_CREATE);
input = (EditText) findViewById(R.id.input);
button = (Button) findViewById(R.id.speak);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String text = input.getText().toString();
if (iVoiceInterface!=null){
try {
iVoiceInterface.speak(text);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
}
}
我們在創(chuàng)建Activity的時候去綁定插件的那個Service,新建一個ServiceConnection并獲取iVoiceInterface對象宗弯,當(dāng)點(diǎn)擊朗讀按鈕的時候調(diào)用插件Service的speak( )方法脯燃。
這里大家可以發(fā)現(xiàn)是顯示啟動Service的,因?yàn)锳ndroid 5.0之后google出于安全的角度禁止了隱式聲明Intent來啟動Service.也禁止使用Intent filter蒙保,隱式啟動會報錯辕棚,所以采用這種啟動方式。
如此一來我們輸入文字邓厕,就可以聽到插件給我們朗讀的聲音逝嚎!
其實(shí)這里我遇到一個問題,就是這個插件apk可能會被有的系統(tǒng)自帶的手機(jī)管家禁止關(guān)聯(lián)啟動详恼,我用的vivo x6splus測試就是被禁止了补君,目前還沒找到比較好的解決方案,希望有知道的同學(xué)告之~
這里其實(shí)只是AIDL最簡單的使用方式了昧互,如果有問題請指出挽铁,謝謝~