直面原理: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 之间做了什么准备工作。

  1. 其触发的 initTTS() 将按照如下顺序查找需要连接到哪个 Engine:

    • 如果构造 TTS 接口的实例时指定了目标 Engine 的 package,那么首选连接到该 Engine
    • 反之,获取设备设置的 default Engine 并连接,设置来自于 TtsEngines 从系统设置数据 SettingsProvider 中 读取 TTS_DEFAULT_SYNTH 而来
    • 如果 default 不存在或者没有安装的话,从 TtsEngines 获取第一位的系统 Engine 并连接。第一位指的是从所有 TTS Service 实现 Engine 列表里获得第一个属于 system image 的 Engine
  2. 连接的话均是调用 connectToEngine(),其将依据调用来源来采用不同的 Connection 内部实现去 connect()

    • 如果调用不是来自 system,采用 DirectConnection

      • 其 connect() 实现较为简单,封装 Action 为 INTENT_ACTION_TTS_SERVICE 的 Intent 进行 bindService(),后续由 AMS 执行和 Engine 的绑定,这里不再展开
    • 反之,采用 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

  3. connect 执行完毕并结果 OK 的话,还要暂存到 mConnectingServiceConnection,以在结束 TTS 需求的时候释放连接使用。并通过 dispatchOnInit() 传递 SUCCESS 给 Request App

    • 实现很简单,将结果 Enum 回调给初始化传入的 OnInitListener 接口
  4. 如果连接失败的话,则调用 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(),系统做了什么具体实现。

  1. 首先将 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);
                    
                    ...
                
            
        
    
    
  2. 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 的实现环节。

4 TextToSpeechService 实现

  1. 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( ... );
                        ...
                    
                    ...
                ;
        ...
    
    
  2. 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;
                
            
            ...
        
    
  3. 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整个的知识体系

    两张图彻底搞懂MyBatis的Mapper原理!

    图解源码 | 两张图彻底搞懂MyBatis的Mapper原理!

    13张图彻底搞懂分布式系统服务注册与发现原理

    1.4 w字,25 张图让你彻底掌握分布式事务原理

    七张图彻底讲清楚ZooKeeper分布式锁的实现原理石杉的架构笔记