最近因为一些特殊需求,公司希望使用离线TTS,不花钱的中文离线TTS,最好音色还要多一点,语速,语调都要可控;总之一句话,以不花钱的代价达到最好的效果。其实现在很多的开发者平台也都提供一两种音色的离线TTS开发SDK,比如科大,云知声等等,我之前也做过一个百度在线ASR与云知声离线TTS结合实现的一个复读机的小Demo,有兴趣的朋友可以点击看一下
因为了解过Android有原生的TTS功能,于是就简单使用了一下,Google的产品不支持中文也很容易理解,毕竟市场都不给别人,而且中文的处理相较于英文等处理起来也比较麻烦,所以,原生的TTS是不支持中文的。但是里面是有预留的常量中文普通话,*话等,但是是设置不了的。所以在具体使用的时候需要借助中文TTS引擎的帮助,所以就需要安装其他的软件或者服务,网上推荐比较多的是科大讯飞+这个APK,里面可以设置发音人,语速,语调等。我简单看了一下,里面支持五种普通话音色;但是我们从科大官网上看上面只提供两种离线的音色语音;所以使用这个APK可以丰富一下你的离线TTS音色;从另一个方面来说,不知道有没有考虑过,如果我们能找到一个开源框架可以实现加载这些离线语音包,然后想办法获取到这些离线语音包,那么我们就可以多使用其另外的三种离线语音包。但是这样的开源框架估计不好找。其他还有度秘语音引擎,google的文字转语音引擎;可以点击下载,感谢这位博主。说的好像有点多了,下面进入正题;今天主要介绍一下Android原生的TTS接口的基本使用。
1,主要使用步骤
(1)新建一个类(内部类也是可以的),实现OnInitListener接口,重写onInit()方法,通常是判断TTS引擎初始化的状态
private class TTSListener implements OnInitListener { @Override public void onInit(int status) { // TODO Auto-generated method stub if (status == TextToSpeech.SUCCESS) { // int supported = mSpeech.setLanguage(Locale.US); // if ((supported != TextToSpeech.LANG_AVAILABLE) && (supported != TextToSpeech.LANG_COUNTRY_AVAILABLE)) { // Toast.makeText(MainActivity.this, "不支持当前语言!", Toast.LENGTH_SHORT).show(); // Log.i(TAG, "onInit: 支持当前选择语言"); // }else{ // // } Log.i(TAG, "onInit: TTS引擎初始化成功"); } else{ Log.i(TAG, "onInit: TTS引擎初始化失败"); } } }
(2)获取TTS引擎
mSpeech = new TextToSpeech(MainActivity.this, new TTSListener());
(3)在使用的时候,如果有需要可以调整TTS引擎参数,包括上面说的语速,语调,语言等等(当然,当前不支持中文,使用的话,先下载上面提到的服务或者应用并安装,然后在“设置”--》“语音与输入”--》“文本转语音输出”--》选择你安装中文TTS就可以了)
mSpeech.setLanguage(SharedData.languageList.get(choosedLanguage)); mSpeech.setSpeechRate(SharedData.voice_speed); mSpeech.setPitch(SharedData.voice_pitch);
2,示例代码:
示例代码很简单,我简单加了一点东西,也很好理解。
MainActivity.java代码:
package com.hfut.operationandroidtts; import android.app.AlertDialog; import android.speech.tts.TextToSpeech; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.EditText; import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech.OnInitListener; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Locale; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private TextToSpeech mSpeech = null; private EditText edit = null; private String choosedLanguage; private RadioButton english,chainese,german,french,*; private RadioGroup languageGroup; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initUI(); initLanguageList(); mSpeech = new TextToSpeech(MainActivity.this, new TTSListener()); // langs = getResources().getStringArray(R.array.languages); // 得到语言数组 // langSpinner = (Spinner) findViewById(R.id.spinner); // edit = (EditText) findViewById(R.id.edit); // btn = (Button) findViewById(R.id.btn); // btn.setOnClickListener(new BtnListener()); // // for (int i = 0; i < langs.length; i++) { // langList.add(langs[i]); // } // // 设置下拉框的适配器和样式 // langAdapter = new ArrayAdapter<String>(MainActivity.this, // android.R.layout.simple_spinner_item, langList); // langAdapter // .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // langSpinner.setAdapter(langAdapter); // // // 下拉框监听器 // langSpinner // .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { // @Override // public void onItemSelected(AdapterView<?> adapter, // View view, int position, long id) { // // TODO Auto-generated method stub // curLang = (String) langSpinner.getAdapter().getItem( // (int) id); // if(mSpeech != null) // { // mSpeech.stop(); // mSpeech.shutdown(); // mSpeech = null; // } // // 创建TTS对象 // mSpeech = new TextToSpeech(MainActivity.this, new TTSListener()); // Toast.makeText(MainActivity.this, "select = " + curLang, Toast.LENGTH_SHORT).show(); // } // // @Override // public void onNothingSelected(AdapterView<?> arg0) { // // TODO Auto-generated method stub // // } // }); // } // private int SetLanguage(String lang) { // int result = 0; // if (lang.equals("CANADA")) { // result = mSpeech.setLanguage(Locale.CANADA); // } else if (lang.equals("CANADA_FRENCH")) { // result = mSpeech.setLanguage(Locale.CANADA_FRENCH); // } else if (lang.equals("CHINA")) { // result = mSpeech.setLanguage(Locale.CHINA); // } else if (lang.equals("CHINESE")) { // result = mSpeech.setLanguage(Locale.CHINESE); // } else if (lang.equals("ENGLISH")) { // result = mSpeech.setLanguage(Locale.ENGLISH); // } else if (lang.equals("FRANCE")) { // result = mSpeech.setLanguage(Locale.FRANCE); // } else if (lang.equals("FRENCH")) { // result = mSpeech.setLanguage(Locale.FRENCH); // } else if (lang.equals("GERMAN")) { // result = mSpeech.setLanguage(Locale.GERMAN); // } else if (lang.equals("GERMANY")) { // result = mSpeech.setLanguage(Locale.GERMANY); // } else if (lang.equals("ITALIAN")) { // result = mSpeech.setLanguage(Locale.ITALIAN); // } else if (lang.equals("ITALY")) { // result = mSpeech.setLanguage(Locale.ITALY); // } else if (lang.equals("JAPAN")) { // result = mSpeech.setLanguage(Locale.JAPAN); // } else if (lang.equals("JAPANESE")) { // result = mSpeech.setLanguage(Locale.JAPANESE); // } else if (lang.equals("KOREA")) { // result = mSpeech.setLanguage(Locale.KOREA); // } else if (lang.equals("KOREAN")) { // result = mSpeech.setLanguage(Locale.KOREAN); // } else if (lang.equals("PRC")) { // result = mSpeech.setLanguage(Locale.PRC); // } else if (lang.equals("ROOT")) { // result = mSpeech.setLanguage(Locale.ROOT); // } else if (lang.equals("SIMPLIFIED_CHINESE")) { // result = mSpeech.setLanguage(Locale.SIMPLIFIED_CHINESE); // } else if (lang.equals("*")) { // result = mSpeech.setLanguage(Locale.*); // } else if (lang.equals("TRADITIONAL_CHINESE")) { // result = mSpeech.setLanguage(Locale.TRADITIONAL_CHINESE); // } else if (lang.equals("UK")) { // result = mSpeech.setLanguage(Locale.UK); // } else if (lang.equals("US")) { // result = mSpeech.setLanguage(Locale.US); // } // return result; // } // // private class TTSListener implements OnInitListener { // // @Override // public void onInit(int status) { // // TODO Auto-generated method stub // if (status == TextToSpeech.SUCCESS) { // //int result = mSpeech.setLanguage(Locale.ENGLISH); // int result = SetLanguage(curLang); // //如果打印为-2,说明不支持这种语言 // Toast.makeText(MainActivity.this, "-------------result = " + result, Toast.LENGTH_LONG).show(); // if (result == TextToSpeech.LANG_MISSING_DATA // || result == TextToSpeech.LANG_NOT_SUPPORTED) { // System.out.println("-------------not use"); // } else { // mSpeech.speak("i love you", TextToSpeech.QUEUE_FLUSH, null); // } // } // } // // } // // private class BtnListener implements OnClickListener { // // @Override // public void onClick(View v) { // // TODO Auto-generated method stub // mSpeech.speak(edit.getText().toString(), TextToSpeech.QUEUE_FLUSH, // null); // } // // } // // @Override // protected void onDestroy() { // // TODO Auto-generated method stub // if (mSpeech != null) { // mSpeech.stop(); // mSpeech.shutdown(); // mSpeech = null; // } // super.onDestroy(); // } // //} } private void initUI() { edit = findViewById(R.id.test_text); languageGroup=findViewById(R.id.language_Group); english = findViewById(R.id.language_English); chainese = findViewById(R.id.language_Chainese); german = findViewById(R.id.language_German); french = findViewById(R.id.language_French); *=findViewById(R.id.language_*); // View.OnClickListener handle = new View.OnClickListener(){ // public void onClick(View v){ // switch(v.getId()){ // case R.id.language_English: // choosedLanguage="英文"; // break; // case R.id.language_Chainese: // choosedLanguage="中文"; // break; // case R.id.language_German: // choosedLanguage="德语"; // break; // case R.id.language_French: // choosedLanguage="法语"; // break; // } // // } // }; // english.setOnClickListener(handle); // chainese.setOnClickListener(handle); // german.setOnClickListener(handle); // french.setOnClickListener(handle); } private void initLanguageList() { SharedData.languageList.put("英语",Locale.ENGLISH); SharedData.languageList.put("中文",Locale.CHINESE); SharedData.languageList.put("德语",Locale.GERMAN); SharedData.languageList.put("法语",Locale.FRENCH); SharedData.languageList.put("*话",Locale.*); } public void openAudioFile(View view) { choosedLanguage=getLanguage(languageGroup); int supported = mSpeech.setLanguage(SharedData.languageList.get(choosedLanguage)); mSpeech.setSpeechRate(SharedData.voice_speed); mSpeech.setPitch(SharedData.voice_pitch); Log.i(TAG, "选择语言: "+choosedLanguage+"--"+SharedData.languageList.get(choosedLanguage)); //mSpeech.setAudioAttributes(new AudioAttributes()); // mSpeech.setVoice(new Voice(null,Locale.US,Voice.QUALITY_HIGH,Voice.LATENCY_NORMAL,false,null)); if((supported != TextToSpeech.LANG_AVAILABLE) && (supported != TextToSpeech.LANG_COUNTRY_AVAILABLE)){ //语言设置失败 Log.i(TAG, "语言设置失败: "+choosedLanguage); } else{ Log.i(TAG, "语言设置成功: "+choosedLanguage); } String tempStr = edit.getText().toString(); mSpeech.speak(tempStr, TextToSpeech.QUEUE_FLUSH, null); Log.i(TAG, "测试文本: "+tempStr); Log.i(TAG, "当前语速: "+SharedData.voice_speed+", 最快语速1.5"); Log.i(TAG, "当前音调:"+SharedData.voice_pitch+", 最高音调2.0"); //Log.i(TAG, "test: 执行了"); } //保存音频文件 public void saveAudioFile(View view) { HashMap<String, String> myHashRender = new HashMap<>(); myHashRender.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, edit.getText().toString()); mSpeech.synthesizeToFile(edit.getText().toString(), myHashRender, "/mnt/sdcard/"+ new Date().toString().replace(" ","_").trim()+".wav"); Log.i(TAG, "saveAudioFile: "+"/mnt/sdcard/"+ new Date().toString().replace(" ","_").trim()+".wav"+"文件保存成功"); Toast.makeText(this,"文件保存成功",Toast.LENGTH_SHORT).show(); } private String getLanguage(RadioGroup languageGroup) { int choosedButtonID=languageGroup.getCheckedRadioButtonId(); String tempStr=""; if(choosedButtonID==english.getId()){ tempStr="英语"; } else if(choosedButtonID==chainese.getId()){ tempStr="中文"; } else if(choosedButtonID==german.getId()){ tempStr="德语"; } else if(choosedButtonID==french.getId()){ tempStr="法语"; } else if(choosedButtonID==*.getId()){ tempStr="*话"; } else{ } return tempStr; } //增加音量 public void increVoice(View view){ if(SharedData.voice_speed>=1.5f){ AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setMessage("速度已经最快,无法调整"); dialog.show(); } else{ SharedData.voice_speed+=0.1f; } } //减小音量 public void decreVoice(View view){ if(SharedData.voice_speed<=0.1f){ AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setMessage("速度已经最慢,无法调整"); dialog.show(); } else{ SharedData.voice_speed-=0.1f; } } //升高音调 public void increPitch(View view){ if(SharedData.voice_pitch>=2.0f){ AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setMessage("音调已经最高,无法调整"); dialog.show(); } else{ SharedData.voice_pitch+=0.1f; } } //减低音调 public void decrePitch(View view){ if(SharedData.voice_pitch<=0.1f){ AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setMessage("音调已经最低,无法调整"); dialog.show(); } else{ SharedData.voice_pitch-=0.1f; } } private class TTSListener implements OnInitListener { @Override public void onInit(int status) { // TODO Auto-generated method stub if (status == TextToSpeech.SUCCESS) { // int supported = mSpeech.setLanguage(Locale.US); // if ((supported != TextToSpeech.LANG_AVAILABLE) && (supported != TextToSpeech.LANG_COUNTRY_AVAILABLE)) { // Toast.makeText(MainActivity.this, "不支持当前语言!", Toast.LENGTH_SHORT).show(); // Log.i(TAG, "onInit: 支持当前选择语言"); // }else{ // // } Log.i(TAG, "onInit: TTS引擎初始化成功"); } else{ Log.i(TAG, "onInit: TTS引擎初始化失败"); } } } @Override protected void onDestroy() { // TODO Auto-generated method stub if (mSpeech != null) { mSpeech.stop(); mSpeech.shutdown(); mSpeech = null; } super.onDestroy(); } // private int SetLanguage(String lang) { // int result = 0; // if (lang.equals("CANADA")) { // result = mSpeech.setLanguage(Locale.CANADA); // } else if (lang.equals("CANADA_FRENCH")) { // result = mSpeech.setLanguage(Locale.CANADA_FRENCH); // } else if (lang.equals("CHINA")) { // result = mSpeech.setLanguage(Locale.CHINA); // } else if (lang.equals("CHINESE")) { // result = mSpeech.setLanguage(Locale.CHINESE); // } else if (lang.equals("ENGLISH")) { // result = mSpeech.setLanguage(Locale.ENGLISH); // } else if (lang.equals("FRANCE")) { // result = mSpeech.setLanguage(Locale.FRANCE); // } else if (lang.equals("FRENCH")) { // result = mSpeech.setLanguage(Locale.FRENCH); // } else if (lang.equals("GERMAN")) { // result = mSpeech.setLanguage(Locale.GERMAN); // } else if (lang.equals("GERMANY")) { // result = mSpeech.setLanguage(Locale.GERMANY); // } else if (lang.equals("ITALIAN")) { // result = mSpeech.setLanguage(Locale.ITALIAN); // } else if (lang.equals("ITALY")) { // result = mSpeech.setLanguage(Locale.ITALY); // } else if (lang.equals("JAPAN")) { // result = mSpeech.setLanguage(Locale.JAPAN); // } else if (lang.equals("JAPANESE")) { // result = mSpeech.setLanguage(Locale.JAPANESE); // } else if (lang.equals("KOREA")) { // result = mSpeech.setLanguage(Locale.KOREA); // } else if (lang.equals("KOREAN")) { // result = mSpeech.setLanguage(Locale.KOREAN); // } else if (lang.equals("PRC")) { // result = mSpeech.setLanguage(Locale.PRC); // } else if (lang.equals("ROOT")) { // result = mSpeech.setLanguage(Locale.ROOT); // } else if (lang.equals("SIMPLIFIED_CHINESE")) { // result = mSpeech.setLanguage(Locale.SIMPLIFIED_CHINESE); // } else if (lang.equals("*")) { // result = mSpeech.setLanguage(Locale.*); // } else if (lang.equals("TRADITIONAL_CHINESE")) { // result = mSpeech.setLanguage(Locale.TRADITIONAL_CHINESE); // } else if (lang.equals("UK")) { // result = mSpeech.setLanguage(Locale.UK); // } else if (lang.equals("US")) { // result = mSpeech.setLanguage(Locale.US); // } // return result; // } }
SharedData.java代码:
package com.hfut.operationandroidtts; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * author:why * created on: 2018/6/5 11:28 * description: */ public class SharedData { //语速 public static float voice_speed=0.5f; //音调 public static float voice_pitch=1.0f; // public static Map<String,Locale> languageList=new HashMap<String,Locale>(); }
activity_main.xml代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <EditText android:id="@+id/test_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:textSize="20dp" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="openAudioFile" android:text="播放合成语音" android:textSize="20dp" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:onClick="saveAudioFile" android:text="保存合成音频" android:textSize="20dp" /> <LinearLayout android:layout_marginLeft="160dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <RadioGroup android:id="@+id/language_Group" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom" android:gravity="bottom" android:orientation="vertical" android:paddingTop="2.0dip"> <RadioButton android:id="@+id/language_English" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true" android:text="英文" /> <RadioButton android:id="@+id/language_Chainese" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="中文" /> <RadioButton android:id="@+id/language_German" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="德语" /> <RadioButton android:id="@+id/language_French" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="法语" /> <RadioButton android:id="@+id/language_*" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="*话" /> </RadioGroup> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_marginLeft="80dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/voice_incre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="increVoice" android:text="加快语速" android:textSize="20dp" /> <Button android:id="@+id/voice_decre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="decreVoice" android:text="减慢语速" android:textSize="20dp" /> </LinearLayout> <LinearLayout android:layout_marginLeft="80dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/pitch_incre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="increPitch" android:text="升高音调" android:textSize="20dp" /> <Button android:id="@+id/pitch_decre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="decrePitch" android:text="降低音调" android:textSize="20dp" /> </LinearLayout> </LinearLayout> </LinearLayout> </LinearLayout>
主配置文件AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hfut.operationandroidtts"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
3,运行效果图
主界面
运行日志
保存的音频文件
注:
(1)这里我把语速,语调做了上限2.0,事实不是这样的
(2)这里没有展示具体中文TTS引擎配置,比较简单,上面说了步骤,只是没有附图
(3)很多其他好玩的东西,可以自己结合兴趣和API学习
(4)关于语音识别,语音合成,语义识别,包括上下文等等和语音相关以及人际交互相关的应用知识,这段时间看了不少,也写过一些博客,有兴趣的朋友可以参考一下。当然,我只是停留在应用层。