Android 录音功能直接拿去用

作者: 小小小小怪兽_666 | 来源:发表于2019-02-28 17:38 被阅读16次

    前言

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

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

    一、实现录音的 Service

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

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

    
    public class RecordingService extends Service {
     
        private String mFileName;
        private String mFilePath;
     
        private MediaRecorder mRecorder;
     
        private long mStartingTimeMillis;
        private long mElapsedMillis;
     
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            startRecording();
            return START_STICKY;
        }
     
        @Override
        public void onDestroy() {
            if (mRecorder != null) {
                stopRecording();
            }
            super.onDestroy();
        }
     
        // 开始录音
        public void startRecording() {
            setFileNameAndPath();
     
            mRecorder = new MediaRecorder();
            mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //录音文件保存的格式,这里保存为 mp4
            mRecorder.setOutputFile(mFilePath); // 设置录音文件的保存路径
            mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            mRecorder.setAudioChannels(1);
            // 设置录音文件的清晰度
            mRecorder.setAudioSamplingRate(44100);
            mRecorder.setAudioEncodingBitRate(192000);
     
            try {
                mRecorder.prepare();
                mRecorder.start();
                mStartingTimeMillis = System.currentTimeMillis();
            } catch (IOException e) {
                Log.e(LOG_TAG, "prepare() failed");
            }
        }
     
        // 设置录音文件的名字和保存路径
        public void setFileNameAndPath() {
            File f;
     
            do {
                count++;
                mFileName = getString(R.string.default_file_name)
                        + "_" + (System.currentTimeMillis()) + ".mp4";
                mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath();
                mFilePath += "/SoundRecorder/" + mFileName;
                f = new File(mFilePath);
            } while (f.exists() && !f.isDirectory());
        }
     
        // 停止录音
        public void stopRecording() {
            mRecorder.stop();
            mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis);
            mRecorder.release();
     
            getSharedPreferences("sp_name_audio", MODE_PRIVATE)
                    .edit()
                    .putString("audio_path", mFilePath)
                    .putLong("elpased", mElapsedMillis)
                    .apply();
            if (mIncrementTimerTask != null) {
                mIncrementTimerTask.cancel();
                mIncrementTimerTask = null;
            }
     
            mRecorder = null;
        }
     
    }
    

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

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

    public void setFileNameAndPath() {
           File f;
    
           do {
               count++;
               mFileName = getString(R.string.default_file_name)
                       + "_" + (System.currentTimeMillis()) + ".mp4";
               mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath();
               mFilePath += "/SoundRecorder/" + mFileName;
               f = new File(mFilePath);
           } while (f.exists() && !f.isDirectory());
       }
    

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

    mRecorder = new MediaRecorder();
           mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
           mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //录音文件保存的格式,这里保存为 mp4
           mRecorder.setOutputFile(mFilePath); // 设置录音文件的保存路径
           mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
           mRecorder.setAudioChannels(1);
           // 设置录音文件的清晰度
           mRecorder.setAudioSamplingRate(44100);
           mRecorder.setAudioEncodingBitRate(192000);
    
           try {
               mRecorder.prepare();
               mRecorder.start();
               mStartingTimeMillis = System.currentTimeMillis();
           } catch (IOException e) {
               Log.e(LOG_TAG, "prepare() failed");
           }
    

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

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

     public void stopRecording() {
            mRecorder.stop();
            mElapsedMillis = (System.currentTimeMillis() - mStartingTimeMillis);
            mRecorder.release();
     
            getSharedPreferences("sp_name_audio", MODE_PRIVATE)
                    .edit()
                    .putString("audio_name", mFileName)
                    .putString("audio_path", mFilePath)
                    .putLong("elpased", mElapsedMillis)
                    .apply();
            if (mIncrementTimerTask != null) {
                mIncrementTimerTask.cancel();
                mIncrementTimerTask = null;
            }
     
            mRecorder = null;
        }
    

    二、显示录音界面的 RecordAudioDialogFragment

    用户进行的时候,总不能让 App 跳转到另外一个界面吧,这样用户体验并不是很好,比较好的方法是显示一个对话框,让用户进行操作,既然要用对话框,必然离不开 DialogFragment。

    public class RecordAudioDialogFragment extends DialogFragment {
     
        private boolean mStartRecording = true;
     
        long timeWhenPaused = 0;
     
        private FloatingActionButton mFabRecord;
        private Chronometer mChronometerTime;
     
        public static RecordAudioDialogFragment newInstance(int maxTime) {
            RecordAudioDialogFragment dialogFragment = new RecordAudioDialogFragment();
            Bundle bundle = new Bundle();
            bundle.putInt("maxTime", maxTime);
            dialogFragment.setArguments(bundle);
            return dialogFragment;
        }
     
        @NonNull
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Dialog dialog = super.onCreateDialog(savedInstanceState);
            final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_record_audio, null);
     
            mFabRecord.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(getActivity()
                                , new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);
                    }else {
                        onRecord(mStartRecording);
                        mStartRecording = !mStartRecording;
                    }
                }
            });
     
            builder.setView(view);
            return builder.create();
        }
     
        private void onRecord(boolean start) {
            Intent intent = new Intent(getActivity(), RecordingService.class);
            if (start) {
                File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");
                if (!folder.exists()) {
                    folder.mkdir();
                }
     
                mChronometerTime.setBase(SystemClock.elapsedRealtime());
                mChronometerTime.start();
                getActivity().startService(intent);
     
            } else {
                mChronometerTime.stop();
                timeWhenPaused = 0;
                getActivity().stopService(intent);
            }
        }
    }
    

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

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

    public Dialog onCreateDialog(Bundle savedInstanceState) {
            Dialog dialog = super.onCreateDialog(savedInstanceState);
            final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_record_audio, null);
     
            mFabRecord.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(getActivity()
                                , new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO}, 1);
                    }else {
                        onRecord(mStartRecording);
                        mStartRecording = !mStartRecording;
                    }
                }
            });
     
            builder.setView(view);
            return builder.create();
        }
    

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

    接下来看下 onRecord 干了什么

    private void onRecord(boolean start) {
            Intent intent = new Intent(getActivity(), RecordingService.class);
            if (mStartRecording) {
                File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");
                if (!folder.exists()) {
                    folder.mkdir();
                }
     
                mChronometerTime.setBase(SystemClock.elapsedRealtime());
                mChronometerTime.start();
                getActivity().startService(intent);
     
            } else {
                mChronometerTime.stop();
                timeWhenPaused = 0;
                getActivity().stopService(intent);
            }
        }
    

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

    三、播放录音的 PlaybackDialogFragment

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

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

     public static PlaybackDialogFragment newInstance(RecordingItem item) {
            PlaybackDialogFragment fragment = new PlaybackDialogFragment();
            Bundle bundle = new Bundle();
            bundle.putParcelable(ARG_ITEM, item);
            fragment.setArguments(b);
            return fragment;
        }
    

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

    public Dialog onCreateDialog(Bundle savedInstanceState) {
     
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_media_playback, null);
     
            mTvFileLength.setText(String.valueOf(mFileLength));
            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    if(mMediaPlayer != null && fromUser) {
                        mMediaPlayer.seekTo(progress);
                        mHandler.removeCallbacks(mRunnable);
     
                        long minutes = TimeUnit.MILLISECONDS.toMinutes(mMediaPlayer.getCurrentPosition());
                        long seconds = TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getCurrentPosition())
                                - TimeUnit.MINUTES.toSeconds(minutes);
                        mCurrentProgressTextView.setText(String.format("%02d:%02d", minutes,seconds));
     
                        updateSeekBar();
     
                    } else if (mMediaPlayer == null && fromUser) {
                        prepareMediaPlayerFromPoint(progress);
                        updateSeekBar();
                    }
                }
     
            });
     
            mPlayButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onPlay(isPlaying);
                    isPlaying = !isPlaying;
                }
            });
     
            mTvFileLength.setText(String.format("%02d:%02d", minutes,seconds));
            builder.setView(view);
            return builder.create();
        }
    

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

     private void onPlay(boolean isPlaying){
            if (!isPlaying) {
                if(mMediaPlayer == null) {
                    startPlaying(); //start from beginning
                } 
            } else {
                pausePlaying();
            }
        }
    

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

    private void startPlaying() {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setDataSource(item.getFilePath());
            mMediaPlayer.prepare();
            mSeekBar.setMax(mMediaPlayer.getDuration());
     
            mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(MediaPlayer mp) {
                        mMediaPlayer.start();
                    }
                });
     
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    stopPlaying();
                }
            });
            updateSeekBar();
        }
    

    以上便是本文的全部内容,喜欢的可以关注

    资料文佳图.png
    想学习更多Android知识,或者获取相关资料请加入Android技术开发交流2群:862625886。 有面试资源系统整理分享,Java语言进阶和Kotlin语言与Android相关技术内核,APP开发框架知识, 360°Android App全方位性能优化。Android前沿技术,高级UI、Gradle、RxJava、小程序、Hybrid、 移动架构师专题项目实战环节、等技术教程!架构师课程、NDK模块开发、 Flutter等全方面的 Android高级实践技术讲解。还有在线答疑

    相关文章

      网友评论

        本文标题:Android 录音功能直接拿去用

        本文链接:https://www.haomeiwen.com/subject/uaskuqtx.html