Android音频录制研究

上一篇实现了Android端文字的传输 点击打开链接,由于此系列要实现Android端语音的传输,所以这篇就先研究一下Android端语音的录制。先上效果图吧:

android播放aif Android播放udp的播放器_信号处理

这是主页就是几个按钮:音频的录制分为文件录制和字节流录制,

(1)文件采用Media Record录制和Media Player播放

(2)字节流采用Audio Record录制和Audio Track播放

(3)音量可视化就是实时获取音量大小,显示到屏幕上面

(4)简单实现声音的变速,加速播放和减速播放
点击发送按钮,开始发送消息:
上代码:

(1)文件录制
需要说明的是录音JNI函数不具备线程安全性,所以采用了单线程的线程池

1executorService = Executors.newSingleThreadExecutor();

因为录音线程在子线程,录音失败和成功与主线程交互,采用了Handler

1mainThreadHandler = new Handler(Looper.getMainLooper());

继续走,点击事件

1tvSpeak.setOnTouchListener(new View.OnTouchListener() {
 2@Override
 3public boolean onTouch(View v, MotionEvent event) {
 4switch (event.getAction()) {
 5case MotionEvent.ACTION_DOWN:
 6//按下按钮开始录制
 7startRecord();
 8break;
 9case MotionEvent.ACTION_UP:
10case MotionEvent.ACTION_CANCEL:
11//松开按钮结束录制
12stopRecord();
13break;
14}
15return true;
16}
17
18
19});

开始录音

1private void startRecord() {
 2tvSpeak.setText("正在说话");
 3//提交后台任务,执行录音逻辑
 4executorService.submit(new Runnable() {
 5@Override
 6public void run() {
 7//释放之前录音的recorder
 8releaseRecorder();
 9//执行录音逻辑,如果失败 提示用户
10if (!doStart()) {
11recordFail();
12}
13}
14});
15}

停止录音

1private void stopRecord() {
 2tvSpeak.setText("按住说话");
 3//提交后台任务,执行停止逻辑
 4executorService.submit(new Runnable() {
 5@Override
 6public void run() {
 7//执行停止录音逻辑,失败就要提醒用户
 8if (!doStop()) {
 9recordFail();
10}
11//释放recorder
12releaseRecorder();
13
14}
15});
16
17}

创建录音

1private boolean doStart() {
 2try {
 3//创建mediaRecorder
 4mediaRecorder = new MediaRecorder();
 5//创建录音文件
 6mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyUdpDemo/" + System.currentTimeMillis() + ".m4a");
 7mAudioFile.getParentFile().mkdirs();
 8mAudioFile.createNewFile();
 9//配置Media Recorder
10mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
11mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
12mediaRecorder.setAudioSamplingRate(44100);
13mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
14mediaRecorder.setAudioEncodingBitRate(96000);
15//设置录音文件的位置
16mediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());
17//开始录音
18mediaRecorder.prepare();
19mediaRecorder.start();
20//记录开始录音时间 用于统计时长
21mStartRecordTime = System.currentTimeMillis();
22
23} catch (IOException e) {
24e.printStackTrace();
25return false;
26}
27return true;
28}

停止

1private boolean doStop() {
 2//停止录音
 3try {
 4mediaRecorder.stop();
 5//记录停止时间
 6mStopRecordTime=System.currentTimeMillis();
 7//只接受超过三秒的录音
 8final int second = (int) (mStopRecordTime - mStartRecordTime)/1000;
 9if (second > 3) {
10mainThreadHandler.post(new Runnable() {
11@Override
12public void run() {
13tvLog.setText(tvLog.getText() + "\n录音成功" + second + "秒");
14}
15});
16}
17//停止成功
18} catch (Exception e) {
19e.printStackTrace();
20return false;
21}
22
23return true;
24
25}

录音失败

1private void recordFail() {
 2mAudioFile = null;
 3//要在主线程执行
 4mainThreadHandler.post(new Runnable() {
 5@Override
 6public void run() {
 7Toast.makeText(FileActivity.this, "录音失败", Toast.LENGTH_SHORT).show();
 8}
 9});
10
11}

释放

1private void releaseRecorder() {
2//检查mediaRecorder不为空
3if (mediaRecorder != null) {
4mediaRecorder.release();
5mediaRecorder = null;
6}
7}

录音成功后,下面就是播放了:

1@OnClick(R.id.play)
 2public void onViewClicked() {
 3if (mAudioFile != null && !isPlaying) {
 4play.setText("停止");
 5executorService.submit(new Runnable() {
 6@Override
 7public void run() {
 8startPlay(mAudioFile);
 9}
10});
11} else {
12play.setText("播放");
13executorService.submit(new Runnable() {
14@Override
15public void run() {
16stopPlay();
17}
18});
19}
20}

开始播放

1private void startPlay(File audioFile) {
 2//配置播放器
 3mMediaPlayer = new MediaPlayer();
 4try {
 5
 6//设置声音文件
 7mMediaPlayer.setDataSource(audioFile.getAbsolutePath());
 8//设置监听回掉
 9mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
10@Override
11public void onCompletion(MediaPlayer mp) {
12stopPlay();
13}
14});
15
16mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
17@Override
18public boolean onError(MediaPlayer mp, int what, int extra) {
19//提示用户 释放播放器
20playFail();
21stopPlay();
22return true;
23}
24});
25
26//配置音量 是否循环
27mMediaPlayer.setVolume(1, 1);
28mMediaPlayer.setLooping(false);
29
30//准备 开始
31mMediaPlayer.prepare();
32mMediaPlayer.start();
33} catch (RuntimeException e) {
34e.printStackTrace();
35//异常处理防止闪退
36playFail();
37} catch (IOException e) {
38e.printStackTrace();
39}
40}

停止播放

1private void stopPlay() {
 2//重置播放状态
 3isPlaying = false;
 4play.setText("播放");
 5if (mMediaPlayer != null) {
 6mMediaPlayer.setOnCompletionListener(null);
 7mMediaPlayer.setOnErrorListener(null);
 8mMediaPlayer.stop();
 9mMediaPlayer.reset();
10mMediaPlayer.release();
11mMediaPlayer = null;
12}
13}

播放失败

1private void playFail() {
2mainThreadHandler.post(new Runnable() {
3@Override
4public void run() {
5Toast.makeText(FileActivity.this, "播放失败", Toast.LENGTH_SHORT).show();
6}
7});
8}

在onDestroy方法里面注销

1@Override
2protected void onDestroy() {
3super.onDestroy();
4//activity销毁时停止后台任务 避免后台任务
5executorService.shutdown();
6releaseRecorder();
7stopPlay();
8}

至此文件的录制和播放就结束了
(2)字节流录制

1@OnClick({R.id.btnStart, R.id.play})
 2public void onViewClicked(View view) {
 3switch (view.getId()) {
 4case R.id.btnStart:
 5if (mIsRecording) {
 6btnStart.setText("开始");
 7mIsRecording = false;
 8} else {
 9btnStart.setText("停止");
10mIsRecording = true;
11
12executorService.submit(new Runnable() {
13@Override
14public void run() {
15if (!startRecord()) {
16recordFail();
17}
18
19
20}
21});
22}
23break;
24case R.id.play:
25//检查播放状态 防止重复播放
26if (mAudioFile != null && !isPlaying) {
27isPlaying = true;
28executorService.submit(new Runnable() {
29@Override
30public void run() {
31startPlay(mAudioFile);
32}
33});
34}
35break;
36}
37}
1private boolean startRecord() {
 2try {
 3//创建录音文件
 4mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyUdpDemo/" + System.currentTimeMillis() + ".pcm");
 5mAudioFile.getParentFile().mkdirs();
 6mAudioFile.createNewFile();
 7//创建文件输出流
 8fileOutputStream = new FileOutputStream(mAudioFile);
 9//配置Audio Record
10int minBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
11//buffer不能小于最低要求,也不能小于我们每次读取的大小
12mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(minBufferSize, BUFFERSIZE));
13
14//开始录音
15mAudioRecord.startRecording();
16//记录开始录音时间,用于统计时长
17mStartTime = System.currentTimeMillis();
18//循环读取数据,写到输出流中
19while (mIsRecording) {
20int read = mAudioRecord.read(buffer, 0, BUFFERSIZE);
21//返回值是这次读到了多少
22if (read > 0) {
23//读取失败
24fileOutputStream.write(buffer, 0, read);
25} else {
26//读取失败
27return false;
28}
29}
30//退出循环,停止录音,释放资源
31return stopRecord();
32
33} catch (IOException e) {
34e.printStackTrace();
35return false;
36} finally {
37//释放Audio Record
38if (mAudioRecord != null) {
39mAudioRecord.release();
40}
41}
42
43}
44
45private boolean stopRecord() {
46
47try {
48//停止录音 关闭文件输出流
49mAudioRecord.stop();
50mAudioRecord.release();
51mAudioRecord = null;
52fileOutputStream.close();
53//记录结束时间 统计时长
54mStopTime = System.currentTimeMillis();
55final int second = (int) ((mStopTime - mStartTime) / 1000);
56//大于3秒的成功 在主线程改变UI
57if (second > 3) {
58mMainHandler.post(new Runnable() {
59@Override
60public void run() {
61tvLog.setText(tvLog.getText() + "\n录音成功" + second + "秒");
62}
63});
64}
65} catch (IOException e) {
66e.printStackTrace();
67return false;
68}
69
70return true;
71}
72
73
74private void recordFail() {
75mMainHandler.post(new Runnable() {
76@Override
77public void run() {
78Toast.makeText(StreamActivity.this, "录音失败", Toast.LENGTH_SHORT).show();
79//重置录音状态 UI状态
80mIsRecording = false;
81btnStart.setText("开始");
82}
83});
84}

播放

1private void startPlay(File mAudioFile) {
 2//配置播放器
 3//扬声器播放
 4int streamType = AudioManager.STREAM_MUSIC;
 5//播放的采样频率 和录制的采样频率一样
 6int sampleRate = 44100;
 7
 8//和录制的一样的
 9int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
10
11//流模式
12int mode = AudioTrack.MODE_STREAM;
13
14//录音用输入单声道 播放用输出单声道
15int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
16
17int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
18
19AudioTrack audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, BUFFERSIZE), mode);
20audioTrack.play();
21//从文件流读数据
22FileInputStream fileInputStream = null;
23try {
24fileInputStream = new FileInputStream(mAudioFile);
25int read;
26while ((read = fileInputStream.read(buffer)) > 0) {
27int ret = audioTrack.write(buffer, 0, read);
28//检查write 返回值 错误处理
29switch (ret) {
30case AudioTrack.ERROR_BAD_VALUE:
31case AudioTrack.ERROR_INVALID_OPERATION:
32case AudioTrack.ERROR_DEAD_OBJECT:
33playFail();
34break;
35default:
36break;
37}
38}
39} catch (RuntimeException | IOException e) {
40e.printStackTrace();
41playFail();
42
43} finally {
44//关闭文件流
45isPlaying = false;
46if (fileInputStream != null) {
47closeQuatily(fileInputStream);
48}
49resetAudioTrack(audioTrack);
50
51
52}
53
54}
55
56private void playFail() {
57mAudioFile = null;
58mMainHandler.post(new Runnable() {
59@Override
60public void run() {
61Toast.makeText(StreamActivity.this, "播放失败", Toast.LENGTH_SHORT).show();
62}
63});
64
65}
66
67private void resetAudioTrack(AudioTrack audioTrack) {
68try {
69audioTrack.stop();
70audioTrack.release();
71} catch (RuntimeException e) {
72e.printStackTrace();
73}
74}
75
76private void closeQuatily(FileInputStream fileInputStream) {
77try {
78fileInputStream.close();
79} catch (IOException e) {
80e.printStackTrace();
81}
82}
1@Override
2protected void onDestroy() {
3super.onDestroy();
4executorService.shutdownNow();
5}

(3)音频可视化
主要就是获取音量大小,划分等级进行显示

1public void getRecordVolume() {
 2if (mediaRecorder != null) {
 3
 4}
 5int maxAmplitude;
 6
 7//获取音量大小
 8try {
 9maxAmplitude = mediaRecorder.getMaxAmplitude();
10} catch (RuntimeException e) {
11e.printStackTrace();
12//异常发生后 用一个随机数代表当前音量大小
13maxAmplitude = random.nextInt();
14}
15
16final int level = maxAmplitude / (MAXAMPLITUDE / MAXLEVEL);
17//把音量规划到五个等级
18//把等级显示到UI上面
19mainThreadHandler.post(new Runnable() {
20@Override
21public void run() {
22refreshVolume(level);
23}
24});
25//如果仍在录音,就隔一段时间再次获取音量大小
26if (isRecording) {
27executorService.schedule(new Runnable() {
28@Override
29public void run() {
30getRecordVolume();
31}
32}, 50, TimeUnit.MILLISECONDS);
33}
34}
35
36private void refreshVolume(int level) {
37for (int i = 0; i < 5; i++) {
38imageViewList.get(i).setVisibility(i < level ? View.VISIBLE : View.GONE);
39}
40}

(4)音频变速播放
添加三个支持的播放采样率

1private static final int[] SUPPORTSAMPLERATE = {11025, 22050, 44100};

录制的时候采用中间的频率,点击不同的播放按钮,采用不同的播放频率

1case R.id.play:
 2//检查播放状态 防止重复播放
 3play(SUPPORTSAMPLERATE[1]);
 4break;
 5case R.id.playFast:
 6//检查播放状态 防止重复播放
 7play(SUPPORTSAMPLERATE[2]);
 8break;
 9case R.id.playSlowly:
10//检查播放状态 防止重复播放
11play(SUPPORTSAMPLERATE[0]);
12break;

这样一个简单的通信demo就完成了,当然在demo中我没有做严谨的校验,只是为了实现整个流程
到此就结束啦!下一篇将会进行音频的传输研究了