今天就顺便来拆解一下用到的核心,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里面的步骤分成三步

  1. 初始化tts服务和tts的引擎,初始化成功,会有onInit回调
  2. 给tts设置一个监听,用于监听tts引擎处理的状态,可以是onStart,onDone,onError,还可以有原数数据的处理
  3. 开始进行播报文字,这里需要tts最核心的引擎处理

3源码解析

为了看明白后续的源码,这里简单梳理了比较重要的几个类图,分别是SpeechItemAbstractSynthesisCallbackPlaybackQueueItem

3.1init 绑定

首先从 TextToSpeech() 的实现入手,以了解在 TTS 播报之前,系统和 TTS Engine 之间做了什么准备工作。

3.1.1 initTTS

将按照如下顺序查找需要连接到哪个 Engine

  1. 如果构造 TTS 接口的实例时指定了目标 Engine 的 package,那么首选连接到该 Engine
  2. 反之,获取设备设置的 default Engine 并连接,设置来自于 TtsEngines 从系统设置数据 SettingsProvider 中 读取 TTS_DEFAULT_SYNTH 而来
  3. 如果 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

  1. speak 请求封装给 Handler 的是 SynthesisSpeechItem
  2. 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与框架的交互过程。

  1. TTS Request App 调用 TextToSpeech 构造函数,由系统准备播报工作前的准备,比如通过 Connection 绑定和初始化目标的 TTS Engine
  2. Request App 提供目标 text 并调用 speak() 请求
  3. TextToSpeech 会检查目标 text 是否设置过本地的 audio 资源,没有的话回通过 Connection 调用 ITextToSpeechService AIDL 的 speak() 继续
  4. TextToSpeechService 收到后封装请求 SynthesisRequest 和用于回调结果的 SynthesisCallback 实例
  5. 之后将两者作为参数调用核心实现 onSynthesizeText(),其将解析 Request 并进行 Speech 音频数据合成
  6. 此后通过 SynthesisCallback 将合成前后的关键回调告知系统,尤其是 AudioTrack 播放
  7. 同时需要将 speak 请求的结果告知 Request App,即通过 UtteranceProgressDispatcher中转,实际上是调用 ITextToSpeechCallback AIDL
  8. 最后通过 UtteranceProgressListener 告知 TextToSpeech 初始化时设置的各回调

参考

[1] Zephyr Cai, android系统tts TextToSpeech源码原理解析及定制tts引擎, 2020.

[2] 小虾米君, 直面原理:5 张图彻底了解 Android TextToSpeech 机制, 2023.