作者:developerHaoz
地址:http://www.jianshu.com/p/6bbb51ac4938
声明:本文为 developerHaoz 原创,已获授权发表,未经原作者允许请勿转载.


前言

最近项目中需要用到录音的功能,借鉴了外国一位哥们的项目 https://github.com/dkim0419/SoundRecorder ,搞定需求之后,花了些时间封装成一个录音的工具包,分享给大家,需要源码的点击这里 https://github.com/developerHaoz/SoundRecorderUtils

先贴个效果图给大家看一下,看看这个录音包的功能

一、实现录音的 Service

这个类可以说是这个包的核心了,如果理解了这个 Service,录音这一块基本就没什么问题了。

录音主要是利用 MediaRecoder 这个类,进行声音的记录,接下来我们一起来看看具体的实现。

  1. public class RecordingService extends Service {

  2.    private String mFileName;

  3.    private String mFilePath;

  4.    private MediaRecorder mRecorder;

  5.    private long mStartingTimeMillis;

  6.    private long mElapsedMillis;

  7.    @Override

  8.    public int onStartCommand(Intent intent, int flags, int startId) {

  9.        startRecording();

  10.        return START_STICKY;

  11.    }

  12.    @Override

  13.    public void onDestroy() {

  14.        if (mRecorder != null) {

  15.            stopRecording();

  16.        }

  17.        super.onDestroy();

  18.    }

  19.    // 开始录音

  20.    public void startRecording() {

  21.        setFileNameAndPath();

  22.        mRecorder = new MediaRecorder();

  23.        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

  24.        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //录音文件保存的格式,这里保存为 mp4

  25.        mRecorder.setOutputFile(mFilePath); // 设置录音文件的保存路径

  26.        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

  27.        mRecorder.setAudioChannels(1);

  28.        // 设置录音文件的清晰度

  29.        mRecorder.setAudioSamplingRate(44100);

  30.        mRecorder.setAudioEncodingBitRate(192000);

  31.        try {

  32.            mRecorder.prepare();

  33.            mRecorder.start();

  34.            mStartingTimeMillis = System.currentTimeMillis();

  35.        } catch (IOException e) {

  36.            Log.e(LOG_TAG, "prepare() failed");

  37.        }

  38.    }

  39.    // 设置录音文件的名字和保存路径

  40.    public void setFileNameAndPath() {

  41.        File f;

  42.        do {

  43.            count++;

  44.            mFileName = getString(R.string.default_file_name)

  45.                    + "_" + (System.currentTimeMillis()) + ".mp4";

  46.            mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath();

  47.            mFilePath += "/SoundRecorder/" + mFileName;

  48.            f = new File(mFilePath);

  49.        } while (f.exists() && !f.isDirectory());

  50.    }

  51.    // 停止录音

  52.    public void stopRecording() {

  53.        mRecorder.stop();

  54.        mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis);

  55.        mRecorder.release();

  56.        getSharedPreferences("sp_name_audio", MODE_PRIVATE)

  57.                .edit()

  58.                .putString("audio_path", mFilePath)

  59.                .putLong("elpased", mElapsedMillis)

  60.                .apply();

  61.        if (mIncrementTimerTask != null) {

  62.            mIncrementTimerTask.cancel();

  63.            mIncrementTimerTask = null;

  64.        }

  65.        mRecorder = null;

  66.    }

  67. }

可以看到在 onStartCommand() 里面有一个 startRecording() 方法,在外部启动这个 RecordingService 的时候,便会调用这个 startRecording() 方法开始录音。

在 startRecording() 方法中先调用了 setFileNameAndPath 方法,初始化了录音文件的名字和保存的路径,为了让每个录音文件都有唯一的名字,我调用 System.currentMillis() 拼接到录音文件的名字里面。

  1.    public void setFileNameAndPath() {

  2.        File f;

  3.        do {

  4.            count++;

  5.            mFileName = getString(R.string.default_file_name)

  6.                    + "_" + (System.currentTimeMillis()) + ".mp4";

  7.            mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath();

  8.            mFilePath += "/SoundRecorder/" + mFileName;

  9.            f = new File(mFilePath);

  10.        } while (f.exists() && !f.isDirectory());

  11.    }

设置好了文件的名字和保存路径之后,对 mRecorder 进行一系列参数的设置,这个 mRecorder 是 MediaRecorder 的一个实例,专门用于录音的存储。

  1.        mRecorder = new MediaRecorder();

  2.        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

  3.        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //录音文件保存的格式,这里保存为 mp4

  4.        mRecorder.setOutputFile(mFilePath); // 设置录音文件的保存路径

  5.        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

  6.        mRecorder.setAudioChannels(1);

  7.        // 设置录音文件的清晰度

  8.        mRecorder.setAudioSamplingRate(44100);

  9.        mRecorder.setAudioEncodingBitRate(192000);

  10.        try {

  11.            mRecorder.prepare();

  12.            mRecorder.start();

  13.            mStartingTimeMillis = System.currentTimeMillis();

  14.        } catch (IOException e) {

  15.            Log.e(LOG_TAG, "prepare() failed");

  16.        }

设置好参数之后,启动 mRecorder 开始录音,可以看到启动 mRecorder 开始录音后,我还将当前的时间赋值给 mStartingTimeMills,这里主要是为了记录录音的时长,等到录音结束后再获取一次当前的时间,然后将两个时间进行相减,就能得到录音的具体时长了。

等到录音结束,停止服务后,便会回调 RecordingService 的 onDestroy() 方法,这时候便会调用 stopRecording() 方法,关闭 mRecorder,并用 SharedPreferences 保存录音文件的信息,最后将 mRecorder 置空,防止内存泄露

  1.    public void stopRecording() {

  2.        mRecorder.stop();

  3.        mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis);

  4.        mRecorder.release();

  5.        getSharedPreferences("sp_name_audio", MODE_PRIVATE)

  6.                .edit()

  7.                .putString("audio_name", mFileName)

  8.                .putString("audio_path", mFilePath)

  9.                .putLong("elpased", mElapsedMillis)

  10.                .apply();

  11.        if (mIncrementTimerTask != null) {

  12.            mIncrementTimerTask.cancel();

  13.            mIncrementTimerTask = null;

  14.        }

  15.        mRecorder = null;

  16.    }

二、显示录音界面的 RecordAudioDialogFragment

用户进行的时候,总不能让 App 跳转到另外一个界面吧,这样用户体验并不是很好,比较好的方法是显示一个对话框,让用户进行操作,既然要用对话框,必然离不开 DialogFragment,对于 DialogFragment 不是很了解,可以先看看我这篇文章 Android 撸起袖子,自己封装 DialogFragment。

  1. public class RecordAudioDialogFragment extends DialogFragment {

  2.    private boolean mStartRecording = true;

  3.    long timeWhenPaused = 0;

  4.    private FloatingActionButton mFabRecord;

  5.    private Chronometer mChronometerTime;

  6.    public static RecordAudioDialogFragment newInstance(int maxTime) {

  7.        RecordAudioDialogFragment dialogFragment = new RecordAudioDialogFragment();

  8.        Bundle bundle = new Bundle();

  9.        bundle.putInt("maxTime", maxTime);

  10.        dialogFragment.setArguments(bundle);

  11.        return dialogFragment;

  12.    }

  13.    @NonNull

  14.    @Override

  15.    public Dialog onCreateDialog(Bundle savedInstanceState) {

  16.        Dialog dialog = super.onCreateDialog(savedInstanceState);

  17.        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  18.        View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_record_audio, null);

  19.        mFabRecord.setOnClickListener(new View.OnClickListener() {

  20.            @Override

  21.            public void onClick(View v) {

  22.                if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)

  23.                        != PackageManager.PERMISSION_GRANTED) {

  24.                    ActivityCompat.requestPermissions(getActivity()

  25.                            , new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);

  26.                }else {

  27.                    onRecord(mStartRecording);

  28.                    mStartRecording = !mStartRecording;

  29.                }

  30.            }

  31.        });

  32.        builder.setView(view);

  33.        return builder.create();

  34.    }

  35.    private void onRecord(boolean start) {

  36.        Intent intent = new Intent(getActivity(), RecordingService.class);

  37.        if (start) {

  38.            File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");

  39.            if (!folder.exists()) {

  40.                folder.mkdir();

  41.            }

  42.            mChronometerTime.setBase(SystemClock.elapsedRealtime());

  43.            mChronometerTime.start();

  44.            getActivity().startService(intent);

  45.        } else {

  46.            mChronometerTime.stop();

  47.            timeWhenPaused = 0;

  48.            getActivity().stopService(intent);

  49.        }

  50.    }

  51. }

可以看到在 RecordAudioDialogFragment 有一个 newInstance(int maxTime) 的静态方法供外部调用,如果想设置录音的最大时长,直接传参数进去就行了。

好的,敲黑板,重点来了,其实这个对话框的重点部分就是在 onCreateDialog()中,我们先加载了我们自定义的对话框的布局,当点击录音的按钮的时候,先进行相关权限的申请,这里有个巨坑,录音权限 android.permission.RECORD_AUDIO 在不久前还是普通权限的,不知道什么时候突然变成了危险权限,需要我们进行申请,Google 真是会玩。

  1.    public Dialog onCreateDialog(Bundle savedInstanceState) {

  2.        Dialog dialog = super.onCreateDialog(savedInstanceState);

  3.        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  4.        View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_record_audio, null);

  5.        mFabRecord.setOnClickListener(new View.OnClickListener() {

  6.            @Override

  7.            public void onClick(View v) {

  8.                if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)

  9.                        != PackageManager.PERMISSION_GRANTED) {

  10.                    ActivityCompat.requestPermissions(getActivity()

  11.                            , new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);

  12.                }else {

  13.                    onRecord(mStartRecording);

  14.                    mStartRecording = !mStartRecording;

  15.                }

  16.            }

  17.        });

  18.        builder.setView(view);

  19.        return builder.create();

  20.    }

申请好权限之后便会调用 onRecord() 这个方法,然后将 boolean mStartRecording 进行反转,这样就不用写难看的 if else 了,直接改变 mStartRecording 的值,然后在 onRecord() 里面进行处理

接下来看下 onRecord 干了什么

  1.    private void onRecord(boolean start) {

  2.        Intent intent = new Intent(getActivity(), RecordingService.class);

  3.        if (mStartRecording) {

  4.            File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");

  5.            if (!folder.exists()) {

  6.                folder.mkdir();

  7.            }

  8.            mChronometerTime.setBase(SystemClock.elapsedRealtime());

  9.            mChronometerTime.start();

  10.            getActivity().startService(intent);

  11.        } else {

  12.            mChronometerTime.stop();

  13.            timeWhenPaused = 0;

  14.            getActivity().stopService(intent);

  15.        }

  16.    }

好吧,其实并没有干了什么大事,只是创建了保存录音文件的文件夹,然后根据 mStartRecording 的值进行 RecordingService 的启动和关闭罢了。在启动时还顺便开始了 mChronometer 的计时显示,这是一个 Android 原生的显示计时的一个控件。

三、播放录音的 PlaybackDialogFragment

其实,如果只是录音这一块的话,写个 MediaPlayer 就可以了,然而还要写播放的时间进度,以及显示一个稍微好看点的进度条,我能怎样,我也很烦啊。

外部调用这个对话框的时候,只需要传入一个包含录音文件信息的 RecordingItem,因为包含的信息比较多,所以最好将 RecordingItem 进行序列化。

  1.    public static PlaybackDialogFragment newInstance(RecordingItem item) {

  2.        PlaybackDialogFragment fragment = new PlaybackDialogFragment();

  3.        Bundle bundle = new Bundle();

  4.        bundle.putParcelable(ARG_ITEM, item);

  5.        fragment.setArguments(b);

  6.        return fragment;

  7.    }

好,重点又来了,来看看 onCreateDialog() 方法,在加载了布局之后,给 mSeekBar 设置监听,mSeekBar 是一个显示进度条的控件,当开始播放录音时候,将录音文件的时长,设置进 mSeekBar 里面,播放录音的同时,运行 mSeekBar,通过监听 mSeekBar 的进度,刷新显示的播放进度。

  1.    public Dialog onCreateDialog(Bundle savedInstanceState) {

  2.        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  3.        View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_media_playback, null);

  4.        mTvFileLength.setText(String.valueOf(mFileLength));

  5.        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

  6.            @Override

  7.            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

  8.                if(mMediaPlayer != null && fromUser) {

  9.                    mMediaPlayer.seekTo(progress);

  10.                    mHandler.removeCallbacks(mRunnable);

  11.                    long minutes = TimeUnit.MILLISECONDS.toMinutes(mMediaPlayer.getCurrentPosition());

  12.                    long seconds = TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getCurrentPosition())

  13.                            - TimeUnit.MINUTES.toSeconds(minutes);

  14.                    mCurrentProgressTextView.setText(String.format("%02d:%02d", minutes,seconds));

  15.                    updateSeekBar();

  16.                } else if (mMediaPlayer == null && fromUser) {

  17.                    prepareMediaPlayerFromPoint(progress);

  18.                    updateSeekBar();

  19.                }

  20.            }

  21.        });

  22.        mPlayButton.setOnClickListener(new View.OnClickListener() {

  23.            @Override

  24.            public void onClick(View v) {

  25.                onPlay(isPlaying);

  26.                isPlaying = !isPlaying;

  27.            }

  28.        });

  29.        mTvFileLength.setText(String.format("%02d:%02d", minutes,seconds));

  30.        builder.setView(view);

  31.        return builder.create();

  32.    }

当点击播放录音的按钮之后,会调用 onPlay() 方法,然后根据 isPlaying(标识当前是否播放录音)的值,来调用不同的方法

  1.    private void onPlay(boolean isPlaying){

  2.        if (!isPlaying) {

  3.            if(mMediaPlayer == null) {

  4.                startPlaying(); //start from beginning

  5.            }

  6.        } else {

  7.            pausePlaying();

  8.        }

  9.    }

我们最关心的,莫过于 startPlaying() 这个方法,这个方法便是来开启播放录音的,我们首先将外部传入的有关的录音信息,设置给 MediaPlayer,然后开始调用 mMediaPlayer.start() 进行录音的播放,然后调用 updateSeekbar() 实时更新进度条的内容。当 MediaPlayer 的内容播放完成后,调用 stopPlaying() 方法,关闭 mMediaPlayer。

  1.    private void startPlaying() {

  2.        mMediaPlayer = new MediaPlayer();

  3.        mMediaPlayer.setDataSource(item.getFilePath());

  4.        mMediaPlayer.prepare();

  5.        mSeekBar.setMax(mMediaPlayer.getDuration());

  6.        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

  7.                @Override

  8.                public void onPrepared(MediaPlayer mp) {

  9.                    mMediaPlayer.start();

  10.                }

  11.            });

  12.        mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

  13.            @Override

  14.            public void onCompletion(MediaPlayer mp) {

  15.                stopPlaying();

  16.            }

  17.        });

  18.        updateSeekBar();

  19.    }

以上便是本文的全部内容,有关的代码我已经上传到 Github 上了,需要的 点击这里,喜欢的话,欢迎来波 star 和 fork

与之相关

  1.  6 重福利(赠书),5 个月精选干货,感谢你与 code小生共同成长

  2.  Android 撸起袖子,自己封装 DialogFragment


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注