介绍
目前语音合成这方面做得最好的就是科大讯飞了,合成速度快,准确度高,模型多。但问题也是相当明显,只有在线合成是免费的,离线则是一笔不小的开销,个人到是可以申请免费的几个机器使用。百度tts则算是专门为他的导航做的一套吧,性能相对也还可以,但是他只支持离在线混合模式,默认是在wifi情况下是使用在线模式,4g或无网络情况下使用离线模式。云知声tts则可以实现完全的离线合成模式,当然,相比于上面的两个,性能可能没那没完美,不过对于一般的应用来说,完全足够了,唯一的缺点就是模型实在是太少了。
在线合成和离线合成(合成速度)
这个应该很好理解,在线合成必须将数据传到第三方平台,调用他们的服务接口进行合成,这中间牵扯到网络状况,在网络良好的情况下,合成速度和离线模式没有太大的差别,但是有时候服务器也会来大姨妈,比如我之前使用的是百度tts(理论上,百度tts的wifi在线模式下,合成时间超过0.5秒后会自动使用离线模式,但是并没有)就遇到过有一整天都延迟在2秒左右,后来放弃了。再也不相信在线模式了,你无法保证你的网络一直都是畅通无阻的。
问题
在线模式虽然不是很稳定,但是也有他的优点,不需要吧模型和合成底层代码放在本地,离线合成虽然稳定快速,但是apk体积增加的有点夸张。
云知声
云知声的解决办法:把语音合成模型放在服务器后端,你要使用的时候下载到本地。
1
注册云知声开发者,创建应用,下载离线语音合成sdk,里面要有东西就两个。
一个是libs
还有一个是assets
将libs里面的所有东西都拷贝到你的项目的对应的libs下(比如app目录下的libs)
在你的模块的 gradle的android括号下加上
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
assets文件夹里面的文件是合成模型文件,有两种处理方式。
第一种:将assets拷贝到你的项目的assets下,然后访问assets文件流,将assets里面的文件复制到本地(多此一举)。
第二种:直接将assets里面的文件复制到你手机的/unisound/tts(把我解压后直接发送到手机),如图
2
权限
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<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.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
3
新建你的BaseApplication, 在 AndroidManifest里面声明,然后在BaseApplication中初始化你的TTs,
TTSUtils.getInstance().init();
附录(云知声的ttsutils)
public class TTSUtils implements SpeechSynthesizerListener {
private static final String TAG = "TTSUtils";
private static volatile TTSUtils instance = null;
private boolean isInitSuccess = false;
private SpeechSynthesizer mTTSPlayer;
public static final String SAMPLE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + "/unisound/tts/";
public static final String FRONTEND_MODEL = "frontend_model";
public static final String BACKEND_MODEL = "backend_lzl";
private static final String APPKEY = "wiouzjcsmxvqes7ibqqm5bcgqrzrvddsvtceiwa5";//这里换成你的key和secret
private static final String SECRET = "3e20d7aff586ffe7dce62b302e7cc378";
private TTSUtils() {
}
public static TTSUtils getInstance() {
if (instance == null) {
synchronized (TTSUtils.class) {
if (instance == null) {
instance = new TTSUtils();
}
}
}
return instance;
}
public void init() {
try {
Context context = BaseApplication.getContext();
//判断文件是否完整
File _FrontendModelFile = new File(SAMPLE_DIR + FRONTEND_MODEL);
File _BackendModelFile = new File(SAMPLE_DIR + BACKEND_MODEL);
//校验文件的完整性
String file1 = getFileMD5(_FrontendModelFile);
String file2 = getFileMD5(_BackendModelFile);
// if(!file1.equals("27ce3b75c2784353e33840c5e63b5f0c")||!file2.equals("cfcdd50077ee5a6c5d673b728a8d6f5")){
if(!file1.equals("27ce3b75c2784353e33840c5e63b5f0c")||!file2.equals("57c9e96801d1173186407193e26a5ecf")){
_FrontendModelFile.delete();
_BackendModelFile.delete();
Toast.makeText(context, "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
return;
}
mTTSPlayer = new SpeechSynthesizer(context, APPKEY, SECRET);
mTTSPlayer.setOption(SpeechConstants.TTS_SERVICE_MODE, SpeechConstants.TTS_SERVICE_MODE_LOCAL); // 设置本地合成
File file = new File(SAMPLE_DIR);
if (!file.exists()) {
file.mkdirs();
}
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONTEND_MODEL_PATH, SAMPLE_DIR + FRONTEND_MODEL);// 设置前端模型
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACKEND_MODEL_PATH, SAMPLE_DIR + BACKEND_MODEL);// 设置后端模型
// setOption(int key, java.lang.Object value)
// 设置合成相关参数
// 示例:
// 设置合成语速 SpeechConstants.TTS_KEY_VOICE_SPEED 范围 0 ~ 100 int
// 设置合成音高 SpeechConstants.TTS_KEY_VOICE_PITCH 范围 0 ~ 100 int
// 设置合成音量 SpeechConstants.TTS_KEY_VOICE_VOLUME 范围 0 ~ 100 int
// 设置是否将英文按拼音读 SpeechConstants.TTS_KEY_IS_READ_ENLISH_IN_PINYIN 如:wang->王 boolean
// 设置语音结尾段的静音时长 SpeechConstants.TTS_KEY_BACK_SILENCE 0 ~ 1000 单位ms int
// 设置语音开始段的静音时长 SpeechConstants.TTS_KEY_FRONT_SILENCE 0 ~ 1000 单位ms int
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_SPEED,85);
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_PITCH,55);
// mTTSPlayer.setOption(SpeechConstants.TTS_KEY_VOICE_VOLUME,200);
//文字加数字,“玩啊”+106 106会读一百零六 “H”+106 会一个一个读出来 H 1 0 6
// mTTSPlayer.setOption(SpeechConstants.TTS_KEY_IS_READ_ENLISH_IN_PINYIN,false);
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_BACK_SILENCE,300);//设置尾音
// mTTSPlayer.setOption(SpeechConstants.TTS_KEY_FRONT_SILENCE,1000);//设置开始音
mTTSPlayer.setOption(SpeechConstants.TTS_KEY_PLAY_START_BUFFER_TIME,250);//语音开始缓冲时间
// mTTSPlayer.setOption(SpeechConstants.TTS_KEY_STREAM_TYPE, AudioManager.STREAM_SYSTEM);
mTTSPlayer.setTTSListener(this);// 设置回调监听
mTTSPlayer.init(null);// 初始化合成引擎
} catch (Exception e) {
// e.printStackTrace();
Toast.makeText(BaseApplication.getContext(), "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
return;
}
}
/**获取文件的md5码,判断文件的完整性*/
private static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
if (!file.exists()||!file.isFile()) {
// 不是文件,或者不存在
return "";
}
MessageDigest digest;
FileInputStream in;
byte buffer[] = new byte[1024];
int len;
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
in.close();
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(16);
}
/**一个字节一个字节读*/
public static void speckText(String msg){
TTSUtils.getInstance().speak(getS(msg));
}
/**语义识别朗读*/
public static void speeckTrueText(String msg){
if(msg==null||"".equals(msg)){
msg="";
}
if(msg.length()>25){
msg=msg.substring(0,15);
}
TTSUtils.getInstance().speak(msg);
}
public static String getS(String msg){
if(msg==null||"".equals(msg)){
return "";
}
char[] chars = msg.toCharArray();
StringBuilder sb=new StringBuilder();
for (char aChar : chars) {
sb.append(aChar+"\"");
}
return sb.toString();
}
public void speak(String msg) {
try {
if (isInitSuccess) {
mTTSPlayer.playText(msg);
}else {
init();
}
} catch (Exception e) {
Toast.makeText(BaseApplication.getContext(), "下载离线包后即可使用语音合成功能!", Toast.LENGTH_SHORT).show();
}
}
public void stop() {
mTTSPlayer.stop();
}
public void pause() {
mTTSPlayer.pause();
}
public void resume() {
mTTSPlayer.resume();
}
public void release() {
if (null != mTTSPlayer) {
// 释放离线引擎
mTTSPlayer.release(SpeechConstants.TTS_RELEASE_ENGINE, null);
}
}
@Override
public void onEvent(int type) {
switch (type) {
case SpeechConstants.TTS_EVENT_INIT:
isInitSuccess = true;
break;
case SpeechConstants.TTS_EVENT_SYNTHESIZER_START:
// 开始合成回调
Log.i(TAG, "beginSynthesizer");
break;
case SpeechConstants.TTS_EVENT_SYNTHESIZER_END:
// 合成结束回调
Log.i(TAG, "endSynthesizer");
break;
case SpeechConstants.TTS_EVENT_BUFFER_BEGIN:
// 开始缓存回调
Log.i(TAG, "beginBuffer");
break;
case SpeechConstants.TTS_EVENT_BUFFER_READY:
// 缓存完毕回调
Log.i(TAG, "bufferReady");
break;
case SpeechConstants.TTS_EVENT_PLAYING_START:
// 开始播放回调
Log.i(TAG, "onPlayBegin");
break;
case SpeechConstants.TTS_EVENT_PLAYING_END:
// 播放完成回调
Log.i(TAG, "onPlayEnd");
break;
case SpeechConstants.TTS_EVENT_PAUSE:
// 暂停回调
Log.i(TAG, "pause");
break;
case SpeechConstants.TTS_EVENT_RESUME:
// 恢复回调
Log.i(TAG, "resume");
break;
case SpeechConstants.TTS_EVENT_STOP:
// 停止回调
Log.i(TAG, "stop");
break;
case SpeechConstants.TTS_EVENT_RELEASE:
// 释放资源回调
Log.i(TAG, "release");
break;
default:
break;
}
}
@Override
public void onError(int type, String errorMSG) {
Log.e(TAG, "语音合成错误回调: " + errorMSG);
}
/**如果将assets里面的文件放在你自己的assets下,就需要用到下面的方法*/
// public static void copyAssetsFile2SDCard(Context context, String fileName, String path) {
// InputStream is=null;
// FileOutputStream fos=null;
// try {
// is= context.getAssets().open(fileName);
// fos= new FileOutputStream(new File(path));
// byte[] buffer = new byte[1024];
// int byteCount = 0;
// while ((byteCount = is.read(buffer)) != -1) {// 循环从输入流读取buffer字节
// fos.write(buffer, 0, byteCount);// 将读取的输入流写入到输出流
// }
// fos.flush();
// } catch (IOException e) {
// Log.e(TAG, "copyAssetsFile2SDCard: " + e.toString());
// } finally {
// if(fos!=null){
// try {
// fos.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// if(is!=null){
// try {
// is.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// }
}
4调用
TTSUtils.speeckTrueText(msg);
或者TTSUtils.speckText(msg);
后记
在集成的时候遇到过很多bug,比如模型文件放在不正确的地方会导致没有声音,模型文件不完整的时候回导致程序崩溃,发音不是预期的效果等等,还有一些参数的设置,具体参数还是得看官方的开发文档,看api就行了。demo暂时没有整理比较乱,有需要的可以留言。
关于科大讯飞
科大讯飞也有一种离线的玩法,但是得下载一个语记app,然后在手机语言输入法设置下tts服务为科大讯飞的,然后调用android自带的语音合成,就是中文了(听说这里也可以调用讯飞在线的api进行设置也可以,本人没试过)。