Android音频处理知识(一)MediaRecorder录制音频

3,096 阅读9分钟

在Android中处理音频方面的知识一直是一块很重要的知识,正好最近公司做一个关于打卡的内容,所以正好总结一下相应的知识,其实这块的知识挺杂挺乱的,所以这个不打算一次讲解完,分开给大家讲解,如果有什么不对的,希望大家一起讨论下!怎么突然感觉自己正经起来挺可笑的呢?哈哈!!!

先唠叨一下关于平时开发时候的一个细节,我也是最近才觉得这个东西比较有用的!什么呢?大家平时接到任务的时候怎么着手开发的呢?说简单点,当你开始敲代码的时候是怎么开始的!最近我看见我同事有一个很好的习惯,他每次开始敲代码的时候,先拿张纸写下来,然后简单分析一下,其实我平时也这样。但是重点不在这里,每次我分析完了就直接写代码了?然后又什么BUG改什么BUG。他不是,他写每一个页面都先搭一个架子,把具体的每一块代码都分工明确,其实就是把方法都抽解出来,他还和我说,这样做的好处是如果你某一块代码不会写的话,找别人写就OK了,不会对自己的内容产生影响!其实我感觉这种做法是很好的,所以和大家分享一下!!!如果你有什么更好的方案,不妨分享一下!!!

关于音频的内容包括一下的知识点:

处理方式 文件方式 字节流方式
录制音频 MediaRecorder AudioRecord
播放音频 MediaPlayer AudioTrack

因为这里面每个内容都涉及到很多内容,所以这里准备一块一块去讲!感觉这样总结的话,还能讲的更加透彻一点

本文知识点


  • MediaRecorder录制音频

1. MediaRecorder录制音频

MediaRecorder录制音频是不保证线程安全的。其次,MediaRecorder采集音频是以文件的形式保存的,所以你可以不用去考虑相应的字节写入问题,这点比较简单。其他的知识我会在后面使用到的时候进行讲解!

1.1 MediaRecoeder录制音频注意的一些问题

当我们录制音频的时候要考虑以下几个问题?

  • 录制音频的时候在那个线程执行?如果不是在主线程执行的话,怎么保证状态的及时更新?
  • MediaRecorder录制开始时有哪些设置?
  • 出现的异常怎么解决?
  • 录制音频的时候用户按下HOME键怎么处理?
  • 用户退出页面需要哪些操作?
  • 录制音频文件的名称需要注意什么?

以上这些问题都是我们需要考虑的!其实简单的实现录音的话相对简单一些,但是如果所有的问题都加到一块的话,就比较复杂了!!!我们还是一点一点开始说吧

1.2 开始录制音频的准备

为了能给大家讲解明白!所以我觉得我还是由必要唠叨几句,强烈建议大家把架子先搭建好!养成了这样的习惯可以让你在一个月甚至更长的时间回头看代码的时候,最起码不会说!这他妈是谁写的代码?然后默默的看了署名居然是自己的!!!我现在在单位些代码,基本上都在最上面把里面要实现的内容大体写写,然后注释写的稍微多点,最起码在你以后离开公司的时候别人不会说你!!!相信我,有的时候公司之前写的代码,真的是看不懂!!!

权限千万别忘了

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

首先先现象录制音频我们都需要什么?权限不能少吧!6.0权限的适配不能少吧?首先状态你不能少吧,反正很多东西要考虑!先说一下我的处理思路吧!我创建了一个线程去处理相应的音频录制,我通过一个按钮,在按下的时候开始录制音频,松开的时候停止录制音频,成功的话就保存相应的音频,如果时间小于3秒的话,就放弃保存文件(其实这里是删除了相应的文件,因为录制之前已经把文件路径传进去了,你录制多久都会有相应文件)!对了这里还有一个问题需要注意,就是录制文件的名称问题?我们项目在以前保存录制文件的名称是带有"-"的,像这样"2018-09-23-15:23:23"这种形式的,在大多数手机上是没有问题的,但是在OPPO手机上怎么也播放不出来,后来才知道有"-"就不行,后来干脆就直接用时间戳了,怎么也不会重复的!说了这么多,估计你也烦了,好吧!我先把基础的架子贴出来!!!

public class FileActivity extends AppCompatActivity {


    @BindView(R.id.tv_show)
    TextView mTvShow;
    @BindView(R.id.btn_start)
    TextView mBtnStart;
    @BindView(R.id.btn_play)
    TextView mBtnPlay;

    private ExecutorService mExecutorService;
    private Handler mMainHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file);
        ButterKnife.bind(this);

        mMainHandler = new Handler(Looper.getMainLooper());
        mExecutorService = Executors.newSingleThreadExecutor();

        //对按钮进行监听
        mBtnStart.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //开始录音
                        startRecord();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        //停止录音
                        stopRecord();
                        break;
                }
                return true;
            }
        });
    }

    /**
     * 开始录音
     */
    private void startRecord() {
        //更改UI的状态
        mBtnStart.setText("松手就可以停止录音");

        //开始录音
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                //释放之前存在的资源,因为这里是按下的时候就开始录音,所以按下的时候一定要先释放相应的资源
                releaseResources();
                if (!doStart()) {//不成功弹Toast提示用户
                    ToastFail();//提示用户
                }
            }
        });
    }

    /**
     * 提示用户失败信息
     */
    private void ToastFail() {

    }

    /**
     * 开始录音是否成功,
     * 所以所有的逻辑就直接在这里去写了
     */
    private boolean doStart() {
        return true;
    }


    /**
     * 停止录音
     */
    private void stopRecord() {
        mBtnStart.setText("按下开始录音");
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {

                if(!doStop()){
                    ToastFail();
                }
                //这里停止后应该,释放相应的资源
                releaseResources();
            }
        });
    }

    /**
     * 停止录音
     */
    private boolean doStop() {
        //这里说处理停止播放的逻辑
        return false;
    }

    /**
     * 释放资源
     */
    private void releaseResources() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //在主线程关闭的时候一定要停止线程,避免内存泄露
        mExecutorService.shutdownNow();
    }
}

注释还是比较全的,相信大家一看就能看懂,看不懂的可以骚扰我!!!

1.3 MediaRecorder录制音频的设置

这里主要是设置MediaRecorder的一些配置,主要包括一下几个内容。

  • 创建MediaRecorder对象
  • 创建录音文件保存的位置
  • 配置MediaRecorder
  • 开始录音
  • 保存开始时间,因为这里要做时间判断,所以要记录开始时间

基本上就以上这些内容,这里要是展开讲太多,其实是我也不会!真的,没个流媒体工程师你能说你会?不可能的!

    /**
     * 开始录音是否成功,
     * 所以所有的逻辑就直接在这里去写了
     */
    private boolean doStart() {
        /*
         * 1.创建MediaRecorder对象
         * 2.创建相应的保存文件
         * 3.配置相应的MediaRecorder
         * 4.开始录音
         */
        try {
            //创建mediaRecorder对象
            MediaRecorder recorder = new MediaRecorder();
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                //创建保存的文件
                String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Video/" + System.currentTimeMillis() + ".m4a";
                mAudioFile = new File(path);
                mAudioFile.getParentFile().mkdirs();
                mAudioFile.createNewFile();

                /*
                 * 重点来了,配置MediaRecorder
                 */
                //配置采集方式,这里用的是麦克风的采集方式
                recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                //配置输出方式,这里用的是MP4,
                recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
                //配置采样频率,频率越高月接近原始声音,Android所有设备都支持的采样频率为44100
                recorder.setAudioSamplingRate(44100);
                //配置文件的编码格式,AAC是比较通用的编码格式
                recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
                //配置码率,这里一般通用的是96000
                recorder.setAudioEncodingBitRate(96000);
                //配置录音文件的位置
                recorder.setOutputFile(mAudioFile.getAbsolutePath());

                //开始录制音频
                recorder.prepare();//准备
                recorder.start();//开始录音

                /*因为这里要做相应的时间判断,所以要做时间的记录*/
                mStartRecordTime = System.currentTimeMillis();

            } else {
                //因为这里SD卡没有挂在,所以就直接返回false,如果你真的想在项目中使用的话,需要判断内存大小什么的,这里为了简便就没有写,
                //但是现在基本上都没事,因为手机内容都很大,但是如果做项目的话这些都要做的!
                return false;
            }
        } catch (IOException|RuntimeException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

我个人感觉已经写的很清楚了,你就这么整就一定能开始录制音频,别点赞。要脸!!!然后你在失败的ToastFail方法中随便提示点什么就可以了,但是有一点需要注意,你要注意,这个ToastFail方法是在线程中执行的,那么问题来了?怎么在主线程提示呢?眼尖的童鞋应该注意到我在最开始的时候定义了一个主线程的Handler,这个时候它就排上用场了!整体代码如下:

    /**
     * 提示用户失败信息
     */
    private void ToastFail() {
        mAudioFile = null;
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(FileActivity.this, "录制音频失败", Toast.LENGTH_SHORT).show();
            }
        });
    }

1.4 MediaRecorder停止录音的方法

其实这里的逻辑还是很简单的,基本上就是调用一个stop的方法,判断一下时间,和上面一样,需要注意线程的问题,做Android的应该都知道,在非UI线程里面修改UI会报异常的!!!代码就像这个样子!

    /**
     * 停止录音
     */
    private boolean doStop() {
        try {
            //这里说处理停止播放的逻辑
            mMediaRecorder.stop();

            //因为这里要判断相应的时间,如果大于三秒就直接保存,否则删除文件
            mEndRecordTime = System.currentTimeMillis();
            final int time = (int) ((mEndRecordTime - mStartRecordTime) / 1000);
            if (time > 3) {
                mMainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        String des = "录音成功" + time + "秒";
                        mTvShow.setText(des);
                    }
                });
            } else {
                if (mAudioFile.exists()) {
                    mAudioFile.delete();
                }
                mMainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mTvShow.setText("录音小于3秒没有保存文件");
                    }
                });
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
            Log.e(TAG, "doStop: " + e);
            return false;
        }
        return true;
    }

上面就是停止录音的方法了,基本的实现逻辑就是大于3秒钟保存文件,否则就删除文件,其实MediaRecorder保存文件,不管你录制多少秒都会保存的!所以为了节约空间,所以这里做了删除的操作!就是把之前录制的文件进行删除!

1.5 释放相应的资源

其实释放资源就比较简单了,直接释放了滞空就可以了

    /**
     * 释放资源
     */
    private void releaseResources() {
        if (mMediaRecorder != null) {
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
    }

关于音频的录制就这么多的内容了,对了!提醒一下,测试的时候最好找个真机去测试,虚拟机会有各种各样的毛病!真心的,这一点千万要记得。我没有做权限的适配,因为不是重点!!!好了今天就到这里吧。詹姆斯输球了,很是不开心,激动的我一上午没怎么写代码,能赢的比赛就这么葬送了。。。可怜了詹姆斯啊!!!不说了,心塞。。。

忘了说了,代码地址