前言
上一节介绍了AudioRecord的录音,但是AudioRecord录音生成出来的是原始音频数据,无法直接使用常规播放器播放的。要播放这些数据,可以用安卓提供的AudioTrack类来播放,同时AudioTrack类的参数要跟AudioRecord的参数要保持一致。除此之外,还可以通过给这些原始音频数据添加WAV文件头,让它成为真正意义上的音频文件,这样常规的播放器就可以直接播放该文件了。
AudioTrack播放PCM
AudioTrack有两种数据加载模式(MODE_STREAM和MODE_STATIC),对应的是数据加载模式和音频流类型, 对应着两种完全不同的使用场景。
- MODE_STREAM:在这种模式下,通过write一次次把音频数据写到AudioTrack中。这和平时通过write系统调用往文件中写数据类似,但这种工作方式每次都需要把数据从用户提供的Buffer中拷贝到AudioTrack内部的Buffer中,这在一定程度上会使引入延时。为解决这一问题,AudioTrack就引入了第二种模式。
- MODE_STATIC:这种模式下,在play之前只需要把所有数据通过一次write调用传递到AudioTrack中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺点,就是一次write的数据不能太多,否则系统无法分配足够的内存来存储全部数据。
AudioTrack的创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() val audioFormat = AudioFormat.Builder() .setSampleRate(AudioConstants.SAMPLE_RATE_IN_HZ) .setEncoding(AudioConstants.AUDIO_FORMAT) .setChannelMask(AudioConstants.CHANNEL_OUT_CONFIG) .build() audioTrack = AudioTrack( audioAttributes, audioFormat, bufferSize, mode, AudioManager.AUDIO_SESSION_ID_GENERATE )
|
MODE_STATIC模式
static模式下须先调用write写数据,然后再调用play。
1 2 3 4 5
| audioTrack?.let { it.write(audioData, 0, audioData.size) it.play() }
|
MODE_STREAM模式
1 2 3 4 5 6 7 8 9 10
| val AUDIO_TRACK_BUFFER_SIZE = AudioTrack.getMinBufferSize(SAMPLE_RATE_IN_HZ, CHANNEL_OUT_CONFIG, AUDIO_FORMAT) val byteArray = ByteArray(AUDIO_TRACK_BUFFER_SIZE) audioTrack?.let { it.play() while (playSignal.get()) { val readCount = inputStream!!.read(byteArray) if (readCount <= 0) break it.write(byteArray, 0, readCount) } }
|
播放结束时机
在stream模式下,由于是我们主动向audioTrack喂数据的,所以我们可以很清楚的知道播放结束的时机。但是在static模式下,由于是预先给audioTrack喂了全部视频数据再开始播放的,音频播放结束的时机不能直接得到。
AudioTrack有一个setPlaybackPositionUpdateListener方法,该方法接收一个接口参数,该接口参数有一个onMarkerReached的方法,即是播放到了这个marker的时候会调用到这个方法,然后AudioTrack有一个notificationMarkerPosition的属性,是用于设置这个marker的,最多只能设置一个,这个marker指的是Frame的位置。Frame是一个单位,用来描述数据量的多少。1单位的Frame等于1个采样点的字节数×声道数(比如PCM16,双声道的1个Frame等于2×2=4字节)。1个采样点只针对一个声道,而实际上可能会有一或多个声道。由于不能用一个独立的单位来表示全部声道一次采样的数据量,也就引出了Frame的概念。Frame的大小,就是一个采样点的字节数×声道数。另外,在目前的声卡驱动程序中,其内部缓冲区也是采用Frame作为单位来分配和管理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| audioTrack?.let { it.write(audioData, 0, audioData.size) val frameSize = 16 / 8 it.notificationMarkerPosition = audioData.size / frameSize it.setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener { override fun onMarkerReached(track: AudioTrack?) { Log.d("TAG", "播放结束") }
override fun onPeriodicNotification(track: AudioTrack?) { } }) it.play() }
|
PCM转WAV
PCM只有原始的音频数据,但是却不包含录制的配置信息,所以是不能直接播放的。而WAV是一种音频格式,在pcm数据中添加了一个文件头。文件头中包含了播放该pcm数据所需的信息,于是常规播放器就能直接播放了。
起始地址 |
占用空间 |
本地址数字的含义 |
00H |
4byte |
RIFF,资源交换文件标志。 |
04H |
4byte |
从下一个地址开始到文件尾的总字节数。高位字节在后面,这里就是001437ECH,换成十进制是1325036byte,算上这之前的8byte就正好1325044byte了。 |
08H |
4byte |
WAVE,代表wav文件格式。 |
0CH |
4byte |
FMT ,波形格式标志 |
10H |
4byte |
00000010H,16PCM,我的理解是用16bit的数据表示一个量化结果。 |
14H |
2byte |
为1时表示线性PCM编码,大于1时表示有压缩的编码。这里是0001H。 |
16H |
2byte |
1为单声道,2为双声道,这里是0001H。 |
18H |
4byte |
采样频率,这里是00002B11H,也就是11025Hz。 |
1CH |
4byte |
Byte率=采样频率*音频通道数*每次采样得到的样本位数/8 ,00005622H,也就是22050Byte/s=11025*1*16/2 |
20H |
2byte |
块对齐=通道数*每次采样得到的样本位数/8,0002H,也就是 2 == 1*16/8 |
22H |
2byte |
样本数据位数,0010H即16,一个量化样本占2byte。 |
24H |
4byte |
data,一个标志而已。 |
28H |
4byte |
Wav文件实际音频数据所占的大小,这里是001437C8H即1325000,再加上2CH就正好是1325044,整个文件的大小。 |
2CH |
不定 |
量化数据 |
将pcm文件转化成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
| fun generateWavHeader(totalAudioLen: Long, longSampleRate: Long, channels: Long): ByteArray? { val totalDataLen = totalAudioLen + 36 val byteRate = longSampleRate * channels * 16 / 8 val header = ByteArray(44) header[0] = 'R'.code.toByte() header[1] = 'I'.code.toByte() header[2] = 'F'.code.toByte() header[3] = 'F'.code.toByte() header[4] = (totalDataLen and 0xff).toByte() header[5] = (totalDataLen shr 8 and 0xff).toByte() header[6] = (totalDataLen shr 16 and 0xff).toByte() header[7] = (totalDataLen shr 24 and 0xff).toByte() header[8] = 'W'.code.toByte() header[9] = 'A'.code.toByte() header[10] = 'V'.code.toByte() header[11] = 'E'.code.toByte() header[12] = 'f'.code.toByte() header[13] = 'm'.code.toByte() header[14] = 't'.code.toByte() header[15] = ' '.code.toByte() header[16] = 16 header[17] = 0 header[18] = 0 header[19] = 0 header[20] = 1 header[21] = 0 header[22] = channels.toByte() header[23] = 0 header[24] = (longSampleRate and 0xff).toByte() header[25] = (longSampleRate shr 8 and 0xff).toByte() header[26] = (longSampleRate shr 16 and 0xff).toByte() header[27] = (longSampleRate shr 24 and 0xff).toByte() header[28] = (byteRate and 0xff).toByte() header[29] = (byteRate shr 8 and 0xff).toByte() header[30] = (byteRate shr 16 and 0xff).toByte() header[31] = (byteRate shr 24 and 0xff).toByte() header[32] = (2 * channels).toByte() header[33] = 0 header[34] = 16 header[35] = 0 header[36] = 'd'.code.toByte() header[37] = 'a'.code.toByte() header[38] = 't'.code.toByte() header[39] = 'a'.code.toByte() header[40] = (totalAudioLen and 0xff).toByte() header[41] = (totalAudioLen shr 8 and 0xff).toByte() header[42] = (totalAudioLen shr 16 and 0xff).toByte() header[43] = (totalAudioLen shr 24 and 0xff).toByte() return header }
|
结语
上一节介绍了AudioRecord的使用,这是音频的采集,这一节介绍了AudioTrack的使用以及PCM数据转Wav文件,这是音频的播放,下一节我们就可以来进行视频的采集了。当然,视频采集主要手段就是Camera了。
代码
https://github.com/hotdl/learnmedia/tree/main/session003
参考
https://developer.android.com/reference/android/media/AudioTrack
https://developer.android.com/guide/topics/media-apps/volume-and-earphones