源码地址:https://github.com/yellowgreatsun/MXTtsEngine
前两篇文章即Android TTS系列一——如何让app具备tts能力和Android TTS系列二——如何开发一款系统级tts引擎?分别分享了如何让app具备tts能力和如何开发tts引擎,这篇文章会分享下speech包源码,我们会更明白speech包接口的来龙去脉。这部分之前在公司内部分享讲过一次,但发现写成文章还是很吃力的,我会尽量讲解清楚。另外,引用的代码我会略去一些不太关键的部分,避免文章太冗长。
为便于理解,我按照这个思路来进行分享:
speech包的源码结构
TextToSpeech的接口剖析
TextToSpeech的接口如何通过TextToSpeechService来实现
TextToSpeechService的实现如何回调回去
一、speech包的源码结构
speech源码路径:/frameworks/base/core/java/android/speech
先来看一下speech包的源码结构:
speech包源码.jpg
这里面,比较关键的有:TextToSpeech、TextToSpeechService、ITextToSpeechService、ITextToSpeechCallback、SynthesisCallback及其实现类(PlaybackSynthesisCallback和FileSynthesisCallback)。前两个小伙伴们已经不陌生了,一个是调用tts的接口,一个是实现tts的能力,后几个则是将它俩关联起来。
二、TextToSpeech的接口剖析
这里,我们只对构造方法、speck、synthesizeToFile进行剖析,其它的诸如setLanguage、getEngines,看了这篇分享后,会很容易理解的,所以不再写出来。
1.构造方法
构造方法有三个:
TextToSpeech(Context context, OnInitListener listener)
TextToSpeech(Context context, OnInitListener listener, String engine)
TextToSpeech(Context context, OnInitListener listener, String engine,String packageName, boolean useFallback)
看代码会发现,前两个最终还是调用了第三个。
public TextToSpeech(Context context, OnInitListener listener, String engine, String packageName, boolean useFallback) { mContext = context; mInitListener = listener; mRequestedEngine = engine; mUseFallback = useFallback; initTts(); }
前面一些赋值容易明白,关键信息在initTts()里。
private int initTts() { // Step 1: Try connecting to the engine that was requested. if (connectToEngine(mRequestedEngine)) { mCurrentEngine = mRequestedEngine; return SUCCESS; } // Step 2: Try connecting to the user's default engine. final String defaultEngine = getDefaultEngine(); if (defaultEngine != null && !defaultEngine.equals(mRequestedEngine)) { if (connectToEngine(defaultEngine)) { mCurrentEngine = defaultEngine; return SUCCESS; } } // Step 3: Try connecting to the highest ranked engine in the // system. final String highestRanked = mEnginesHelper.getHighestRankedEngineName(); if (highestRanked != null && !highestRanked.equals(mRequestedEngine) && !highestRanked.equals(defaultEngine)) { if (connectToEngine(highestRanked)) { mCurrentEngine = highestRanked; return SUCCESS; } } mCurrentEngine = null; dispatchOnInit(ERROR); return ERROR; }
看注释也能明白,initTts会按照这样一个顺序:指定的tts引擎(通过包名,构造方法中的参数engine)、默认的tts引擎、最常用的tts引擎。但这些对于我们理解speech源码不是最关键的,关键的在于connectToEngine(engine),有没有发现,三种创建引擎的方式最终都调它了。如果connectToEngine返回false了,也就通过 dispatchOnInit(ERROR)告诉调用者失败了。那就看下connectToEngine吧。
private boolean connectToEngine(String engine) { Connection connection = new Connection(); Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); intent.setPackage(engine); boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); if (!bound) { return false; } else { mConnectingServiceConnection = connection; return true; } }
到这里,我们就明白了,哦,原来是bind一个Service呀。指定了action为Engine.INTENT_ACTION_TTS_SERVICE,这就是在第二篇文章中,TextToSpeechService的实现类在mainfest中声明该action的原因。Connection太长,只看onServiceConnected吧。
@Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized(mStartLock) { mService = ITextToSpeechService.Stub.asInterface(service); mServiceConnection = Connection.this; mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name); mOnSetupConnectionAsyncTask.execute(); } }
在这里,拿到了mService的实例,bind的是远程Service,TextToSpeech的其它接口,也是再调用 ITextToSpeechService下的接口。然后通过mOnSetupConnectionAsyncTask中的dispatchOnInit(result)来回调初始化成功。
构造方法就先说到这里。
2.speak(text,queueMode,params,utteranceId)
看代码会发现,最终调用了service.speak(getCallerIdentity(), text, queueMode, getParams(params),utteranceId)。明白这一点就先可以了。
3.synthesizeToFile(text,params,file,utteranceId)
类似于speak,synthesizeToFile最终调用的service.synthesizeToFileDescriptor(getCallerIdentity(), text,fileDescriptor, getParams(params), utteranceId)。
接下来,我们将进入TextToSpeechService,去探究下service.speak和service.synthesizeToFileDescriptor。
三、TextToSpeech的接口如何通过TextToSpeechService来实现
这部分比较长,我们一点点来看。
1.mBinder
通过对TextToSpeech构造方法的剖析,我们了解了它是通过绑定远程Service来实现的,而远程Service(也就是TTSEngine中的Service,比如我们的MoxiangTtsService)是继承的TextToSpeechService,所以重点看它就行了。首先要看下它的onBind。
@Override public IBinder onBind(Intent intent) { if (Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { return mBinder; } return null; }
mBinder 的实例化非常长,毕竟要把ITextToSpeechService的各个接口实现完。这里只贴出来我们关心的两个,即speak和synthesizeToFileDescriptor。
private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { @Override public int speak(IBinder caller, CharSequence text, int queueMode, Bundle params, String utteranceId) { SpeechItem item = new SynthesisSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @Override public int synthesizeToFileDescriptor(IBinder caller, CharSequence text, ParcelFileDescriptor fileDescriptor, Bundle params, String utteranceId) { final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( fileDescriptor.detachFd()); SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text, new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); return mSynthHandler.enqueueSpeechItem(android.speech.tts.TextToSpeech.QUEUE_ADD, item); } …… }
这样,一下子就明白了,speak的实现又调了mSynthHandler.enqueueSpeechItem,mSynthHandler就是一个Handler,字面理解该接口,就是将text的语音合成加入了队列。synthesizeToFileDescriptor的实现竟也是调了mSynthHandler.enqueueSpeechItem,当然了,参数item不同,奥秘,这里有奥秘。
2.speak
上面已经说了,其实现是调mSynthHandler.enqueueSpeechItem,来,继续。
public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { UtteranceProgressDispatcher utterenceProgress = null; if (queueMode == android.speech.tts.TextToSpeech.QUEUE_FLUSH) { stopForApp(speechItem.getCallerIdentity()); } else if (queueMode == android.speech.tts.TextToSpeech.QUEUE_DESTROY) { stopAll(); } Runnable runnable = new Runnable() { @Override public void run() { if (isFlushed(speechItem)) { speechItem.stop(); } else { setCurrentSpeechItem(speechItem); speechItem.play(); setCurrentSpeechItem(null); } } }; Message msg = Message.obtain(this, runnable); msg.obj = speechItem.getCallerIdentity(); if (sendMessage(msg)) { return android.speech.tts.TextToSpeech.SUCCESS; } else { if (utterenceProgress != null) { utterenceProgress.dispatchOnError(android.speech.tts.TextToSpeech.ERROR_SERVICE); } return android.speech.tts.TextToSpeech.ERROR; } }
先看前面的,TextToSpeech的speak接口的参数mode在这里体现了,如果是TextToSpeech.QUEUE_FLUSH,会调用stopForApp停掉其它tts的播放,这里就不把代码贴出来了。我们关注的是speechItem.play()。
public void play() { playImpl(); }
playImpl,要看SynthesisSpeechItemV1下的实现了。
@Override protected void playImpl() { AbstractSynthesisCallback synthesisCallback; synchronized (this) { mSynthesisCallback = createSynthesisCallback(); synthesisCallback = mSynthesisCallback; } TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); // Fix for case where client called .start() & .error(), but did not called .done() if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { synthesisCallback.done(); } }
哎呦,终于看到了我们熟悉的onSynthesizeText(SynthesisRequest request,SynthesisCallback callback)。reques就是封装的tts合成请求的参数,在SynthesisSpeechItemV1的构造方法中封装的,callback是调用createSynthesisCallback创建的。
protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getAudioParams(), mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); }
3.synthesizeToFileDescriptor
上面我们已经说了,synthesizeToFileDescriptor和speak的区别,就在于SpeechItemV1的子类不同。speak是SynthesisSpeechItemV1,synthesizeToFileDescriptor是SynthesisToFileOutputStreamSpeechItemV1。二者到了playImpl的区别,也主要在于callback的不同,其创建方法是:
protected AbstractSynthesisCallback createSynthesisCallback() { return new FileSynthesisCallback(mFileOutputStream.getChannel(), this, false); }
到了这里,我们已经明白了,TextToSpeech的接口是如何通过TextToSpeechService来实现。接下来,我们该关心如何将TextToSpeechService的实现回调回去。
四、TextToSpeechService的实现如何回调回去
1.speak
在第二篇时,我们已经讲解过要通过调用callback的各个接口来回传结果。这里,就看一下PlaybackSynthesisCallback中的start、audioAvailable和done。
关注几个实例:UtteranceProgressDispatcher mDispatcher、AudioPlaybackHandler mAudioTrackHandler和SynthesisPlaybackQueueItem mItem,它们均是在TextToSpeech中实例化的。第一个关系到最终到UtteranceProgressListener的回调,后两个关系到音频的播放。
贴出来三个方法:
public int start(int sampleRateInHz, int audioFormat, int channelCount) { SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem( mAudioParams, sampleRateInHz, audioFormat, channelCount, mDispatcher, mCallerIdentity, mLogger); mAudioTrackHandler.enqueue(item); mItem = item; }
public int audioAvailable(byte[] buffer, int offset, int length) { item = mItem; final byte[] bufferCopy = new byte[length]; item.put(bufferCopy); }
public int done() { if (mStatusCode == android.speech.tts.TextToSpeech.SUCCESS) { mDispatcher.dispatchOnSuccess(); } else { mDispatcher.dispatchOnError(mStatusCode); } item = mItem; if (statusCode == android.speech.tts.TextToSpeech.SUCCESS) { item.done(); } else { item.stop(statusCode); } }
先说回调,可以在PlaybackSynthesisCallback的done中看到,调用了mDispatcher.dispatchOnSuccess,进一步看是调用了ITextToSpeechCallback.onSuccess,进一步掉用了UtteranceProgressListener的onDone。其它也是也是如此,不再细说。注意的是,onStart不是直接在PlaybackSynthesisCallback中,而是在SynthesisPlaybackQueueItem中,毕竟这时候才是开始播放。
再说播放,AudioPlaybackHandler 虽然不是继承自Handler,但其作用与其类似,是管理各个tts合成请求是的队列。SynthesisPlaybackQueueItem 继承自Runnable的子类,播放是在这完成的,它会创建一个BlockingAudioTrack,具体播放不再叙述。
2.synthesizeToFile
它的callback是FileSynthesisCallback。
大体与PlaybackSynthesisCallback类似,只是它不是播放,而是写入指定的文件。这里面需要关注的实例有:UtteranceProgressDispatcher mDispatcher和FileChannel mFileChannel,后者就是写文件的。
贴出来三个方法:
public int start(int sampleRateInHz, int audioFormat, int channelCount) { mDispatcher.dispatchOnStart(); fileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); }
public int audioAvailable(byte[] buffer, int offset, int length) { fileChannel.write(ByteBuffer.wrap(buffer, offset, length)); }
public int done() { fileChannel.position(0); int dataLength = (int) (fileChannel.size() - WAV_HEADER_LENGTH); fileChannel.write(makeWavHeader(sampleRateInHz, audioFormat, channelCount, dataLength)); mDispatcher.dispatchOnSuccess(); }
需要注意的是done中的makeWavHeader,它是写入文件头信息。
OK,Android speech包源码剖析就讲到这里。至此,Android Tts系列就基本完成了,虽然还有一些细节没有讲到,比如Locale语言、设置默认引擎,但主体思路算是差不多了。
在这个过程中,一是对Android Tts的来龙去脉有了比较充分的认识;二是Android speech包的架构设计思想,甭管具体的Tts引擎是啥,你都得按照我的规范来(集成Service、实现接口),这样,都可以通过TextToSpeech的接口来调用。