Android TextToSpeech浅析
1700 Words|Read in about 8 Min|本文总阅读量次
今天就顺便来拆解一下用到的核心,TextToSpeech功能(即tts)。
前几天突然看到以前手机安装的App叫Instapaper,可以保存网页以便稍后阅读的服务,提供离线阅读功能。那年16年,我用这个App保存公众号文章,方便走路的时候边走边听。
以笔者的手机型号为例,红米K30,MUUI11.1.23版本。
tts功能可以在无障碍模式中找到,无障碍模式通常是专门给特别的人群使用的功能(但是现在好多人都用来淘宝刷任务金币或者其他操作等)。
可以看到笔者的无障碍模式中,除了自带的tts输出,还有一个产商定制的TalkBack的tts功能
进到TalkBack设置里面,可以看到除了tts功能,还有其他辅助功能,类似一个工具箱。
打开tts的ui内容,默认的引擎是系统引擎,说明是小米产商自己开发的一套东西。当然如果有需要的话,也可以类似讯飞,思必驰,云知声等一些语音解决方案的公司一样,可以自行开发一套新的,覆盖掉这个默认引擎。这里支持的语言主要是中文和英文。
1简介
TextToSpeech 即文字转语音服务,是Android系统提供的原生接口服务,原生的tts引擎应用通过检测系统语言,用户可以下载对应语言的资源文件,达到播报指定语音的文字的能力。一般google原生时不支持中文的。
通过 TextToSpeech机制,任意App都可以方便地采用系统内置或第三方提供的TTS Engine进行播放铃声提示、语音提示的请求,Engine 可以由系统选择默认的provider来执行操作,也可由App具体指定偏好的目标Engine来完成。
2demo
1public class TtsTest implements TextToSpeech.OnInitListener{
2 private TextToSpeech textToSpeech = null;
3 private Context mContext = null;
4
5 TtsTest(Context context)
6 {
7 mContext = context;
8 textToSpeech = new TextToSpeech(mContext, this); // 参数Context,TextToSpeech.OnInitListener
9 textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
10 @Override
11 public void onStart(String utteranceId) {
12 //自定义回调函数的处理
13 }
14
15 @Override
16 public void onDone(String utteranceId) {
17 //自定义回调函数的处理
18 }
19
20 @Override
21 public void onError(String utteranceId) {
22 //自定义回调函数的处理
23 }
24
25 @Override
26 public void onAudioAvailable(String utteranceId, byte[] audio) {
27 super.onAudioAvailable(utteranceId, audio);
28 //这里的audio是原始数据,可以进行音频数据处理,比如音频3A处理
29 }
30 });
31 }
32 /**
33 * 初始化TextToSpeech引擎
34 * status:SUCCESS或ERROR
35 * setLanguage设置语言
36 */
37 @Override
38 public void onInit(int status) {
39 if (status == TextToSpeech.SUCCESS) {
40 int result = textToSpeech.setLanguage(Locale.CHINA);
41 if (result == TextToSpeech.LANG_MISSING_DATA
42 || result == TextToSpeech.LANG_NOT_SUPPORTED) {
43 Toast.makeText(mContext, "数据丢失或不支持", Toast.LENGTH_SHORT).show();
44 }
45 }
46 }
47
48 public void speak() {
49 if (textToSpeech != null && !textToSpeech.isSpeaking()) {
50 //初始化配置
51 Bundle bundle = new Bundle();
52 bundle.putInt("streamType", 0);
53 bundle.putInt("sessionId", 0);
54 bundle.putString("utteranceId", null);
55 bundle.putFloat("volume", 1.0f);
56 bundle.putFloat("pan", 0.0f);
57
58 textToSpeech.setPitch(0.0f);// 设置音调
59 textToSpeech.speak("我是要播放的文字",
60 TextToSpeech.QUEUE_FLUSH, bundle, null);
61 }
62 }
63
64 public void onStop() {
65 textToSpeech.stop(); // 停止tts
66 textToSpeech.shutdown(); // 关闭,释放资源
67 }
68
69}
这里App里面的步骤分成三步
- 初始化tts服务和tts的引擎,初始化成功,会有onInit回调
- 给tts设置一个监听,用于监听tts引擎处理的状态,可以是onStart,onDone,onError,还可以有原数数据的处理
- 开始进行播报文字,这里需要tts最核心的引擎处理
3源码解析
为了看明白后续的源码,这里简单梳理了比较重要的几个类图,分别是SpeechItem
、AbstractSynthesisCallback
和PlaybackQueueItem
。
3.1init 绑定
首先从 TextToSpeech()
的实现入手,以了解在 TTS 播报之前,系统和 TTS Engine 之间做了什么准备工作。
3.1.1 initTTS
将按照如下顺序查找需要连接到哪个 Engine
- 如果构造 TTS 接口的实例时指定了目标 Engine 的 package,那么首选连接到该 Engine
- 反之,获取设备设置的 default Engine 并连接,设置来自于 TtsEngines 从系统设置数据 SettingsProvider 中 读取 TTS_DEFAULT_SYNTH 而来
- 如果 default 不存在或者没有安装的话,从 TtsEngines 获取第一位的系统 Engine 并连接。第一位指的是从所有 TTS Service 实现 Engine 列表里获得第一个属于 system image 的 Engine
1//frameworks/base/core/java/android/speech/tts/TextToSpeech.java
2public TextToSpeech(Context context, OnInitListener listener) {
3 this(context, listener, null);
4}
5
6private TextToSpeech( ... ) {
7 ...
8 initTts();
9}
10//这里直接是第二种方式,默认的系统引擎
11private int initTts() {
12 // Step 1: Try connecting to the engine that was requested.
13 if (mRequestedEngine != null) {
14 ...
15 }
16
17 // Step 2: Try connecting to the user's default engine.
18 final String defaultEngine = getDefaultEngine();
19 ...
20
21 // Step 3: Try connecting to the highest ranked engine in the system.
22 final String highestRanked = mEnginesHelper.getHighestRankedEngineName();
23 ...
24
25 dispatchOnInit(ERROR);
26 return ERROR;
27}
3.1.2连接
调用 connectToEngine(),其将依据调用来源来采用不同的 Connection内部实现去 connect():
具体是直接获取名为 texttospeech 、管理 TTS Service 的系统服务 TextToSpeechManagerService 的接口代理并直接调用它的 createSession() 创建一个 session,同时暂存其指向的 ITextToSpeechSession 代理接口。
该 session 实际上还是
AIDL
机制,TTS 系统服务的内部会创建专用的TextToSpeechSessionConnection
去 bind 和 cache Engine,这里不再赘述
1//frameworks/base/core/java/android/speech/tts/TextToSpeech.java
2private boolean connectToEngine(String engine) {
3 Connection connection;
4 if (mIsSystem) {
5 connection = new SystemConnection();
6 } else {
7 connection = new DirectConnection();
8 }
9 //相当于直接连接绑定服务了,用于aidl通信
10 boolean bound = connection.connect(engine);
11 if (!bound) {
12 return false;
13 } else {
14 mConnectingServiceConnection = connection;
15 return true;
16 }
17}
3.1.3OnInit
connect 执行完毕并结果 OK 的话,还要暂存到 mConnectingServiceConnection,以在结束 TTS 需求的时候释放连接使用。并通过 dispatchOnInit() 传递 SUCCESS 给 Request App
实现很简单,将结果 Enum 回调给初始化传入的 OnInitListener 接口 如果连接失败的话,则调用 dispatchOnInit() 传递 ERROR
1//frameworks/base/core/java/android/speech/tts/TextToSpeech.java
2private class Connection implements ServiceConnection {
3 private ITextToSpeechService mService;
4 //异步方式
5 private class SetupConnectionAsyncTask extends AsyncTask<Void, Void, Integer> {
6 private final ComponentName mName;
7
8 public SetupConnectionAsyncTask(ComponentName name) {
9 mName = name;
10 }
11
12 @Override
13 protected Integer doInBackground(Void... params) {
14 ...
15 }
16
17 @Override
18 protected void onPostExecute(Integer result) {
19 synchronized(mStartLock) {
20 if (mOnSetupConnectionAsyncTask == this) {
21 mOnSetupConnectionAsyncTask = null;
22 }
23 mEstablished = true;
24 //最终调用到dispatchOnInit
25 dispatchOnInit(result);
26 }
27 }
28 }
29
30 @Override
31 public void onServiceConnected(ComponentName name, IBinder service) {
32 synchronized(mStartLock) {
33 ...
34 //这个mService指代的是TextToSpeechService的代理binder
35 mService = ITextToSpeechService.Stub.asInterface(service);
36 mServiceConnection = Connection.this;
37 mEstablished = false;
38 //最终调用异步操作,进行回调
39 mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name);
40 mOnSetupConnectionAsyncTask.execute();
41 }
42 }
43}
3.2 speak
3.2.1调用远端接口Action
首先将 speak() 对应的调用远程接口的操作封装为 Action 接口实例,并交给 init() 时暂存的已连接的 Connection 实例去调度。
1
2public int speak(final CharSequence text,
3 final int queueMode,
4 final Bundle params,
5 final String utteranceId) {
6 //最终回调到这里,service就是Connection#runAction中的mService
7 return runAction((ITextToSpeechService service) -> {
8 ...
9 }, ERROR, "speak");
10}
11
12private class Connection implements ServiceConnection {
13 ...
14 public <R> R runAction(Action<R> action, R errorResult, String method,
15 boolean reconnect, boolean onlyEstablishedConnection) {
16 synchronized (mStartLock) {
17 try {
18 ...
19 //直接执行run,传入的mService就是3.1中初始化的TextToSpeechService的代理binder
20 return action.run(mService);
21 } catch (RemoteException ex) {
22 ...
23 return errorResult;
24 }
25 }
26 }
27}
3.2.2 Uri区分
Action 的实际内容是先从 mUtterances Map 里查找目标文本是否有设置过本地的 audio 资源
1//frameworks/base/core/java/android/speech/tts/TextToSpeech.java
2public int speak(final CharSequence text, ... ) {
3 //最终回调到这里,service就是Connection#runAction中的mService
4 return runAction((ITextToSpeechService service) -> {
5 Uri utteranceUri = mUtterances.get(text);
6 if (utteranceUri != null) {
7 //这里分成两种情况,直接播放音频,铃声闹钟等
8 return service.playAudio(getCallerIdentity(), utteranceUri, queueMode,
9 getParams(params), utteranceId);
10 } else {
11 //这里是tts转换
12 return service.speak(getCallerIdentity(), text, queueMode, getParams(params),
13 utteranceId);
14 }
15 }, ERROR, "speak");
16}
3.4TextToSpeechService实现
3.4.1封装成SpeechItem
TextToSpeechService 内接收的实现是向内部的 SynthHandler 发送封装的 speak 或 playAudio 请求的 SpeechItem。
1//frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
2private final ITextToSpeechService.Stub mBinder =
3 new ITextToSpeechService.Stub() {
4 @Override
5 public int speak(
6 IBinder caller,
7 CharSequence text,
8 int queueMode,
9 Bundle params,
10 String utteranceId) {
11 SpeechItem item =
12 new SynthesisSpeechItem(
13 caller,
14 Binder.getCallingUid(),
15 Binder.getCallingPid(),
16 params,
17 utteranceId,
18 text);
19 return mSynthHandler.enqueueSpeechItem(queueMode, item);
20 }
21
22 @Override
23 public int playAudio( ... ) {
24 SpeechItem item =
25 new AudioSpeechItem( ... );
26 ...
27 }
28 ...
29};
3.4.2异步handler
SynthHandler 绑定到 TextToSpeechService 初始化的时候启动的、名为 “SynthThread” 的 HandlerThread
- speak 请求封装给 Handler 的是 SynthesisSpeechItem
- playAudio 请求封装的是 AudioSpeechItem
SynthHandler 拿到 SpeechItem ,封装之后进一步 play 的操作 Message 给 Handler
1//frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
2private class SynthHandler extends Handler {
3 ...
4 public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
5 UtteranceProgressDispatcher utterenceProgress = null;
6 if (speechItem instanceof UtteranceProgressDispatcher) {
7 utterenceProgress = (UtteranceProgressDispatcher) speechItem;
8 }
9 //queueMode就是speak的时候参数
10 if (queueMode == TextToSpeech.QUEUE_FLUSH) {
11 stopForApp(speechItem.getCallerIdentity());
12 }
13 Runnable runnable = new Runnable() {
14 @Override
15 public void run() {
16 if (setCurrentSpeechItem(speechItem)) {
17 //最终会调用到这里
18 speechItem.play();
19 removeCurrentSpeechItem();
20 } else {
21 speechItem.stop();
22 }
23 }
24 };
25 //这里就是handler操作,相当于开启另一个线程来执行异步操作
26 Message msg = Message.obtain(this, runnable);
27 msg.obj = speechItem.getCallerIdentity();
28 if (sendMessage(msg)) {
29 return TextToSpeech.SUCCESS;
30 }
31 }
32 ...
33}
3.4.3playimpl
play() 具体是调用 playImpl() 继续。对于 SynthesisSpeechItem 来说,将初始化时创建的 SynthesisRequest 实例和 SynthesisCallback 实例(此处的实现是 PlaybackSynthesisCallback)收集和调用 onSynthesizeText() 进一步处理,用于请求和回调结果。
1//frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
2private abstract class SpeechItem {
3 public void play() {
4 ...
5 playImpl();
6 }
7}
8//这里是tts,所以只看SynthesisSpeechItem这个item
9class SynthesisSpeechItem extends UtteranceSpeechItemWithParams {
10 public SynthesisSpeechItem(
11 Object callerIdentity,
12 int callerUid,
13 int callerPid,
14 Bundle params,
15 String utteranceId,
16 CharSequence text) {
17 //初始化SynthesisRequest,将text文本封装成一个请求
18 mSynthesisRequest = new SynthesisRequest(mText, mParams);
19 ...
20 }
21 ...
22 @Override
23 protected void playImpl() {
24 AbstractSynthesisCallback synthesisCallback;
25
26 synchronized (this) {
27 ...
28 //这个createSynthesisCallback实际上是PlaybackSynthesisCallback实例
29 mSynthesisCallback = createSynthesisCallback();
30 synthesisCallback = mSynthesisCallback;
31 }
32 //最终调用到这里onSynthesizeText,这个是需要引擎去实现的
33 TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
34 if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
35 synthesisCallback.done();
36 }
37 }
38 ...
39}
3.4.4synthesisCallback回调
关于onSynthesizeText这个是需要引擎做的操作,具体可以参照demo来做,一般SDK由产商实现(离线在线的引擎,讯飞,阿里,百度,腾讯,思必驰,云之声等等)。
当然系统也为我们提供了一个例子 /development/samples/TtsEngine/src/com/example/android/ttsengine/RobotSpeakTtsService.java 当然,需要实现TextToSpeechService中的抽象方法即可。
需要 Engine 复写以将 text 合成 audio 数据,也是 TTS 功能里最核心的实现,功能实现完成之后,就会监听的回调
按照顺序,依次是start、audioAvailable和done。
1//frameworks/base/core/java/android/speech/tts/PlaybackSynthesisCallback.java
2class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
3 ...
4 @Override
5 public int start(int sampleRateInHz, int audioFormat, int channelCount) {
6 ...
7 synchronized (mStateLock) {
8 ...
9 //封装成一个个Item,发送到阻塞队列中
10 SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
11 mAudioParams, sampleRateInHz, audioFormat, channelCount,
12 mDispatcher, mCallerIdentity, mLogger);
13 mAudioTrackHandler.enqueue(item);
14 mItem = item;
15 }
16 return TextToSpeech.SUCCESS;
17 }
18
19 @Override
20 public int audioAvailable(byte[] buffer, int offset, int length) {
21 SynthesisPlaybackQueueItem item = null;
22 synchronized (mStateLock) {
23 ...
24 item = mItem;
25 }
26 //对数据进行拷贝
27 final byte[] bufferCopy = new byte[length];
28 System.arraycopy(buffer, offset, bufferCopy, 0, length);
29 //将拷贝的内容发送给App选择处理
30 mDispatcher.dispatchOnAudioAvailable(bufferCopy);
31
32 try {
33 //处理完成之后加入队列中以供消费
34 //上述的 QueueItem 触发 Lock 接口的 take Condition 恢复执行,最后调用 AudioTrack 去播放
35 item.put(bufferCopy);
36 }
37 ...
38 return TextToSpeech.SUCCESS;
39 }
40
41 @Override
42 public int done() {
43 ...
44 return TextToSpeech.SUCCESS;
45 }
46 ...
47}
3.4.5dispatchOnAudioAvailable数据分发
上述 PlaybackSynthesisCallback 在通知 QueueItem 的同时,会通过 UtteranceProgressDispatcher 接口将数据、结果一并发送给 Request App
UtteranceProgressDispatcher 接口的实现就是 TextToSpeechService 处理 speak 请求的 UtteranceSpeechItem 实例,其通过缓存着各 ITextToSpeechCallback
接口实例的 CallbackMap 发送回调给 TTS 请求的 App。(这些 Callback 来自于 TextToSpeech 初始化时候通过 ITextToSpeechService 将 Binder 接口传递来和缓存起来的。)
1//frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
2private abstract class UtteranceSpeechItem extends SpeechItem
3 implements UtteranceProgressDispatcher {
4 @Override
5 public void dispatchOnAudioAvailable(byte[] audio) {
6 final String utteranceId = getUtteranceId();
7 if (utteranceId != null) {
8 //直接调用到mCallbacks中方法,mCallbacks实际上是CallbackMap
9 mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);
10 }
11 }
12 ...
13}
14
15private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
16 public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {
17 ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
18 if (cb == null) return;
19 try {
20 //最终回调到cb
21 cb.onAudioAvailable(utteranceId, buffer);
22 } ...
23 }
24 ...
25}
3.4.6 回调状态给App
ITextToSpeechCallback,这个非常眼熟。实际上在上述2demo中的执行将通过 TextToSpeech 的中转抵达请求 App 的 Callback
而上述cb的初始化正好和App对接上了
1//frameworks/base/core/java/android/speech/tts/TextToSpeech.java
2public int setOnUtteranceProgressListener(UtteranceProgressListener listener) {
3 mUtteranceProgressListener = listener;
4 return TextToSpeech.SUCCESS;
5}
App的回调
1//App
2textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
3 @Override
4 public void onStart(String utteranceId) {
5 //自定义回调函数的处理
6 }
7
8 @Override
9 public void onDone(String utteranceId) {
10 //自定义回调函数的处理
11 }
12
13 @Override
14 public void onError(String utteranceId) {
15 //自定义回调函数的处理
16 }
17
18 @Override
19 public void onAudioAvailable(String utteranceId, byte[] audio) {
20 super.onAudioAvailable(utteranceId, audio);
21 //这里的audio是原始数据,可以进行音频数据处理,比如音频3A处理
22 }
23});
4总结
总的来说流程不是很复杂,其实就是App与框架的交互过程。
- TTS Request App 调用
TextToSpeech
构造函数,由系统准备播报工作前的准备,比如通过Connection
绑定和初始化目标的 TTS Engine - Request App 提供目标 text 并调用
speak()
请求 - TextToSpeech 会检查目标 text 是否设置过本地的 audio 资源,没有的话回通过 Connection 调用
ITextToSpeechService
AIDL 的 speak() 继续 TextToSpeechService
收到后封装请求SynthesisRequest
和用于回调结果的SynthesisCallback
实例- 之后将两者作为参数调用核心实现
onSynthesizeText()
,其将解析 Request 并进行 Speech 音频数据合成 - 此后通过 SynthesisCallback 将合成前后的关键回调告知系统,尤其是
AudioTrack
播放 - 同时需要将 speak 请求的结果告知 Request App,即通过
UtteranceProgressDispatcher
中转,实际上是调用ITextToSpeechCallback
AIDL - 最后通过
UtteranceProgressListener
告知 TextToSpeech 初始化时设置的各回调
参考
[1] Zephyr Cai, android系统tts TextToSpeech源码原理解析及定制tts引擎, 2020.