Android音频数据的采集和播放
音频与视频数据的采集和播放,是多媒体开发的两个基本要素,Android平台上,音频与视频数据的采集和播放是分开的,采集到原始的数据后,经过压缩、编码等处理,后续把音视频资源同步播放。
Android 提供了两套用于录制和播放音频的 API,分别是 MediaRecorder & MediaPlayer 和 AudioRecord & AudioTrack。MediaRecorder & MediaPlayer 是偏上层的API,使用简单,对想做基本的音视频录制和播放的用户很友好。AudioRecord & AudioTrack 是偏底层的API, 可以对原始的音频数据做混音、变音等处理,灵活度更高。
首先介绍 MediaRecorder & MediaPlayer。
MediaRecorder 既可以用来录制音频,也可以用来录制视频,它内部使用一个状态机控制录制过程,如下图所示:
录制音频前,需要先申请录制音频的权限。使用 MeidaRecorder 录制音频的工作流如下:
1 | MediaRecorder recorder = new MediaRecorder(); |
2 | recorder.setAudioSource(MediaRecorder.AudioSource.MIC); |
3 | recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); |
4 | recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); |
5 | recorder.setOutputFile(PATH_NAME); |
6 | recorder.prepare(); |
7 | // 开始录制 |
8 | recorder.start(); |
9 | ... |
10 | recorder.stop(); |
11 | //reset 以后可以从 setAudioSource() 开始重用 recorder 实例 |
12 | recorder.reset(); |
13 | //release 以后recorder实例不能被重用 |
14 | recorder.release(); |
MediaPlayer 类是 Android 媒体框架最重要的组成部分之一,只需极少量的配置,就能获取录制、解码以及播放音频和视频。它支持本地资源、内部 URI 和外部流式传输的网址这三种不同的媒体源。MediaPlayer 内部同样使用状态机来控制和管理媒体播放,Android 另一个 View 级别用来播放视频的组件 VideoView 内部使用的MediaPlayer 来播放视频。MediaPlayer 播放音频的方法非常简单,需要注意的是 MediaPlayer 占用了宝贵的系统资源,使用后需及时释放资源,示例代码如下:
1 | private lateinit var mMediaPlayer: MediaPlayer |
2 | |
3 | fun startPlaying(fileName: String) { |
4 | mMediaPlayer = MediaPlayer().apply { |
5 | try { |
6 | setDataSource(fileName) |
7 | prepare() |
8 | start() |
9 | } catch (e: IOException) { |
10 | Log.e(LOG_TAG, "prepare() failed") |
11 | } |
12 | } |
13 | } |
14 | |
15 | fun stopPlaying() { |
16 | mMediaPlayer.release() |
17 | } |
开篇提到 MediaRecorder 是偏上层录制音频的API,它的内部也是使用偏底层的 API, AudioRecord 实现的,下面介绍 AudioRecord & AudioTrack。
AudioRecord 运行过程中,会不断记录硬件中输入的音频资源,需要循环读取这些数据,存储为原始的 PCM 格式的文件。PCM,全称 Pulse Code Modulation,脉冲编码调制,是信号处理专业的内容,自然界中的声音波形比较复杂,通常我们采用脉冲编码调制,即PCM编码,通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字信号,PCM 格式的数据是一种未经压缩和处理的音频采样原始数据流。
使用 AudioRecord 录制音频的详细操作如下:
1 | private lateinit var mAudioRecordRecord: Button |
2 | private var mIsRecording = false |
3 | private val mExecutorService = Executors.newSingleThreadExecutor() |
4 | private var mPCMFileName: String = "" |
5 | //采样率 |
6 | private val mSampleRate = 44100 |
7 | //音频格式 |
8 | private val mAudioFormat = AudioFormat.ENCODING_PCM_16BIT |
9 | |
10 | private fun initView(){ |
11 | //path in device: sdcard/Android/data/packageName/cache/RecorderTest/audiorecordtest.pcm |
12 | mPCMFileName = "${externalCacheDir?.absolutePath}/RecorderTest/audiorecordtest.pcm" |
13 | |
14 | mAudioRecordRecord = findViewById(R.id.audio_record_record) |
15 | mAudioRecordRecord.setOnClickListener { |
16 | if (mIsRecording) { |
17 | mIsRecording = false |
18 | mAudioRecordRecord.text = "audiorecord start record" |
19 | } else { |
20 | mIsRecording = true |
21 | //开启新的线程,循环录制音频 |
22 | mExecutorService.submit { |
23 | if (!startAudioRecord(File(mPCMFileName))) { |
24 | recordFail() |
25 | } |
26 | } |
27 | mAudioRecordRecord.text = "audiorecord stop record" |
28 | } |
29 | } |
30 | } |
31 | |
32 | private fun startAudioRecord(audioFile: File): Boolean { |
33 | return try { |
34 | audioFile.parentFile?.mkdirs() |
35 | audioFile.createNewFile() |
36 | //创建输出流 |
37 | mFileOutputStream = FileOutputStream(audioFile) |
38 | //设置音频输入源 |
39 | val audioSource = MediaRecorder.AudioSource.MIC |
40 | //设置声道 |
41 | val channelConfig = AudioFormat.CHANNEL_IN_MONO |
42 | val minBufferSize = |
43 | AudioRecord.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat) |
44 | mAudioRecord = AudioRecord( |
45 | audioSource, mSampleRate, channelConfig, |
46 | mAudioFormat, max(minBufferSize, 2048) |
47 | ) |
48 | mAudioRecord.startRecording() |
49 | while (mIsRecording) { |
50 | val read = mAudioRecord.read(mBuffer, 0, 2048) |
51 | if (read > 0) { |
52 | mFileOutputStream.write(mBuffer, 0, read) |
53 | } else { |
54 | return false |
55 | } |
56 | } |
57 | stopAudioRecord() |
58 | } catch (e: IOException) { |
59 | Log.e(TAG, e.toString()) |
60 | false |
61 | } catch (e: java.lang.RuntimeException) { |
62 | Log.e(TAG, e.toString()) |
63 | false |
64 | } finally { |
65 | mAudioRecord.release() |
66 | } |
67 | } |
68 | |
69 | private fun stopAudioRecord(): Boolean { |
70 | try { |
71 | mIsRecording = false |
72 | //释放资源 |
73 | mAudioRecord.stop() |
74 | mAudioRecord.release() |
75 | mFileOutputStream.close() |
76 | } catch (e: IOException) { |
77 | Log.e(TAG, e.toString()) |
78 | return false |
79 | } |
80 | return true |
81 | } |
82 | |
83 | private fun recordFail() { |
84 | mIsRecording = false |
85 | // UI操作回到主线程 |
86 | runOnUiThread { |
87 | Toast.makeText(this, "record fail", Toast.LENGTH_SHORT).show() |
88 | } |
89 | } |
AudioTrack 类管理和播放单个音频资源,可以在两种模式下运行,静态模式 MODE_STATIC 和流模式MODE_STREAM。
当音频资源需要以最小延迟播放,并且占用内存不会太大时,应选择静态模式。对于经常播放的 UI 和游戏声音,静态模式是首选方式,并且开销可能最小。静态模式下,需要在调用 play() 函数播放前,先调用 write() 函数写入数据。
流模式适合处理较大的音频资源,推荐在调用 play() 函数播放前调用 write() 函数写入数据,否则因为没有充足的数据,调用 play() 函数不会立即播放。这种模式下需要连续不断的往 AudioTrack 中写入数据。
使用 AndioTrack 流模式播放音频的示例代码如下:
1 | private val mSampleRate = 44100 |
2 | private val mAudioFormat = AudioFormat.ENCODING_PCM_16BIT |
3 | private var mIsPlaying = false |
4 | private val mExecutorService = Executors.newSingleThreadExecutor() |
5 | private val mBuffer = ByteArray(2048) |
6 | |
7 | private fun initView(){ |
8 | //文件在设备中的存储路径: sdcard/Android/data/packageName/cache/RecorderTest/audiorecordtest.pcm |
9 | mPCMFileName = "${externalCacheDir?.absolutePath}/RecorderTest/audiorecordtest.pcm" |
10 | |
11 | val mAudioTrackPlay = findViewById(R.id.audio_track_play) |
12 | mAudioTrackPlay.setOnClickListener { |
13 | if (!mIsPlaying) { |
14 | mIsPlaying = true |
15 | mExecutorService.submit { |
16 | audioTrackPlay(File(mPCMFileName)) |
17 | } |
18 | } |
19 | } |
20 | } |
21 | |
22 |
|
23 | private fun audioTrackPlay(audioFile: File) { |
24 | val streamType = AudioManager.STREAM_MUSIC |
25 | //设置声道 |
26 | val channelConfig = AudioFormat.CHANNEL_OUT_MONO |
27 | //设置流模式 |
28 | val mode = AudioTrack.MODE_STREAM |
29 | //设置最小缓冲大小 |
30 | val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channelConfig, mAudioFormat) |
31 | |
32 | val attributesBuilder = AudioAttributes.Builder() |
33 | .setLegacyStreamType(streamType) |
34 | .build() |
35 | val formatBuilder = AudioFormat.Builder() |
36 | .setChannelMask(channelConfig) |
37 | .setEncoding(mAudioFormat) |
38 | .setSampleRate(mSampleRate) |
39 | .build() |
40 | val bufferSize = max(minBufferSize, 2048) |
41 | val sessionId = AudioManager.AUDIO_SESSION_ID_GENERATE |
42 | |
43 | val audioTrack = AudioTrack(attributesBuilder, formatBuilder, bufferSize, mode, sessionId) |
44 | |
45 | var mFileInputStream: FileInputStream? = null |
46 | try { |
47 | mFileInputStream = FileInputStream(audioFile) |
48 | audioTrack.play() |
49 | var read: Int |
50 | while (mFileInputStream.read(mBuffer).also { read = it } > 0) { |
51 | when (audioTrack.write(mBuffer, 0, read)) { |
52 | AudioTrack.ERROR_BAD_VALUE, AudioTrack.ERROR_INVALID_OPERATION, AudioManager.ERROR_DEAD_OBJECT -> audioTrackPlayFail() |
53 | else -> { |
54 | //play success |
55 | } |
56 | } |
57 | } |
58 | } catch (e: RuntimeException) { |
59 | Log.e(TAG, e.toString()) |
60 | audioTrackPlayFail() |
61 | } catch (e: IOException) { |
62 | Log.e(TAG, e.toString()) |
63 | audioTrackPlayFail() |
64 | } finally { |
65 | mIsPlaying = false |
66 | mFileInputStream?.let { closeQuietly(it) } |
67 | audioTrack.stop() |
68 | audioTrack.release() |
69 | } |
70 | } |
71 | |
72 | private fun closeQuietly(mFileInputStream: FileInputStream) { |
73 | try { |
74 | mFileInputStream.close() |
75 | } catch (e: IOException) { |
76 | Log.e(TAG, e.toString()) |
77 | } |
78 | } |
79 | |
80 | private fun audioTrackPlayFail() { |
81 | runOnUiThread { Toast.makeText(this, "play fail", Toast.LENGTH_SHORT).show() } |
82 | } |
Android 音频数据的采集和播放就介绍到这里,文章中使用到的详细代码示例请移步 github-StreamingTour