关于MP3波形显示网上其实也有不少的文章,但主要讲的是在录音过程中的展示,方法是通过获取MediaRecorder的getMaxAmplitude得到正在录音过程的最大振幅值来做的,但笔者遇到的问题是要从已经录好的文件里解析出波形,关于离线文件的解析波形也有个开源的项目叫做ringdroid
这个项目的原理是使用andrid原生MediaCodec先解析出pcm数据,然后再把pcm数据转换成波形数据,转换过程:
//获得采样数,其实就是解出来pcm数据的字节数
mNumSamples = mDecodedBytes.position() / (mChannels * 2); // One sample = 2 bytes.
mDecodedBytes.rewind();
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
//将字节数组转换成short数组
mDecodedSamples = mDecodedBytes.asShortBuffer();
//计算出平均比特率一般是64
mAvgBitRate = (int)((mFileSize * 8) * ((float)mSampleRate / mNumSamples) / 1000);
......
//计算有多少帧,这里是写死每帧是1024个字节的
mNumFrames = mNumSamples / getSamplesPerFrame();
if (mNumSamples % getSamplesPerFrame() != 0){
mNumFrames++;
}
//波形数组,长度是帧个数
mFrameGains = new int[mNumFrames];
mFrameLens = new int[mNumFrames];
mFrameOffsets = new int[mNumFrames];
int j;
int gain, value;
int frameLens = (int)((1000 * mAvgBitRate / 8) *
((float)getSamplesPerFrame() / mSampleRate));
//计算每帧波形数据
for (i=0; i<mNumFrames; i++){
gain = -1;
for(j=0; j<getSamplesPerFrame(); j++) {
value = 0;
for (int k=0; k<mChannels; k++) {
if (mDecodedSamples.remaining() > 0) {
//累加每个样本数据
value += java.lang.Math.abs(mDecodedSamples.get());
}
}
value /= mChannels;
//算出每帧中的最大值
if (gain < value) {
gain = value;
}
}
//将每帧的最大值开平方得到最终的每帧的波形值
mFrameGains[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
mFrameLens[i] = frameLens; // totally not accurate...
mFrameOffsets[i] = (int)(i * (1000 * mAvgBitRate / 8) * // = i * frameLens
((float)getSamplesPerFrame() / mSampleRate));
}
mDecodedSamples.rewind();
刚开始接触音频波形的人可能会纠结波形大小是不是对的,其实波形最重要的是要反应出来音频的大小变化的趋势,至于每个时间点的波形要有多高是不用太关心的,从上面的波形计算也可以知道是计算音频数据每帧最大值再开平方得来的,而且每帧的帧数也是写死的,其实完全可以自定义每帧的数,获取波形值时也可以用其他算法比如用平均值什么的,这里的处理要要根据最终使用时(一般是显示时的容器大小)场景来设计.
使用ringdroid项目功能是可以达到的,但是性能太差,关键在于获取pcm数据时时间太长,解析17分钟的音频大概要十分秒,这在笔者的使用场景下是不能接受的,于是得想办法如何优化速度,优化思路主要有两个方面:
1.提高获取数据速度
2.采用分段加载的办法
第一个方法如果使用原生的MediaCodec由于封装的太多,无法修改底层的逻辑因此无法优化速度,只能想办法有没第三方的解析mp3的速度,还好上天不负有心人,在github上找到了个使用Jni解析mp3数据的项目android-mp3decoders
使用此项目解析17分钟数据可以只要4秒多.
第二方法其实是关键,ringdroid的方法是先完全解析出数据再来计算,其实是没有必要的,因为笔者的需求是一开始只要看到一部分,在播放过程中或者滑动过程中才能看到后面的波形.因此可以一开始只加载必要的部分,在插放过程中或者滑动时再加载必要的,做到按需加载.于是把整个文件按一分钟时长分割文件为多个部分,再按需要加载,由于波形的显示是按顺序加载的,于是加载部分可以是顺序向前的,实现起来并不复杂,而波形计算那部分逻辑是一样.
/**
* 读第一帧数据,这里的帧的概念不是MP3里的数据结构,是按一分钟一分钟的揪放时间来划分的
* @return
*/
public int[] readFirstFrame() {
long start = System.currentTimeMillis();
mLoadingMap.put(0, true);
mMp3Decoder.seek(0);
ShortBuffer buffer = nextBuffer();
int[] frames = convertToWaveForm(buffer);
mFrames = new FrameArray(frames.length);
mFrames.add(frames);
Log.e(SoundFile.class.getSimpleName(), "cost=" + (System.currentTimeMillis() - start));
return frames;
}
public int[] readPart(int minute) {
ShortBuffer buffer = nextBuffer();
int[] frames = convertToWaveForm(buffer);
return frames;
}
/**
* 读取大约一分钟数据
* @return
*/
public ShortBuffer nextBuffer() {
// long start = System.currentTimeMillis();
ShortBuffer buffer = ShortBuffer.allocate((1 << 20) * 5);
short[] pcm = new short[2048];
int count = 0;
while (true) {
int samples = mMp3Decoder.readFrame(pcm);
if (samples == 0 || (samples == -1 && mMp3Decoder.isStreamComplete())) {
break;
} else if (samples == -2) {
Log.e(SoundFile.class.getSimpleName(), "samples=-2");
break;
} else {
if (buffer.remaining() < samples) {
// Getting a rough estimate of the total size, allocate 20% more, and
// make sure to allocate at least 5MB more than the initial size.
int position = buffer.position();
int newSize = (int)((position * (1.0 * mFileSize / samples * 1152)) * 1.2);
if (newSize - position < samples + 5 * (1<<20)) {
newSize = position + samples + 5 * (1<<20);
}
ShortBuffer newDecodedBytes = null;
// Try to allocate memory. If we are OOM, try to run the garbage collector.
int retry = 10;
while(retry > 0) {
try {
newDecodedBytes = ShortBuffer.allocate(newSize);
break;
} catch (OutOfMemoryError oome) {
// setting android:largeHeap="true" in <application> seem to help not
// reaching this section.
oome.printStackTrace();
retry--;
}
}
if (retry == 0) {
// Failed to allocate memory... Stop reading more data and finalize the
// instance with the data decoded so far.
break;
}
//ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize);
buffer.rewind();
newDecodedBytes.put(buffer);
buffer = newDecodedBytes;
}
buffer.put(pcm, 0, samples);
count++;
}
//readFrame每次是读出1152个short其实就是mp3标准的一帧,一帧大约26ms,由此计算出多次数大约为一分钟
if (count == 2708 + 120) {
break;
}
}
// Log.e(SoundFile.class.getSimpleName(), "cost=" + (System.currentTimeMillis() - start));
return buffer;
}
/**
* 转成波形
* aram buffer
* @return
*/
private int[] convertToWaveForm(ShortBuffer buffer) {
int numFrames = buffer.position() / getSamplesPerFrame();
if (buffer.position() % getSamplesPerFrame() != 0){
numFrames++;
}
buffer.rewind();
int j;
int gain, value;
int[] frames = new int[numFrames];
for (int i = 0; i < numFrames; i++){
gain = -1;
for(j=0; j< getSamplesPerFrame(); j++) {
value = 0;
// for (int k=0; k<mChannels; k++) {
if (buffer.remaining() > 0) {
value += java.lang.Math.abs(buffer.get());
}
// }
// value /= 1;
if (gain < value) {
gain = value;
}
}
frames[i] = (int)Math.sqrt(gain); // here gain = sqrt(max value of 1st channel)...
}
return frames;
}