欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android实时显示手机麦克风录音的时域图

程序员文章站 2022-06-12 13:38:07
...

Android实时显示手机麦克风录音的时域图

先看效果!

Android实时显示手机麦克风录音的时域图

绘制原理

首先我们需要使用AudioRecord进行录音,不能够用MediaRecord。如果对这里不是很了解的朋友,可以先去看一看关于AudioRecord方面的资料。如果了解的,那么继续往下面看。
AudioRecord通过read()得到音频数据,有两种数据格式,一种是byte[],还有一种是short[],这里我们选择使用short[]。
首先是绘制线条,short[]中的每一个值都看做一个点,然后我们需要做的就是将点连接起来。

计算横坐标

横坐标的计算相当简单,如果控件宽度为width,控件需要显示音频的长度等于length,当前short[]索引为index,那么横坐标就等于index/length*width

计算纵坐标

纵坐标实际上也很简单,首先我们录音数据为short[],每一个short的值范围是-32767~32768,这里我们取最大的一个值,32768,然后用纵坐标等于height/2+short[index]/32768*height/2,为何这里最后还要除以2?因为short可能是负数,还有前面也说了short的取值范围,那就是我们绘制的点的范围,如果不除以2的话,则只会显示上半部分线条,效果如下图:
Android实时显示手机麦克风录音的时域图

Hold on

到这里是不是感觉曾经认为很玄学的东西真的很简单,抛开效率不谈,是否真的没有一丝难度?

绘制思路

首先这里的音频是实时录制进来做展示,那么就代表着图形是一点一点增加的,但是控件外部真实录音实现的地方就可能是一次性read 1600的音频,那么如果是16000的采样率,那么这里每秒钟只有10帧,那UI显示看起来就会相当卡顿。所以我们应该实现一个缓冲区,用于存放音频,这里就由外部录音传入数据存入缓冲区,控件内再另起线程从缓冲区取出音频数据,再进行绘制操作。
绘图方法使用canvas.drawLines(),这个方法效率比canvas.drawLine()要高得多了。
在每次绘制新图形进来时,之前的老图形也应该一起展示,只不过应该向左边平移相应的距离。

    //总共需要绘制的音频长度
    int audioSampleNum = 16000;
    //链表用作存所有界面上显示的点
    LinkedList<float[]> pointArray = new LinkedList<>();
    //用作向canvas传参
    float[] points = new float[audioSampleNum * 4];

    protected void drawWave(Canvas canvas, short audio[]) {
        if (audio == null) {
            audio = new short[0];
        }
        //先计算Y轴
        for (int i = 0; i < audio.length - 1; i += accuracy) {
            float[] floats = new float[]{
                    0f,
                    heightPixels / 2 + (float) audio[i] / 32768 * heightPixels / 2,
                    0f,
                    heightPixels / 2 + (float) audio[i + 1] / 32768 * heightPixels / 2
            };
            pointArray.add(floats);
        }
        //从头部去掉超出的部分
        int overSize = pointArray.size() - audioSampleNum;
        if (overSize > 0) {
            for (int i = 0; i < overSize; i++) {
                //这里就是为何需要使用LinkedList的原因了,如果是ArrayList,remove的效率相当低下
                pointArray.removeFirst();
            }
        }
        //遍历拼接成去canvas绘制线条
        float[] floats;
        int index = 0;
        for (Iterator<float[]> iterator = pointArray.iterator(); iterator.hasNext(); ) {
            floats = iterator.next();
            floats[0] = (float) index / audioSampleNum * widthPixels;
            floats[2] = (float) (index + 1) / audioSampleNum * widthPixels;
            if (index * 4 >= points.length) {
                break;
            }
            points[4 * index] = floats[0];
            points[4 * index + 1] = floats[1];
            points[4 * index + 2] = floats[2];
            points[4 * index + 3] = floats[3];
            index++;
        }
        canvas.drawLines(points, paint);
    }

Problem

虽然这样一个最简单的版本基本上已经实现了,但是有没有发现有什么问题?之前老的点,全部都重新进入循环重新计算坐标了,当然这里的效率是相当高的,这一点点计算了,也就是1ms不到的时间就能够完成的,但是计算归计算,这样做的话,canvas的任务就加重了呀,每次上一次才绘制过的点,又放进来重新绘制了,它们唯一不同的地方仅仅是横坐标不同,但是却加重了GPU的负担了,那需要怎么办呢?
如果用一个Bitmap来缓存上一次的绘图结果,然后在绘制的时候先将Bitmap绘制到canvas中,并且向左平移一定的距离,再绘制新的线条到canvas中会怎样呢?

    Bitmap bitmapCache;

    private void drawBitmap(short audio[]) {
        if (widthPixels == 0 || heightPixels == 0) {
            return;
        }
        if (audio == null) {
            return;
        }
        Bitmap bitmap = Bitmap.createBitmap(widthPixels, heightPixels, Bitmap.Config.ARGB_4444);
        Canvas canvas = new Canvas(bitmap);
        float moveDistance = (float) audio.length / audioSampleNum * widthPixels;
        //往左边移动audio长度一样的宽度
        if (this.bitmapCache != null && !this.bitmapCache.isRecycled()) {
            canvas.drawBitmap(this.bitmapCache, -moveDistance, 0, paint);
        }
        //把新的线条画到最右边
        float[] pointAdd = new float[audio.length * 4];
        for (int i = 0; i < audio.length - 1; i += accuracy) {
            pointAdd[4 * i] = (float) i / audioSampleNum * widthPixels + widthPixels - moveDistance;//本来的比例,再加上左边被移动的距离
            pointAdd[4 * i + 1] = heightPixels / 2 + (float) audio[i] / 32768 * heightPixels / 2;
            pointAdd[4 * i + 2] = (float) (i + 1) / audioSampleNum * widthPixels + widthPixels - moveDistance;
            pointAdd[4 * i + 3] = heightPixels / 2 + (float) audio[i + 1] / 32768 * heightPixels / 2;
        }
        canvas.drawLines(pointAdd, paint);
        //保存上一帧的Bitmap用作下一帧的缓存
        this.bitmapCache = bitmap;
        canvas.save();
        canvas.restore();        
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmapCache != null && !bitmapCache.isRecycled()) {
            canvas.drawBitmap(bitmapCache, 0, 0, null);
        }
    }

如果换成是这样的实现方式,是不是感觉要好得多了呢?

总结

很多我们看见过感觉很玄学,很复杂的操作,实际上在了解原理之后真的是挺简单的,只要敢去想,敢于动手去操作,真的没有什么是做不到的。最后跟上GitHub地址:https://github.com/michaellee123/AntiAudioWaveView