1. 程式人生 > >(語音播報/語音合成/TTS)關於一個語音合成元件封裝的正確姿勢——支援更換底層實現,上層呼叫API不變

(語音播報/語音合成/TTS)關於一個語音合成元件封裝的正確姿勢——支援更換底層實現,上層呼叫API不變

關於一個語音合成元件封裝的正確姿勢——支援更換底層實現,上層呼叫API不變

目錄

[TOC]來生成目錄:

最近換了新環境,然後發現公司之前關於語音合成這塊底層用的訊飛,但是訊飛的離線語音合成是收費的,咳咳,由於種種原因現在想更換成百度的語音合成(這裡不得不誇一下百度爸爸,離線/線上都免費),但是訊飛的又不馬上去掉。所以呢我這邊就乾脆搞一個元件出來,可以自由切換底層的實現,上層直接呼叫相關API就可以

乾貨

架構

首先看一下元件架構
這裡寫圖片描述

架構說明:元件整體架構用到了Java的策略模式(不懂的同學自行去百度啊,這裡篇幅限制就不多做說明)。base裡是上層介面以及自定義監聽,impl裡放的是具體的實現類,media這個可以不用關心,是為了語音元件的完整加入了播放本地資原始檔的類。至於具體的管理類,也就是我們需要在程式碼裡呼叫的,就是SpeakStrategyManager這個類了,所有呼叫的API定義以及做語音播放分級的操作都在這裡完成。

一、定義統一的管理類——SpeakStrategyManager

先看程式碼,這裡用一個全域性的單例模式實現這個管理類供外部呼叫。

現在市面上主流的幾個語音合成的SDK在播放時都會做了順序播放,就是說不會主動打斷,而是播放完一個繼續播放下一個。

那麼如果我們想要的是新的一個語音播放的需求可以打斷正在播放的語音,這個時候就需要自定義語音的級別,在播放的時候自己處理分級打斷。我這裡的處理方式是高級別的可以打斷低級別的,同級別的可以選擇打斷或者順序播放。

/**
 * Created by dangyao on 2017/11/6.
 * 語音播報元件支援庫的管理類,可以實時更換元件--需實現SpeakStrategy介面
 */
public class SpeakStrategyManager{ private static final String TAG = "SpeakStrategyManager"; //定義語音的等級,這裡根據自己的需求自己設定 值為0時為預設狀態。 public static final int VOICE_TYPE_IDLE = 0; public static final int VOICE_TYPE_NOTICE = 2; public static final int VOICE_OTHER_STATUS = 3; public static
final int VOICE_IM_MESSAGE = 4; public static final int VOICE_TYPE_NAVI = 5; //存貯當前播放型別 private int currentType; private static final SpeakStrategyManager INSTANCE = new SpeakStrategyManager(); private SpeakStrategy mSpeakStrategy; private Context mContext; private SpeakStrategyManager() { //預設使用百度語音播報 mSpeakStrategy = new SpeechSynthesizerStrategy(); } public static SpeakStrategyManager getInstance() { return INSTANCE; } /** * 實時更換語音合成元件,更換後需呼叫init()方法 * @param speakStrategy */ public SpeakStrategyManager setSpeakStrategy(SpeakStrategy speakStrategy){ if(mSpeakStrategy != null){ this.mSpeakStrategy = speakStrategy; } return this; } /** * 初始化語音合成元件 * @param context */ public void init(final Context context){ mContext = context; ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(new Runnable() { @Override public void run() { mSpeakStrategy.init(context); } }); mSpeakStrategy.setOnTTSListener(new TTSListener() { @Override public void onSynthesizeFinish(String s) { } @Override public void onSpeechStart(String s) { } @Override public void onSpeechProgressChanged(String s, int i) { } @Override public void onSpeechFinish(String s) { currentType = VOICE_TYPE_IDLE; } @Override public void onError(String s) { currentType = VOICE_TYPE_IDLE; } }); } /** * 文字轉語音 * @param text */ private void speak(String text) { if (mSpeakStrategy != null) { mSpeakStrategy.speak(text); } } /** * 文字轉語音對外暴露方法 * @param text * @param type 型別 */ public void speak(String text,@NonNull int type) { //這裡如果需要播放本地的資原始檔那麼就需要打斷SDK的語音合成 /*if (type == VOICE_TYPE_REALTIME || type == VOICE_TYPE_RELAY) { //tts如果在播放就停止 if(isSpeaking()){ stop(); } if(!MediaPlayerManager.getInstance().isPlaying()){ MediaPlayerManager.getInstance().create(mContext,type); }else { MediaPlayerManager.getInstance().stop(); MediaPlayerManager.getInstance().create(mContext,type); } return; }*/ if(currentType > type){ return; }else if(currentType <= type){ //不需要同等級打斷就去掉= stop(); } currentType = type; speak(text); } /** * 停止播報語音 */ public void stop(){ currentType = VOICE_TYPE_IDLE; if (mSpeakStrategy != null) { mSpeakStrategy.stop(); } } public boolean isSpeaking(){ if (mSpeakStrategy == null) { return false; } return mSpeakStrategy.isSpeaking(); } public void destroy() { if (mSpeakStrategy == null) { mSpeakStrategy.destroy(); } } }

注意:這裡的SpeechSynthesizerStrategy就是我們使用第三方SDK的具體實現類,也就是說如果我們需要更換底層的實現,那麼就可以替換不同的具體實現類(該實現類需要實現同一個介面)。

二、 定義上層Interface

這裡提供一個上層介面,也就是我們要實現的功能 。播放的API是基礎,作為一個元件肯定還要有初始化以及GC的API。

重點內容
下面上程式碼:

public interface SpeakStrategy {
    /**
     * 初始化
     * @param context
     */
    void init(Context context);
    void setOnTTSListener(TTSListener listener);
    /**
     * 語音播放
     * @param text
     */
    void speak(String text);

    /**
     * 終止語音播放
     */
    void stop();

    /**
     * 是否正在播報語音
     */
    boolean isSpeaking();

    /**
     * 釋放語音合成
     */
    void destroy();

}

這裡加入listener是為了後續做語音播放分級以及主動打斷埋的伏筆,當然如果有另外的需求需要在播放中或者播放完成時做處理也需要這個自定義監聽。

public interface TTSListener {

    /**
     * 語音合成完成
     * @param s
     */
    void onSynthesizeFinish(String s);

    /**
     * 語音播放開始
     * @param s
     */
    void onSpeechStart(String s);

    /**
     * 播放程序中
     * @param s
     * @param i
     */
    void onSpeechProgressChanged(String s, int i);

    /**
     * 語音播放完成
     * @param s
     */
    void onSpeechFinish(String s);

    /**
     * 錯誤資訊
     * @param s
     */
    void onError(String s);

三、語音合成的具體實現類

這裡就簡單了,利用百度語音合成的SDK,實現我們自定義的上層介面。關於SDK的使用有不明白的地方,可以去下百度的demo參考,這裡就不多做說明了。

/**
 * Created by dangyao on 2017/11/6.
 * 百度語音播報元件
 */

public class SpeechSynthesizerStrategy implements SpeakStrategy {

    private SpeechSynthesizerListener mListener;

    private SpeechSynthesizer mSpeechSynthesizer;
    private boolean mSpeaking;

    private static final String APPID = "xxx";

    private static final String APPKEY = "xxx";

    private static final String SECRETKEY = "xxx";
    private OfflineResource offlineResource;


    @Override
    public void init(Context context) {

        initResource(context);
        mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        mSpeechSynthesizer.setContext(context);
        mSpeechSynthesizer.setAppId(APPID);
        mSpeechSynthesizer.setApiKey(APPKEY,SECRETKEY);
        // 文字模型檔案路徑 (離線引擎使用)
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, offlineResource.getTextFilename());
        // 聲學模型檔案路徑 (離線引擎使用)
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, offlineResource.getModelFilename());
        mSpeechSynthesizer.auth(TtsMode.MIX);
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");  //普通女聲
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "6");    //語速,0-9 ,預設 5
        mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE,SpeechSynthesizer.MIX_MODE_HIGH_SPEED_NETWORK);
        mSpeechSynthesizer.initTts(TtsMode.MIX);
        initListener();

    }

    private void initResource(Context context) {

        try {
            offlineResource = new OfflineResource(context, OfflineResource.VOICE_FEMALE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void initListener() {

        mListener = new SpeechSynthesizerListener() {
            @Override
            public void onSynthesizeStart(String s) {}

            @Override
            public void onSynthesizeDataArrived(String s, byte[] bytes, int i) {}

            @Override
            public void onSynthesizeFinish(String s) {}

            @Override
            public void onSpeechStart(String s) {}

            @Override
            public void onSpeechProgressChanged(String s, int i) {
                mTTSListener.onSpeechProgressChanged(s,i);
                if (i >=0 ) {
                    mSpeaking = true;
                }else {
                    mSpeaking = false;
                }
            }

            @Override
            public void onSpeechFinish(String s) {
                mTTSListener.onSpeechFinish(s);
            }

            @Override
            public void onError(String s, SpeechError speechError) {
                mTTSListener.onError(s);
            }
        };

        if(mSpeechSynthesizer != null ){
            mSpeechSynthesizer.setSpeechSynthesizerListener(mListener);
        }


    }

    @Override
    public void speak(String text) {
        if(mSpeechSynthesizer != null){
            mSpeechSynthesizer.speak(text);
        }

    }

    @Override
    public void stop() {
        if(mSpeechSynthesizer != null){
            mSpeechSynthesizer.stop();
        }

    }

    @Override
    public boolean isSpeaking() {
        return mSpeaking;
    }

    private TTSListener mTTSListener;
    @Override
    public void setOnTTSListener(TTSListener listener){
        mTTSListener = listener;
    }

    public void destroy() {
        if(mSpeechSynthesizer != null) {
            mSpeechSynthesizer.stop();
            mSpeechSynthesizer.release();
            mSpeechSynthesizer = null;
        }
    }

}

注意:OfflineResource是百度離線語音合成讀取資源用到的。如果純線上語音合成就不需要操作離線資源,直接設定 mSpeechSynthesizer.initTts(TtsMode.ONLINE) 即可。

到這裡就差不多可以實現我們自己的語音合成元件了,再次重申一下,如果更換底層實現,也就是更換語音合成的SDK,那麼就需要利用SDK來實現我們定義的上層介面。對應的,在manager裡就需要呼叫setSpeakStrategy方法傳入實現類,並且執行初始化操作。

另外補充一點,針對init方法,在呼叫的時候個人建議做一個非同步操作,在application中另外開啟一個執行緒執行,這樣就不會阻塞我們的UI執行緒導致應用開啟卡頓。

關於

這裡只是初步提供實現一個元件的思路,算是自己分享的嘗試吧,由於開發中各自的需求都不一致,放在這裡篇幅也顯得太長,後續會將元件分享到github上,對原始碼感興趣的同學也可以留言,歡迎大家來交流。