Android開源:SoundRecorderUtils-錄音功能直接拿去用
前言
最近項目中需要用到錄音的功能,搞定需求之后,花了些時間封裝成一個錄音的工具包,分享給大家,需要源碼的 點擊這里
先貼個效果圖給大家看一下,看看這個錄音包的功能
一、實現錄音的 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;
}
}</code></pre>
可以看到在 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());
}</code></pre>
設置好了文件的名字和保存路徑之后,對 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,對于 DialogFragment 不是很了解,可以先看看我這篇文章 Android 擼起袖子,自己封裝 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);
mFileLengthTextView.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;
}
});
mFileLengthTextView.setText(String.format("%02d:%02d", minutes,seconds));
builder.setView(view);
return builder.create();
}
當點擊播放錄音的按鈕之后,會調用 onPlay() 方法,然后根據 isPlaying (標識當前是否播放錄音)的值,來調用不同的方法
private void onPlay(boolean isPlaying){
if (!isPlaying) {
//currently MediaPlayer is not playing audio
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();
}
以上便是本文的全部內容,有關的代碼我已經上傳到 Github 上了,需要的 點擊這里 ,喜歡的話,歡迎來波 star 和 fork