android項目接入科大訊飛語音評測功能過程

前言

最近項目需要接入語音評測功能,公司有做過這方面的同事推薦了科大訊飛語音評測,於是根據官網的開發指南接入了sdk,可以成功評測用戶的口語能力,並給出合適的分數,但是期間遇到了很多小問題,於是寫在這篇文章記錄一下開發及填坑的過程。

正文

1.接入sdk:

如何接入sdk請去看科大訊飛官網提供的接入指南,這裏就不做介紹了

傳送門:https://doc.xfyun.cn/msc_android/%E8%AF%AD%E9%9F%B3%E8%AF%84%E6%B5%8B.html

2.編寫語音評測工具類:

因爲有兩個地方用到了這個評測功能,所以爲了使用方便,寫了一個工具類,直接上代碼:

/**
 * @ClassName: SpeechEvaluatorUtil
 * @Desciption: //語音評測工具類
 * @author: jesse
 * @date: 2018-06-29
 */
public class SpeechEvaluatorUtil {

    private static final String TAG = SpeechEvaluatorUtil.class.getSimpleName();
    public static final String EVA_RECORD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/msc/ise.wav";

    private static SpeechEvaluator mIse;

    public static void init(Context context) {
        if (mIse == null) {
            mIse = SpeechEvaluator.createEvaluator(context, null);
        }
    }

    /**
     * @param evaText            評測用句
     * @param mEvaluatorListener 語音評測回調接口
     * @return 評測錄音存儲路徑
     */
    public static void startSpeechEva(String evaText, EvaluatorListener mEvaluatorListener) {
        setParams();
        // 設置音頻保存路徑,保存音頻格式支持pcm、wav,設置路徑爲sd卡請注意WRITE_EXTERNAL_STORAGE權限
        // 注:AUDIO_FORMAT參數語記需要更新版本才能生效
        mIse.startEvaluating(evaText, null, mEvaluatorListener);
    }

    //通過寫入音頻文件進行評測
    public static void startEva(byte[] audioData,String evaText,EvaluatorListener mEvaluatorListener){
        setParams();
        //通過writeaudio方式直接寫入音頻時才需要此設置
        mIse.setParameter(SpeechConstant.AUDIO_SOURCE,"-1");

        int ret = mIse.startEvaluating(evaText, null, mEvaluatorListener);
        //在startEvaluating接口調用之後,加入以下方法,即可通過直接
        //寫入音頻的方式進行評測業務
        if (ret != ErrorCode.SUCCESS) {
            Log.i(TAG,"識別失敗,錯誤碼:" + ret);
        } else {
            if(audioData != null) {
                //防止寫入音頻過早導致失敗
                try{
                    new Thread().sleep(100);
                }catch (InterruptedException e) {
                    Log.d(TAG,"InterruptedException :"+e);
                }
                mIse.writeAudio(audioData,0,audioData.length);
                mIse.stopEvaluating();
            }else{
                Log.i(TAG,"audioData == null");
            }
        }
    }

    private static void setParams() {
        Log.i(TAG, "setParams()");
        // 設置評測語種:英語
        mIse.setParameter(SpeechConstant.LANGUAGE, "en_us");
        // 設置評測題型:句子
        mIse.setParameter(SpeechConstant.ISE_CATEGORY, "read_sentence");
        mIse.setParameter(SpeechConstant.RESULT_LEVEL,"plain");
        mIse.setParameter(SpeechConstant.ISE_AUDIO_PATH, EVA_RECORD_PATH);
        mIse.setParameter(SpeechConstant.AUDIO_FORMAT, "wav");
    }

    //停止評測
    public static void stopSpeechEva() {
        if (mIse.isEvaluating()) {
            mIse.stopEvaluating();
        }
    }

    //取消評測
    public static void cancelSpeechEva() {
        mIse.cancel();
    }

}

這裏寫了兩種評測方式:

第一種是“直接根據mic錄到的音頻進行評測”(startSpeechEva()),這種方式會在EVA_RECORD_PATH路徑下生成一個約44kb的wav格式音頻文件,但是這裏有一個巨大的坑--音頻文件不會立即刷新覆蓋上一次錄音,大概會延遲0.7-1.2s的時間,這樣就造成了一個問題:如果想要錄音完後立即播放這次的錄音的話,會發現播放的錄音是上一次的錄音!而很不巧,我就需要做這樣的一個功能,所以我棄用了第一種方式,改用了第二種方式。

第二種是“先自己把音頻錄下來,生成wav格式文件,然後再轉換成byte數組進行評測”(startEva()),這種方式因爲是自己錄音,所以沒有刷新錄音文件的延遲,可以實現錄音完後立即播放錄音音頻的效果,這就解決了第一種方式裏的大坑。但是還有個坑就是,較之第一種方式,這種方式的評分偏低很多(第一種方式能得90分的發音,第二種方式大概得70分)。如果有人能夠解決這個坑的話,希望你能給我留言告知一下方法。

3.編寫錄音工具類

這裏我寫了兩個工具類,一個用的是MediaRecorder進行錄音,一個是用AudioRecord,第2步裏的第二種方式用到的是AudioRecorder這個工具類。這裏兩種都奉上。

MediaRecorder工具類:

/**
 * @ClassName: MediaRecordUtil
 * @Desciption: //錄音工具類
 * @author: jesse
 * @date: 2018-06-15
 */
public class MediaRecordUtil {

    //文件路徑
    private String filePath;
    //文件夾路徑
    private String FolderPath;

    private MediaRecorder mMediaRecorder;
    private final String TAG = MediaRecordUtil.class.getSimpleName();
    public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大錄音時長1000*60*10;

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    /**
     * 文件存儲默認sdcard/record
     */
    public MediaRecordUtil(){

        //默認保存路徑爲/sdcard/record/下
        this(Environment.getExternalStorageDirectory().getAbsolutePath()+"/ShushanRecord/");
    }

    public MediaRecordUtil(String filePath) {

        File path = new File(filePath);
        if(!path.exists())
            path.mkdirs();

        this.FolderPath = filePath;
    }

    private long startTime;
    private long endTime;



    /**
     * 開始錄音 使用amr格式
     *      錄音文件
     * @return
     */
    public void startRecord() {
        // 開始錄音
        /* ①Initial:實例化MediaRecorder對象 */
        if (mMediaRecorder == null)
            mMediaRecorder = new MediaRecorder();
        try {
            /* ②setAudioSource/setVedioSource */
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 設置麥克風
            /* ②設置音頻文件的編碼:AAC/AMR_NB/AMR_MB/Default 聲音的(波形)的採樣 */
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
            /*
             * ②設置輸出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
             * ,H263視頻/ARM音頻編碼)、MPEG-4、RAW_AMR(只支持音頻且音頻編碼要求爲AMR_NB)
             */
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);

            filePath = FolderPath + DateUtils.createFileName() + ".amr" ;
            Log.i(TAG,"utils : filePath == "+filePath);
            /* ③準備 */
            mMediaRecorder.setOutputFile(filePath);
//            mMediaRecorder.setMaxDuration(MAX_LENGTH);
            mMediaRecorder.prepare();
            /* ④開始 */
            mMediaRecorder.start();
            // AudioRecord audioRecord.
            /* 獲取開始時間* */
            startTime = System.currentTimeMillis();
//            updateMicStatus();
        } catch (IllegalStateException e) {
            e.printStackTrace();
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.toString());
        } catch (IOException e) {
            e.printStackTrace();
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.toString());
        }
    }

    /**
     * 停止錄音
     */
    public long stopRecord() {
        if (mMediaRecorder == null)
            return 0L;
        endTime = System.currentTimeMillis();

        //有一些網友反應在5.0以上在調用stop的時候會報錯,翻閱了一下谷歌文檔發現上面確實寫的有可能會報錯的情況,捕獲異常清理一下就行了,感謝大家反饋!
        try {
            mMediaRecorder.setOnErrorListener(null);
            mMediaRecorder.setOnInfoListener(null);
            mMediaRecorder.setPreviewDisplay(null);
            mMediaRecorder.stop();
            mMediaRecorder.release();
            audioStatusUpdateListener.onStop(filePath);
            filePath = "";

        }catch (RuntimeException e){
            mMediaRecorder.release();
            File file = new File(filePath);
            if (file.exists())
                file.delete();
            filePath = "";
            Log.i(TAG,"stopRecord : "+e.toString());
            e.printStackTrace();
        }finally {
            mMediaRecorder = null;
        }
        return endTime - startTime;
    }

    /**
     * 取消錄音
     */
    public void cancelRecord(){

        try {

            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;

        }catch (RuntimeException e){
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
        File file = new File(filePath);
        if (file.exists())
            file.delete();

        filePath = "";

    }

    private final Handler mHandler = new Handler();
    private Runnable mUpdateMicStatusTimer = new Runnable() {
        public void run() {
//            updateMicStatus();
        }
    };


    private int BASE = 1;
    private int SPACE = 100;// 間隔取樣時間

    public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
        this.audioStatusUpdateListener = audioStatusUpdateListener;
    }

    /**
     * 更新麥克狀態
     */
    private void updateMicStatus() {

        if (mMediaRecorder != null) {
            double ratio = (double)mMediaRecorder.getMaxAmplitude() / BASE;
            double db = 0;// 分貝
            if (ratio > 1) {
                db = 20 * Math.log10(ratio);
                if(null != audioStatusUpdateListener) {
                    audioStatusUpdateListener.onUpdate(db, System.currentTimeMillis()-startTime);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }

    public interface OnAudioStatusUpdateListener {
        /**
         * 錄音中...
         * @param db 當前聲音分貝
         * @param time 錄音時長
         */
        public void onUpdate(double db, long time);

        /**
         * 停止錄音
         * @param filePath 保存路徑
         */
        public void onStop(String filePath);
    }

}

AudioRecord工具類:

/**
 * @ClassName: AudioRecordUtil
 * @Desciption: //錄製wav格式音頻
 * @author: jesse
 * @date: 2018-07-21
 */
public class AudioRecordUtil {

    private static AudioRecordUtil mInstance;
    private AudioRecord recorder;
    //錄音源
    private static int audioSource = MediaRecorder.AudioSource.MIC;
    //錄音的採樣頻率
    private static int audioRate = 16000;//這個採樣率是官方提供的標準採樣率,評測精度很高
    //錄音的聲道,單聲道
    private static int audioChannel = AudioFormat.CHANNEL_IN_MONO;
    //量化的深度
    private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    //緩存的大小
    private static int bufferSize = AudioRecord.getMinBufferSize(audioRate,audioChannel,audioFormat);
    //記錄播放狀態
    private boolean isRecording = false;
    //數字信號數組
    private byte [] noteArray;
    //PCM文件
    private File pcmFile;
    //WAV文件
    private File wavFile;
    //文件輸出流
    private OutputStream os;
    //文件根目錄
    private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/eva/";

    //wav文件目錄
    private String outFileName = basePath+"/eva.wav";

    //pcm文件目錄
    private String inFileName = basePath+"/eva.pcm";

    private AudioRecordUtil(){
        createFile();//創建文件
        recorder = new AudioRecord(audioSource,audioRate,audioChannel,audioFormat,bufferSize);
    }

    public synchronized static AudioRecordUtil getInstance(){
        if(mInstance == null){
            mInstance = new AudioRecordUtil();
        }
        return mInstance;
    }

    //讀取錄音數字數據線程
    class WriteThread implements Runnable{
        public void run(){
            writeData();
        }
    }

    //開始錄音
    public void startRecord(){
        isRecording = true;
        recorder.startRecording();
    }

    //停止錄音
    public void stopRecord(){
        isRecording = false;
        recorder.stop();
    }

    //將數據寫入文件夾,文件的寫入沒有做優化
    public void writeData(){
        noteArray = new byte[bufferSize];
        //建立文件輸出流
        try {
            os = new BufferedOutputStream(new FileOutputStream(pcmFile));
        }catch (IOException e){

        }
        while(isRecording == true){
            int recordSize = recorder.read(noteArray,0,bufferSize);
            if(recordSize>0){
                try{
                    os.write(noteArray);
                }catch(IOException e){

                }
            }
        }
        if (os != null) {
            try {
                os.close();
            }catch (IOException e){

            }
        }
    }

    // 這裏得到可播放的音頻文件
    public void convertWaveFile() {
        FileInputStream in = null;
        FileOutputStream out = null;
        long totalAudioLen = 0;
        long totalDataLen;
        long longSampleRate = AudioRecordUtil.audioRate;
        int channels = 1;
        long byteRate = 16 *AudioRecordUtil.audioRate * channels / 8;
        byte[] data = new byte[bufferSize];
        try {
            in = new FileInputStream(inFileName);
            out = new FileOutputStream(outFileName);
            totalAudioLen = in.getChannel().size();
            //由於不包括RIFF和WAV
            totalDataLen = totalAudioLen + 36;
            WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /* 任何一種文件在頭部添加相應的頭文件才能夠確定的表示這種文件的格式,wave是RIFF文件結構,每一部分爲一個chunk,其中有RIFF WAVE chunk, FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以選擇的, */
    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate,
                                     int channels, long byteRate) throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);//數據大小
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//過渡字節
        //數據大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //編碼方式 10H爲PCM編碼格式
        header[20] = 1; // format = 1
        header[21] = 0;
        //通道數
        header[22] = (byte) channels;
        header[23] = 0;
        //採樣率,每個通道的播放速度
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        //音頻數據傳送速率,採樣率*通道數*採樣深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 確定系統一次要處理多少個這樣字節的數據,確定緩衝區,通道數*採樣位數
        header[32] = (byte) (1 * 16 / 8);
        header[33] = 0;
        //每個樣本的數據位數
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }

    //創建文件夾,首先創建目錄,然後創建對應的文件
    public void createFile(){
        File baseFile = new File(basePath);
        if(!baseFile.exists())
            baseFile.mkdirs();
        pcmFile = new File(basePath+"/eva.pcm");
        wavFile = new File(basePath+"/eva.wav");
        if(pcmFile.exists()){
            pcmFile.delete();
        }
        if(wavFile.exists()){
            wavFile.delete();
        }
        try{
            pcmFile.createNewFile();
            wavFile.createNewFile();
        }catch(IOException e){

        }
    }

    //音頻文件轉byte數組
    public static byte[] getAudioData(String audioPath){
        byte[] buffer = null;
        try {
            File file = new File(audioPath);
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
            byte[] b = new byte[1000];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return buffer;
    }

    //記錄數據
    public void recordData(){
        new Thread(new WriteThread()).start();
    }

    public String getOutFileName() {
        return outFileName;
    }

4.開始評測

1.錄音開始

AudioRecordUtil.getInstance().startRecord();
AudioRecordUtil.getInstance().recordData();

2.錄音結束 

AudioRecordUtil.getInstance().stopRecord();
AudioRecordUtil.getInstance().convertWaveFile();

3.開始評測 

SpeechEvaluatorUtil.startEva(AudioRecordUtil.getAudioData(recordPath),text,ReadReciteExamFragment.this);

注意: 使用評測前先要執行SpeechEvaluatorUtil.init(getContext());我是在fragment的oncreate方法中執行的。

4.顯示得分

這一步需要實現EvaluatorListener接口,這裏只分享一下onResult這個回調的實現:

@Override
    public void onResult(EvaluatorResult result, boolean isLast) {
        Log.d(TAG,"onresult : isLast == "+isLast);
        if (isLast) {
            StringBuilder builder = new StringBuilder();
            builder.append(result.getResultString());
            if(oldScorePad != null && oldScorePad.getVisibility() == View.VISIBLE){
                oldScorePad.setVisibility(View.GONE);
            }
            showScorePad(parseXml(builder.toString()));
        }
    }

 parseXml方法,直接返回得分,滿分5分(換算成100分制乘以20即可):

private float parseXml(String xmlStr){
        float totalScore = 0f;
        try {
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlStr));
            int eventType = xmlPullParser.getEventType();
            String value;
            while(eventType != xmlPullParser.END_DOCUMENT) {
                String nodeName = xmlPullParser.getName();
                switch (eventType){
                    case XmlPullParser.START_TAG:
                        if("total_score".equals(nodeName)){
                            value = xmlPullParser.getAttributeValue(0);
                            totalScore = Float.parseFloat(value);
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        break;
                }
                eventType = xmlPullParser.next();
            }
        }catch (XmlPullParserException xppe){
            Log.i(TAG,xppe.toString());
        }catch (IOException ioe){
            Log.i(TAG,ioe.toString());
        }
        return totalScore;
    }

再發一個完整錄音代碼,可以作爲參考:

                ExamAudioPlayUtil.stopPlay();
                coverPopup = PopupWindowUtil.showCoverPopupWindow(getActivity(),rootView);
                final Button btn = btnList.get(index).get(index);
                AudioRecordUtil.getInstance().startRecord();
                AudioRecordUtil.getInstance().recordData();
                pb.countBack(pb,(int) ((end-begin)*1000));
                Timer timer = new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        AudioRecordUtil.getInstance().stopRecord();
                        AudioRecordUtil.getInstance().convertWaveFile();
                        mHandler.sendMessage(mHandler.obtainMessage(0,btn));
                        SpeechEvaluatorUtil.startEva(AudioRecordUtil.getAudioData(recordPath),text,ReadReciteExamFragment.this);
                        ExamAudioPlayUtil.playAudio(pbRecordPlay, recordPath, new MediaPlayer.OnCompletionListener() {
                            @Override
                            public void onCompletion(MediaPlayer mp) {
                                coverPopup.dismiss();
                            }
                        });
                    }
                },(long)((end-begin)*1000));

以上。

總結

科大訊飛的語音評測,評分還是蠻準的,但是其中也有一些坑,文中已經介紹。這裏再次希望如果有人能填上坑的話,給我留言說一下方法,也希望這篇文章能夠幫到想要接入科大訊飛語音評測功能的安卓工程師。

說句題外話,csdn寫博客的體驗真是越來越好了!

源碼下載:https://download.csdn.net/download/yonghuming_jesse/10616924

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章