|
|
@@ -0,0 +1,316 @@ |
|
|
|
package com.aispeech.nativedemo.tts; |
|
|
|
|
|
|
|
import android.content.Context; |
|
|
|
import android.os.Environment; |
|
|
|
import android.os.Handler; |
|
|
|
import android.os.HandlerThread; |
|
|
|
import android.text.TextUtils; |
|
|
|
import android.util.Log; |
|
|
|
|
|
|
|
import com.aispeech.nativedemo.DuiApplication; |
|
|
|
import com.aispeech.nativedemo.asr.AsrManagerInterface; |
|
|
|
import com.aispeech.nativedemo.asr.observer.DuiMessageObserver; |
|
|
|
import com.aispeech.nativedemo.iat.AsrResult; |
|
|
|
import com.aispeech.nativedemo.log.Logger; |
|
|
|
import com.aispeech.nativedemo.network.ws.MessageUtils; |
|
|
|
import com.aispeech.nativedemo.voiceprint.VoicePrintManager; |
|
|
|
import com.google.gson.Gson; |
|
|
|
import com.iflytek.cloud.SpeechConstant; |
|
|
|
import com.shareopen.library.MVVM.application.AppContext; |
|
|
|
import com.shareopen.library.helper.LogUtils; |
|
|
|
import com.shareopen.library.helper.StringUtils; |
|
|
|
import com.youdao.ydasr.AsrListener; |
|
|
|
import com.youdao.ydasr.AsrManager; |
|
|
|
import com.youdao.ydasr.AsrParams; |
|
|
|
import com.youdao.ydasr.asrengine.model.AsrError; |
|
|
|
|
|
|
|
import org.java_websocket.WebSocket; |
|
|
|
import org.java_websocket.client.WebSocketClient; |
|
|
|
import org.java_websocket.enums.ReadyState; |
|
|
|
import org.java_websocket.handshake.ServerHandshake; |
|
|
|
import org.jetbrains.annotations.NotNull; |
|
|
|
|
|
|
|
import java.io.File; |
|
|
|
import java.io.FileOutputStream; |
|
|
|
import java.io.IOException; |
|
|
|
import java.io.OutputStream; |
|
|
|
import java.net.URI; |
|
|
|
import java.net.URL; |
|
|
|
import java.nio.charset.StandardCharsets; |
|
|
|
import java.text.SimpleDateFormat; |
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.Base64; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.LinkedHashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Locale; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Objects; |
|
|
|
import java.util.TimeZone; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue; |
|
|
|
|
|
|
|
import javax.crypto.Mac; |
|
|
|
import javax.crypto.spec.SecretKeySpec; |
|
|
|
|
|
|
|
import okhttp3.HttpUrl; |
|
|
|
|
|
|
|
public class XFTTSManager implements AsrManagerInterface { |
|
|
|
public static final String ASR_ERROR = "ASR_ERROR"; |
|
|
|
private static String TAG = XFTTSManager.class.getSimpleName(); |
|
|
|
private static XFTTSManager mInstance; |
|
|
|
private Context mContext; |
|
|
|
private StringBuffer buffer = new StringBuffer(); |
|
|
|
private HashMap<String, String> mIatResults = new LinkedHashMap<>(); |
|
|
|
private HashMap<String, String> mIatZXResults = new LinkedHashMap<>(); |
|
|
|
// 地址与鉴权信息 |
|
|
|
public static final String hostUrl = "https://tts-api.xfyun.cn/v2/tts"; |
|
|
|
// 均到控制台-语音合成页面获取 |
|
|
|
public static final String appid = "948cf4b6"; |
|
|
|
public static final String apiSecret = "ZDYyMjNmMTlkYTE0YWRmOWUwZTYxNjYz"; |
|
|
|
public static final String apiKey = "54f6e81f40a31d66d976496de895a7a4"; |
|
|
|
// 合成文本 |
|
|
|
public static final String TEXT = "欢迎来到讯飞开放平台"; |
|
|
|
// 合成文本编码格式 |
|
|
|
public static final String TTE = "UTF8"; // 小语种必须使用UNICODE编码作为值 |
|
|
|
// 发音人参数。到控制台-我的应用-语音合成-添加试用或购买发音人,添加后即显示该发音人参数值,若试用未添加的发音人会报错11200 |
|
|
|
public static final String VCN = "x4_xiaoling"; |
|
|
|
// 合成文件名称 |
|
|
|
public static final String OUTPUT_FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() |
|
|
|
+"/Android/data/com.aispeech.nativedemo/xftts_" + System.currentTimeMillis() + ".pcm"; |
|
|
|
// json |
|
|
|
public static final Gson gson = new Gson(); |
|
|
|
public static boolean wsCloseFlag = false; |
|
|
|
|
|
|
|
int ret = 0; // 函数调用返回值 |
|
|
|
String mTime; |
|
|
|
String mFileName; |
|
|
|
private ConcurrentHashMap<String, AsrResult> mTimeStrMap = new ConcurrentHashMap<>(); |
|
|
|
|
|
|
|
private ConcurrentLinkedQueue<String> mTimeList = new ConcurrentLinkedQueue(); |
|
|
|
private volatile boolean mIsAsring; |
|
|
|
private long start; |
|
|
|
|
|
|
|
private HandlerThread backgroundThread; |
|
|
|
private Handler backgroundHandler; |
|
|
|
private static long lastSendTime; |
|
|
|
private ConcurrentLinkedQueue<List<byte[]>> buffsList = new ConcurrentLinkedQueue<>(); |
|
|
|
private ConcurrentLinkedQueue<String> timeList = new ConcurrentLinkedQueue<>(); |
|
|
|
private String mLang; |
|
|
|
|
|
|
|
private final Runnable asrStartRunnable = new Runnable() { |
|
|
|
@Override |
|
|
|
public void run() { |
|
|
|
String time = timeList.poll(); |
|
|
|
mTime = time; |
|
|
|
mTimeList.offer(time); |
|
|
|
mTimeStrMap.put(String.valueOf(mTime), new AsrResult()); |
|
|
|
LogUtils.e("testtts", "start listen " + Thread.currentThread()); |
|
|
|
buffer.setLength(0); |
|
|
|
mIatResults.clear(); |
|
|
|
mIatZXResults.clear(); |
|
|
|
try { |
|
|
|
String wsUrl = getAuthUrl(hostUrl, apiKey, apiSecret).replace("https://", "wss://"); |
|
|
|
OutputStream outputStream = new FileOutputStream(OUTPUT_FILE_PATH); |
|
|
|
wsCloseFlag = false; |
|
|
|
websocketWork(wsUrl, outputStream, mTime); |
|
|
|
Log.e("testtts", "zx 开始生成TTS音频数据 " + Thread.currentThread()); |
|
|
|
} catch (Exception e) { |
|
|
|
Log.e("testtts", "zx " + e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
private XFTTSManager(Context context) { |
|
|
|
mContext = context; |
|
|
|
initIat(); |
|
|
|
} |
|
|
|
|
|
|
|
public static XFTTSManager getInstance() { |
|
|
|
if (mInstance == null) { |
|
|
|
synchronized (XFTTSManager.class) { |
|
|
|
if (mInstance == null) { |
|
|
|
mInstance = new XFTTSManager(DuiApplication.getContext()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return mInstance; |
|
|
|
} |
|
|
|
|
|
|
|
private void initIat(){ |
|
|
|
} |
|
|
|
|
|
|
|
public void startBackgroundThread() { |
|
|
|
if (backgroundThread == null) { |
|
|
|
backgroundThread = new HandlerThread("XFTTSManager"); |
|
|
|
backgroundThread.start(); |
|
|
|
backgroundHandler = new Handler(backgroundThread.getLooper()); |
|
|
|
backgroundThread.setPriority(10); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public void onDestroy() { |
|
|
|
if (backgroundThread != null) { |
|
|
|
backgroundThread.quitSafely(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public void startVadAsr(String text) { |
|
|
|
timeList.offer(text); |
|
|
|
backgroundHandler.post(asrStartRunnable); |
|
|
|
lastSendTime = 0; |
|
|
|
|
|
|
|
Log.e("testtts", "startVadAsr"); |
|
|
|
} |
|
|
|
|
|
|
|
public static void websocketWork(String wsUrl, OutputStream outputStream, String text) { |
|
|
|
try { |
|
|
|
URI uri = new URI(wsUrl); |
|
|
|
WebSocketClient webSocketClient = new WebSocketClient(uri) { |
|
|
|
@Override |
|
|
|
public void onOpen(ServerHandshake serverHandshake) { |
|
|
|
Log.e("testtts", "ws建立连接成功..."); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void onMessage(String text) { |
|
|
|
Log.e("testtts", "onMessage " + text); |
|
|
|
JsonParse myJsonParse = gson.fromJson(text, JsonParse.class); |
|
|
|
if (myJsonParse.code != 0) { |
|
|
|
Log.e("testtts", "发生错误,错误码为:" + myJsonParse.code); |
|
|
|
Log.e("testtts", "本次请求的sid为:" + myJsonParse.sid); |
|
|
|
} |
|
|
|
if (myJsonParse.data != null) { |
|
|
|
try { |
|
|
|
MessageUtils.sendTts(myJsonParse.data.audio); |
|
|
|
byte[] textBase64Decode = Base64.getDecoder().decode(myJsonParse.data.audio); |
|
|
|
outputStream.write(textBase64Decode); |
|
|
|
outputStream.flush(); |
|
|
|
} catch (Exception e) { |
|
|
|
e.printStackTrace(); |
|
|
|
Log.e("testtts", "Exception " + e.getMessage()); |
|
|
|
} |
|
|
|
if (myJsonParse.data.status == 2) { |
|
|
|
try { |
|
|
|
outputStream.close(); |
|
|
|
} catch (IOException e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} |
|
|
|
Log.e("testtts", "本次请求的sid==>" + myJsonParse.sid); |
|
|
|
Log.e("testtts", "合成成功,文件保存路径为==>" + OUTPUT_FILE_PATH); |
|
|
|
// 可以关闭连接,释放资源 |
|
|
|
wsCloseFlag = true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void onClose(int i, String s, boolean b) { |
|
|
|
Log.e("testtts", "ws链接已关闭,本次请求完成..."); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void onError(Exception e) { |
|
|
|
Log.e("testtts", "发生错误 " + e.getMessage()); |
|
|
|
} |
|
|
|
}; |
|
|
|
// 建立连接 |
|
|
|
webSocketClient.connect(); |
|
|
|
while (!webSocketClient.getReadyState().equals(ReadyState.OPEN)) { |
|
|
|
//System.out.println("正在连接..."); |
|
|
|
Thread.sleep(100); |
|
|
|
} |
|
|
|
MyThread webSocketThread = new MyThread(webSocketClient, text); |
|
|
|
webSocketThread.start(); |
|
|
|
} catch (Exception e) { |
|
|
|
System.out.println(e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 线程来发送音频与参数 |
|
|
|
static class MyThread extends Thread { |
|
|
|
WebSocketClient webSocketClient; |
|
|
|
String text; |
|
|
|
|
|
|
|
public MyThread(WebSocketClient webSocketClient, String text) { |
|
|
|
this.webSocketClient = webSocketClient; |
|
|
|
this.text = text; |
|
|
|
} |
|
|
|
|
|
|
|
public void run() { |
|
|
|
String requestJson;//请求参数json串 |
|
|
|
try { |
|
|
|
requestJson = "{\n" + |
|
|
|
" \"common\": {\n" + |
|
|
|
" \"app_id\": \"" + appid + "\"\n" + |
|
|
|
" },\n" + |
|
|
|
" \"business\": {\n" + |
|
|
|
" \"aue\": \"raw\",\n" + |
|
|
|
" \"tte\": \"" + TTE + "\",\n" + |
|
|
|
" \"ent\": \"intp65\",\n" + |
|
|
|
" \"vcn\": \"" + VCN + "\",\n" + |
|
|
|
" \"pitch\": 50,\n" + |
|
|
|
" \"speed\": 50\n" + |
|
|
|
" },\n" + |
|
|
|
" \"data\": {\n" + |
|
|
|
" \"status\": 2,\n" + |
|
|
|
" \"text\": \"" + Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)) + "\"\n" + |
|
|
|
//" \"text\": \"" + Base64.getEncoder().encodeToString(TEXT.getBytes("UTF-16LE")) + "\"\n" + |
|
|
|
" }\n" + |
|
|
|
"}"; |
|
|
|
webSocketClient.send(requestJson); |
|
|
|
// 等待服务端返回完毕后关闭 |
|
|
|
while (!wsCloseFlag) { |
|
|
|
Thread.sleep(200); |
|
|
|
} |
|
|
|
webSocketClient.close(); |
|
|
|
} catch (Exception e) { |
|
|
|
e.printStackTrace(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 鉴权方法 |
|
|
|
public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception { |
|
|
|
URL url = new URL(hostUrl); |
|
|
|
// 时间 |
|
|
|
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); |
|
|
|
format.setTimeZone(TimeZone.getTimeZone("GMT")); |
|
|
|
String date = format.format(new Date()); |
|
|
|
// 拼接 |
|
|
|
String preStr = "host: " + url.getHost() + "\n" + |
|
|
|
"date: " + date + "\n" + |
|
|
|
"GET " + url.getPath() + " HTTP/1.1"; |
|
|
|
//System.out.println(preStr); |
|
|
|
// SHA256加密 |
|
|
|
Mac mac = Mac.getInstance("hmacsha256"); |
|
|
|
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256"); |
|
|
|
mac.init(spec); |
|
|
|
byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); |
|
|
|
// Base64加密 |
|
|
|
String sha = Base64.getEncoder().encodeToString(hexDigits); |
|
|
|
// 拼接 |
|
|
|
String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha); |
|
|
|
// 拼接地址 |
|
|
|
HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder().// |
|
|
|
addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8))).// |
|
|
|
addQueryParameter("date", date).// |
|
|
|
addQueryParameter("host", url.getHost()).// |
|
|
|
build(); |
|
|
|
|
|
|
|
return httpUrl.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
//返回的json结果拆解 |
|
|
|
class JsonParse { |
|
|
|
int code; |
|
|
|
String sid; |
|
|
|
Data data; |
|
|
|
} |
|
|
|
|
|
|
|
class Data { |
|
|
|
int status; |
|
|
|
String audio; |
|
|
|
} |
|
|
|
} |