Android 实现能够暂停的录音功能
转载请注明出处:http://blog.csdn.net/yegongheng/article/details/40624267
好久没更新博客了,着实有点惭愧,以后不管工作是忙是闲都得坚持更新博客,持之以恒地做下去!
正式进入主题,今天我分享一个在工作中过程中遇到的一个技术难点以及我解决该难点的方案,该问题困扰了我许久,通过不断地研究和翻阅资料,终于在满足工作需求的情况下将该问题解决,希望我的经验能够对读者有所帮助。我们知道Android ApI提供了MediaRecorder和AudioRecord两个类给开发者来很方便地实现音视频的录制(前者可以实现音频和视频的录制,后者只能实现音频的录制)。这两个类都提供了start()和stop()方法用于开始和结束音频或视频的录制,但令人费解的是这两个类都没有提供pause()方法用于暂停录制音视频,因为在实际应用当中,暂停录制的功能是非常有必要的,暂不清楚Google工程师们在设计API时是如何考量的而没有添加这个方法,可能另有玄机吧。那既然Android自身没有提供这样一个方法,就只有我们自己来实现了,那么问题就来了,就是到底如何实现音频录制的暂停方法呢?别急,先讲一下我在工作中所遇到的需求,如下:需实现音频录制的暂停功能,并且生成的音频文件格式必须是m4a格式。为什么项目中音频文件一定要采用m4a格式的呢?有以下几点原因:
1. 录制相同时间的音频,使用m4a格式存储的文件的大小要比使用其它格式类型存储的文件的大小要小(通过实验多次,在相同采样率16000的情况下,一般录制5分钟的音频,采用m4a格式存储的音频文件只有1.2Mb,而采用arm、mp3及其它格式的一般都有2-5Mb),这样当用户需要下载或上传录制的音频文件时,可以节省流量,并且相同压缩率的前提下,m4a格式音频的音质相比其它格式的也更高;
2.产品同时拥有Android客户端和IOS客户端,那为了避免使用Android客户端的用户录制的音频上传到服务器之后,使用IOS客户端的用户下载下来发生无法播放的问题,我们需统一录制音频的存储格式。由于Iphone手机官方推荐的音频格式是m4a且对m4a格式的音频文件支持度较高,再综合第一点来看,于是我们选择m4a格式作为音频文件的存储格式。
好了,解释了为什么音频录制文件必须使用m4a存储格式之后,接下来我们来解决如何实现音频的录制的暂停功能。前面讲了,Android SDK API提供了MediaRecorder和AudioRecord两个类来完成音视频的录制方法,我们看下它们两者之间的特点和区别:
MediaRecorder:
特性:该类集成了录音、编码和压缩等功能,可根据设置的编码格式的参数直接生成各种格式的音频文件(如arm、 mp3或m4a等),由于集成度较高,因此使用起来简单,但灵活度不高,不能实现像AudioRecord那样进行音 频的实时处理。
AudioRecord:
特性:该类录制的音频为原始的PCM二进制音频数据,没有文件头和文件尾,生成的PCM文件不能直接使用 Mediaplayer播放,只能使用AudioTrack播放。使用AudioRecord可以实现边录边播的音频实时处理。
了解了这两个类的特性之后,起初我决定使用MediaRecorder类来解决录制暂停的问题,具体的思路如下:
(1)每次触发开始录制和暂停录制音频的事件时都单独保存一个m4a格式的音频文件,直到最后触发停止录制音频的事件时,将之前录制的若干m4a格式的音频文件合并成一个文件。如图下:
这种方法比较好理解,也容易想到,不过在实现过程中遇到了一个技术难点,那就是多个m4a格式的音频文件的合并并不是简单地将文件的内容拷贝到一个文件中,而是要通过分析每一个m4a格式的音频文件,计算出每个文件头的结构大小,并将文件头去掉,再将文件进行拷贝合并。通过查阅资料,发现m4a格式的音频文件头是由多个包含关系的ATOM结构组成,且每个不同的m4a格式的音频文件的文件头的大小都不一样,这样使得多个m4a文件头文件解析和合并变得较为复杂,若有多个m4a文件需要合并,那么会变得较为耗时。再者,对于没有足够音视频文件解析和编解码经验的开发者来讲,要精准地得解析一个m4a文件,挑战性太大(网上这方面的资料也寥寥无几),有兴趣的读者可以进行深入研究。
上述方法行不通,于是只好作罢,后来又想到了另外一种方法,也是我解决问题的最终方案,具体的思路如下:
(2)由于使用AudioRecord类提供的方法录制的音频是原始的PCM格式的二进制数据,该格式的文件没有文件头信息,那么我们在进行文件合并时就就无需解析文件结构去掉对应的文件头,这样就变成了二进制数据地简单拷贝和合并。我在这里实现的方式是在录制音频的过程中采用边录制边写入的方式不断地向同一个文件写入录制的二进制音频数据。当触发暂停录音事件时,停止录制停止写入二进制数据,当触发继续录音事件时,则继续录制和向文件中写入数据。最后停止写入数据时,将PCM二进制音频文件编码成m4a格式的音频文件。如图下:
上面方法描述中,实现边录制边写入的功能倒比较简单,关键难点是如何将PCM二进制数据编码成目标的m4a格式的音频数据,要实现音视频的编解码,一般都是使用第三方开源的编解码库,比较著名的有FFMpeg和Speex,这些库都提供了录制、转换以及流化音视频的完整解决方案,不过在此我的需求只是需要简单地实现编码工作,使用这些开源库体积太大,有点杀鸡用牛刀的感觉。因此,通过研究和查阅资料,我在github上找到了一个非常有用的编解码开源项目android-aac-enc(地址:https://github.com/timsu/android-aac-enc),该开源项目能完美地实现将原始的pcm格式的二进制数据编码成m4a格式的数据文件,相比于FFmpeg库,这个库有以下几点优点:
1. aac-enc库的体积比FFmpeg库的体积更小;
2. 相比FFMpeg, aac-enc实现格式转换更加简单和快速;
3. aac-enc比FFmpeg需要编译更少的底层的代码。
该开源项目使用起来也非常地简单,通过分析其示例代码我们可以通过以下四个步骤来实现音频的编码工作,代码如下:
/** * 1.初始化编码配置 * * 32000 : 音频的比特率 * 2 : 音频的声道 * sampleRateInHz : 音频采样率 * 16 :音频数据格式,PCM 16位每个样本 * FileUtils.getAAcFilePath(mAudioRecordFileName) : aac音频文件的存储路径 */ encoder.init(32000, 2, sampleRateInHz, 16, FileUtils. getAAcFilePath(mAudioRecordFileName)); /** * 2.对二进制代码进行编码 * * b :需要编码的二进制音频流 */ encoder.encode(b); /** * 3. 从pcm二进制数据转aac音频文件编码完成 * */ encoder.uninit(); /** * 4. 将aac文件转码成m4a文件 * * FileUtils.getAAcFilePath(mAudioRecordFileName) :需要编码的aac文件路径 * FileUtils.getM4aFilePath(mAudioRecordFileName) :编码成m4a文件的目标路径 */ new AACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName), FileUtils.getM4aFilePath(mAudioRecordFileName));使用起来是不是很简单方便,我们无需对音频文件格式和文件头进行判断和解析,只需要通过该开源项目封装的api方法直接调用就可以很快速的将原始的二进制PCM音频数据转换成m4a格式的音频数据文件。感兴趣的读者可以去研究一下该项目的源码,了解一下其内部的实现,这里暂且不深入探究。
基本上明确好思路和编码的实现方法后,接下来就是具体的实现过程了,我们将依据上面的思路和方法来实现一个具有暂停功能的音频录制Demo。首先看下Demo的项目结构,如下图:
如何使用AudioRecord类来实现音频的录制,这方面的资料很多,读者可以先学习,简单地入一下门。接下来我们先运行一下Demo,来看一下效果图:
(1)初始界面 (2)正在录制界面 (2)暂停界面
(4)播放界面 (5)暂停播放界面
粗略看了Demo的运行效果图后,接下来我们就要来实现,这里由于要使用aac-encode项目来实现音频的编码,则需将该项目以library的形式集成到我们的Demo中,做完该项工作后,我们就可以在Demo工程中写其它相关的逻辑代码了,下面看一下实现demo的关键代码,首先是RecordAct.java文件中的代码,该类为主界面类,主要实现了界面的初始化、音频的录制和音频播放的功能,具体的代码如下:
public class RecordAct extends Activity implements OnClickListener{ /** * Status:录音初始状态 */ private static final int STATUS_PREPARE = 0; /** * Status:正在录音中 */ private static final int STATUS_RECORDING = 1; /** * Status:暂停录音 */ private static final int STATUS_PAUSE = 2; /** * Status:播放初始状态 */ private static final int STATUS_PLAY_PREPARE = 3; /** * Status:播放中 */ private static final int STATUS_PLAY_PLAYING = 4; /** * Status:播放暂停 */ private static final int STATUS_PLAY_PAUSE = 5; private int status = STATUS_PREPARE; /** * 录音时间 */ private TextView tvRecordTime; /** * 录音按钮 */ private ImageView btnRecord;// 录音按钮 private PopupWindow popAddWindow; /** * 试听界面 */ private LinearLayout layoutListen; /** * 录音长度 */ private TextView tvLength; private TextView recordContinue; /** * 重置按钮 */ private View resetRecord; /** * 结束录音 */ private View recordOver; private ImageView audioRecordNextImage; private TextView audioRecordNextText; /** * 音频播放进度 */ private TextView tvPosition; long startTime = 0; /** * 最大录音长度 */ private static final int MAX_LENGTH = 300 * 1000; private Handler handler = new Handler(); private Runnable runnable; /** * 音频录音的总长度 */ private static int voiceLength; /** * 音频录音帮助类 */ private AudioRecordUtils mRecordUtils; /** * 播放进度条 */ private SeekBar seekBar; /** * 音频播放类 */ private Player player; /** * 录音文件名 */ private String audioRecordFileName; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.pop_add_record); initView(); } public void initView(){ //音频录音的文件名称 audioRecordFileName = TimeUtils.getTimestamp(); //初始化音频录音对象 mRecordUtils = new AudioRecordUtils(this,audioRecordFileName); View view = LayoutInflater.from(this).inflate(R.layout.pop_add_record, null); tvRecordTime = (TextView)findViewById(R.id.tv_time); btnRecord = (ImageView)findViewById(R.id.iv_btn_record); btnRecord.setOnClickListener(this); recordContinue = (TextView)findViewById(R.id.record_continue_txt); resetRecord = findViewById(R.id.btn_record_reset); recordOver = findViewById(R.id.btn_record_complete); resetRecord.setOnClickListener(this); recordOver.setOnClickListener(this); audioRecordNextImage = (ImageView)findViewById(R.id.recrod_complete_img); audioRecordNextText = (TextView)findViewById(R.id.record_complete_txt); layoutListen = (LinearLayout)findViewById(R.id.layout_listen); tvLength = (TextView)findViewById(R.id.tv_length); tvPosition = (TextView)findViewById(R.id.tv_position); seekBar = (SeekBar)findViewById(R.id.seekbar_play); seekBar.setOnSeekBarChangeListener(new SeekBarChangeEvent()); seekBar.setEnabled(false); player = new Player(seekBar, tvPosition); player.setMyPlayerCallback(new MyPlayerCallback() { @Override public void onPrepared() { seekBar.setEnabled(true); } @Override public void onCompletion() { status = STATUS_PLAY_PREPARE; seekBar.setEnabled(false); seekBar.setProgress(0); tvPosition.setText("00:00"); recordContinue.setBackgroundResource(R.drawable.record_audio_play); } }); popAddWindow = new PopupWindow(view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); popAddWindow.setFocusable(true); popAddWindow.setAnimationStyle(R.style.pop_anim); popAddWindow.setBackgroundDrawable(new BitmapDrawable()); } public void handleRecord(){ switch(status){ case STATUS_PREPARE: mRecordUtils.startRecord(); btnRecord.setBackgroundResource(R.drawable.record_round_red_bg); status = STATUS_RECORDING; voiceLength = 0; timing(); break; case STATUS_RECORDING: pauseAudioRecord(); resetRecord.setVisibility(View.VISIBLE); recordOver.setVisibility(View.VISIBLE); btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg); recordContinue.setVisibility(View.VISIBLE); status = STATUS_PAUSE; break; case STATUS_PAUSE: mRecordUtils.startRecord(); resetRecord.setVisibility(View.INVISIBLE); recordOver.setVisibility(View.INVISIBLE); btnRecord.setBackgroundResource(R.drawable.record_round_red_bg); recordContinue.setVisibility(View.INVISIBLE); status = STATUS_RECORDING; timing(); break; case STATUS_PLAY_PREPARE: player.playUrl(FileUtils.getM4aFilePath(audioRecordFileName)); recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause); status = STATUS_PLAY_PLAYING; break; case STATUS_PLAY_PLAYING: player.pause(); recordContinue.setBackgroundResource(R.drawable.record_audio_play); status = STATUS_PLAY_PAUSE; break; case STATUS_PLAY_PAUSE: player.play(); recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause); status = STATUS_PLAY_PLAYING; break; } } /** * 暂停录音 */ public void pauseAudioRecord(){ mRecordUtils.pauseRecord(); if (handler != null && runnable != null) { handler.removeCallbacks(runnable); runnable = null; } } /** * 停止录音 */ public void stopAudioRecord(){ pauseAudioRecord(); mRecordUtils.stopRecord(); status = STATUS_PLAY_PREPARE; showListen(); } /** * 重新录音参数初始化 */ @SuppressLint("NewApi") public void resetAudioRecord(){ //停止播放音频 player.stop(); pauseAudioRecord(); mRecordUtils.reRecord(); status = STATUS_PREPARE; voiceLength = 0; tvRecordTime.setTextColor(Color.WHITE); tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength)); recordContinue.setText(R.string.record_continue); recordContinue.setBackground(null); recordContinue.setVisibility(View.GONE); layoutListen.setVisibility(View.GONE); tvRecordTime.setVisibility(View.VISIBLE); audioRecordNextImage.setImageResource(R.drawable.btn_record_icon_complete); audioRecordNextText.setText(R.string.record_over); btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg); resetRecord.setVisibility(View.INVISIBLE); recordOver.setVisibility(View.INVISIBLE); } /** * 计时功能 */ private void timing() { runnable = new Runnable() { @Override public void run() { voiceLength += 100; if (voiceLength >= (MAX_LENGTH - 10 * 1000)) { tvRecordTime.setTextColor(getResources().getColor( R.color.red_n)); } else { tvRecordTime.setTextColor(Color.WHITE); } if (voiceLength > MAX_LENGTH) { stopAudioRecord(); } else { tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength)); handler.postDelayed(this, 100); } } }; handler.postDelayed(runnable, 100); } @Override public void onClick(View v) { // TODO Auto-generated method stub switch (v.getId()) { case R.id.iv_btn_record: handleRecord(); break; case R.id.btn_record_reset: resetAudioRecord(); break; case R.id.btn_record_complete: stopAudioRecord(); break; default: break; } } /** * 显示播放界面 */ private void showListen() { layoutListen.setVisibility(View.VISIBLE); tvLength.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength)); tvRecordTime.setVisibility(View.GONE); resetRecord.setVisibility(View.VISIBLE); recordOver.setVisibility(View.INVISIBLE); recordContinue.setVisibility(View.VISIBLE); seekBar.setProgress(0); tvPosition.setText("00:00"); btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg); recordContinue.setText(null); recordContinue.setBackgroundResource(R.drawable.record_audio_play); } /** * * SeekBar进度条改变事件监听类 */ class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener { int progress; @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (null != player && player.mediaPlayer != null) { this.progress = progress * player.mediaPlayer.getDuration() / seekBar.getMax(); tvPosition.setText(TimeUtils .convertMilliSecondToMinute2(player.currentPosition)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { if (player.mediaPlayer != null) { player.mediaPlayer.seekTo(progress); } } } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); player.stop(); } }
上面代码注释比较清楚,且好理解,因此不多分析,读者自行学习。下面再来看一下AudioRecordUtils类的代码,该类是音频录制功能的主要实现代码,里面简单地封装了开始录音、暂停录音、停止录音和重新录音几个方法,在开发中只要调用就行,来看看具体的实现代码,如下:
public class AudioRecordUtils { private final int audioSource = MediaRecorder.AudioSource.MIC; // 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025 private final int sampleRateInHz = 16000; // 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道 private final int channelConfig = AudioFormat.CHANNEL_IN_STEREO; // 音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。 private final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private int inBufSize = 0; private AudioRecord audioRecord; private AACEncoder encoder = null; private ProgressDialog mProgressDialog = null; private boolean isRecord = false; private Context mContext; /** * 录制的音频文件名称 */ private String mAudioRecordFileName; private static final int RECORDED_INIT_DELETE = 0; private static final int RECORDED_COMPLETED_DELETE = 1; public AudioRecordUtils(Context context,String audioRecordFileName){ mContext = context; mAudioRecordFileName = audioRecordFileName; initAudioRecord(); } /** * 初始化对象 */ private void initAudioRecord(){ inBufSize = AudioRecord.getMinBufferSize( sampleRateInHz, channelConfig, audioFormat); audioRecord = new AudioRecord( audioSource, sampleRateInHz, channelConfig, audioFormat, inBufSize); encoder = new AACEncoder(); deleteAllFiles(RECORDED_INIT_DELETE); mProgressDialog = new ProgressDialog(mContext); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); mProgressDialog.setCanceledOnTouchOutside(false); mProgressDialog.setCancelable(false); mProgressDialog.setTitle("提示"); mProgressDialog.setMessage("正在保存录音,请耐心等候......"); } /** * 开始录音 */ public void startRecord(){ new AudioRecordTask().execute(); } /** * 暂停录音 */ public void pauseRecord(){ isRecord = false; } /** * 停止录音 */ public void stopRecord(){ new AudioEncoderTask().execute(); } /** * 重新录制 */ public void reRecord(){ //重新录制时,删除录音文件夹中的全部文件 deleteAllFiles(RECORDED_INIT_DELETE); } private void encodeAudio(){ try { //读取录制的pcm音频文件 DataInputStream mDataInputStream = new DataInputStream(new FileInputStream( FileUtils.getPcmFilePath(mAudioRecordFileName))); byte[] b = new byte[(int) new File(FileUtils. getPcmFilePath(mAudioRecordFileName)).length()]; mDataInputStream.read(b); //初始化编码配置 encoder.init(32000, 2, sampleRateInHz, 16, FileUtils. getAAcFilePath(mAudioRecordFileName)); //对二进制代码进行编码 encoder.encode(b); //编码完成 encoder.uninit(); //关闭流 mDataInputStream.close(); try { //将aac文件转码成m4a文件 new AACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName), FileUtils.getM4aFilePath(mAudioRecordFileName)); } catch (IOException e) { Log.e("ERROR", "error converting", e); } deleteAllFiles(RECORDED_COMPLETED_DELETE); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } class AudioRecordTask extends AsyncTask<Void, Void, Void>{ @Override protected Void doInBackground(Void... params) { // TODO Auto-generated method stub if(audioRecord == null){ initAudioRecord(); } RandomAccessFile mRandomAccessFile = null; try { mRandomAccessFile = new RandomAccessFile(new File( FileUtils.getPcmFilePath(mAudioRecordFileName)), "rw"); byte[] b = new byte[inBufSize/4]; //开始录制音频 audioRecord.startRecording(); //判断是否正在录制 isRecord = true; while(isRecord){ audioRecord.read(b, 0, b.length); //向文件中追加内容 mRandomAccessFile.seek(mRandomAccessFile.length()); mRandomAccessFile.write(b, 0, b.length); } //停止录制 audioRecord.stop(); mRandomAccessFile.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } } class AudioEncoderTask extends AsyncTask<Void, Void, Long>{ @Override protected void onPreExecute() { // TODO Auto-generated method stub super.onPreExecute(); if(mProgressDialog != null && !mProgressDialog.isShowing()){ mProgressDialog.show(); } } @Override protected Long doInBackground(Void... params) { // TODO Auto-generated method stub encodeAudio(); return null; } @Override protected void onPostExecute(Long result) { // TODO Auto-generated method stub super.onPostExecute(result); if(mProgressDialog.isShowing()){ mProgressDialog.cancel(); mProgressDialog.dismiss(); } } } /** * 清空音频录制文件夹中的所有文件 * @param isRecorded */ public void deleteAllFiles(int isRecorded){ File[] files = new File(FileUtils.getAudioRecordFilePath()).listFiles(); switch (isRecorded) { case RECORDED_INIT_DELETE: for(File file: files){ file.delete(); } break; case RECORDED_COMPLETED_DELETE: for(File file: files){ if(!file.getName().equals(mAudioRecordFileName + Constants.M4A_SUFFIX)){ file.delete(); } } break; default: break; } } }
上面代码关键处都有注释,读者可自行学习。自此,我们基本熟悉了实现能够暂停录音功能的关键代码,代码没有全部贴出,想要完整的Demo可在文章末尾下载来仔细研究。最后我再补充一点,就是若读者对录制的音频格式没有严格的要求话,如录制的音频格式是arm格式,则没有必要考虑到音频的编解码问题,因为arm格式的音频文件的文件头信息固定是6个字节的大小,那这种情况读者可以采用文章开头所说的第一种方法,就是每次点击暂停事件都录制成一个arm文件,在最后合并的时候,只需要去掉第2至n个文件的前6个字节,然后进行文件的拷贝合并就行,这里有一篇采用该方法的的文章(地址:http://blog.csdn.net/wanli_smile/article/details/7715030),有需求的读者应该会有所收获。
源代码下载,请戳下面:
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。