直面原理:5 张图彻底了解 Android TextToSpeech 机制

news2024/9/23 18:20:08

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) {
                    ...
                    mSynthesisCallback = createSynthesisCallback();
                    synthesisCallback = mSynthesisCallback;
                }
    
                TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
                if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
                    synthesisCallback.done();
                }
            }
            ...
        }
    
  4. onSynthesizeText() 是 abstract 方法,需要 Engine 复写以将 text 合成 audio 数据,也是 TTS 功能里最核心的实现。

    • Engine 需要从 SynthesisRequest 中提取 speak 的目标文本、参数等信息,针对不同信息进行区别处理。并通过 SynthesisCallback 的各接口将数据和时机带回:
    • 在数据合成前,通过 start() 告诉系统生成音频的采样频率,多少位 pcm 格式音频,几通道等等。PlaybackSynthesisCallback 的实现将会创建播放的 SynthesisPlaybackQueueItem 交由 AudioPlaybackHandler 去排队调度
    • 之后,通过 audioAvailable() 接口将合成的数据以 byte[] 形式传递回来,会取出 start() 时创建的 QueueItem put 该 audio 数据开始播放
    • 最后,通过 done() 告知合成完毕
    // PlaybackSynthesisCallback.java
    class PlaybackSynthesisCallback extends AbstractSynthesisCallback {
        ...
        @Override
        public int start(int sampleRateInHz, int audioFormat, int channelCount) {
            mDispatcher.dispatchOnBeginSynthesis(sampleRateInHz, audioFormat, channelCount);
    
            int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);
    
            synchronized (mStateLock) {
                ...
                SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(
                        mAudioParams, sampleRateInHz, audioFormat, channelCount,
                        mDispatcher, mCallerIdentity, mLogger);
                mAudioTrackHandler.enqueue(item);
                mItem = item;
            }
    
            return TextToSpeech.SUCCESS;
        }
    
        @Override
        public int audioAvailable(byte[] buffer, int offset, int length) {
            SynthesisPlaybackQueueItem item = null;
            synchronized (mStateLock) {
                ...
                item = mItem;
            }
    
            final byte[] bufferCopy = new byte[length];
            System.arraycopy(buffer, offset, bufferCopy, 0, length);
            mDispatcher.dispatchOnAudioAvailable(bufferCopy);
    
            try {
                item.put(bufferCopy);
            }
            ...
            return TextToSpeech.SUCCESS;
        }
    
        @Override
        public int done() {
            int statusCode = 0;
            SynthesisPlaybackQueueItem item = null;
            synchronized (mStateLock) {
                ...
                mDone = true;
    
                if (mItem == null) {
                    if (mStatusCode == TextToSpeech.SUCCESS) {
                        mDispatcher.dispatchOnSuccess();
                    } else {
                        mDispatcher.dispatchOnError(mStatusCode);
                    }
                    return TextToSpeech.ERROR;
                }
    
                item = mItem;
                statusCode = mStatusCode;
            }
    
            if (statusCode == TextToSpeech.SUCCESS) {
                item.done();
            } else {
                item.stop(statusCode);
            }
            return TextToSpeech.SUCCESS;
        }
        ...
    }
    

    上述的 QueueItem 的放置 audio 数据和消费的逻辑如下,主要是 put 操作触发 Lock 接口的 take Condition 恢复执行,最后调用 AudioTrack 去播放。

    // SynthesisPlaybackQueueItem.java
    final class SynthesisPlaybackQueueItem ... {
        void put(byte[] buffer) throws InterruptedException {
            try {
                mListLock.lock();
                long unconsumedAudioMs = 0;
                ...
                mDataBufferList.add(new ListEntry(buffer));
                mUnconsumedBytes += buffer.length;
                mReadReady.signal();
            } finally {
                mListLock.unlock();
            }
        }
    
        private byte[] take() throws InterruptedException {
            try {
                mListLock.lock();
    
                while (mDataBufferList.size() == 0 && !mStopped && !mDone) {
                    mReadReady.await();
                }
                ...
                ListEntry entry = mDataBufferList.poll();
                mUnconsumedBytes -= entry.mBytes.length;
                mNotFull.signal();
    
                return entry.mBytes;
            } finally {
                mListLock.unlock();
            }
        }
    
        public void run() {
            ...
            final UtteranceProgressDispatcher dispatcher = getDispatcher();
            dispatcher.dispatchOnStart();
    
            if (!mAudioTrack.init()) {
                dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);
                return;
            }
    
            try {
                byte[] buffer = null;
                while ((buffer = take()) != null) {
                    mAudioTrack.write(buffer);
                }
    
            } ...
            mAudioTrack.waitAndRelease();
            dispatchEndStatus();
        }
    
        void done() {
            try {
                mListLock.lock();
                mDone = true;
                mReadReady.signal();
                mNotFull.signal();
            } finally {
                mListLock.unlock();
            }
        }
    }
    
  5. 上述 PlaybackSynthesisCallback 在通知 QueueItem 的同时,会通过 UtteranceProgressDispatcher 接口将数据、结果一并发送给 Request App。

    // TextToSpeechService.java
        interface UtteranceProgressDispatcher {
            void dispatchOnStop();
    
            void dispatchOnSuccess();
    
            void dispatchOnStart();
    
            void dispatchOnError(int errorCode);
    
            void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount);
    
            void dispatchOnAudioAvailable(byte[] audio);
    
            public void dispatchOnRangeStart(int start, int end, int frame);
        }
    

    事实上该接口的实现就是 TextToSpeechService 处理 speak 请求的 UtteranceSpeechItem 实例,其通过缓存着各 ITextToSpeechCallback 接口实例的 CallbackMap 发送回调给 TTS 请求的 App。(这些 Callback 来自于 TextToSpeech 初始化时候通过 ITextToSpeechService 将 Binder 接口传递来和缓存起来的。)

        private abstract class UtteranceSpeechItem extends SpeechItem
            implements UtteranceProgressDispatcher  {
            ...
            @Override
            public void dispatchOnStart() {
                final String utteranceId = getUtteranceId();
                if (utteranceId != null) {
                    mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
                }
            }
    
            @Override
            public void dispatchOnAudioAvailable(byte[] audio) {
                final String utteranceId = getUtteranceId();
                if (utteranceId != null) {
                    mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);
                }
            }
    
            @Override
            public void dispatchOnSuccess() {
                final String utteranceId = getUtteranceId();
                if (utteranceId != null) {
                    mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
                }
            }
    
            @Override
            public void dispatchOnStop() { ... }
    
            @Override
            public void dispatchOnError(int errorCode) { ... }
    
            @Override
            public void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) { ... }
    
            @Override
            public void dispatchOnRangeStart(int start, int end, int frame) { ... }
        }
    
        private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
            ...
            public void dispatchOnStart(Object callerIdentity, String utteranceId) {
                ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
                if (cb == null) return;
                try {
                    cb.onStart(utteranceId);
                } ...
            }
    
            public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {
                ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
                if (cb == null) return;
                try {
                    cb.onAudioAvailable(utteranceId, buffer);
                } ...
            }
    
            public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
                ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
                if (cb == null) return;
                try {
                    cb.onSuccess(utteranceId);
                } ...
            }
            ...
        }
    
  6. ITextToSpeechCallback 的执行将通过 TextToSpeech 的中转抵达请求 App 的 Callback,以执行“TextToSpeech 调用”章节提到的进一步操作

    // TextToSpeech.java
    public class TextToSpeech {
        ...
        private abstract class Connection implements ServiceConnection {
            ...
            private final ITextToSpeechCallback.Stub mCallback =
                    new ITextToSpeechCallback.Stub() {
                        @Override
                        public void onStart(String utteranceId) {
                            UtteranceProgressListener listener = mUtteranceProgressListener;
                            if (listener != null) {
                                listener.onStart(utteranceId);
                            }
                        }
                        ...
                    };
        }
    }
        
    // TTSTest.kt
    class TTSTest(context: Context) {
        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?)  { ... }
            })
        }
        ....
    }
    

5 使用和实现上的注意

对于 TTS 请求方有几点使用上的建议:

  1. TTS 播报前记得申请对应 type 的音频焦点
  2. TTS Request App 的 Activity 或 Service 生命周期销毁的时候,比如 onDestroy() 等时候,需要调用 TextToSpeech 的 shutdown() 释放连接、资源
  3. 可以通过 addSpeech() 指定固定文本的对应 audio 资源(比如说语音里常用的几套唤醒后的欢迎词 audio),在后续的文本请求时直接播放该 audio,免去文本转语音的过程、提高效率

对于 TTS Engine 提供方也有几点实现上的建议:

  1. TTS Engine 的各实现要和 TTS 的 SynthesisCallback 做好对接,要留意只能在该 callback 已经执行了 start() 并未结束的条件下调用 done()。否则 TTS 会发生如下两种错误:

    • Duplicate call to done()
    • done() was called before start() call
  2. TTS Engine 核心作用是将 text 文本合成 speech 音频数据,合成到数据之后 Engine 当然可以选择直接播报,甚至不回传音频数据。但建议将音频数据回传,交由系统 AudioTrack 播报。一来交由系统统一播报;二来 Request App 亦可以拿到音频数据进行 cache 和分析

6 结语

可以看到 Request App 不关心实现、只需通过 TextToSpeech 几个 API 便可完成 TTS 的播报操作。而且 TTS 的实现也只需要按照 TextToSpeechService 约定的框架、回调实现即可,和 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 初始化时设置的各回调

7 语音文章推荐

  • 如何打造车载语音交互:Google Voice Interaction 给你答案

8 参考 / 源码

  • OnUtteranceCompletedListener 已废弃
  • TextToSpeech.java
  • TextToSpeechManagerService.java
  • TextToSpeechService.java

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

rar压缩文件暴力破解

1. 简介 rar 压缩文件资源又不少是被加密的&#xff0c;密码通常也比较简单&#xff0c;我们可以通过暴力破解的方式来获取&#xff0c;通常耗时也比较小 程序地址&#xff1a;rar压缩密码暴力破解程序 2. 使用说明 2.1 基本语法 rar-bruteforce-crack.py [--start START] …

【visio使用技巧】图片导出pdf时去掉多余空白

问题 在visio导出pdf格式的图片时&#xff0c;往往会存在多余的白边&#xff0c;如下图所示&#xff1a; 解决方法 依次点击&#xff1a;菜单栏→文件→选项→自定义功能区→勾选“开发工具”→确定。 依次点击菜单栏→开发工具→显示ShapeSheet→页→Print Properties→将…

Ros中如何给UR5配置自定义工具 | 在Rviz中给UR5机器人装载定义工具 | UR5配置自定义末端执行器

前言 在学习和项目研究的过程中&#xff0c;我需要在Ur5e上装上工具&#xff0c;以对现实场景进行仿真。网上会有一些装载/配置现成的夹爪&#xff0c;例如Robotiq等。但和我们装载自定义工具的场景还有些差异&#xff0c;因此写一篇博客记录&#xff0c;可能有偏差。如果有问…

宝塔面板轻量云服务器部署spring boot java后端、vite vue3前端、nginx。

环境 初始时宝塔面板推荐应用安装选择 根据开发或者测试。选择安装配置&#xff0c;等待两个安装任务完成。 niginxmysql 安装jdk 显示jdk列表 yum -y list java* 安装指定版本 yum install -y java-1.8.0-openjdk.x86_64 默认安装到 usr/lib/jvm/ &#xff0c;无需再配置…

干货 | 简单了解运算放大器...

运算放大器发明至今已有数十年的历史&#xff0c;从最早的真空管演变为如今的集成电路&#xff0c;它在不同的电子产品中一直发挥着举足轻重的作用。而现如今信息家电、手机、PDA、网络等新兴应用的兴起更是将运算放大器推向了一个新的高度。01 运算放大器简述运算放大器&#…

Linux环境变量讲解

目录 环境变量 alias命令 type命令 变量分类 Linux最主要的全局环境变量 环境变量 变量是计算机系统用于保存可变数值的数据类型 在Linux中&#xff0c;一般变量都是大写&#xff0c;命令是小写 在Linux中&#xff0c;变量直接使用&#xff0c;不需要定义&#xff08;更快…

Spring框架自定义实现IOC基础功能/IDEA如何手动实现IOC功能

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库Java设计模式克隆下载学习使用&#xff01; 7.4 自定义Spring IOC 创建新模块&#xff0c;结构如图![[Pasted image 20230210173222.png]] 7.4.1 定义bean相关POJO类 7.4.1.1 定义propertyValue类 /** …

Linux--POSIX信号量--基于环形队列的生产消费模型-0208

1. 什么是信号量 共享资源由于原子性的原则&#xff0c;任何时刻都只有一个执行流在进行访问。表现为互斥&#xff0c;也就代表共享资源起始是被当做整体来访问的。 那如果有一个共享资源&#xff0c;不当成一个整体&#xff0c;让不同的执行流访问不同的资源区域代码&#x…

67 自注意力【动手学深度学习v2】

67 自注意力【动手学深度学习v2】 深度学习学习笔记 学习视频&#xff1a;https://www.bilibili.com/video/BV19o4y1m7mo/?spm_id_fromautoNext&vd_source75dce036dc8244310435eaf03de4e330 给定长为n 的序列&#xff0c;每个xi为长为d的向量&#xff0c;自注意力将xi 既当…

Java中的异常处理

1.概述 在 Java 中&#xff0c;所有的异常都有一个共同的祖先java.lang包中的 Throwable类。 异常是程序中的一些错误&#xff0c;但并不是所有的错误都是异常&#xff0c;并且错误有时候是可以避免的。 比如说&#xff0c;你的代码少了一个分号&#xff0c;那么运行出来结果…

二叉树和堆的讲解和实现(图解+代码/C语言)

今天和大家分享的是二叉树的实现&#xff0c;关于遍历二叉树部分均采用递归的方式实现&#xff0c;最后附上部分OJ题供大家练习。 文章目录一、树的概念及结构1.1 树的概念1.2 树的相关概念1.3 树的表示二、二叉树的概念及结构2.1 概念2.2 二叉树的性质2.3 二叉树的存储结构2.…

proxy代理与reflect反射

proxy代理与reflect 在这之前插入一个知识点arguments&#xff0c;每个函数里面都有一个arguments&#xff0c;执行时候不传默认是所有参数&#xff0c;如果传了就是按顺序匹配&#xff0c;箭头函数没有 代理函数 代理对象也就是生成一个替身&#xff0c;然后这个替身处理一切的…

【深度学习】认识神经网络

上一章——过拟合与正则化 从本章开始我们将学习深度学习的内容&#xff1a;包括神经网络和决策树等高级算法 文章目录神经网络的生物学原理神经网络的算法结构案例——图像感知神经网络的前向传播神经网络的生物学原理 在最开始&#xff0c;人们想要构建一个能够模拟生物大脑…

Python __doc__属性:查看文档

在使用 dir() 函数和 __all__ 变量的基础上&#xff0c;虽然我们能知晓指定模块&#xff08;或包&#xff09;中所有可用的成员&#xff08;变量、函数和类&#xff09;&#xff0c;比如&#xff1a;import string print(string.__all__)程序执行结果为&#xff1a;[ascii_lett…

Zabbix 构建监控告警平台(六)

监控TCP连接监控MySQL监控php-fpm监控 Apache监控 MySQL A-B监控磁盘I/O1.监控TCP连接 1.1 tcp状态简介 netstat中的各种状态&#xff1a; CLOSED 初始&#xff08;无连接&#xff09;状态。 LISTEN 侦听状态&#xff0c;等待远程机器的连接…

自动驾驶规控课程学习——决策规划

行为决策系统的规划1 行为决策基础1.1 基本概念与任务行为类型&#xff1a;系统输入输出&#xff1a;输入&#xff1a;定位、感知、地图等输出&#xff1a;决策意图小例子&#xff1a;1.2决策系统的评价与挑战评价指标挑战&#xff08;1&#xff09;决策密度&#xff08;2&…

卡尔曼滤波器与DSP实现

卡尔曼滤波器是利用系统状态方程&#xff0c;结合测量结果对系统状态进行进行最优估计的算法。本文介绍它的主要公式&#xff0c;并举例在C6000 DSP上实现。 推荐资料 KalmanFilter.NETUnderstanding Kalman Filters卡尔曼滤波与组合导航原理 “If you can’t explain it sim…

rust 程序设计语言入门(1)

本文是阅读《Rust程序设计语言》的学习记录&#xff0c;配合视频《Rust编程语言入门教程》食用更佳 环境搭建 windows下载rustup_init.exe&#xff0c;点击安装&#xff0c;默认选择msvc的toolchain&#xff0c;一路default即可 解决下载慢的问题&#xff0c;在powershell中修…

libxlsxwriter条件格式

今天来看一个libxlsxwriter的高级用法&#xff1a;一个条件格式的示例。 说它“高级”&#xff0c;也是基于非Excel专家的小白们的视角。对&#xff0c;没错&#xff0c;本小白正是这样的小白。 1 一个简单的问题 来看我们今天的场景问题&#xff1a;有一列数据&#xff0c;有…

操作系统(一): 进程和线程,进程的多种状态以及进程的调度算法

文章目录前言一、进程和线程1. 进程2. 线程二、进程和线程的区别(面试常问)三、进程调度算法3.1. 批处理系统3.2. 交互式系统3.2.1 时间片轮转3.2.2 优先级调度3.2.3 多级别反馈队列3.3. 实时系统四、进程的状态五、进程同步5.1 什么是进程同步5.2 进程同步应该遵循的几点原则前…