大家应该都见过波形图,生活中很多仪表上面都有。例如心电机,录音软件,甚至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();
// }
// });
}
}
开始绘制:
/**
* 开始绘制
*/
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;
}
贴效果图: