直面原理:5 张图彻底了解 Android TextToSpeech 机制
Posted TechMerger
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了直面原理:5 张图彻底了解 Android TextToSpeech 机制相关的知识,希望对你有一定的参考价值。
ChatGPT
如此火爆,但它的强悍在于NLU
(自然语言理解)、DM
(对话管理)和NLG
(自然语言生成)这三块,而Recognition
识别和TTS
播报这两块是缺失的。假使你的 App 接入了 ChatGPT,但如果需要播报出来的话,TextToSpeech
机制就可以派上用场了。
1 前言
关于语音方面的交互,android SDK 提供了用于语音交互的 VoiceInteraction
机制、语音识别的 Recognition
接口、语音播报的 TTS 接口。
前者已经介绍过,本次主要聊聊第 3 块即 TTS,后续会分析下第 2 块即 Android 标准的 Recognition 机制。
通过 TextToSpeech
机制,任意 App 都可以方便地采用系统内置或第三方提供的 TTS Engine 进行播放铃声提示、语音提示的请求,Engine 可以由系统选择默认的 provider 来执行操作,也可由 App 具体指定偏好的目标 Engine 来完成。
默认 TTS Engine 可以在设备设置的路径中找到,亦可由用户手动更改:Settings -> Accessibility -> Text-to-speech ouput -> preferred engine
TextToSpeech 机制的优点有很多:
- 对于需要使用 TTS 的请求 App 而言:无需关心 TTS 的具体实现,通过
TextToSpeech
API 即用即有 - 对于需要对外提供 TTS 能力的实现 Engine 而言,无需维护复杂的 TTS 时序和逻辑,按照
TextToSpeechService
框架的定义对接即可,无需关心系统如何将实现和请求进行衔接
本文将会阐述 TextToSpeech 机制的调用、Engine 的实现以及系统调度这三块,彻底梳理清楚整个流程。
2 TextToSpeech 调用
TextToSpeech API 是为 TTS 调用准备,总体比较简单。
最主要的是提供初始化 TTS 接口的 TextToSpeech()
构造函数和初始化后的回调 OnInitListener
,后续的播放 TTS 的 speak()
和播放铃声的 playEarcon()
。
比较重要的是处理播放请求的 4 种回调结果,需要依据不同结果进行 TTS 播报开始的状态记录、播报完毕后的下一步动作、抑或是在播报出错时对音频焦点的管理等等。
之前的 OnUtteranceCompletedListener
在 API level 18 时被废弃,可以使用回调更为精细的 UtteranceProgressListener
。
// TTSTest.kt
class TTSTest(context: Context)
private val tts: TextToSpeech = TextToSpeech(context) initResult -> ...
init
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener()
override fun onStart(utteranceId: String?) ...
override fun onDone(utteranceId: String?) ...
override fun onStop(utteranceId: String?, interrupted: Boolean) ...
override fun onError(utteranceId: String?) ...
)
fun testTextToSpeech(context: Context)
tts.speak(
"你好,汽车",
TextToSpeech.QUEUE_ADD,
Bundle(),
"xxdtgfsf"
)
tts.playEarcon(
EARCON_DONE,
TextToSpeech.QUEUE_ADD,
Bundle(),
"yydtgfsf"
)
companion object
const val EARCON_DONE = "earCon_done"
3 TextToSpeech 系统调度
3.1 init 绑定
首先从 TextToSpeech()
的实现入手,以了解在 TTS 播报之前,系统和 TTS Engine 之间做了什么准备工作。
-
其触发的
initTTS()
将按照如下顺序查找需要连接到哪个 Engine:- 如果构造 TTS 接口的实例时指定了目标 Engine 的 package,那么首选连接到该 Engine
- 反之,获取设备设置的 default Engine 并连接,设置来自于
TtsEngines
从系统设置数据SettingsProvider
中 读取TTS_DEFAULT_SYNTH
而来 - 如果 default 不存在或者没有安装的话,从 TtsEngines 获取第一位的系统 Engine 并连接。第一位指的是从所有 TTS Service 实现 Engine 列表里获得第一个属于 system image 的 Engine
-
连接的话均是调用
connectToEngine()
,其将依据调用来源来采用不同的Connection
内部实现去connect()
:-
如果调用不是来自 system,采用
DirectConnection
- 其 connect() 实现较为简单,封装 Action 为
INTENT_ACTION_TTS_SERVICE
的 Intent 进行bindService()
,后续由AMS
执行和 Engine 的绑定,这里不再展开
- 其 connect() 实现较为简单,封装 Action 为
-
反之,采用
SystemConnection
,原因在于系统的 TTS 请求可能很多,不能像其他 App 一样总是创建一个新的连接,而是需要 cache 并复用这种连接-
具体是直接获取名为
texttospeech
、管理 TTS Service 的系统服务TextToSpeechManagerService
的接口代理并直接调用它的createSession()
创建一个 session,同时暂存其指向的ITextToSpeechSession
代理接口。该 session 实际上还是
AIDL
机制,TTS 系统服务的内部会创建专用的TextToSpeechSessionConnection
去 bind 和 cache Engine,这里不再赘述
-
-
无论是哪种方式,在 connected 之后都需要将具体的 TTS Eninge 的
ITextToSpeechService
接口实例暂存,同时将 Connection 实例暂存到 mServiceConnection,给外部类接收到 speak() 的时候使用。而且要留意,此刻还会启动一个异步任务SetupConnectionAsyncTask
将自己作为 Binder 接口ITextToSpeechCallback
返回给 Engine 以处理完之后回调结果给 Request
-
-
connect 执行完毕并结果 OK 的话,还要暂存到
mConnectingServiceConnection
,以在结束 TTS 需求的时候释放连接使用。并通过dispatchOnInit()
传递SUCCESS
给 Request App- 实现很简单,将结果 Enum 回调给初始化传入的
OnInitListener
接口
- 实现很简单,将结果 Enum 回调给初始化传入的
-
如果连接失败的话,则调用
dispatchOnInit()
传递ERROR
// TextToSpeech.java
public class TextToSpeech
public TextToSpeech(Context context, OnInitListener listener)
this(context, listener, null);
private TextToSpeech( ... )
...
initTts();
private int initTts()
// Step 1: Try connecting to the engine that was requested.
if (mRequestedEngine != null)
if (mEnginesHelper.isEngineInstalled(mRequestedEngine))
if (connectToEngine(mRequestedEngine))
mCurrentEngine = mRequestedEngine;
return SUCCESS;
...
else if (!mUseFallback)
...
dispatchOnInit(ERROR);
return ERROR;
// Step 2: Try connecting to the user's default engine.
final String defaultEngine = getDefaultEngine();
...
// Step 3: Try connecting to the highest ranked engine in the system.
final String highestRanked = mEnginesHelper.getHighestRankedEngineName();
...
dispatchOnInit(ERROR);
return ERROR;
private boolean connectToEngine(String engine)
Connection connection;
if (mIsSystem)
connection = new SystemConnection();
else
connection = new DirectConnection();
boolean bound = connection.connect(engine);
if (!bound)
return false;
else
mConnectingServiceConnection = connection;
return true;
Connection 内部类和其两个子类的实现:
// TextToSpeech.java
public class TextToSpeech
...
private abstract class Connection implements ServiceConnection
private ITextToSpeechService mService;
...
private final ITextToSpeechCallback.Stub mCallback =
new ITextToSpeechCallback.Stub()
public void onStop(String utteranceId, boolean isStarted)
throws RemoteException
UtteranceProgressListener listener = mUtteranceProgressListener;
if (listener != null)
listener.onStop(utteranceId, isStarted);
;
@Override
public void onSuccess(String utteranceId) ...
@Override
public void onError(String utteranceId, int errorCode) ...
@Override
public void onStart(String utteranceId) ...
...
;
@Override
public void onServiceConnected(ComponentName componentName, IBinder service)
synchronized(mStartLock)
mConnectingServiceConnection = null;
mService = ITextToSpeechService.Stub.asInterface(service);
mServiceConnection = Connection.this;
mEstablished = false;
mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask();
mOnSetupConnectionAsyncTask.execute();
...
private class DirectConnection extends Connection
@Override
boolean connect(String engine)
Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
intent.setPackage(engine);
return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
...
private class SystemConnection extends Connection
...
boolean connect(String engine)
IBinder binder = ServiceManager.getService(Context.TEXT_TO_SPEECH_MANAGER_SERVICE);
...
try
manager.createSession(engine, new ITextToSpeechSessionCallback.Stub()
...
);
return true;
...
...
3.2 speak 播报
后面看看重要的 speak(),系统做了什么具体实现。
-
首先将 speak() 对应的调用远程接口的操作封装为 Action 接口实例,并交给 init() 时暂存的已连接的 Connection 实例去调度。
// TextToSpeech.java public class TextToSpeech ... private Connection mServiceConnection; public int speak(final CharSequence text, ... ) return runAction((ITextToSpeechService service) -> ... , ERROR, "speak"); private <R> R runAction(Action<R> action, R errorResult, String method) return runAction(action, errorResult, method, true, true); private <R> R runAction( ... ) synchronized (mStartLock) ... return mServiceConnection.runAction(action, errorResult, method, reconnect, onlyEstablishedConnection); private abstract class Connection implements ServiceConnection public <R> R runAction( ... ) synchronized (mStartLock) try ... return action.run(mService); ...
-
Action
的实际内容是先从mUtterances
Map 里查找目标文本是否有设置过本地的 audio 资源:- 如有设置的话,调用用 TTS Engine 的
playAudio()
直接播放 - 反之调用 text 转 audio 的接口
speak()
// TextToSpeech.java public class TextToSpeech ... public int speak(final CharSequence text, ... ) return runAction((ITextToSpeechService service) -> Uri utteranceUri = mUtterances.get(text); if (utteranceUri != null) return service.playAudio(getCallerIdentity(), utteranceUri, queueMode, getParams(params), utteranceId); else return service.speak(getCallerIdentity(), text, queueMode, getParams(params), utteranceId); , ERROR, "speak"); ...
后面即是 TextToSpeechService 的实现环节。
- 如有设置的话,调用用 TTS Engine 的
4 TextToSpeechService 实现
-
TextToSpeechService 内接收的实现是向内部的
SynthHandler
发送封装的 speak 或 playAudio 请求的SpeechItem
。SynthHandler 绑定到 TextToSpeechService 初始化的时候启动的、名为 “SynthThread” 的 HandlerThread。
- speak 请求封装给 Handler 的是
SynthesisSpeechItem
- playAudio 请求封装的是
AudiospeechItem
// TextToSpeechService.java public abstract class TextToSpeechService extends Service 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 SynthesisSpeechItem( caller, Binder.getCallingUid(), Binder.getCallingPid(), params, utteranceId, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); @Override public int playAudio( ... ) SpeechItem item = new AudioSpeechItem( ... ); ... ... ; ...
- speak 请求封装给 Handler 的是
-
SynthHandler 拿到 SpeechItem 后根据 queueMode 的值决定是 stop() 还是继续播放。播放的话,是封装进一步 play 的操作 Message 给 Handler。
// TextToSpeechService.java private class SynthHandler extends Handler ... public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) UtteranceProgressDispatcher utterenceProgress = null; if (speechItem instanceof UtteranceProgressDispatcher) utterenceProgress = (UtteranceProgressDispatcher) speechItem; if (!speechItem.isValid()) if (utterenceProgress != null) utterenceProgress.dispatchOnError( TextToSpeech.ERROR_INVALID_REQUEST); return TextToSpeech.ERROR; if (queueMode == TextToSpeech.QUEUE_FLUSH) stopForApp(speechItem.getCallerIdentity()); else if (queueMode == TextToSpeech.QUEUE_DESTROY) stopAll(); Runnable runnable = new Runnable() @Override public void run() if (setCurrentSpeechItem(speechItem)) speechItem.play(); removeCurrentSpeechItem(); else speechItem.stop(); ; Message msg = Message.obtain(this, runnable); msg.obj = speechItem.getCallerIdentity(); if (sendMessage(msg)) return TextToSpeech.SUCCESS; else if (utterenceProgress != null) utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE); return TextToSpeech.ERROR; ...
-
play() 具体是调用 playImpl() 继续。对于 SynthesisSpeechItem 来说,将初始化时创建的
SynthesisRequest
实例和SynthesisCallback
实例(此处的实现是PlaybackSynthesisCallback
)收集和调用onSynthesizeText()
进一步处理,用于请求和回调结果。// TextToSpeechService.java private abstract class SpeechItem ... public void play() synchronized (this) if (mStarted) throw new IllegalStateException("play() called twice"); mStarted = true; playImpl(); class SynthesisSpeechItem extends UtteranceSpeechItemWithParams public SynthesisSpeechItem( ... String utteranceId, CharSequence text) mSynthesisRequest = new SynthesisRequest(mText, mParams); ... ... @Override protected void playImpl() AbstractSynthesisCallback synthesisCallback; mEventLogger.onRequestProcessingStart(); synchronized (this) ..
以上是关于直面原理:5 张图彻底了解 Android TextToSpeech 机制的主要内容,如果未能解决你的问题,请参考以下文章
几张图让你彻底了解JAVASEJAVAEEJAVAWEB整个的知识体系