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

大家应该都见过波形图,生活中很多仪表上面都有。例如心电机,录音软件,甚至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;
    }


贴效果图:


你可能感兴趣的:(自定义view)