android MediaCodec 音频解码+讯飞语音 实现本地语音转文字功能

android MediaCodec 音频解码+讯飞语音 实现本地语音转文字功能

因项目需要,需求是:把语音消息转换成文本,功能类似微信的语音转文字。于是下载了科大讯飞的语音sdk,研究使用。怎样把本地的音频转成文字,讯飞的开发文档上很清楚,这里就不说明了,直接调用讯飞的方法就好。

但是问题来了:
因项目录制的语音消息,保存的格式AMR格式,因为发送消息,如果保存的格式是wav的则文件太大,所以才保存成AMR格式。但是讯飞语音,进行音频识别,只支持wav格式,或者pcm格式的,于是就牵扯到音频格式的转换。由于个人技术还不到位,所以在网上搜索资料,虽然有一些ffmpeg这样类似的格式转换工具,也曾尝试过,但是结果都不理想,其他相关的内容,能够帮到我的很少,不过还好,最终找到一篇利用MediaCodec 进行解码的博客(搜索关键字很重要,一开始徒劳了太久),仔细研究一下,参照着,修改了一下,原博客地址 http://blog.csdn.net/TinsanMr/article/details/51049179
博主写的是mp3->PCM->aac,而我不需要进行二次编码成aac,所以只需要拿到pcm数据就够了。

改好后,运行也成功了,但是使用的测试机是5.0以下的,当时也没注意这个,当公司测试说魅族无法成功转写文字,我以为是和机型有关,后来自己的手机也不行,这就不对了,然后恍然大悟,和手机系统有关?因为我的手机和出现问题的魅族测试机都是Android 5.0以上的系统,于是又测试了几次,果然是和系统版本有关。

只能再从网上查查相关资料了。看到了一篇博客,博客地址 http://blog.csdn.net/zgcqflqinhao/article/details/52525697?locationNum=6&fps=1,这篇博客的博主,用的也是上文提到的工具类,遇到了和我一样的问题,看完后才明白,原来,MediaCodec 的两个方法getInputBuffers()和getOutputBuffers(),适用于android 4.0以上5.0以下,而对于android 5.0以上的系统,从API 21开始就弃用了这两个方法,因为列中移除位置和输出缓冲区的限制将被设置为有效数据范围,所以不要使用这种方法,而选择用getInputBuffer(int
index)和getOutputBuffer(int index)。

修改后5.0系统还是没有结果,问题仍然出在解码上,然后参考了博客 http://blog.csdn.net/qq_24554061/article/details/52318622,最终成功了,真是一波三折。
好了,直接上代码。修改后,代码见:下面的封装工具类,可以直接使用—–AudioDecode

/**
 * 本地AMR录音解码成PCM数据流
 */
public class AudioDecode {

    private static final String TAG = "AudioDecode";
    private String srcPath;//语音本地路径
    private MediaCodec mediaDecode;
    private MediaExtractor mediaExtractor;
    private ByteBuffer[] decodeInputBuffers;
    private ByteBuffer[] decodeOutputBuffers;
    private MediaCodec.BufferInfo decodeBufferInfo;
    private ArrayList<byte[]> chunkPCMDataContainer;//PCM数据
    private OnCompleteListener onCompleteListener;
    private boolean codeOver = false;//解码结束
    private Thread decoderThread;

    public static AudioDecode newInstance() {
        return new AudioDecode();
    }

    /**
     * 设置要读取的文件位置
     *
     * @param srcPath
     */
    public void setFilePath(String srcPath) {
        this.srcPath = srcPath;
    }

    /**
     * 准备工作
     */
    public void prepare() {

        if (srcPath == null || "".equas(srcPath)) {//其实这个路径在使用转码前就已经判断本地文件是否存在了,这个判断可有可无
            throw new IllegalArgumentException("srcPath can't be null");
        }
        chunkPCMDataContainer = new ArrayList<>();
        initMediaDecode();//解码器
    }

    /**
     * 初始化解码器
     */
    private void initMediaDecode() {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                mediaExtractor = new MediaExtractor();//此类可分离视频文件的音轨和视频轨道
                mediaExtractor.setDataSource(srcPath);//媒体文件的位置
                for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {//遍历媒体轨道 此处我们传入的是音频文件,所以也就只有一条轨道
                    MediaFormat format = mediaExtractor.getTrackFormat(i);;
                    format.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
                    String mime = format.getString(MediaFormat.KEY_MIME);
                    if (mime.startsWith("audio")) {//获取音频轨道
                        mediaExtractor.selectTrack(i);//选择此音频轨道
                        mediaDecode = MediaCodec.createDecoderByType(mime);//创建Decode解码器
                        mediaDecode.configure(format, null, null, 0);
                        break;
                    }
                }
                if (mediaDecode == null) {
                    Log.e(TAG, "create mediaDecode failed");
                    return;
                }
                mediaDecode.start();//启动MediaCodec ,等待传入数据
                decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中获取输入数据
                decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec将解码后的数据放到此ByteBuffer[]中 我们可以直接在这里面得到PCM数据
                decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
                showLog("buffers:" + decodeInputBuffers.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /**
     * 开始转码
     * 音频数据 解码成PCM,获取到PCM数组
     */
    public void startAsync() {
        decoderThread = new Thread(new DecodeRunnable());
        decoderThread.start();
    }

    /**
     * 将PCM数据存入{@link #chunkPCMDataContainer}
     *
     * @param pcmChunk PCM数据块
     */
    private void putPCMData(byte[] pcmChunk) {
        synchronized (AudioDecode.class) {//记得加锁
            chunkPCMDataContainer.add(pcmChunk);
        }
    }

    /**
     * 在Container中{@link #chunkPCMDataContainer}取出PCM数据
     *
     * @return PCM数据块
     */
    private byte[] getPCMData() {
        synchronized (AudioDecode.class) {//记得加锁
            if (chunkPCMDataContainer.isEmpty()) {
                return null;
            }

            byte[] pcmChunk = chunkPCMDataContainer.get(0);//每次取出index 0 的数据
            chunkPCMDataContainer.remove(pcmChunk);//取出后将此数据remove掉 既能保证PCM数据块的取出顺序 又能及时释放内存
            return pcmChunk;
        }
    }


    /**
     * 解码{@link #srcPath}音频文件 得到PCM数据块
     *
     * @return 是否解码完所有数据
     */
    private void srcAudioFormatToPCM() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
            if(decodeInputBuffers!=null){
                try {
                    for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
                        int inputIndex = 0;//获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
                        inputIndex = mediaDecode.dequeueInputBuffer(-1);
                        if (inputIndex < 0) {
                            codeOver = true;
                            return;
                        }
                        ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];//拿到inputBuffer
                        inputBuffer.clear();//清空之前传入inputBuffer内的数据
                        int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中
                        if (sampleSize < 0) {//小于0 代表所有数据已读取完成
                            codeOver = true;
                        } else {
                            mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解码刚刚传入的数据
                            mediaExtractor.advance();//MediaExtractor移动到下一取样处
                        }
                    }

                    //获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
                    //此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
                    int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);

                    ByteBuffer outputBuffer;
                    byte[] chunkPCM;
                    while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
                        outputBuffer = decodeOutputBuffers[outputIndex];//拿到用于存放PCM数据的Buffer
                        chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo内定义了此数据块的大小
                        outputBuffer.get(chunkPCM);//将Buffer内的数据取出到字节数组中
                        outputBuffer.clear();//数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
                        putPCMData(chunkPCM);//自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
                        mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
                        outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
                    }

                    if(codeOver){
                        if (onCompleteListener != null) {
                            onCompleteListener.completed(chunkPCMDataContainer);
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                    codeOver = true;
                    if (onCompleteListener != null) {
                        onCompleteListener.completed(chunkPCMDataContainer);
                    }
                }
            }
        }
    }

    /**
     * android 5.0以上
     */
    private void srcAudioFormatToPCMHigherApi() {
        if (android.os.Build.VERSION.SDK_INT >= 21){
            boolean sawOutputEOS = false;
            final long kTimeOutUs = 10000;
            long presentationTimeUs = 0;
            while (!sawOutputEOS){
                try{
                    int inputIndex = mediaDecode.dequeueInputBuffer(-1);
                    if (inputIndex >= 0){
                        ByteBuffer inputBuffer = mediaDecode.getInputBuffer(inputIndex);
                        if(inputBuffer!=null){
                            int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
                            if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
                                sawOutputEOS = true;
                                codeOver = true;
                                break;
                            }else{
                                presentationTimeUs = mediaExtractor.getSampleTime();
                                mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);// 通知MediaDecode解码刚刚传入的数据
                                mediaExtractor.advance();
                            }
                        }
                    }else{
                        sawOutputEOS = true;
                        codeOver = true;
                    }
                    int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
                    ByteBuffer outputBuffer ;//= mediaDecode.getOutputBuffer(outputIndex);// 拿到用于存放PCM数据的Buffer
                    while (outputIndex >= 0){
                        outputBuffer = mediaDecode.getOutputBuffer(outputIndex);
                        boolean doRender = (decodeBufferInfo.size != 0);
                        if(doRender && outputBuffer!=null){
                            outputBuffer.position(decodeBufferInfo.offset);
                            outputBuffer.limit(decodeBufferInfo.offset + decodeBufferInfo.size);
                            byte[] chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
                            outputBuffer.get(chunkPCM);
                            outputBuffer.clear();// 数据取出后一定记得清空此Buffer   MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
                            putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
                            mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后将不能向外输出数据
                            outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                    sawOutputEOS = true;
                    codeOver = true;
                }
            }
            if(codeOver){
                if (onCompleteListener != null) {
                    onCompleteListener.completed(chunkPCMDataContainer);
                }
            }
        }
    }

    /**
     * 释放资源
     */
    public void release() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN){

            try{
                if(decoderThread!=null && decoderThread.isAlive()){
                    decoderThread.interrupt();
                    codeOver = true;
                }

                if (mediaDecode != null) {
                    mediaDecode.stop();
                    mediaDecode.release();
                    mediaDecode = null;
                }

                if (mediaExtractor != null) {
                    mediaExtractor.release();
                    mediaExtractor = null;
                }

                if (onCompleteListener != null) {
                    onCompleteListener = null;
                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

    /**
     * 解码线程
     */
    private class DecodeRunnable implements Runnable {

        @Override
        public void run() {
            while (!codeOver) {
                if(Build.VERSION.SDK_INT>=21){
                    srcAudioFormatToPCMHigherApi();
                }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){
                    srcAudioFormatToPCM();
                }
            }
        }
    }

    /**
     * 解码完成回调接口
     */
    public interface OnCompleteListener {
        void completed(ArrayList<byte[]> chunkPCMDataContainer);
    }

    /**
     * 设置转码完成监听器
     * @param onCompleteListener 监听器
     */
    public void setOnCompleteListener(OnCompleteListener onCompleteListener) {
        this.onCompleteListener = onCompleteListener;
    }

    private void showLog(String msg) {
        Log.e("AudioCodec", msg);
    }
}

调用方法
这里只列出在Activity中主要的使用部分,其他的都是基本的控件使用还有讯飞的相关方法调用就不列了。

public class AudioToTextActivity extends BaseActivity{

private AudioDecode audioDecode;
//语音转换
    private void startTranslate(){
        int ret = mIat.startListening(mRecognizerListener);
        if (ret != ErrorCode.SUCCESS) {
            LogUtil.d(TAG+"--->识别失败,错误码:" + ret);
        }else{
            try {
                audioDecode = AudioDecode.newInstance();
                audioDecode.setFilePath(audioPath);
                audioDecode.prepare();
                audioDecode.setOnCompleteListener(new AudioDecode.OnCompleteListener() {
                    @Override
                    public void completed(final ArrayList<byte[]> pcmData) {
                        if(pcmData!=null){
                            //写入音频文件数据,数据格式必须是采样率为8KHz或16KHz(本地识别只支持16K采样率,云端都支持),位长16bit,单声道的wav或者pcm
                            //必须要先保存到本地,才能被讯飞识别
                            for (byte[] data : pcmData){
                                mIat.writeAudio(data, 0, data.length);
                            }
                            mIat.stopListening();
                        }else{
                            mIat.cancel();
                            LogUtil.d(TAG+"--->读取音频流失败");
                        }
                        audioDecode.release();
                    }
                });
                audioDecode.startAsync();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}

好了,一个类似微信,语音转文字的功能就实现了。不过还有点小缺陷,因为讯飞本地语音转文字,最大支持60s的,而且技术人员的建议是最长40s左右,这样的成功率较高,而且,这个工具类,在Android 5.0以上的系统,40s左右的语音在转写时经常出现网络异常错误,30s左右以及以下基本没问题。不过蛮期待讯飞的新产品:语音转文字,因为这个产品支持长语音了!!

第一次写博客,不喜勿喷,哈哈~

另附上Demo,可以参考,链接如下
https://github.com/Alvin9234/SpeechDemo

你可能感兴趣的:(android,语音解码,科大讯飞,语音转文字)