Android开发语音助手插件(AIDL跨进程通信)

最近无意中发现有需要做语音播报插件的需求,因为考虑到语音助手集成会使得原应用程序包体积变大,所以采取插件的形式去从外部下载,然后安装这样一个插件即可完成语音合成(文字转成语音)。主要用到的技术就是集成 百度语音合成SDK,AIDL跨进程通信。 github下载地址

前戏

无图无真相,关键这并没有什么绚丽的特效,哈哈 ~ 主要功能就是使用插件apk去朗读主apk输入的文字,两个apk用AIDL跨进程通信。

Android开发语音助手插件(AIDL跨进程通信)_第1张图片
截屏_20170311_164451.jpg

集成百度语音合成SDK

这个集成也没啥难度,大家可以取参考上面链接对应的开发文档去看一下。简单说下几个重要步骤:
1.下载语音合成SDK,包含Sample里面,但是是Eclipse的工程,所以这里还需要自己新建一个AS工程。
2.拷贝资源文件到自己项目里面,再把SO文件拷贝到libs目录下,并把jar包add到项目里面,还需要在gradle文件里面去把libs目录加到项目里面,如下图所示:


Android开发语音助手插件(AIDL跨进程通信)_第2张图片
添加资源文件及so文件

3.在AndroidManifest.xml文件配置需要的权限:









官网上是吧Appid,Key都放到AndroidManifest.xml里面,我这里想在代码里面设置,等下看下代码就知道。

编写AIDL通信

首先在main目录下新建aidl目录,并且新建一个aidl类,如图所示:aidl类很简单,就是一个接口,因为我们是要接收文字然后读取的所以定义了一个方法:void speak(String text);

Android开发语音助手插件(AIDL跨进程通信)_第3张图片
新建aidl

这个时候可以rebuild一把这个插件module,这样会生成该接口对应的java文件,这个文件里面虽然不用我们自己去写,自动生成,但是我们要知道里面有哪些主要的方法。

Android开发语音助手插件(AIDL跨进程通信)_第4张图片
生成对应java文件

好了 ,现在我们需要一个Service在后台默默的等待着主app去调用它,所以这个Service应该具有可读取传过来文字的功能,我们来看一下这个Service代码:


import android.app.Service;
import android.content.Intent;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import com.baidu.tts.auth.AuthInfo;
import com.baidu.tts.client.SpeechError;
import com.baidu.tts.client.SpeechSynthesizer;
import com.baidu.tts.client.SpeechSynthesizerListener;
import com.baidu.tts.client.TtsMode;
import com.wzh.aidl.IVoiceInterface;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * author:Administrator on 2017/3/10 16:49
 * description:文件说明
 * version:版本
 */
public class VoiceService extends Service implements SpeechSynthesizerListener {
    private SpeechSynthesizer mSpeechSynthesizer;
    private static final String TAG = "AIDLService";
    private static final String SAMPLE_DIR_NAME = "baiduTTS";
    private static final String SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female.dat";
    private static final String SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male.dat";
    private static final String TEXT_MODEL_NAME = "bd_etts_text.dat";
    private static final String LICENSE_FILE_NAME = "temp_license";
    private static final String ENGLISH_SPEECH_FEMALE_MODEL_NAME = "bd_etts_speech_female_en.dat";
    private static final String ENGLISH_SPEECH_MALE_MODEL_NAME = "bd_etts_speech_male_en.dat";
    private static final String ENGLISH_TEXT_MODEL_NAME = "bd_etts_text_en.dat";
    private String mSampleDirPath;
    private static final int PRINT = 0;
    private static final int UI_CHANGE_INPUT_TEXT_SELECTION = 1;
    private static final int UI_CHANGE_SYNTHES_TEXT_SELECTION = 2;
    @Override
    public void onCreate() {
        super.onCreate();
        initialEnv();
        initialTts();
    }
    private void initialEnv() {
        if (mSampleDirPath == null) {
            String sdcardPath = Environment.getExternalStorageDirectory().toString();
            mSampleDirPath = sdcardPath + "/" + SAMPLE_DIR_NAME;
        }
        makeDir(mSampleDirPath);
        copyFromAssetsToSdcard(false, SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/" + SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, TEXT_MODEL_NAME, mSampleDirPath + "/" + TEXT_MODEL_NAME);
        copyFromAssetsToSdcard(false, LICENSE_FILE_NAME, mSampleDirPath + "/" + LICENSE_FILE_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_SPEECH_MALE_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_SPEECH_MALE_MODEL_NAME);
        copyFromAssetsToSdcard(false, "english/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath + "/"
                + ENGLISH_TEXT_MODEL_NAME);
    }
    private void makeDir(String dirPath) {
        File file = new File(dirPath);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    /**
     * 将sample工程需要的资源文件拷贝到SD卡中使用(授权文件为临时授权文件,请注册正式授权)
     *
     * @param isCover 是否覆盖已存在的目标文件
     * @param source
     * @param dest
     */
    private void copyFromAssetsToSdcard(boolean isCover, String source, String dest) {
        File file = new File(dest);
        if (isCover || (!isCover && !file.exists())) {
            InputStream is = null;
            FileOutputStream fos = null;
            try {
                is = getResources().getAssets().open(source);
                String path = dest;
                fos = new FileOutputStream(path);
                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = is.read(buffer, 0, 1024)) >= 0) {
                    fos.write(buffer, 0, size);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private void initialTts() {
        this.mSpeechSynthesizer = SpeechSynthesizer.getInstance();
        this.mSpeechSynthesizer.setContext(this);
        this.mSpeechSynthesizer.setSpeechSynthesizerListener(this);
        // 文本模型文件路径 (离线引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, mSampleDirPath + "/"
                + TEXT_MODEL_NAME);
        // 声学模型文件路径 (离线引擎使用)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, mSampleDirPath + "/"
                + SPEECH_FEMALE_MODEL_NAME);
        // 本地授权文件路径,如未设置将使用默认路径.设置临时授权文件路径,LICENCE_FILE_NAME请替换成临时授权文件的实际路径,仅在使用临时license文件时需要进行设置,如果在[应用管理]中开通了正式离线授权,不需要设置该参数,建议将该行代码删除(离线引擎)
        // 如果合成结果出现临时授权文件将要到期的提示,说明使用了临时授权文件,请删除临时授权即可。
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_LICENCE_FILE, mSampleDirPath + "/"
                + LICENSE_FILE_NAME);
        // 请替换为语音开发者平台上注册应用得到的App ID (离线授权)
        this.mSpeechSynthesizer.setAppId("9378271"/*这里只是为了让Demo运行使用的APPID,请替换成自己的id。*/);
        // 请替换为语音开发者平台注册应用得到的apikey和secretkey (在线授权)
        this.mSpeechSynthesizer.setApiKey("RC33pEHG3Kd3DOOrI2GMhHMC",
                "e8d7636d58dc96322ab704aec5999744"/*这里只是为了让Demo正常运行使用APIKey,请替换成自己的APIKey*/);
        // 发音人(在线引擎),可用参数为0,1,2,3。。。(服务器端会动态增加,各值含义参考文档,以文档说明为准。0--普通女声,1--普通男声,2--特别男声,3--情感男声。。。)
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0");
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "6");
        // 设置Mix模式的合成策略
        this.mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_DEFAULT);
        // 授权检测接口(只是通过AuthInfo进行检验授权是否成功。)
        // AuthInfo接口用于测试开发者是否成功申请了在线或者离线授权,如果测试授权成功了,可以删除AuthInfo部分的代码(该接口首次验证时比较耗时),不会影响正常使用(合成使用时SDK内部会自动验证授权)
        AuthInfo authInfo = this.mSpeechSynthesizer.auth(TtsMode.MIX);

        if (authInfo.isSuccess()) {
        } else {
            String errorMsg = authInfo.getTtsError().getDetailMessage();
        }
        // 初始化tts
        mSpeechSynthesizer.initTts(TtsMode.MIX);
        // 加载离线英文资源(提供离线英文合成功能)
        int result =
                mSpeechSynthesizer.loadEnglishModel(mSampleDirPath + "/" + ENGLISH_TEXT_MODEL_NAME, mSampleDirPath
                        + "/" + ENGLISH_SPEECH_FEMALE_MODEL_NAME);
    }
    IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
        @Override
        public void speak(String text) throws RemoteException {
            int result = mSpeechSynthesizer.speak(text);
            if (result < 0) {
                Log.i(TAG, "failed");
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind() called");
        return stub;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.i(TAG, "onUnbind() called");
        this.mSpeechSynthesizer.release();
        return true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy() called");
    }

    /*
     * @param arg0
     */
    @Override
    public void onSynthesizeStart(String utteranceId) {
    }

    /**
     * 合成数据和进度的回调接口,分多次回调
     *
     * @param utteranceId
     * @param data 合成的音频数据。该音频数据是采样率为16K,2字节精度,单声道的pcm数据。
     * @param progress 文本按字符划分的进度,比如:你好啊 进度是0-3
     */
    @Override
    public void onSynthesizeDataArrived(String utteranceId, byte[] data, int progress) {

    }

    /**
     * 合成正常结束,每句合成正常结束都会回调,如果过程中出错,则回调onError,不再回调此接口
     *
     * @param utteranceId
     */
    @Override
    public void onSynthesizeFinish(String utteranceId) {

    }

    /**
     * 播放开始,每句播放开始都会回调
     *
     * @param utteranceId
     */
    @Override
    public void onSpeechStart(String utteranceId) {

    }

    /**
     * 播放进度回调接口,分多次回调
     *
     * @param utteranceId
     * @param progress 文本按字符划分的进度,比如:你好啊 进度是0-3
     */
    @Override
    public void onSpeechProgressChanged(String utteranceId, int progress) {

    }

    /**
     * 播放正常结束,每句播放正常结束都会回调,如果过程中出错,则回调onError,不再回调此接口
     *
     * @param utteranceId
     */
    @Override
    public void onSpeechFinish(String utteranceId) {

    }

    /**
     * 当合成或者播放过程中出错时回调此接口
     *
     * @param utteranceId
     * @param error 包含错误码和错误信息
     */
    @Override
    public void onError(String utteranceId, SpeechError error) {

    }
}

这里部分代码可以参考百度语音助手文档,我们来看主要的一个地方:

 IVoiceInterface.Stub stub = new IVoiceInterface.Stub() {
        @Override
        public void speak(String text) throws RemoteException {
            int result = mSpeechSynthesizer.speak(text);
            if (result < 0) {
                Log.i(TAG, "failed");
            }
        }
    };
 @Override
 public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind() called");
        return stub;
 }

这里声明的stub对象我们可以看到,在这个方法里面是实现了读取文字的功能,因为他是集成IBinder 的,所以可以直接返回。
好了,到现在基本这个插件已经具备语音合成的功能了,但是我不想要图标显示在桌面,所以这里去掉图标,并且注册Service,这里的action我们要记住,因为客户端也需要用到:



    
        
            
            
        
    

到这里插件基本可以使用了,现在来新建一个客户端程序。
拷贝aidl包及aidl文件,要保持和插件的aidl包名及文件一致。
新建一个布局一个按钮一个EditText,我们要实现的就是输入文字,插件去郎读。

import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.wzh.aidl.IVoiceInterface;

public class MainActivity extends AppCompatActivity {
    private EditText input ;
    private Button button ;
    /**
     * 创建远程服务
     */
    private IVoiceInterface iVoiceInterface;
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iVoiceInterface = IVoiceInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iVoiceInterface = null ;
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /**
         * 绑定、启动所谓的服务端 服务
         */
        final Intent intent = new Intent();
        intent.setAction("android.intent.action.VoiceService");
        intent.setPackage("com.wzh.baiduvoice");
        bindService(intent,conn, Service.BIND_AUTO_CREATE);

        input = (EditText) findViewById(R.id.input);
        button = (Button) findViewById(R.id.speak);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String text = input.getText().toString();
                if (iVoiceInterface!=null){
                    try {
                        iVoiceInterface.speak(text);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

我们在创建Activity的时候去绑定插件的那个Service,新建一个ServiceConnection并获取iVoiceInterface对象,当点击朗读按钮的时候调用插件Service的speak( )方法。
这里大家可以发现是显示启动Service的,因为Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service.也禁止使用Intent filter,隐式启动会报错,所以采用这种启动方式。

如此一来我们输入文字,就可以听到插件给我们朗读的声音!

其实这里我遇到一个问题,就是这个插件apk可能会被有的系统自带的手机管家禁止关联启动,我用的vivo x6splus测试就是被禁止了,目前还没找到比较好的解决方案,希望有知道的同学告之~

这里其实只是AIDL最简单的使用方式了,如果有问题请指出,谢谢~

你可能感兴趣的:(Android开发语音助手插件(AIDL跨进程通信))