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 调用 
ITextToSpeechServiceAIDL 的 speak() 继续 TextToSpeechService收到后封装请求SynthesisRequest和用于回调结果的SynthesisCallback实例- 之后将两者作为参数调用核心实现 
onSynthesizeText(),其将解析 Request 并进行 Speech 音频数据合成 - 此后通过 SynthesisCallback 将合成前后的关键回调告知系统,尤其是 
AudioTrack播放 - 同时需要将 speak 请求的结果告知 Request App,即通过 
UtteranceProgressDispatcher中转,实际上是调用ITextToSpeechCallbackAIDL - 最后通过 
UtteranceProgressListener告知 TextToSpeech 初始化时设置的各回调 
参考
[1] Zephyr Cai, android系统tts TextToSpeech源码原理解析及定制tts引擎, 2020.