主要实现功能点:
1.实现了长按录音及播放音频功能,并把录音资源格式转化为mp3(基于开源库LAME)
2.实现了把mp3文件上传到服务器(基于Retrofit2),支持再次下载播放,缓存功能
3.对于权限的管理,本Demo用的第三方库:https://github.com/yanzhenjie/AndPermission
4.本Demo内部封装了长按录音控件,解决了与列表滚动View的冲突
用法参考MainActivity。
核心类
录音类:RecorderManager
播音类:MediaManager
录音控件:LongPressRecordView
前期调研:
android原生SDK里提供了两种语音方案:
1、AudioRecord
主要是实现边录边播(AudioRecord+AudioTrack)以及对音频的实时处理(如会说话的汤姆猫、语音)
优点:语音的实时处理,可以用代码实现各种音频的封装
缺点:输出是PCM语音数据,如果保存成音频文件,是不能够被播放器播放的,所以必须先写代码实现数据编码以及压缩
示例:
使用AudioRecord类录音,并实现WAV格式封装。录音20s,输出的音频文件大概为3.5M左右(已写测试代码)
2、MediaRecorder
已经集成了录音、编码、压缩等,支持少量的录音音频格式,大概有.aac(API = 16) .amr .3gp
优点:大部分以及集成,直接调用相关接口即可,代码量小
缺点:无法实时处理音频;输出的音频格式不是很多,例如没有输出mp3格式文件
示例:
使用MediaRecorder类录音,输出amr格式文件。录音20s,输出的音频文件大概为33K(已写测试代码)
以上引用自:,传送门里有具体的代码实现。
考虑到跨平台的需求,aac,amr做的并不好,我们可以将录音文件转为为mp3格式的文件(基于LAME开源库),文件体积小,而且还可以平台。
技术实现:
大体思路是:利用AudioRecord类录制音频文件-录制的同时借助LAME库进行转化-存储mp3音频文件
之所以用AudioRecord是为了获取录制的实时资源;
之所以边录边转是为了如果录制时间太久,录完再转,用户等待的时间太过于长久。
对于将android原生的音频格式转为mp3,可以参考GavinCT提供的开源库:https://github.com/GavinCT/AndroidMP3Recorder
我是在此库的基础上添加了播放,上传,下载等功能,同时由于此库作者已不再维护,期间也踩过坑,后面会细说。
项目结构及核心代码分析:
1.录音手势长按实现类LongPressRecordView,通过重写setOnTouchListener将控件的手势操作传递给GestureDetector对应的对象来处理。
注意有个小坑:如果此控件的父控件是列表类的,比如Scrollview,如果Scrollview足够长(超过全屏)可以滚动的话,会产生bug,长按录音控件,滚动到此控件外,会发现父控件也跟着滚动了。解决方案也很简单,我们需要处理下事件分发机制,重写dispatchTouchEvent方法(因为此方法发生在onTouchEvent之前),调用下
getParent().requestDisallowInterceptTouchEvent(true);
方法,告诉父控件我的事件自己处理,不要给我拦截了。
LongPressRecordView具体代码:
/**
* Created by wangmaobo on 2018/9/14.
*/
public class LongPressRecordView extends android.support.v7.widget.AppCompatTextView {
private OnGestureListener mListener;
public LongPressRecordView(Context context) {
this(context, null, -1);
}
public LongPressRecordView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public LongPressRecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setListener(OnGestureListener listener) {
mListener = listener;
}
private void init() {
GestureDetector gesture = new GestureDetector(new RecordLongClickGesture().setLongPressListener(new OnLongPressListener() {
@Override
public void startRecord() {
if (null != mListener) {
mListener.longClick();
}
}
}));
setOnTouchListener((view, motionEvent) -> {
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
if (null != mListener) {
mListener.actionUp();
}
}
return gesture.onTouchEvent(motionEvent);
});
setFocusable(true);
setClickable(true);
setLongClickable(true);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(event);
}
public interface OnGestureListener {
void longClick();
void actionUp();
}
}
2.mp3转化类MP3Recorder,这个类有个bug,就是对于低版本即android6.0(api23)以下的手机由于无法真正判断录音权限是否开启,导致权限被禁止后无法正常录音,app崩溃。
我们可以重写一下此类的开始录音,即start方法,通过
mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING
间接获取录音权限(关于权限的坑,可以在传送门里细看)。
initAudioRecorder();
mAudioRecord.startRecording();
if (!checkPermission()) {
mListener.noRecordRight();
return;
} else {
mListener.hasRecordRight();
}
public boolean checkPermission() {
if (null != mAudioRecord) {
return mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING;
}
return false;
}
3.播音控制器MediaManager
如果播放时需要倒计时,可以在构造函数里传入handler,提供了两种播放方式,播放本地资源和播放网络url资源:
/**
* 加载本地资源
*
* @param path
* @param onCompletionListener
*/
public void playSound(String path, OnCompletionListener onCompletionListener) {
if (mPlayer == null) {
mPlayer = new MediaPlayer();
mPlayer.setOnErrorListener((mp, what, extra) -> {
mPlayer.reset();
return false;
});
} else {
mPlayer.reset();
}
try {
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mPlayer.setOnCompletionListener(onCompletionListener);
mPlayer.setDataSource(path);
mPlayer.prepare();
mPlayer.start();
if (null != mHandler) {
mHandler.sendMessage(Message.obtain(mHandler, 2, 1));
}
Log.e("tag", "time=" + mPlayer.getDuration());
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加载网络资源
*/
public void play(String url) {
try {
this.uri = url;
mPlayer = new MediaPlayer();
mPlayer.setLooping(false);
mPlayer.setOnCompletionListener(this);
mPlayer.setOnErrorListener(this);
mPlayer.setOnPreparedListener(this);
mPlayer.setDataSource(mContext, Uri.parse(url));
mHud.show();
mPlayer.prepareAsync();
} catch (IOException e) {
Log.v("AudioHttpPlayer", e.getMessage());
}
}
最后附上Demo地址:https://github.com/MarsToken/RecordDemo,传送门
参考资料: