Android 音频

AudioRecord

API详解

AudioRecord 是 Android 系统提供的用于实现录音的功能类。接下来实现使用 AudioRecord 采集音频 PCM 并保存到文件。

AndioRecord 类的主要功能是让各种 JAVA 应用能够管理音频资源,以便它们通过此类能够录制声音相关的硬件所收集的声音。此功能的实现就是通过 ”pulling”(读取)AudioRecord 对象的声音数据来完成的。在录音过程中,应用所需要做的就是通过后面三个类方法中的一个去及时地获取 AudioRecord 对象的录音数据. AudioRecord 类提供的三个获取声音数据的方法分别是 read(byte[], int, int),read(short[], int, int),read(ByteBuffer, int). 无论选择使用哪一个方法都必须事先设定方便用户的声音数据的存储格式。

开始录音的时候,AudioRecord 需要初始化一个相关联的声音 buffer, 这个 buffer 主要是用来保存新的声音数据。这个 buffer 的大小,我们可以在对象构造期间去指定。它表明一个 AudioRecord 对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化 buffer 容量的数据。

实现 Android 录音的流程为:

  1. 构造一个 AudioRecord 对象,其中需要的最小录音缓存 buffer 大小可以通过 getMinBufferSize 方法得到。如果 buffer 容量过小,将导致对象构造的失败。
  2. 初始化一个 buffer,该 buffer 大于等于 AudioRecord 对象用于写声音数据的 buffer 大小。
  3. 开始录音
  4. 创建一个数据流,一边从 AudioRecord 中读取声音数据到初始化的 buffer,一边将 buffer 中数据导入数据流。
  5. 关闭数据流
  6. 停止录音

实现录音并生成 wav

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
class MainActivity : AppCompatActivity() {

private val TAG = "TAG_MainActivity"
private val MY_PERMISSIONS_REQUEST = 1001

private lateinit var audioRecord: AudioRecord // 声明 AudioRecord 对象
private var recordBufSize = 0 // 声明 recordBuffer 的大小字段
private var isRecording = false

/**
* 需要申请的运行时权限
*/
private val permissions = arrayOf<String>(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
/**
* 被用户拒绝的权限列表
*/
private val mPermissionList = ArrayList<String>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

checkPermissions()
}

private fun checkPermissions() {
// Marshmallow 开始才用申请运行时权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissionsSize = permissions.size
for (i in 0 until permissionsSize) {
if (ContextCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
mPermissionList.add(permissions[i])
}
}
if (!mPermissionList.isEmpty()) {
val permissions = mPermissionList.toArray(arrayOfNulls<String>(mPermissionList.size))
ActivityCompat.requestPermissions(this, permissions, MY_PERMISSIONS_REQUEST)
}
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == MY_PERMISSIONS_REQUEST){
val grantResultsSize = grantResults.size
for (i in 0 until grantResultsSize){
if (grantResults[i] != PackageManager.PERMISSION_GRANTED){
Log.d(TAG,permissions[i] + "权限被用户禁止!")
}
}
// 运行时权限的申请不是本demo的重点,所以不再做更多的处理,请同意权限申请。
}
}

fun btnStart(view: View) {
// 获取buffer的大小并创建AudioRecord
// 采样率,声道数,返回的音视频数据的格式
recordBufSize = AudioRecord.getMinBufferSize(GlobalConfig.SAMPLE_RATE_INHZ,
GlobalConfig.CHANNEL_CONFIG,GlobalConfig.AUDIO_FORMAT)
audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC,GlobalConfig.SAMPLE_RATE_INHZ,
GlobalConfig.CHANNEL_CONFIG,GlobalConfig.AUDIO_FORMAT,recordBufSize)

// 初始化一个buffer
val data = ByteArray(recordBufSize)

// getExternalFilesDir:SDCard/Android/data/应用的包名/files/ 目录
val file = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm")
Log.d(TAG,file.absolutePath) // /storage/emulated/0/Android/data/com.example.testdemo/files/Music/test.pcm
if (!file.mkdirs()) {
Log.d(TAG, "Directory not created")
}
if (file.exists()) {
file.delete()
}

// 开始录音
audioRecord.startRecording()
isRecording = true

// pcm 数据无法直接播放,保存为WAV格式。
Thread(Runnable {
// 创建一个数据流,一边从AudioRecord中读取声音数据到初始化的buffer,一边将buffer中数据导入数据流
var os: FileOutputStream? = null
try {
os = FileOutputStream(file)
} catch (e: FileNotFoundException) {
e.printStackTrace()
}

if (null != os) {
while (isRecording) {
val read = audioRecord.read(data, 0, recordBufSize)
// 如果读取音频数据没有出现错误,就将数据写入到文件
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
os!!.write(data)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
try {
Log.d(TAG, "run: close file output stream !")
os!!.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}).start()
}

fun btnStop(view: View) {
// 关闭数据流,修改标志位:isRecording 为false,上面的while循环就自动停止了,数据流也就停止流动了,Stream也就被关闭了。
isRecording = false
// 停止录音,释放资源。
audioRecord.stop()
audioRecord.release()
}
}

基本的录音流程走完了,但是现在的文件里面的内容仅仅是最原始的音频数据,术语称为raw(中文解释是“原材料”或“未经处理的东西”),这时候,播放器既不知道保存的格式是什么,又不知道如何进行解码操作。所以是无法播放的。

想要在播放器中播放录制的内容需要在文件的数据开头加入WAVE HEAD数据即可,也就是文件头。只有加上文件头部的数据,播放器才能正确的知道里面的内容到底是什么,进而能够正常的解析并播放里面的内容。

1
2
3
4
5
6
7
8
9
10
val pcmToWavUtil = PcmToWavUtil(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT)
val pcmFile = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm")
val wavFile = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.wav")
if (!wavFile.mkdirs()) {
Log.e(TAG, "wavFile Directory not created")
}
if (wavFile.exists()) {
wavFile.delete()
}
pcmToWavUtil.pcmToWav(pcmFile.absolutePath, wavFile.absolutePath)

附言

Android SDK 提供了两套音频采集的API,分别是:

  • MediaRecorder:更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件。

  • AudioRecord:更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。

如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时采集音频自然是要用 AudioRecord 了。


AudioTrack

使用 AudioTrack 播放PCM音频

基本使用

AudioTrack 类可以完成 Android 平台上音频数据的输出任务。AudioTrack 有两种数据加载模式(MODE_STREAM和MODE_STATIC),对应的是数据加载模式和音频流类型, 对应着两种完全不同的使用场景。

  • MODE_STREAM:在这种模式下,通过 write 一次次把音频数据写到 AudioTrack 中。这和平时通过 write 系统调用往文件中写数据类似,但这种工作方式每次都需要把数据从用户提供的 Buffer 中拷贝到 AudioTrack 内部的 Buffer 中,这在一定程度上会使引入延时。为解决这一问题,AudioTrack 就引入了第二种模式。
  • MODE_STATIC:这种模式下,在 play 之前只需要把所有数据通过一次 write 调用传递到 AudioTrack 中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺点,就是一次 write 的数据不能太多,否则系统无法分配足够的内存来存储全部数据。

MODE_STATIC 模式输出音频的方式如下(注意:如果采用STATIC模式,须先调用write写数据,然后再调用play。):

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
fun btnPayInSTATIC(view: View){
// static 模式,需要将音频数据一次性 write 到 AudioTrack 的内部缓冲区
// 这里应该使用协程来代替 AsyncTask。
MyAsyncTask().execute()
}

inner class MyAsyncTask : AsyncTask<Void, Int, Void>() {

override fun onPreExecute() { }

override fun doInBackground(vararg param: Void?): Void? {
try {
val inputStream = resources.openRawResource(R.raw.ding)
try {
val out = ByteArrayOutputStream()
var b: Int
while (inputStream.read().also { b = it } != -1) {
out.write(b)
}
d(TAG, "Got the data")
audioData = out.toByteArray()
} finally {
inputStream.close()
}
} catch (e: IOException) {
Log.wtf(TAG, "Failed to read", e)
}
return null
}

override fun onProgressUpdate(vararg values: Int?) { }

override fun onPostExecute(result: Void?) {
Log.d(TAG, "Creating track...audioData.length = " + audioData.size)
// R.raw.ding铃声文件的相关属性为 22050Hz, 8-bit, Mono
audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
AudioFormat.Builder().setSampleRate(22050)
.setEncoding(AudioFormat.ENCODING_PCM_8BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
audioData.size,
AudioTrack.MODE_STATIC,
AudioManager.AUDIO_SESSION_ID_GENERATE)
d(TAG, "Writing audio data...")
audioTrack.write(audioData, 0, audioData.size)
d(TAG, "Starting playback")
audioTrack.play()
d(TAG, "Playing")
}

override fun onCancelled() { }
}

MODE_STREAM 模式输出音频的方式如下:

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
fun btnPayInSTREAM(view: View){
/*
* SAMPLE_RATE_INHZ 对应pcm音频的采样率
* channelConfig 对应pcm音频的声道
* AUDIO_FORMAT 对应pcm音频的格式
* */
val channelConfig = AudioFormat.CHANNEL_OUT_MONO
val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE_INHZ, channelConfig, AUDIO_FORMAT)
audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
AudioFormat.Builder().setSampleRate(SAMPLE_RATE_INHZ)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(channelConfig)
.build(),
minBufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE)
audioTrack.play()

val file = File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm")
try {
fileInputStream = FileInputStream(file)
Thread(Runnable {
try {
val tempBuffer = ByteArray(minBufferSize)
while (fileInputStream.available() > 0) {
val readCount = fileInputStream.read(tempBuffer)
if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
continue
}
if (readCount != 0 && readCount != -1) {
audioTrack.write(tempBuffer, 0, readCount)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}).start()

} catch (e: IOException) {
e.printStackTrace()
}
}

与 MediaPlayer 的对比

播放声音可以用MediaPlayer和AudioTrack,两者都提供了Java API供应用开发者使用。虽然都可以播放声音,但两者还是有很大的区别的。

区别

其中最大的区别是MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer会在framework层创建对应的音频解码器。而AudioTrack只能播放已经解码的PCM流,如果对比支持的文件格式的话则是AudioTrack只支持wav格式的音频文件,因为wav格式的音频文件大部分都是PCM流。AudioTrack不创建解码器,所以只能播放不需要解码的wav文件。

联系

MediaPlayer在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,AudioTrack再传递给AudioFlinger进行混音,然后才传递给硬件播放,所以是MediaPlayer包含了AudioTrack。

SoundPool

在接触Android音频播放API的时候,发现SoundPool也可以用于播放音频。下面是三者的使用场景:MediaPlayer 更加适合在后台长时间播放本地音乐文件或者在线的流式资源; SoundPool 则适合播放比较短的音频片段,比如游戏声音、按键声、铃声片段等等,它可以同时播放多个音频; 而 AudioTrack 则更接近底层,提供了非常强大的控制能力,支持低延迟播放,适合流媒体和VoIP语音电话等场景。


备注

参考资料

https://www.cnblogs.com/renhui/category/1011048.html

传送门GitHub

单词音标: