
首先我们需要使用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的话,则只会显示上半部分线条,效果如下图: 
到这里是不是感觉曾经认为很玄学的东西真的很简单,抛开效率不谈,是否真的没有一丝难度?
绘制思路首先这里的音频是实时录制进来做展示,那么就代表着图形是一点一点增加的,但是控件外部真实录音实现的地方就可能是一次性read 1600的音频,那么如果是16000的采样率,那么这里每秒钟只有10帧,那UI显示看起来就会相当卡顿。所以我们应该实现一个缓冲区,用于存放音频,这里就由外部录音传入数据存入缓冲区,控件内再另起线程从缓冲区取出音频数据,再进行绘制操作。 绘图方法使用canvas.drawLines(),这个方法效率比canvas.drawLine()要高得多了。 在每次绘制新图形进来时,之前的老图形也应该一起展示,只不过应该向左边平移相应的距离。
//总共需要绘制的音频长度 int audioSampleNum = 16000; //链表用作存所有界面上显示的点 LinkedList pointArray = new LinkedList(); //用作向canvas传参 float[] points = new float[audioSampleNum * 4]; protected void drawWe(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 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.se(); 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/AntiAudioWeView