我又来掀桌子了(ノಠ益ಠ)ノ彡┻━┻,这次是MP3录制和显示声音波形的一个故事。
</p>
Demo :https://github.com/CarGuo/RecordWave
先看看效果┑( ̄Д  ̄)┍
2 1MP3录制的逻辑过程就忽略了(真的不是因为懒(。・・)ノ),这里用的是开源的<a href="https://github.com/GavinCT/AndroidMP3Recorder">AndroidMP3Recorder</a>,不过大神太忙了,表示停止维护了,录制效果还不错,站在巨人的肩膀上(cao xi),增加了音频录制的权限判断和数据提取的逻辑。<a href="http://www.cnblogs.com/ct2011/p/4080193.html">关于录制的请戳这里传送</a>。
增加权限提示
</p>
在音频相关开发的时候,经常涉及到权限的问题,而6.0以下的时候是没有接口判断的,如何敏感的权限,为啥子没有呢(ノಠ益ಠ)ノ彡┻━┻,所以经常会和camera,拍摄视频一样,通过try这个crash来判断吧。
如下在startRecording的时候把crash捕获了(让你崩溃(ノಠ益ಠ)ノ彡┻━┻),之后通过判断int readSize = mAudioRecord.read(mPCMBuffer, 0, mBufferSize);读出来的是<a>,ERROR_INVALID_OPERATION </a>,<a>ERROR_BAD_VALUE</a>,的话,那么就通知你的页面:<a>这货不给权限还想录音,逗我吗</a>···然后退出录制。
···
try {
mAudioRecord.startRecording();
} catch (Exception ex) {
ex.printStackTrace();
}
···
while (mIsRecording) {
int readSize = mAudioRecord.read(mPCMBuffer, 0, mBufferSize);
if (readSize == AudioRecord.ERROR_INVALID_OPERATION ||
readSize == AudioRecord.ERROR_BAD_VALUE) {
if (errorHandler != null && !sendError) {
sendError = true;
errorHandler.sendEmptyMessage(ERROR_TYPE);
mIsRecording = false;
isError = true;
}
} else {
if (readSize > 0) {
mEncodeThread.addTask(mPCMBuffer, readSize);
calculateRealVolume(mPCMBuffer, readSize);
sendData(mPCMBuffer, readSize);
} else {
if (errorHandler != null && !sendError) {
sendError = true;
errorHandler.sendEmptyMessage(ERROR_TYPE);
mIsRecording = false;
isError = true;
}
}
}
}
再采样(这个词看起来很高端的样子)
其实就是音频数据进行缩减,因为手机每次读上来的音频数据是比较大的,这和我们配置的录制参数也有关系,这里使用的是44100HZ的参数,那么我们直接降频就好了。(这波装的可以啊~)
如下面所看到的,其实就一个for循环,首先把音频数据的长度除了300(哈哈,一下子少了好多),之后每300个里面取出最大和最小值(音频数据是正负数的),目前我们用的是最大值,这样把每300个里面的最大值都存起来,就有了一个降频后的音频数据列表了。
private void sendData(short[] shorts, int readSize) {
if (dataList != null) {
int length = readSize / 300;
short resultMax = 0, resultMin = 0;
for (short i = 0, k = 0; i < length; i++, k += 300) {
for (short j = k, max = 0, min = 1000; j < k + 300; j++) {
if (shorts[j] > max) {
max = shorts[j];
resultMax = max;
} else if (shorts[j] < min) {
min = shorts[j];
resultMin = min;
}
}
if (dataList.size() > maxSize) {
dataList.remove(0);
}
dataList.add(resultMax);
}
}
}
根据音频数据绘制波形
</p>
1、首先,你得有一个女····呸呸呸,你要有一个View,自定义一个View用来绘制,绘制的逻辑有这么几个:
- 获取View的大小,取的宽度,高度,还有基线。对基♂线。
- 生成两张bitmap作为双缓冲绘制,避免闪烁。(别问我为什么,你用一张试试)
- 对音频数据块的数据大小进行判断,取的缩放系数,做缩放波形处理。
- 根据控件宽度,间隔系数,从基线开始绘制波形到bitmap,将这个bitmap绘制到显示的bitmap2。
- 通知界面显示将这个bitmap2 绘制到界面上。
是不是很简单呢?是啊~确实很简单( ̄o ̄) .那到这里就结束88咯。
咦,下面还有啊 (ˉ▽ ̄~) 切~~
这里首先通过onVisibilityChanged,在里面注册了一个addOnPreDrawListener,在addOnPreDrawListener里面获取到了view的大小、基线,生成两张需要的bitmap。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE && mBackgroundBitmap == null) {
ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (getWidth() > 0 && getHeight() > 0) {
mWidthSpecSize = getWidth();
mHeightSpecSize = getHeight();
mBaseLine = mHeightSpecSize / 2;
mBackgroundBitmap = Bitmap.createBitmap(mWidthSpecSize, mHeightSpecSize, Bitmap.Config.ARGB_8888);
mBitmap = Bitmap.createBitmap(mWidthSpecSize, mHeightSpecSize, Bitmap.Config.ARGB_8888);
mBackCanVans.setBitmap(mBackgroundBitmap);
mCanvas.setBitmap(mBitmap);
ViewTreeObserver vto = getViewTreeObserver();
vto.removeOnPreDrawListener(this);
}
return true;
}
});
}
}
音频数据是一块一块的list过来的,所以获取每个块中最大值,与当前view对比,就知道这一块数据需要缩放的系数啦。
/**
* 根据当前块数据来判断缩放音频显示的比例
*
* @param list 音频数据
*/
private void resolveToWaveData(ArrayList<Short> list) {
short allMax = 0;
for (Short sh : list) {
if (sh > allMax) {
allMax = sh;
}
}
int curScale = allMax / mBaseLine;
if (curScale > mScale) {
mScale = ((curScale == 0) ? 1 : curScale);
}
}
开始绘制到bitmap上啦~
首先把bitmap洗干净drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);,然后绘制基线,接着就是for循环绘制音频线了。
绘制以基线为轴,每个音频数据除以缩放系数(需要考虑乘的情况?反正我bu care~),然后绘制上下的竖线,之后把这个bitmap绘制到要完整显示的mBackgroundBitmap,最后通知界面更新(发到UI线程),然后界面把要显示的mBackgroundBitmap贴出来。
if (mBackCanVans != null) {
//洗白白
mBackCanVans.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mBackCanVans.drawLine(0, mBaseLine, mWidthSpecSize, mBaseLine, mPaint);
int drawBufsize = dataList.size();
//判断大小,是否改变显示的比例
for (int i = 0, j = 0; i < drawBufsize; i++, j += mOffset) {
Short sh = dataList.get(i);
short max = (short) (mBaseLine - sh / mScale);
short min;
if (mWaveCount == 2) {
min = (short) (sh / mScale + mBaseLine);
} else {
min = (short) (mBaseLine);
}
mBackCanVans.drawLine(j, mBaseLine, j, max, mPaint);
mBackCanVans.drawLine(j, min, j, mBaseLine, mPaint);
}
synchronized (mLock) {
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mCanvas.drawBitmap(mBackgroundBitmap, 0, 0, mPaint);
}
Message msg = new Message();
msg.what = 0;
handler.sendMessage(msg);
}
···
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
if (mIsDraw && mBitmap != null) {
synchronized (mLock) {
c.drawBitmap(mBitmap, 0, 0, mViewPaint);
}
}
}
音频播放AudioPlayer
</p>
- 这个比较简单,就不废话了,真的不费话了。就是实现了一个内部定时器,不断的获取当前位置,谁让MediaPlayer本来就支持播放网络和本地,接口又丰富呢,有兴趣demo见。
最后~欢迎来个人的<a href="https://github.com/CarGuo">Github</a>
一定要来哈
网友评论
W/System.err: at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1227)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:347)
at com.shuyu.waveview.AudioWaveView.deepCopy(AudioWaveView.java:296)
at com.shuyu.waveview.AudioWaveView$drawThread.run(AudioWaveView.java:209)
点停止,再点播放的话就崩溃
V/MediaPlayer: resetDrmState: mDrmInfo=null mDrmProvisioningThread=null mPrepareDrmInProgress=false mActiveDrmScheme=false
cleanDrmObj: mDrmObj=null mDrmSessionId=null
E/AndroidRuntime: FATAL EXCEPTION: Timer-0
Process: com.shuyu.app, PID: 5614
java.lang.IllegalStateException
at android.media.MediaPlayer.isPlaying(Native Method)
at com.shuyu.waveview.AudioPlayer$1.run(AudioPlayer.java:90)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
at java.util.ArrayList.writeObject(ArrayList.java:766)