Android 能够暂停的录音功能
Android ApI提供了MediaRecorder和AudioRecord两个类给开发者来很方便地实现音视频的录制(前者可以实现音频和视频的录制,后者只能实 现音频的录制)。这两个类都提供了start()和stop()方法用于开始和结束音频或视频的录制,但令人费解的是这两个类都没有提供pause()方 法用于暂停录制音视频,因为在实际应用当中,暂停录制的功能是非常有必要的Android 实现能够暂停的录音功能
需实现音频录制的暂停功能,并且生成的音频文件格式必须是m4a格式
为什么项目中音频文件一定要采用m4a格式的呢?有以下几点原因:
1. 录制相同时间的音频,使用m4a格式存储的文件的大小要比使用其它格式类型存储的文件的大小要小(通过实验多次,在相同采样率16000的情况下,一般录 制5分钟的音频,采用m4a格式存储的音频文件只有1.2Mb,而采用arm、mp3及其它格式的一般都有2-5Mb),这样当用户需要下载或上传录制的音频文件时,可以节省流量,并且相同压缩率的前提下,m4a格式音频的音质相比其它格式的也更高;
2.产品同时拥有Android客户端和IOS客户端,那为了避免使用Android客户端的用户录制的音频上传到服务器之后,使用IOS客户端的用户下
载下来发生无法播放的问题,我们需统一录制音频的存储格式。由于Iphone手机官方推荐的音频格式是m4a且对m4a格式的音频文件支持度较高,再综合
第一点来看,于是我们选择m4a格式作为音频文件的存储格式。
MediaRecorder:
特性:该类集成了录音、编码和压缩等功能,可根据设置的编码格式的参数直接生成各种格式的音频文件(如arm、 mp3或m4a等),由于集成度较高,因此使用起来简单,但灵活度不高,不能实现像AudioRecord那样进行音 频的实时处理。
AudioRecord:
特性:该类录制的音频为原始的PCM二进制音频数据,没有文件头和文件尾,生成的PCM文件不能直接使用 Mediaplayer播放,只能使用AudioTrack播放。使用AudioRecord可以实现边录边播的音频实时处理。
实现边录制边写入的功能倒比较简单,关键难点是如何将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)暂停播放界面
接下来我们就要来实现,这里由于要使用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="">{ @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, 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; } } }
最后我再补充一点,就是若读者对录制的音频格式没有严格的要求话,如录制的音频格式是arm格式,则没有必要考虑到音频的编解码问题,因为arm格式的音 频文件的文件头信息固定是6个字节的大小,那这种情况读者可以采用文章开头所说的第一种方法,就是每次点击暂停事件都录制成一个arm文件,在最后合并的 时候,只需要去掉第2至n个文件的前6个字节,然后进行文件的拷贝合并就行,