在最近的工作中,接触了许多关于MediaPlayer、SoundPool、TextToSpeach的使用,希望能够封装一个MediaPlayer、TextToSpeach功能兼具的library。由此篇博客开始,记录这个依赖库的创建过程。
为什么取这个名字?因为我觉得猪猪憨憨很可爱。
说明: 这算是个学习过程记录,已经写好的代码会随进度而有一定的改变,尤其是方法和变量的命名,在完成功能过程中不会很在意,等完成后会选择恰当方式重新命名,如果需要直接参考,请直接查看当前进度的最后结果
github地址
https://github.com/fytuuu/PigMediaStudio
后续文章链接:
SoundPool封装
https://blog.csdn.net/weixin_43093006/article/details/99355667
首先明确初期功能:
这几个功能的使用场景
首先,我们创建一个EmptyActivity的工程,将AndroidManifest.xml中的application删去。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fytu.pigmediastudio">
manifest>
接下来我们来到app下的build.gradle,将applicationId删去,这里我把它注释掉了
android {
compileSdkVersion 28
defaultConfig {
//applicationId "com.fytu.pigmediastudio"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
重要! 我们接下来要把app下的build.gradle最上一行改为:
apply plugin: 'com.android.library'
配置好了,我们Sync now。好的,我们将工程改为了library。接下来我们开始做我们的开发工作。
明确目标:
1.支持正常使用TextToSpeach的播报功能
2.将文字转语音(.wav)文件保存到本地
PigSpeachManager可以拥有多个textToSpeach实例,并且管理它们,之所以需要多个tts实例,是因为要符合多种场景:一般模式是flush和add 但是如果我这时候希望第二个音频加进来的时候不停止第一个音频的播放,就需要另外一个tts实例来进行播报。
我们现在的思路是:调用者可以让某个tts被后续文本 QUEUE_FLUSH 冲掉,也可以让某个tts被后续文本 QUEUE_ADD 衔接,也可以让它立即停止。同时,通过监听tts状态,Pig会帮助我们把List中闲置的tts清空。
我们将使用UtteranceProgressListener
来监听各个tts的状态,很多朋友使用它的时候发现它根本没有被调用,我们来看一下官方文档:
Listener for events relating to the progress of an utterance through the synthesis queue. Each utterance is associated with a call to
TextToSpeech#speak
orTextToSpeech#synthesizeToFile
with an associated utterance identifier, as perTextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID
. The callbacks specified in this method can be called from multiple threads.
它要被调用,就需要有一个ID,我们可以传入一个HashMap来给它ID,具体的我们代码里可以见到,这里不给例子了
首先创建 PigSpeechManager
类,用于创建和初始化
public class PigSpeechManager{
//管理类的实例
private static PigSpeechManager instance;
//创建一个ttsController来管理tts池
private static PigTTSController ttsController;
//创建一个上下文管理类,因为context不应该被static field持有
private static ContextManager contextManager;
private PigSpeechManager(){}
}
看一下ContextManager
,主要是存放 applicationContext
public class ContextManager {
private Context context;
//获得applicationContext
public ContextManager(Context context){
this.context = context.getApplicationContext();
}
public Context getApplicationContext(){
return context;
}
}
接下来我们就将ContextManager
引入到PigSpeechManager中
public class PigSpeechManager{
//...
private PigSpeechManager(){
//初始化
}
/**
* 为tts准备好applicationContext
* @param context
* @return
*/
public static PigSpeechManager in(Context context){
if (instance == null){
instance = new PigSpeechManager();
}
contextManager = new ContextManager(context);
return instance;
}
}
context配置好后,就可以准备搭载TTS实例了,我们通过PigTTSController来管理TTS池,这里我用HashMap来存放TTS实例,希望最快地找到这个TTS实例并对其进行后续操作。
public class PigTTSController {
private static final String TAG="PigTTSController";
private Context applicationContext;
private TextToSpeech tts;
public PigTTSController(Context context){
this.applicationContext = context;
}
public void loadTTS(final String txtForRead){
tts = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
}
});
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.i(TAG, "OnUtteranceProgressListener onstart...");
}
@Override
public void onDone(String utteranceId) {
//把返回值交出去,在map中找到这个tts,进行后续操作
Log.i(TAG, "OnUtteranceProgressListener onDown'...");
}
@Override
public void onError(String utteranceId) {
Log.i(TAG, "OnUtteranceProgressListener onError...");
}
});
}
}
我们希望通过onUtteranceProgressListener
就要在这个实例上添加utteranceID,添加方法就是通过HashMap
tts = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
Log.i(TAG, "a tts init success");
String ttsTag = "" + System.currentTimeMillis();
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, ttsTag);
tts.speak(txtForRead, TextToSpeech.QUEUE_FLUSH, map);
ttsMap.put(ttsTag, tts);
} else {
Log.i(TAG, " tts init failed");
}
isIniting = false;
}
});
为了防止还在初始化的过程中 全局tts实例就被用于创建新的实例,我们给个标签,判断是否还在初始化过程中,加完后的代码如下:
public class PigTTSController {
private static final String TAG="PigTTSController";
private HashMap<String,TextToSpeech> ttsMap = new HashMap<>();
private Context applicationContext;
private TextToSpeech tts;//可能会指向新的tts实例
private boolean isIniting = false;
public PigTTSController(Context context){
this.applicationContext = context;
}
public void loadTTS(final String txtForRead){
if (!isIniting) {
isIniting = true;
tts = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
Log.i(TAG, "a tts init success");
String ttsTag = "" + System.currentTimeMillis();
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, ttsTag);
tts.speak(txtForRead, TextToSpeech.QUEUE_FLUSH, map);
ttsMap.put(ttsTag, tts);
} else {
Log.i(TAG, " tts init failed");
}
isIniting = false;
}
});
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.i(TAG, "OnUtteranceProgressListener onstart...");
}
@Override
public void onDone(String utteranceId) {
//把返回值交出去,在map中找到这个tts,进行后续操作
Log.i(TAG, "OnUtteranceProgressListener onDown'...");
}
@Override
public void onError(String utteranceId) {
Log.i(TAG, "OnUtteranceProgressListener onError...");
}
});
}
}
private void onTTSFree(String utteranceId){
TextToSpeech freeTTS = ttsMap.get(utteranceId);
//进行后续操作
}
}
当然,通过onIniting来处理肯定是不好的,虽然init速度很快,但是短时间内需要频繁创建tts实例的话,有一部分就会被拒绝,那么我们这个时候可能想到:可以再给请求加一个队列,一个tts实例化完成以后再进行下一个tts实例化,并且是阻塞式获取创建tts实例的请求,这样就会防止某些tts播报丢失。
这样真的可以吗?这样不好。
第一,tts实例过多,第二,我们要意识到,我们创建的TTS实例的context属性是同一个,这意味着多个TTS用着同一个引擎,这会导致一个TTS把内容读完才读下一个TTS的内容,下一个TTS如果播放模式设置为QUEUE_FLUSH,那么它冲刷掉的应该是它自身的文本,并不是前一个TTS的文本,所以这个QUEUE_FLUSH可以说是没有作用的
所以我们只是用TTS进行播报的情况,我们只需要一个TTS实例,它的朗读任务列表是阻塞式的,它的播放效果是QUEUE_ADD。
它的功能有:
1.可以再朗读之前剔去朗读列表中使用者不想让它读的文本 (QUEUE_ADD升级版效果)
2.可以让它立刻停止当前任务,朗读下一个任务 (等同于QUEUE_FLUSH)
首先我们把PigTTSController进行修改,添加用于朗读的TTS实例ttsForRead
, 添加自定义阻塞队列PigBlockQueue
, 添加朗读任务初始化
public class PigTTSController {
private static final String TAG = "PigTTSController";
private PigTTSController instance;
private HashMap<String, TextToSpeech> ttsMap = new HashMap<>();
private Context applicationContext;
//用于朗读的tts实例
private TextToSpeech ttsForRead;
//当前朗读是否结束
private boolean isReadFinish = true;
//是否可以进行朗读
private boolean readyRead = false;
//管理播放线程,用于从队列中获取文本
private Thread thread_read;
//isValid作为标志,当tts释放的时候结束线程循环
private boolean isValid;
//用于read功能的队列
private PigBlockQueue<String> pigReadBlockQueue;
public PigTTSController(Context context){
this.applicationContext = context;
instance = this;
}
//初始化朗读功能
public void initRead() {
//如果初始化过就不用再初始化了
if (!readyRead) {
readyRead = true;
//设置没有队列放置上限
pigReadBlockQueue = new PigBlockQueue();
//实例化tts
ttsForRead = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
//获得状态
if (status == 0){
Log.d(TAG,"tts init Success");
}else{
Log.d(TAG,"tts init Failed");
}
//通过HashMap为其添加OnUtteranceProgressListener监听(要让该监听器能够被调用,需要为其设置UtteranceId,设置方法就是通过传入map)
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TTS_READ_FLAG);
isReadFinish = false;
ttsForRead.speak("", TextToSpeech.QUEUE_FLUSH, map);
}
});
//设置过程监听, 这里可以再onStart后设置一个TimeOut(有的时候系统自带TTS频繁使用会崩溃,这时候onStart正常,然而无法到达onDone,也没有onError)
ttsForRead.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter onstart");
}
@Override
public void onDone(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter ondone");
//设置当前tts朗读完毕
isReadFinish = true;
}
@Override
public void onError(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter onError!!!");
//设置当前tts朗读完毕
isReadFinish = true;
}
});
//线程可循环
isValid = true;
//关闭,或者重启tts的时候要把这个线程关闭,所以我们把引用该线程的对象放到成员变量中
thread_read = new Thread(new Runnable() {
@Override
public void run() {
while (isValid) {
while (isReadFinish) {//当它读完了,再给它安排任务
try {
String textForRead = pigReadBlockQueue.take();//阻塞式
readText(textForRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
//此处可以设置线程优先级,暂且不用设置
thread_read.start();
}
}
//让当前tts正在朗读的文本停止并丢弃,朗读readNow要求的文本,而后继续朗读队列中获取的文本
public void readNow(String txtForReadNow){
isReadFinish = false;
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TTS_READ_FLAG);
Log.d(TTS_READ_FLAG,"call readNow()");
ttsForRead.speak(txtForReadNow,TextToSpeech.QUEUE_FLUSH,map);
}
//在朗读管理线程thread_read中调用,用于朗读
private void readText(String txtForRead) {
isReadFinish = false;
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TTS_READ_FLAG);
Log.d(TTS_READ_FLAG,"call readText()");
ttsForRead.speak(txtForRead,TextToSpeech.QUEUE_FLUSH,map);
}
//往队列中load加载入要朗读的文本
public void loadReadTTS(final String strForRead){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
pigReadBlockQueue.put(strForRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.setPriority(5);
t.start();
}
}
我们再在PigSpeechManager
中准备一下调用方法,现在PigSpeechManager的完整代码如下
public class PigSpeechManager {
private static PigSpeechManager instance;
private static PigTTSController ttsController;
private static ContextManager contextManager;
private PigSpeechManager(){
//初始化
}
/**
* 为tts准备好applicationContext
* @param context
* @return
*/
public static PigSpeechManager in(Context context){
if (instance == null){
instance = new PigSpeechManager();
}
if (contextManager == null || context != contextManager.getApplicationContext() || contextManager.getApplicationContext() == null) {
contextManager = new ContextManager(context);
}
return instance;
}
//通过任务需求获得不同的操作,这里是获取Reader(也就是希望初始化并操作PigTTSController中的ttsForRead对象)
public PigTTSController getReader(){
if (ttsController == null){
ttsController = new PigTTSController(contextManager.getApplicationContext());
}
//初始化朗读器
ttsController.initRead();
return ttsController;
}
}
到此,经过测试,我们已经可以通过调用PigMediaStudio.in(getApplicationContext()).getReader().readNow("正在播放音频,正在测试中");
或者
PigMediaStudio.in(getApplicationContext()).getReader().loadReadTTS("正在播放音频,正在测试中");
来文字转语音播报了。
接下来我们就要为TTS(ttsForRead)设置播报属性,这里先只提供三种,播报速度,播报音高,播报语言。我们在PigTTSController
中添加以下内容
ublic PigTTSController setSpeed(float rate){
int result = ttsForRead.setSpeechRate(rate);
if (result == 0){
Log.d(TAG,"setSpeed success");
}else{
Log.d(TAG,"setSpeed failed");
}
return instance;
}
public PigTTSController setPitch(float pitch){
int result = ttsForRead.setPitch(pitch);
if (result == 0){
Log.d(TAG,"setPitch success");
}else{
Log.d(TAG,"setPitch failed");
}
return instance;
}
public PigTTSController setLanguage(Locale loc){
int result = ttsForRead.setLanguage(loc);
if (result == 0){
Log.d(TAG,"setPitch success");
}else{
Log.d(TAG,"setPitch failed");
}
return instance;
}
以后我们可以在初始化的时候进行设置属性
PigTTSController controller = PigMediaStudio.in(getApplicationContext()).getReader().setLanguage(Locale.CHINA).setSpeed(5.0f).setPitch(0.5f);
往后只需要调用controller.readNow();
或者controller.loadReadTTS();
就可以按此属性进行播报。
可以文字转语音播放了,接下来我们实现以下取消播放某个已添加朗读文本的方法。
通过什么方式除去朗读任务列表中的某一项比较好呢?我暂且把这个列表看成一个字符串,每个要朗读的文本是其中的一个字符。 我们删去字符串中的字符一般通过两个方式,一个就是下标索引,还有一个就是内容索引,我们就从这两个入手。
把PigBlockQueue
进行一个升级,我们不用泛型,因为都是文本只需要String,内容如下
public class PigBlockQueue {
private static final String TAG = "PigBlockQueue";
/**
* 队列中的容器,用来放队列的元素
*/
private final List<String> list = new ArrayList<>();
/**
* 队列的最大容量值
*/
private final int maxSize;
/**
* 队列当前的元素个数
*/
private int size = 0;
/**
* 锁
*/
private final Object object = new Object();
/**
* 对应tts是否结束
*/
private boolean isValid = true;
//有设置最大存放量的初始化
public PigBlockQueue(int maxSize) {
this.maxSize = maxSize;
}
//无限个:不需要设置最大存放量的初始化
public PigBlockQueue() {
maxSize = Integer.MAX_VALUE;
}
/**
* 插入一个元素到队列里,如果空间不足,则等待,直到有空间位置
*
* @param t the element to add
* @throws InterruptedException if interrupted while waiting
*/
public void put(String t) throws InterruptedException {
synchronized (object) {
Log.d(TAG,"put");
while (size == maxSize && isValid) {
object.wait();
}
list.add(t);
size++;
object.notify();
}
}
/**
* 移除队列中第一个元素,如果当前队列没有元素,则一直等待,直到有元素位置。
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
public String take() throws InterruptedException {
String t;
synchronized (object) {
Log.d(TAG,"take");
while (size == 0 && isValid) {
object.wait();
}
if (!isValid){
object.notify();
return "";
}
t = list.remove(0);
size--;
object.notify();
}
return t;
}
//删除List中符合s.equal(string)的对象
public void removeElementByEqual(String string) {
synchronized (object) {
Log.d(TAG, "remove by equal");
while (true) {
//list.remove()方法只能删除集合中第一个对应元素,如果返回false说明集合中没有该元素
if (!list.remove(string)){
break;
}else{
//成功删除
size--;
}
}
object.notify();
}
}
//见名知意
public void removeElementByContains(String string) {
synchronized (object) {
Log.d(TAG, "remove by contains");
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).contains(string)) {
list.remove(i);
size--;
}
}
object.notify();
}
}
//下标索引注意处理index的大小,防止越界
public void removeElementByIndex(int index) {
synchronized (object) {
Log.d(TAG, "remove by index");
if (list.size() == 0){
}else {
if (index >= list.size()) {
list.remove(list.size() - 1);
} else {
list.remove(index);
}
size--;
}
object.notify();
}
}
/**
* 倒数第一个 |countIndex| = 1
*下标索引注意处理index的大小,防止越界
* @param countIndex
*/
public void removeElementByCountIndex(int countIndex) {
synchronized (object) {
Log.d(TAG, "remove by countIndex");
if (list.size()==0){
}else {
if (countIndex < 0) {
list.remove(list.size() + countIndex);
} else if (countIndex > 0) {
list.remove(list.size() - countIndex);
} else {
list.remove(list.size() - 1);
}
size--;
}
object.notify();
}
}
public void removeAllElement() {
synchronized (object) {
Log.d(TAG, "remove all");
list.clear();
object.notify();
}
}
}
有了这些处理手段,我们在PigTTSController
中添加对应的调用方法,注意将线程等级设置得比loadReadTTS
更高
public void removeElementByContains(final String innerStr){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
pigReadBlockQueue.removeElementByContains(innerStr);
}
});
t.setPriority(3);
t.start();
}
public void removeElementByEqual(final String equalStr){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
pigReadBlockQueue.removeElementByEqual(equalStr);
}
});
t.setPriority(3);
t.start();
}
public void removeElementByIndex(final int index){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
pigReadBlockQueue.removeElementByIndex(index);
}
});
t.setPriority(3);
t.start();
}
public void removeElementByCountIndex(final int countIndex){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
pigReadBlockQueue.removeElementByCountIndex(countIndex);
}
});
t.setPriority(3);
t.start();
}
public void removeAllElement(){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
pigReadBlockQueue.removeAllElement();
}
});
t.setPriority(3);
t.start();
}
接下来我们就来处理一下release操作,其实主要只需要两步,stop 、 shutdown,此外我们还要对部分成员变量进行调整,这里我们想要停止线程,除了设置isValid 还要停止阻塞操作。
我们来看一下stop()
、shutdown()
这两个方法在TextToSpeech中的解释:
/**
* Releases the resources used by the TextToSpeech engine.
* It is good practice for instance to call this method in the onDestroy() method of an Activity
* so the TextToSpeech engine can be cleanly stopped.
*/
public void shutdown() {...}
/**
* Interrupts the current utterance (whether played or rendered to file) and discards other
* utterances in the queue.
*
* @return {@link #ERROR} or {@link #SUCCESS}.
*/
public int stop() {...}
按它所说,stop会中断utterance,而shutdown是释放资源,并且推荐在退出应用时候使用
所以我们的在PigTTSController
中release()
方法如下,同时补上暂停播报的方法
/**
* 在onDestroy等地方调用,释放资源
*/
public void release(){
if(null != ttsForRead){
//停止播报
ttsForRead.stop();
//关闭资源
ttsForRead.shutdown();
//结束线程循环
isValid = false;
//结束block
pigReadBlockQueue.release();
pigReadBlockQueue = null;
}
}
/**
* 暂停播放,这里不能简单的stop
*/
public void stop(){
if (ttsForRead != null){
}
}
现在我回顾了一下代码,发现有点乱,重新整理了一下,把PigReader(用于朗读任务)和PigTranslater(Text to Wave file)单独提出来
首先是PigReader部分(篇幅太长,与前文相同的代码我就不贴了):
public class PigReader{
private static final String TAG = "PigTTSController";
private static final String TTS_READ_FLAG = "TtsForReadUtterance";
private Context applicationContext;
private TextToSpeech ttsForRead;
private boolean isReadFinish = true;
private boolean readyRead = false;
private Thread thread_read;
private boolean isValid = true;
//朗读队列
private PigBlockQueue<String> pigReadBlockQueue;
private PigReader pigReader;
public PigReader (Context context){
this.applicationContext = context;
pigReader = this;
initRead();
}
private void initRead() {
if (!readyRead) {
readyRead = true;
pigReadBlockQueue = new PigBlockQueue<>();//无限长
ttsForRead = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == 0){
Log.d(TAG,"tts init Success");
}else{
Log.d(TAG,"tts init Failed");
}
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TTS_READ_FLAG);
isReadFinish = false;
ttsForRead.speak("", TextToSpeech.QUEUE_FLUSH, map);
}
});
ttsForRead.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter onstart");
}
@Override
public void onDone(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter ondone");
isReadFinish = true;
}
@Override
public void onError(String utteranceId) {
Log.d(TTS_READ_FLAG,"utter onError!!!");
isReadFinish = true;
}
@Override
public void onError(String utteranceId,int errorCode) {
Log.d(TTS_READ_FLAG,"a ttsForWav onError with utterance Id"+utteranceId+" and errorcode is"+errorCode);
onError(utteranceId);
}
});
isValid = true;
thread_read = new Thread(new Runnable() {
@Override
public void run() {
while (isValid) {
while (isReadFinish) {//当它读完了,再给它安排任务
try {
String textForRead = pigReadBlockQueue.take();//阻塞式
readText(textForRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
thread_read.start();
}
}
public void removeElementByContains(final String innerStr){... }
public void removeElementByEqual(final String equalStr){...}
public void removeElementByIndex(final int index){...}
public void removeElementByCountIndex(final int countIndex){...}
public void removeAllElement(){...}
public void readNow(String txtForReadNow){... }
private void readText(String txtForRead) {... }
public void loadReadTTS(final String strForRead){... }
public PigReader setSpeed(float rate){... }
public PigReader setPitch(float pitch){... }
public PigReader setLanguage(Locale loc){... }
/**
* 在onDestroy等地方调用,释放资源
*/
public void release(){... }
/**
* 暂停播放
*/
public void stop(){...}
}
然后是PigTranslater部分:
public class PigTranslater{
private PigTranslater pigTranslater;
private Context applicationContext;
//下载中的tts,最大只能是复用池中的数量,防止创建太多tts对象!
private HashMap<String, TextToSpeech> ttsMap = new HashMap<>();
//复用池
private List<String> freePool = new ArrayList<>();
//转换wav文件队列
private PigBlockQueue<PigTextForWav> pigFileBlockQueue = new PigBlockQueue<>();//无限长;
//复用池大小
private int freePoolMaxSize = 3;
private TextToSpeech ttsForWav;
private boolean isTTWbegin = false;
private boolean isTtsProduceSuccess = true;
private boolean isValid2 = true;
/** 管理转换的线程 */
private Thread thread_ttw;
/** 文件存储的文件夹(这里后面要优化,暂时只允许一层) */
private String parentPath;
private boolean isDenied = true;
public interface OnTranslateProgressListener {
void onStart();
void onComplete(String text,String fileAbsolutePath);
void onError(String error);
}
private OnTranslateProgressListener onTranslateProgressListener;
public PigTranslater setOnTranslateProgressListener(OnTranslateProgressListener onTranslateProgressListener) {
this.onTranslateProgressListener = onTranslateProgressListener;
return pigTranslater;
}
public PigTranslater (Context context) {
this.applicationContext = context;
pigTranslater = this;
initTextToWav();
}
public PigTranslater setMaxPoolSize(int maxPoolSize){
if (maxPoolSize < 1) maxPoolSize = 1;
this.freePoolMaxSize = maxPoolSize;
return pigTranslater;
}
//判断是否有权限
private boolean isWriteDenied(){
int permission = ContextCompat.checkSelfPermission(applicationContext,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
if (onTranslateProgressListener!=null){
onTranslateProgressListener.onError(PERMISSION_ERROR);
}
isDenied = true;
}else{
isDenied = false;
}
return isDenied;
}
//未来可支持多层文件夹(其实就是连续从头创建到尾)
//设置文件路径,判断是否加上斜线
public PigTranslater setParentPath(String parentFileDir) {
if (isWriteDenied()){
Log.w(TTS_TTW_FLAG,"write permission denied");
}else {
if (!parentFileDir.endsWith(File.separator)) {
parentFileDir = parentFileDir + File.separator;
}
if (!parentFileDir.startsWith(File.separator)) {
parentFileDir = File.separator + parentFileDir;
}
File dir;
if (!(dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + parentFileDir)).exists()) {
boolean isCreated = dir.mkdir();
if (!isCreated) {
Log.d(TTS_TTW_FLAG, "文件夹创建失败");
} else {
Log.d(TTS_TTW_FLAG, "文件夹创建成功");
//写在判断里面,如果创建,那么修改
parentPath = parentFileDir;
}
}
}
return pigTranslater;
}
private void initTextToWav(){
if (isWriteDenied()){
Log.w(TTS_TTW_FLAG,"write permission denied");
}else {
if (!isTTWbegin) {
isTTWbegin = true;
thread_ttw = new Thread(new Runnable() {
@Override
public void run() {
while (isValid2) {
if (freePool.size() > 0) {
//先判断是否有可用的
TextToSpeech ttsForWav = ttsMap.get(freePool.get(0));
if (ttsForWav == null) {
} else {
HashMap<String, String> map = new HashMap<>();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, freePool.get(0));
try {
final PigTextForWav pigTTW = pigFileBlockQueue.take();
if (wavSpeed != 0f)ttsForWav.setSpeechRate(wavSpeed);
if (wavPitch!= 0f)ttsForWav.setPitch(wavPitch);
if (loc != Locale.CHINA)ttsForWav.setLanguage(loc);
ttsForWav.synthesizeToFile(pigTTW.getText(),
map,
Environment.getExternalStorageDirectory().getAbsolutePath() + parentPath + pigTTW.getFileName());
ttsForWav.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.d(TTS_TTW_FLAG,"a ttsForWav started with utterance Id"+utteranceId);
if (null != onTranslateProgressListener){
onTranslateProgressListener.onStart();
}
}
@Override
public void onDone(String utteranceId) {
Log.d(TTS_TTW_FLAG,"a ttsForWav onDone with utterance Id"+utteranceId);
onTtsForWavFree(utteranceId);
if (null != onTranslateProgressListener){
onTranslateProgressListener.onComplete(pigTTW.getText(),Environment.getExternalStorageDirectory().getAbsolutePath()+parentPath+pigTTW.getFileName());
}
}
@Override
public void onError(String utteranceId) {
}
@Override
public void onError(String utteranceId,int errorCode) {
Log.d(TTS_TTW_FLAG,"a ttsForWav onError with utterance Id"+utteranceId+" and errorcode is"+errorCode);
onError(utteranceId);
}
});
freePool.remove(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else if (ttsMap.size() < freePoolMaxSize) {
//如果没有可用的,已存在的tts又不足3个,直接创建一个新的tts
if (isTtsProduceSuccess) {
try {
startTextToWave(pigFileBlockQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
//有3个都在工作中,等待它们空闲
}
}
}
});
thread_ttw.start();
}
}
}
/**
* 添加任务
*/
public PigTranslater putTFW(final String fileName,final String textForWav){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
if (fileName.endsWith(".wav")){
pigFileBlockQueue.put(new PigTextForWav(fileName+".wav",textForWav));
}else{
pigFileBlockQueue.put(new PigTextForWav(fileName+".wav",textForWav));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.setPriority(5);
t.start();
return pigTranslater;
}
private void startTextToWave(final PigTextForWav pigTTW){
isTtsProduceSuccess = false;
ttsForWav = new TextToSpeech(applicationContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == 0){
Log.d(TAG,"tts init Success");
}else{
Log.d(TAG,"tts init Failed");
}
HashMap<String, String> map = new HashMap<>();
String utteranceId = "UtteranceId"+System.currentTimeMillis();
map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
if (wavSpeed != 0f)ttsForWav.setSpeechRate(wavSpeed);
if (wavPitch!= 0f)ttsForWav.setPitch(wavPitch);
if (loc != Locale.CHINA)ttsForWav.setLanguage(loc);
ttsForWav.synthesizeToFile(pigTTW.getText(),
map,
Environment.getExternalStorageDirectory().getAbsolutePath()+parentPath+pigTTW.getFileName());
ttsMap.put(utteranceId,ttsForWav);
ttsForWav.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Log.d(TTS_TTW_FLAG,"a ttsForWav started with utterance Id"+utteranceId);
if (null != onTranslateProgressListener){
onTranslateProgressListener.onStart();
}
}
@Override
public void onDone(String utteranceId) {
Log.d(TTS_TTW_FLAG,"a ttsForWav onDone with utterance Id"+utteranceId);
onTtsForWavFree(utteranceId);
if (null != onTranslateProgressListener){
onTranslateProgressListener.onComplete(pigTTW.getText(),Environment.getExternalStorageDirectory().getAbsolutePath()+parentPath+pigTTW.getFileName());
}
}
@Override
public void onError(String utteranceId) {
}
@Override
public void onError(String utteranceId,int errorCode) {
Log.d(TTS_TTW_FLAG,"a ttsForWav onError with utterance Id"+utteranceId+" and errorcode is"+errorCode);
onError(utteranceId);
}
});
isTtsProduceSuccess = true;
}
});
}
private void onTtsForWavFree(String freeUtteranceId){
//如果复用池不够,就加进去
if (freePool.size()< freePoolMaxSize && ttsMap.get(freeUtteranceId)!= null){
freePool.add(freeUtteranceId);
}
}
/* wav速度 */
private float wavSpeed;
/* wav音高 ,默认1,0f */
private float wavPitch;
/* wav语言 ,默认汉语 */
private Locale loc = Locale.CHINA;
/**
* 设置wav速度,设置后,只能改变pool中的值
*/
public PigTranslater setWavSpeed(float wavSpeed){
this.wavSpeed = wavSpeed;
return pigTranslater;
}
/**
* 设置wav音高
*/
public PigTranslater setWavPitch(float wavPitch){
this.wavPitch = wavPitch;
return pigTranslater;
}
/**
* 设置wav语言
*/
public PigTranslater setWavLanguage(Locale loc){
this.loc = loc;
return pigTranslater;
}
}
这里面我们要注意,生成的.wav文件中文本朗读速度是对应的tts实例的speed速度,如果不进行设置,那么生成的.wav文件中文本朗读速度将会等于手机TextToSpeech默认的(或者一些手机用户后来设定的)TTS引擎语音速度。
PigTTSController也有一些变化:
public class PigTTSController {
private PigTTSController instance;
private Context applicationContext;
private PigReader pigReader;
private PigTranslater pigTranslater;
public PigTTSController(Context context) {
this.applicationContext = context;
instance = this;
}
public PigReader getReader(){
if (null == pigReader){
pigReader = new PigReader(applicationContext);
}
return pigReader;
}
public PigTranslater getTranslater(){
if (null == pigTranslater){
pigTranslater = new PigTranslater(applicationContext);
}
return pigTranslater;
}
}
由于工作需求,需要直接进入SoundPool和MediaPlayer的封装,TTS暂时封装到这里,后面返回来完善和优化。