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的话,则只会显示上半部分线条,效果如下图:
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