前言:科大訊飛的新版離線語音合成,由於官網demo是kt語言開發的,咱也看不懂kt,搜遍了全網也沒看到一個java版的新版離線語音demo,現記錄下,留給有緣人蔘考!!!!!畢竟咱在這上面遇到了不少的坑。如果能留言指正,那就更好了。
官網註冊賬號---》實名認證---》點選語音合成---》離線語音合成(新版)---》android sdk下載
sdk:下載的sdk是和當前賬號繫結的,檔案上方有appkey,secret等等
安卓專案中設定以下許可權,在AndroidManifest.xml中
<!--連線網路許可權,用於執行雲端語音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--獲取手機錄音機使用許可權,聽寫、識別、語意理解需要用到此許可權 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--讀取網路資訊狀態 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--獲取當前wifi狀態 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允許程式改變網路連線狀態 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--讀取手機資訊許可權 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--讀取聯絡人許可權,上傳聯絡人需要用到此許可權 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外儲存寫許可權,構建語法需要用到此許可權 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外儲存讀許可權,構建語法需要用到此許可權 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--手機定位資訊,用來為語意等功能提供定位,提供更精準的服務-->
<!--定位資訊是敏感資訊,可通過Setting.setLocationEnable(false)關閉定位請求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用臉部辨識,還要新增:攝相頭許可權,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />
<!--設定許可權,用來記錄應用設定資訊 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
android:requestLegacyExternalStorage="true"
第三步:
獲取裝置外部儲存許可權,後續需要把發音人的音訊檔拷貝到裝置中
/**
* 檢視當前裝置是否有儲存許可權:
* 沒有:請求獲取許可權
* 有:複製當前專案assets下的xtts資料夾到裝置根目錄下(語音合成所必須的檔案)
* @param context
*/
private void requestStoragePermission(Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION);
}
}
/**
* 請求獲取儲存許可權
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "onRequestPermissionsResult: permission granted");
//再次判斷儲存許可權是否已授予
boolean permission = FileUtils.hasStoragePermission(getApplicationContext());
if (!permission) {
Toast.makeText(getApplicationContext(), "沒有儲存許可權,請重新獲取!", Toast.LENGTH_SHORT).show();
return;
}
// 應用具有儲存許可權
Log.i(TAG,"成功獲取儲存許可權!");
//判斷xtts檔案是否存在,不存在則複製,存在則忽略
FileUtils.createXttsDirAndCopyFile(getApplicationContext());
} else {
Log.i(TAG, "onRequestPermissionsResult: permission denied");
Toast.makeText(this, "You Denied Permission", Toast.LENGTH_SHORT).show();
}
}
}
拷貝五個發音人的資原始檔到當前裝置的xtts檔案目錄下。這個檔案在官方的demo檔案中:
package com.epean.store.utils;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import com.epean.store.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 訊飛語音合成檔案複製公共功能
* 以下五個檔案:
* e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf
* e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat
* e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf
* e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat
* ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf
*/
public class FileUtils {
private static final String TAG = "FileUtils";
// 獲取外部儲存路徑
public static String getExternalStoragePath() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
// 建立xtts目錄
public static void createDirectory(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists()) {
if (directory.mkdirs()) {
Log.d(TAG, "Directory created: " + directoryPath);
} else {
Log.e(TAG, "Failed to create directory: " + directoryPath);
}
} else {
Log.d(TAG, "Directory already exists: " + directoryPath);
}
}
// 判斷目錄是否為空
public static boolean isDirectoryEmpty(String directoryPath) {
File directory = new File(directoryPath);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
return files == null || files.length == 0;
}
return true;
}
// 遞迴複製檔案
public static void copyFiles(Context context, String sourceDir, String destinationDir) throws IOException {
AssetManager assetManager = context.getAssets();
String[] files = assetManager.list(sourceDir);
if (files != null && files.length > 0) {
createDirectory(destinationDir);
for (String fileName : files) {
String sourcePath = sourceDir + File.separator + fileName;
String destinationPath = destinationDir + File.separator + fileName;
if (assetManager.list(sourcePath).length > 0) {
// 如果是目錄,遞迴複製目錄
copyFiles(context, sourcePath, destinationPath);
} else {
// 如果是檔案,複製檔案
copyFile(context, sourcePath, destinationPath);
}
}
}
}
// 複製檔案
public static void copyFile(Context context, String sourcePath, String destinationPath) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getAssets().open(sourcePath);
outputStream = new FileOutputStream(destinationPath);
byte[] buffer = new byte[4096];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
Log.d(TAG, "File copied: " + destinationPath);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close input stream", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close output stream", e);
}
}
}
}
/**
* 建立訊飛語音合成所必須的目錄:xtts並複製音訊檔
* @param context
*/
public static void createXttsDirAndCopyFile(Context context){
// 獲取外部儲存路徑
String externalStoragePath = FileUtils.getExternalStoragePath();
String xttsFolderPath = externalStoragePath + File.separator + context.getString(R.string.dir);
// 建立xtts資料夾
FileUtils.createDirectory(xttsFolderPath);
// 判斷xtts資料夾是否為空
if (FileUtils.isDirectoryEmpty(xttsFolderPath)) {
// 複製assets目錄下的xtts資料夾中的所有檔案到外部儲存的xtts資料夾中
try {
FileUtils.copyFiles(context, context.getString(R.string.dir), xttsFolderPath);
} catch (IOException e) {
Log.e(TAG, "檔案複製失敗"+e.getMessage());
}
} else {
// xtts資料夾不為空
Log.d(TAG, "xtts folder is not empty. Skipping the operation.");
}
}
public static boolean hasStoragePermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permissionResult = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
return permissionResult == PackageManager.PERMISSION_GRANTED;
}
return true;
}
}
通用工具播放類,實現程式碼如下:
initSDK() :sdk整個專案只需要初始化一次
playAudio(String content, Context context):播放通用方法
package com.epean.store.utils;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import com.epean.store.R;
import com.iflytek.aikit.core.AiEvent;
import com.iflytek.aikit.core.AiHandle;
import com.iflytek.aikit.core.AiHelper;
import com.iflytek.aikit.core.AiListener;
import com.iflytek.aikit.core.AiRequest;
import com.iflytek.aikit.core.AiResponse;
import com.iflytek.aikit.core.AiText;
import com.iflytek.aikit.core.AuthListener;
import com.iflytek.aikit.core.ErrType;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.List;
public class AudioPlayByKeyUtils {
private static final String TAG = "AudioPlayByKeyUtils";
private static int sampleRateInHz = 16000;
private static int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private static int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
//語音合成檔案快取陣列
private static byte[] cacheArray;
private static AiHandle handle;
//播放元件
private static AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
//SDK初始化
public static void initSDK(Context context){
try {
//外部儲存絕對路徑
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 初始化引數構建
AiHelper.Params params = AiHelper.Params.builder()
.appId(context.getString(R.string.appId))
.apiKey(context.getString(R.string.apiKey))
.apiSecret(context.getString(R.string.apiSecret))
.workDir(externalStorageDirectory.getAbsolutePath() +File.separator+ context.getString(R.string.dir))//SDK工作路徑,這裡為絕對路徑
.authInterval(60*60*24) //授權更新間隔
.build();
// 初始化
AiHelper.getInst().init(context, params);
// 註冊SDK 初始化狀態監聽
AiHelper.getInst().registerListener(coreListener);
// 註冊能力結果監聽
AiHelper.getInst().registerListener(context.getString(R.string.enginID), aiRespListener);
}catch (Exception e){
Log.e(TAG,"語音合成初始化出現異常"+e.getMessage());
}
}
public static void playAudio(String content, Context context){
if (StringUtils.isEmpty(content)){
Log.e(TAG,"播報內容不能為空!");
return;
}
//已初始化則略過
initSDK(context);
//避免髒資料
cacheArray = null;
//音量及播報人等引數設定
AiRequest.Builder paramBuilder = audioParam();
handle = AiHelper.getInst().start(context.getString(R.string.enginID),paramBuilder.build(),null);
if (!handle.isSuccess()) {
Log.e(TAG, "ERROR::START | handle code:" + handle.getCode());
return;
}
//自定義文字引數
AiRequest.Builder dataBuilder = contentParame(content);
//開始合成,合成結果可通過回撥介面獲取
int ret = AiHelper.getInst().write(dataBuilder.build(), handle);
//ret 值為0 寫入成功;非0失敗
if (ret != 0) {
String error = "start write failed" + ret;
Log.e(TAG, error);
}
}
/**
* 封裝自定義文字引數
* @param content
* @return
*/
@NonNull
private static AiRequest.Builder contentParame(String content) {
AiRequest.Builder dataBuilder = AiRequest.builder();
//輸入文字資料
AiText textData = AiText
.get("text")
.data(content) //輸入文字
.valid();
dataBuilder.payload(textData);
return dataBuilder;
}
/**
* 音量及播報人等引數設定
*/
@NonNull
private static AiRequest.Builder audioParam() {
AiRequest.Builder paramBuilder = AiRequest.builder();
paramBuilder.param("vcn", "xiaoyan");
paramBuilder.param("language", 1);
paramBuilder.param("pitch", 50);
paramBuilder.param("volume", 50);
paramBuilder.param("speed", 50);
paramBuilder.param("reg", 0);
paramBuilder.param("rdn", 0);
paramBuilder.param("textEncoding", "UTF-8");
return paramBuilder;
}
/**
* SDK監聽回撥
*/
private static AuthListener coreListener = new AuthListener() {
@Override
public void onAuthStateChange(final ErrType type, final int code) {
Log.i(TAG,"core listener code:" + code);
switch (type) {
case AUTH:
Log.i(TAG,"SDK狀態:授權結果碼" + code);
break;
case HTTP:
Log.i(TAG,"SDK狀態:HTTP認證結果" + code);
break;
default:
Log.i(TAG,"SDK狀態:其他錯誤");
}
}
};
/**
*能力監聽回撥
*/
private static AiListener aiRespListener = new AiListener() {
//獲取合成結果,封裝到快取陣列中
@Override
public void onResult(int handleID, List<AiResponse> outputData, Object usrContext) {
if (outputData == null || outputData.isEmpty()) {
return;
}
if (null != outputData && outputData.size() > 0) {
for (int i = 0; i < outputData.size(); i++) {
byte[] bytes = outputData.get(i).getValue();
if (bytes == null) {
continue;
}else {
if (cacheArray == null) {
cacheArray = bytes;
} else {
byte[] resBytes = new byte[(cacheArray != null ? cacheArray.length : 0) + bytes.length];
if (cacheArray != null) {
System.arraycopy(cacheArray, 0, resBytes, 0, cacheArray.length);
}
System.arraycopy(bytes, 0, resBytes, cacheArray != null ? cacheArray.length : 0, bytes.length);
cacheArray = resBytes;
}
}
}
}
}
@Override
public void onEvent(int handleID, int event, List<AiResponse> eventData, Object usrContext){
if (event == AiEvent.EVENT_UNKNOWN.getValue()){
}
if (event == AiEvent.EVENT_START.getValue()){
}
if (event == AiEvent.EVENT_END.getValue()){
if (handle != null){
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
cacheArray = null;
}
if (event == AiEvent.EVENT_PROGRESS.getValue()){
if (cacheArray != null) {
audioTrack.write(cacheArray, 0, cacheArray.length);
audioTrack.play();
}
}
}
@Override
public void onError(int handleID, int err, String msg, Object usrContext){
if (handle != null){
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
}
};
/**
* 釋放資源
*/
public static void destory(){
// AiHelper.getInst().unInit();
cacheArray = null;
}
}