录音SoundRecording

效果图

首页 录音 播放

实现录音的 Service

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class RecordingService extends Service {

private static final String LOG_TAG = "RecordingService";

private String mFileName = null;
private String mFilePath = null;

private MediaRecorder mRecorder = null;

private long mStartingTimeMillis = 0;
private long mElapsedMillis = 0;
private TimerTask mIncrementTimerTask = null;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
}

@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);
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() {
int count = 0;
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() 拼接到录音文件的名字里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setFileNameAndPath() {
int count = 0;
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 进行一系列参数的设置,这个mRecorderMediaRecorder 的一个实例,专门用于录音的存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void startRecording() {
setFileNameAndPath();

mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
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,这里主要是为了记录录音的时长,等到录音结束后再获取一次当前的时间,然后将两个时间进行相减,就能得到录音的具体时长了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

显示录音界面的 RecordAudioDialogFragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
public class RecordAudioDialogFragment extends DialogFragment {

private static final String TAG = "RecordAudioDialogFragme";

private int mRecordPromptCount = 0;

private boolean mStartRecording = true;
private boolean mPauseRecording = true;

long timeWhenPaused = 0;

private FloatingActionButton mFabRecord;
private Chronometer mChronometerTime;
private ImageView mIvClose;

private OnAudioCancelListener mListener;

public static RecordAudioDialogFragment newInstance() {
RecordAudioDialogFragment dialogFragment = new RecordAudioDialogFragment();
Bundle bundle = new Bundle();
dialogFragment.setArguments(bundle);
return dialogFragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}

@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);
initView(view);

mFabRecord.setColorNormal(getResources().getColor(R.color.colorAccent));
mFabRecord.setColorPressed(getResources().getColor(R.color.colorAccent));

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;
}

}
});

mIvClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mListener.onCancel();
}
});

builder.setCancelable(false);
builder.setView(view);
return builder.create();
}

private void initView(View view) {
mChronometerTime = (Chronometer) view.findViewById(R.id.record_audio_chronometer_time);
mFabRecord = (FloatingActionButton) view.findViewById(R.id.record_audio_fab_record);
mIvClose = (ImageView) view.findViewById(R.id.record_audio_iv_close);
}

Intent intent;

private void onRecord(boolean start) {

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

if (start) {
// start recording
mFabRecord.setImageResource(R.drawable.ic_media_stop);
//mPauseButton.setVisibility(View.VISIBLE);
Toast.makeText(getActivity(), "开始录音...", Toast.LENGTH_SHORT).show();
File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");
if (!folder.exists()) {
//folder /SoundRecorder doesn't exist, create the folder
folder.mkdir();
}

//start Chronometer
mChronometerTime.setBase(SystemClock.elapsedRealtime());
mChronometerTime.start();

//start RecordingService
getActivity().startService(intent);
//keep screen on while recording
// getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


} else {
//stop recording
mFabRecord.setImageResource(R.drawable.ic_mic_white_36dp);
//mPauseButton.setVisibility(View.GONE);
mChronometerTime.stop();
timeWhenPaused = 0;
Toast.makeText(getActivity(), "录音结束...", Toast.LENGTH_SHORT).show();

getActivity().stopService(intent);
//allow the screen to turn off again once recording is finished
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}

@Override
public void onDestroyView() {
if (!mStartRecording) {
//stop recording
mFabRecord.setImageResource(R.drawable.ic_mic_white_36dp);
//mPauseButton.setVisibility(View.GONE);
mChronometerTime.stop();
timeWhenPaused = 0;
Toast.makeText(getActivity(), "录音结束...", Toast.LENGTH_SHORT).show();

getActivity().stopService(intent);
//allow the screen to turn off again once recording is finished
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
super.onDestroyView();
}

public void setOnCancelListener(OnAudioCancelListener listener) {
this.mListener = listener;
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
onRecord(mStartRecording);
}
break;
}
}

public interface OnAudioCancelListener {
void onCancel();
}
}

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

这个对话框的重点部分就是在 onCreateDialog()中,我们先加载了我们自定义的对话框的布局,当点击录音的按钮的时候,先进行相关权限的申请,录音权限 android.permission.RECORD_AUDIO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
initView(view);

mFabRecord.setColorNormal(getResources().getColor(R.color.colorAccent));
mFabRecord.setColorPressed(getResources().getColor(R.color.colorAccent));

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;
}

}
});

mIvClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mListener.onCancel();
}
});

builder.setCancelable(false);
builder.setView(view);
return builder.create();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void onRecord(boolean start) {

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

if (start) {
// start recording
mFabRecord.setImageResource(R.drawable.ic_media_stop);
//mPauseButton.setVisibility(View.VISIBLE);
Toast.makeText(getActivity(), "开始录音...", Toast.LENGTH_SHORT).show();
File folder = new File(Environment.getExternalStorageDirectory() + "/SoundRecorder");
if (!folder.exists()) {
//folder /SoundRecorder doesn't exist, create the folder
folder.mkdir();
}

//start Chronometer
mChronometerTime.setBase(SystemClock.elapsedRealtime());
mChronometerTime.start();

//start RecordingService
getActivity().startService(intent);
//keep screen on while recording
// getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


} else {
//stop recording
mFabRecord.setImageResource(R.drawable.ic_mic_white_36dp);
//mPauseButton.setVisibility(View.GONE);
mChronometerTime.stop();
timeWhenPaused = 0;
Toast.makeText(getActivity(), "录音结束...", Toast.LENGTH_SHORT).show();

getActivity().stopService(intent);
//allow the screen to turn off again once recording is finished
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}

创建了保存录音文件的文件夹,然后根据 mStartRecording 的值进行 RecordingService 的启动和关闭罢了。在启动时还顺便开始了 mChronometer 的计时显示,这是一个 Android 原生的显示计时的一个控件。

播放录音的 PlaybackDialogFragment

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

1
2
3
4
5
6
7
public static PlaybackDialogFragment newInstance(RecordingItem item) {
PlaybackDialogFragment f = new PlaybackDialogFragment();
Bundle b = new Bundle();
b.putParcelable(ARG_ITEM, item);
f.setArguments(b);
return f;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public Dialog onCreateDialog(Bundle savedInstanceState) {

Dialog dialog = super.onCreateDialog(savedInstanceState);

AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
View view = getActivity().getLayoutInflater().inflate(R.layout.fragment_media_playback, null);

mFileNameTextView = (TextView) view.findViewById(R.id.file_name_text_view);
mFileLengthTextView = (TextView) view.findViewById(R.id.file_length_text_view);
mCurrentProgressTextView = (TextView) view.findViewById(R.id.current_progress_text_view);

mSeekBar = (SeekBar) view.findViewById(R.id.seekbar);
ColorFilter filter = new LightingColorFilter
(getResources().getColor(R.color.green), getResources().getColor(R.color.green));
mSeekBar.getProgressDrawable().setColorFilter(filter);
mSeekBar.getThumb().setColorFilter(filter);
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();
}
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if(mMediaPlayer != null) {
// remove message Handler from updating progress bar
mHandler.removeCallbacks(mRunnable);
}
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (mMediaPlayer != null) {
mHandler.removeCallbacks(mRunnable);
mMediaPlayer.seekTo(seekBar.getProgress());

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();
}
}
});

mPlayButton = (FloatingActionButton) view.findViewById(R.id.fab_play);
mPlayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onPlay(isPlaying);
isPlaying = !isPlaying;
}
});

mFileNameTextView.setText(item.getName());
mFileLengthTextView.setText(String.format("%02d:%02d", minutes,seconds));

builder.setView(view);

// request a window without the title
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);

return builder.create();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
private void onPlay(boolean isPlaying){
if (!isPlaying) {
//currently MediaPlayer is not playing audio
if(mMediaPlayer == null) {
startPlaying(); //start from beginning
} else {
resumePlaying(); //resume the currently paused MediaPlayer
}

} else {
pausePlaying();
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void startPlaying() {
mPlayButton.setImageResource(R.drawable.ic_media_pause);
mMediaPlayer = new MediaPlayer();

try {
mMediaPlayer.setDataSource(item.getFilePath());
mMediaPlayer.prepare();
mSeekBar.setMax(mMediaPlayer.getDuration());

mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
} catch (IOException e) {
Log.e(LOG_TAG, "prepare() failed");
}

mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
stopPlaying();
}
});

updateSeekBar();

//keep screen on while playing audio
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

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

坚持原创技术分享,您的支持将鼓励我继续创作!