天天看点

(语音播报/语音合成/TTS)关于一个语音合成组件封装的正确姿势——支持更换底层实现,上层调用API不变关于一个语音合成组件封装的正确姿势——支持更换底层实现,上层调用API不变

关于一个语音合成组件封装的正确姿势——支持更换底层实现,上层调用API不变

转载请注明出处:http://blog.csdn.net/hnytdangyao/article/details/79174449.

本文出自 [ 党耀的博客 ]

目录

[TOC]

来生成目录:

  • 关于一个语音合成组件封装的正确姿势支持更换底层实现上层调用API不变
      • 目录
    • 干货
      • 架构
      • 一定义统一的管理类SpeakStrategyManager
      • 二 定义上层Interface
      • 三语音合成的具体实现类
    • 关于

最近换了新环境,然后发现公司之前关于语音合成这块底层用的讯飞,但是讯飞的离线语音合成是收费的,咳咳,由于种种原因现在想更换成百度的语音合成(这里不得不夸一下百度爸爸,离线/在线都免费),但是讯飞的又不马上去掉。所以呢我这边就干脆搞一个组件出来,可以自由切换底层的实现,上层直接调用相关API就可以。

干货

架构

首先看一下组件架构

(语音播报/语音合成/TTS)关于一个语音合成组件封装的正确姿势——支持更换底层实现,上层调用API不变关于一个语音合成组件封装的正确姿势——支持更换底层实现,上层调用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 = ;
    public static final int VOICE_TYPE_NOTICE = ;
    public static final int VOICE_OTHER_STATUS = ;
    public static final int VOICE_IM_MESSAGE = ;
    public static final int VOICE_TYPE_NAVI = ;

    //存贮当前播放类型
    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 >= ) {
                    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上,对源码感兴趣的同学也可以留言,欢迎大家来交流。