自定义控件之动态声纹波形图实现

来源:互联网 发布:临床试验数据库软件 编辑:程序博客网 时间:2024/06/06 14:02

大家应该都见过波形图,生活中很多仪表上面都有。例如心电机,录音软件,甚至KTV里面的唱歌系统。用ios系统的人应该用过录音软件,就有这样的波形图。因项目需求,研究了很久,用android代码做个波形图控件。最繁琐的是计算的部分。

1、首先波形图的高低代表数值的大小,数值来源是什么。可以是音量,可以是你自定义的任何属性数值。所以这个自定义控件对外暴露的就是设置数值的接口。

2、如何做一个会动的波形图,实时绘制的波形图,第一个想到的是定时任务,不断的绘制数据到页面上,可是波形图后续的数据是要替换前面的,如何做出波形图的数据在往前移动的效果。用Cavus就可以做到。


贴代码:

数据来源计算:

    /**     * 200ms 一次 回调音频数据 分割成20份byte[]数据     * 每10ms算一次音量     *     * @param audioData     */    public void plusAudioData(byte[] audioData) {        if (mOffsetDistance == 0) {            mOffsetDistance = ((float) mWidthSpecSize) / ((float) (TimeStep.ShortSentence.getValue() / mRefreshInterval));        }        if (mOffsetDistance > splitCount) {            splitCount = (int) (mOffsetDistance + 1); //4.0分辨率高 splitCount必须大于mOffsetDistance 不然会有空白区域        }        LogUtils.e("ljx", " splitCount : " + splitCount);        if (audioData != null && audioData.length > splitCount) {            int splitPart = (int) Math.floor(((float) (audioData.length)) / (float) splitCount);            int[] volumes = new int[splitCount];            for (int i = 0; i < splitCount; i++) {                byte[] tempAudioData = new byte[splitPart];                for (int j = 0; j < splitCount; j++) {                    tempAudioData[j] = audioData[i * splitPart + j];                }                volumes[i] = getVolume(tempAudioData, tempAudioData.length);            }            addData(volumes);            LogUtils.e("ljx", "plusAudioData  ================================>>>>>>>>>>>>>> 200ms 等分" + splitCount + "个数据");        } else {            int[] volumes = new int[splitCount];            addData(volumes);        }    }
外部调用根据你录音说话,不断产生数据传值进来,这里我用的三方的语音sdk返回的实时音量数据作为波形图的数据绘制的,遇到一个问题是,三方返回的频率是200ms一次平均音量值,1s只有5次采集,这样远远不够绘制波形图,会让波形图特别的粗,因为200ms绘制等宽的view,数据填充不够。 后来改成了,根据三方返回的音频文件pcm格式的最原始音频数据,自己计算音量。这里我默认是200ms采集20次音量,需要将pcm 数据等分成20分然后计算每份的音量均值。然后遇到个问题是对于分辨率高的设备20等分还是数据不够,于是offsetDistance是200ms绘制的像素宽度,如果<20,则等分20份,如果>20则等分像素宽度份。这样就不会缺少数据了。

计算音量的方法:(绝密,这个是不能贴的)如果你们用的别的数据来源是不需要我这么算的。

    /**     * 根绝 byte[] pcm音频数据 算出实时音量大小     *     * @param buffer     * @param len     * @return     */    private int getVolume(byte[] buffer, int len) {        int value = 0;        float energy = 0, tmp = 0;        for (int i = 0; i < len && i + 1 < len; i += 2) {            int v1 = buffer[i] & 0xFF;            int v2 = buffer[i + 1] & 0xFF;            short bufShort = (short) (v1 + (v2 << 8));            tmp += bufShort;            energy += bufShort * bufShort;        }        energy = energy / len - (tmp / len) * (tmp / len);        value = (int) (Math.pow(energy, 0.2f) * 2);        if (value < 0) value = 0;        if (value > 100) value = 100;        return value;    }

绘制线程:

    /**     * 绘制波形图线程task     * 定时任务     */    class DrawThreadTask extends Thread {        @SuppressWarnings("unchecked")        @Override        public void run() {            LogUtils.e("ljx", "200ms定时刷新====>>>数据池个数" + mRecDataList.size() + " ====>>>刷新数据位置" + (mAlreadyDrawDataPosition + 1));            LogUtils.e("ljx", "Visibility :" + getVisibility());            if (mBitmap == null) {                LogUtils.e("ljx", "mBackgroundBitmap == null");                return;            }            LogUtils.e("ljx", "mHeightSpecSize :" + mHeightSpecSize + " mWidthSpecSize :" + mWidthSpecSize);            if (mCanvas != null) {                if (mOffsetDistance == 0) {                    mOffsetDistance = ((float) mWidthSpecSize) / ((float) (TimeStep.ShortSentence.getValue() / mRefreshInterval));                    LogUtils.e("ljx", "mOffsetDistance : " + mOffsetDistance);                }                if (mRecDataList.size() <= 0 || mRecDataList.size() < mAlreadyDrawDataPosition + 1) {                    return;                }                if (mAlreadyDrawDataPosition >= TimeStep.ShortSentence.getValue() / mRefreshInterval) {                    //已经刷到波形图边缘 让波形图动起来                    mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);                    int startP = mAlreadyDrawDataPosition - TimeStep.ShortSentence.getValue() / mRefreshInterval + 1;                    for (int i = startP; i <= mAlreadyDrawDataPosition; i++) {                        int[] newData = mRecDataList.get(i);//                        LogUtils.e("ljx","============开始区块=================");                        for (int j = 0; j < newData.length; j++) {                            float boHeight = Math.abs(((float) newData[j]) / 100f * mBaseLine);                            float max = mBaseLine - boHeight * mScale;                            float min = mBaseLine + boHeight * mScale;                            max = max <= 0 ? 0 : max;                            min = min >= 2 * mBaseLine ? 2 * mBaseLine : min;                            float startX = (i - startP) * mOffsetDistance + (((float) (j + 1) / (float) newData.length)) * mOffsetDistance;                            mCanvas.drawLine(startX, min, startX, max, mPaint);//                            LogUtils.e("ljx","====startX :"+startX);                        }//                        LogUtils.e("ljx","============结束区块=================");                    }                } else {                    int[] newData = mRecDataList.get(mAlreadyDrawDataPosition);                    for (int i = 0; i < newData.length; i++) {                        float boHeight = Math.abs(((float) newData[i]) / 100f * mBaseLine);                        float max = mBaseLine - boHeight * mScale;                        float min = mBaseLine + boHeight * mScale;                        max = max <= 0 ? 0 : max;                        min = min >= 2 * mBaseLine ? 2 * mBaseLine : min;                        float startX = mAlreadyDrawDataPosition * mOffsetDistance + (((float) (i + 1) / (float) newData.length)) * mOffsetDistance;                        mCanvas.drawLine(startX, min, startX, max, mPaint);                    }                }                mAlreadyDrawDataPosition++;            }            mHandler.removeMessages(0);            mHandler.sendEmptyMessage(0);//            mHandler.post(new Runnable() {//                @Override//                public void run() {//                    invalidate();//                }//            });        }    }

这是绘制的线程task,原理是这样的设置view的ViewTreeObserver,得到自定义控件的宽高,例如每200ms刷新一次view,向前绘制20条数据,那20条数据绘制多宽,这里用控件的宽/固定的时间。这个时间是我设置绘制到头需要12s的时间,超过12s的数据就开始动起来。如何动起来就是,这里我用AlreadyDrawPosition记录了绘制的数据位置,<12s只向前绘制一份数据,>20s则绘制从AlreadyDrawPosition-12s/200ms位置到AlreadyDrawPosition的数据。这样就会肉眼看上去波形图是在往前移动的。


开始绘制:

    /**     * 开始绘制     */    public void startView() {        if (mDrawTask != null && mExecutor != null && mDrawTask.isAlive()) {            mExecutor.shutdownNow();            while (mExecutor.isTerminated()) ;            LogUtils.e("ljx", "stopView====>>>again ");            mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);        }        mAlreadyDrawDataPosition = 0;        mExecutor = new ScheduledThreadPoolExecutor(2);        mDrawTask = new DrawThreadTask();        mExecutor.scheduleWithFixedDelay(mDrawTask, 0, mRefreshInterval, TimeUnit.MILLISECONDS);    }


停止绘制:

    /**     * 停止绘制     */    public void stopView() {        mRecDataList.clear();        mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);        if (mExecutor != null) {            mExecutor.shutdown();        }        mExecutor = null;        mDrawTask = null;        mAlreadyDrawDataPosition = 0;    }


贴效果图:


原创粉丝点击