commit aea14e50f0a297e14108443a76d9b5a39551d905 Author: KeiferJu Date: Wed Aug 21 19:19:01 2019 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d22a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Created by .ignore support plugin (hsz.mobi) +### Cordova template +# gitignore template for the Cordova framework +# website: https://cordova.apache.org/ +# +# Recommended template: Node.gitignore + +# App platform binaries and built files +/platforms + +# Optional to ignore plugin Git clones +#/plugins + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + diff --git a/.idea/chineseTTS.iml b/.idea/chineseTTS.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/chineseTTS.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6d75720 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e768b5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..20384fb --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "chinesetts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "KeiferJu (myllcn.com)", + "license": "MIT" +} diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..77c03c3 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,40 @@ + + + chineseTTS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/ChineseTTS.java b/src/android/ChineseTTS.java new file mode 100644 index 0000000..c27637e --- /dev/null +++ b/src/android/ChineseTTS.java @@ -0,0 +1,111 @@ +package com.smartmapx.tts; + +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CallbackContext; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.Manifest; +import android.widget.Toast; + +import com.hjq.permissions.OnPermission; +import com.hjq.permissions.Permission; +import com.hjq.permissions.XXPermissions; + +import java.util.List; + +/** + * This class echoes a string called from JavaScript. + */ + +public class ChineseTTS extends CordovaPlugin { + + private Context context; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + + if (action.equals("init")) { + this.init(); + return true; + } + + + if (action.equals("speak")) { + String message = args.getString(0); + this.speak(message, callbackContext); + return true; + } + return false; + } + + + /** + * 初始化引擎 + * + * @param + */ + private void init() { + context = this.cordova.getActivity(); + permissionRequest(); + } + + /** + * 讲话 + * + * @param message + * @param callbackContext + */ + private void speak(String message, CallbackContext callbackContext) { + if (message != null && message.length() > 0) { + SpeechUtilOffline.getInstance(context).play(message, SpeechUtilOffline.PLAY_MODE.QUEUED); + } else { + callbackContext.error("信息为空."); + } + } + + + /** + * 运行时权限-- + */ + public void permissionRequest() { + // Manifest.permission.WRITE_EXTERNAL_STORAGE, +// Manifest.permission.ACCESS_FINE_LOCATION, +// Manifest.permission.READ_PHONE_STATE, +// Manifest.permission.RECEIVE_BOOT_COMPLETED + + if (XXPermissions.isHasPermission(context, Permission.WRITE_EXTERNAL_STORAGE) && XXPermissions.isHasPermission(context, Permission.ACCESS_FINE_LOCATION) && XXPermissions.isHasPermission(context, Permission.READ_PHONE_STATE)) { + + } else { + XXPermissions.with(this.cordova.getActivity()) + // 可设置被拒绝后继续申请,直到用户授权或者永久拒绝 + .constantRequest() + // 支持请求6.0悬浮窗权限8.0请求安装权限 + .permission(Permission.WRITE_EXTERNAL_STORAGE, Permission.ACCESS_FINE_LOCATION, Permission.READ_PHONE_STATE) + // 不指定权限则自动获取清单中的危险权限 +// .permission(Permission.Group.STORAGE, Permission.Group.CALENDAR) + .request(new OnPermission() { + + @Override + public void hasPermission(List granted, boolean isAll) { + if (isAll) { + Toast.makeText(context, "权限获取成功,正在初始化语音包", Toast.LENGTH_SHORT).show(); + SpeechUtilOffline.getInstance(context).play("语音包初始化完成", SpeechUtilOffline.PLAY_MODE.QUEUED); + } + } + + @Override + public void noPermission(List denied, boolean quick) { + Toast.makeText(context, "权限获取失败,语音包初始化失败", Toast.LENGTH_SHORT).show(); + } + }); + } + + + } + + +} diff --git a/src/android/FileUtils.java b/src/android/FileUtils.java new file mode 100755 index 0000000..f3ae623 --- /dev/null +++ b/src/android/FileUtils.java @@ -0,0 +1,152 @@ +package com.smartmapx.tts; + +import android.content.Context; +import android.content.res.AssetManager; +import android.os.Environment; +import android.util.Log; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 文件操作类 + * + * @author KeiferJu + * @date 2019/8/ + */ +public final class FileUtils { + private static final String LOGTAG ="/ing/tts"; + private static FileUtils fileUtils = new FileUtils(); + + private FileUtils() { + } + + public static FileUtils getInstance() { + return fileUtils; + } + + /** + * 获取SDCARD根路径 + * + * @return + */ + private static StringBuffer getRootDir() throws Exception { + return new StringBuffer().append(Environment + .getExternalStorageDirectory()); + } + + /** + * 获取存在SDCARD上文件的绝对路径 + * + * @param mContext + * @param folderName + */ + public static StringBuffer getExternalFileAbsoluteDir(Context mContext, + String folderName, String fileName) throws Exception { + StringBuffer stringBuffer = getRootDir().append(File.separator); + stringBuffer.append(getExternalFilesDir(mContext, folderName)); + if (fileName != null) { + if (0 == fileName.indexOf(File.separator)) { + stringBuffer.append(fileName); + } else { + stringBuffer.append(File.separator); + stringBuffer.append(fileName); + } + } + return stringBuffer; + } + + + /** + * 获取SDCARD上应用存储路径 + * + * @param mContext + * @param folderName + * @return + */ + private static StringBuffer getExternalFilesDir(Context mContext, + String folderName) throws Exception { + String packageName = mContext.getPackageName(); + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("Android").append(File.separator).append("data") + .append(File.separator).append(packageName); + if (folderName != null) { + if (0 == folderName.indexOf(File.separator)) { + stringBuffer.append(folderName); + } else { + stringBuffer.append(File.separator); + stringBuffer.append(folderName); + } + } + Log.d(LOGTAG, "FileUtils getExternalFilesDir " + + stringBuffer.toString()); + return stringBuffer; + } + + + /** + * 创建一个临时目录,用于复制临时文件,如assets目录下的离线资源文件 + * @param context + * @return + */ + + public static String createTmpDir(Context context) { + String sampleDir = "/ing/tts"; + String tmpDir = Environment.getExternalStorageDirectory().toString() + sampleDir; + if (!FileUtils.makeDir(tmpDir)) { + tmpDir = context.getExternalFilesDir(sampleDir).getAbsolutePath(); + if (!FileUtils.makeDir(sampleDir)) { + throw new RuntimeException("create model resources dir failed :" + tmpDir); + } + } + return tmpDir; + } + + public static boolean makeDir(String dirPath) { + File file = new File(dirPath); + if (!file.exists()) { + return file.mkdirs(); + } else { + return true; + } + } + + /** + * assets文件2 sdcard + * @param assets + * @param source + * @param dest + * @param isCover + * @throws IOException + */ + public static void copyFromAssets(AssetManager assets, String source, String dest, boolean isCover) throws IOException { + File file = new File(dest); + if (isCover || (!isCover && !file.exists())) { + InputStream is = null; + FileOutputStream fos = null; + try { + is = assets.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); + } + } finally { + if (fos != null) { + try { + fos.close(); + } finally { + if (is != null) { + is.close(); + } + } + } + } + } + } +} diff --git a/src/android/OfflineResource.java b/src/android/OfflineResource.java new file mode 100644 index 0000000..ceb18d0 --- /dev/null +++ b/src/android/OfflineResource.java @@ -0,0 +1,56 @@ +package com.smartmapx.tts; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; +import java.io.IOException; + +import static android.content.ContentValues.TAG; + +/** + * 离线文件 + * + * @author ing + * @date 2018/3/27 + */ +public class OfflineResource { + + + private AssetManager assets; + private String destPath; + + private String backFilename; + private String modelFilename; + + public OfflineResource(Context context) throws IOException { + this.assets = context.getAssets(); + this.destPath = FileUtils.createTmpDir(context); + setOfflineVoiceType(); + } + + public String getModelFilename() { + return modelFilename; + } + + public String getBackFilename() { + return backFilename; + } + + public void setOfflineVoiceType() throws IOException { + String back = "backend_lzl"; + String model = "frontend_model"; + backFilename = copyAssetsFile(back); + modelFilename = copyAssetsFile(model); + + } + + + private String copyAssetsFile(String sourceFilename) throws IOException { + String destFilename = destPath + "/" + sourceFilename; + FileUtils.copyFromAssets(assets, sourceFilename, destFilename, false); + Log.i(TAG, "Assets to sdcard successed:" + destFilename); + return destFilename; + } + + +} diff --git a/src/android/SpeechUtilOffline.java b/src/android/SpeechUtilOffline.java new file mode 100644 index 0000000..8a96a6d --- /dev/null +++ b/src/android/SpeechUtilOffline.java @@ -0,0 +1,207 @@ +package com.smartmapx.tts; + +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; + +import com.unisound.client.SpeechConstants; +import com.unisound.client.SpeechSynthesizer; +import com.unisound.client.SpeechSynthesizerListener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * 离线语音 + * + * @author KeiferJu + * @date 2019/8/15 + */ +public class SpeechUtilOffline { + public static final String appKey = "_appKey_"; + public static final String secret = "_secret_"; + private static SpeechUtilOffline instance; + private SpeechSynthesizer mTTSPlayer; + private boolean isSpeaking = false; + private List speechList = new ArrayList<>(); + private boolean released = false; + protected OfflineResource offlineResource; + + private SpeechUtilOffline(Context context) { + init(context); + released = false; + } + + public static SpeechUtilOffline getInstance(Context context) { + if (instance == null) { + instance = new SpeechUtilOffline(context); + } + return instance; + } + + + + + /** + * 初始化引擎 + * + */ + private void init(final Context context) { + try { + offlineResource = new OfflineResource(context); + } catch (IOException e) { + Log.e("ing","offlineResouce failed , error msg : "+e.getMessage()); + e.printStackTrace(); + } + // 初始化语音合成对象 + mTTSPlayer = new SpeechSynthesizer(context, appKey, secret); + // 设置本地合成 + mTTSPlayer.setOption(SpeechConstants.TTS_SERVICE_MODE, SpeechConstants.TTS_SERVICE_MODE_LOCAL); + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_PITCH, 50);//音调 + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_SPEED, 52);//语速 + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_VOLUME, 100);//音量 + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_STREAM_TYPE, AudioManager.STREAM_NOTIFICATION); + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONTEND_MODEL_PATH, offlineResource.getModelFilename()); + // 设置后端模型 + mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACKEND_MODEL_PATH, offlineResource.getBackFilename()); + // 设置回调监听 + mTTSPlayer.setTTSListener(new SpeechSynthesizerListener() { + + @Override + public void onEvent(int type) { + switch (type) { + case SpeechConstants.TTS_EVENT_INIT: + // 初始化成功回调 + break; + case SpeechConstants.TTS_EVENT_SYNTHESIZER_START: + // 开始合成回调 + break; + case SpeechConstants.TTS_EVENT_SYNTHESIZER_END: + // 合成结束回调 + break; + case SpeechConstants.TTS_EVENT_BUFFER_BEGIN: + // 开始缓存回调 + break; + case SpeechConstants.TTS_EVENT_BUFFER_READY: + // 缓存完毕回调 + break; + case SpeechConstants.TTS_EVENT_PLAYING_START: + // 开始播放回调 + break; + case SpeechConstants.TTS_EVENT_PLAYING_END: + // 播放完成回调 + break; + case SpeechConstants.TTS_EVENT_PAUSE: + // 暂停回调 + break; + case SpeechConstants.TTS_EVENT_RESUME: + // 恢复回调 + break; + case SpeechConstants.TTS_EVENT_STOP: + // 停止回调 + break; + case SpeechConstants.TTS_EVENT_RELEASE: + // 释放资源回调 + break; + default: + break; + } + + } + + @Override + public void onError(int type, String errorMSG) { + // 语音合成错误回调 + Log.e("ing","TTS onError __ type : "+ type +" errorMsg : " +errorMSG ); + } + }); + // 初始化合成引擎 + mTTSPlayer.init(""); + + } + + /** + * 停止播放 + * + */ + public void stop() { + mTTSPlayer.stop(); + } + + /** + * 播放 + * + */ + public void play(String content) { + playImmediately(content); + } + + public void play(String content, PLAY_MODE playMode) { + switch (playMode) { + case QUEUED: { + playQueued(content); + break; + } + case IMMEDIATELY: { + playImmediately(content); + break; + } + } + } + + private void updateSpeech() { + if (!isSpeaking) { + if (speechList.size() > 0) { + speak(speechList.remove(speechList.size() - 1).content); + } + } + } + + private void speak(String content) { + mTTSPlayer.playText(content); + } + + public void playQueued(String content) { + speechList.add(new SpeechItem(content, PLAY_MODE.QUEUED)); + updateSpeech(); + } + + public void playImmediately(String content) { + speak(content); + } + + /** + * 释放资源 + * + */ + public void release() { + // 主动释放离线引擎 + if (released) { + return; + } + if (mTTSPlayer != null) { + mTTSPlayer.stop(); + mTTSPlayer.release(SpeechConstants.TTS_RELEASE_ENGINE, null); + } + instance = null; + released = true; + } + + + public enum PLAY_MODE { + QUEUED, + IMMEDIATELY + } + + private class SpeechItem { + public String content; + public PLAY_MODE playMode; + + public SpeechItem(String content, PLAY_MODE mode) { + this.content = content; + this.playMode = mode; + } + } +} diff --git a/src/android/assets/backend_lzl b/src/android/assets/backend_lzl new file mode 100755 index 0000000..874345d Binary files /dev/null and b/src/android/assets/backend_lzl differ diff --git a/src/android/assets/frontend_model b/src/android/assets/frontend_model new file mode 100755 index 0000000..cd3766e Binary files /dev/null and b/src/android/assets/frontend_model differ diff --git a/src/android/libs/armeabi/libuscasr.so b/src/android/libs/armeabi/libuscasr.so new file mode 100755 index 0000000..9f31790 Binary files /dev/null and b/src/android/libs/armeabi/libuscasr.so differ diff --git a/src/android/libs/armeabi/libyzstts.so b/src/android/libs/armeabi/libyzstts.so new file mode 100755 index 0000000..564b395 Binary files /dev/null and b/src/android/libs/armeabi/libyzstts.so differ diff --git a/src/android/libs/usc.jar b/src/android/libs/usc.jar new file mode 100755 index 0000000..11fbfd4 Binary files /dev/null and b/src/android/libs/usc.jar differ diff --git a/www/chineseTTS.js b/www/chineseTTS.js new file mode 100644 index 0000000..31d8712 --- /dev/null +++ b/www/chineseTTS.js @@ -0,0 +1,9 @@ +var exec = require('cordova/exec'); + +exports.init = function (arg0, success, error) { + exec(success, error, 'ChineseTTS', 'init', [arg0]); +} + +exports.speak = function (arg0, success, error) { + exec(success, error, 'ChineseTTS', 'speak', [arg0]); +}