我们大家都知道,无论是文字、图像还是声音,都必须以一定的格式来组织和存储起来,然后其它的软件再以相同的协议规则,相应的格式才能去打开解析这一段数据,例如,对于原始的图像数据,我们常见的格式有 YUV、Bitmap,而对于音频来说,最简单常见的格式就是 wav 格式了。今天我们简单的介绍一下如何简单的解析解析最简单的音频数据-wav文件。
在前面几篇主要介绍了如何利用android系统的 API 来完成原始音频信号的采集和播放,今天就讲解如何在 Android 平台上,将采集到的 PCM 音频数据保存到 wav 文件,同时,也介绍如何读取和解析 wav 文件。
在解析wav文件时,我们要先来看看wav文件的组成。
1.wav文件的组成
我们可以简单的分析一下这个 wav 格式头,它主要分为三个部分:
-
第一部分
属于最“顶层”的信息块,通过“ChunkID”来表示这是一个 “RIFF”格式的文件,通过“Format”填入 “WAVE”来标识这是一个 wav 文件。而“ChunkSize”则记录了整个 wav 文件的字节数。
-
第二部分
属于“fmt”信息块,主要记录了本 wav 音频文件的详细音频参数信息,例如:通道数、采样率、位宽等等 -
第三部分
属于“fmt”信息块,主要记录了本 wav 音频文件的详细音频参数信息,例如:通道数、采样率、位宽等等
好了,分析到这里,我想大家应该就明白了,其实,做一种多媒体格式的解析,也不是一件特别复杂的事,说白了,格式就是一种规范,告诉你,我的二进制数据是怎么存储的,你只需要按照我的存储格的式来解析就可以了。
2.简单抽象的定义wav 文件头
我们可以定义一个如下的 Java 类来抽象和描述 wav 文件头:
package com.bnd.myaudioandvideo.wav
class WavFileHeader {
var mChunkID = "RIFF"
var mChunkSize = 0
var mFormat = "WAVE"
var mSubChunk1ID = "fmt "
var mSubChunk1Size = 16
var mAudioFormat: Short = 1
var mNumChannel: Short = 1
var mSampleRate = 8000
var mByteRate = 0
var mBlockAlign: Short = 0
var mBitsPerSample: Short = 8
var mSubChunk2ID = "data"
var mSubChunk2Size = 0
constructor() {
}
constructor(sampleRateInHz: Int, bitsPerSample: Int, channels: Int) {
mSampleRate = sampleRateInHz
mBitsPerSample = bitsPerSample.toShort()
mNumChannel = channels.toShort()
mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8
mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()
}
}
下面我们就简单介绍一下这几个参数的意义,也可以参考:WAVE PCM声音文件格式
规范的WAVE格式以RIFF头开头:
参数名称 | 描述 |
---|---|
ChunkID | 包含ASCII格式的字母“ RIFF”(0x52494646大端格式) |
ChunkSize | 36 + SubChunk2Size,或更准确地说:4 +(8 + SubChunk1Size)+(8 + SubChunk2Size)这是其余块的大小 跟随这个数字。这是大小整个文件(以字节减去8个字节为单位) 此计数中未包括的两个字段: ChunkID和ChunkSize。 |
mFormat | 包含字母“ WAVE”(0x57415645大端格式),“ WAVE”格式包含两个子块:“ fmt”和“ data”;“ fmt”子块描述了声音数据的格式 |
Subchunk1ID | 包含字母“ fmt”(0x666d7420大端格式) |
Subchunk1尺寸 | 用于PCM。这是大小, 该数字后面的Subchunk其余部 |
AudioFormat | PCM = 1(即线性量化), 非1的值表示某些 压缩形式。 |
NumChannels | Mono = 1,Stereo = 2,etc, |
SampleRate | 8000、44100等 |
ByteRate | == SampleRate * NumChannels * BitsPerSample / 8 |
BlockAlign | == NumChannels * BitsPerSample / 8; 一个样本的字节数,包括了所有频道。 |
BitsPerSample | 8位= 8、16位= 16等 |
ExtraParamSize | 如果是PCM,则不存在 |
ExtraParams | 空间用于附加参数 |
Subchunk2ID | 包含字母“ data”(0x64617461大端格式)。 |
Subchunk2Size | == NumSamples * NumChannels * BitsPerSample / 8;这是数据中的字节数。您也可以将其视为大小在此之后的子块的读取 数字。 |
data | 实际声音数据 |
例如,这是WAVE文件的开头72个字节,其中的字节显示为十六进制数字:
52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20 10 00 00 00 01 00 02 00
22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00
24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6 3c f2 24 f2 11 ce 1a 0d
以下是这些字节作为WAVE声音文件的解释:
3.关于RIFF文件的一般讨论
多媒体应用程序需要存储和管理各种数据,包括位图,音频数据,视频数据和外围设备控制信息。RIFF提供了一种存储所有这些不同类型数据的方法。RIFF文件包含的数据类型由文件扩展名指示。可以存储在RIFF文件中的数据的示例是:
- 音频/视频交错数据(.AVI)
- 波形数据(.WAV)
- 位图数据(.RDI)
- MIDI信息(.RMI)
- 调色板(.PAL)
- 多媒体电影(.RMN)
- 动画光标(.ANI)
- 一捆其他RIFF文件(.BND)
注意:在这一点上,AVI文件是使用当前RIFF规范完全实现的唯一RIFF文件类型。尽管已经实现了WAV文件,但是这些文件非常简单,它们的开发人员通常在构建它们时会使用较旧的规范。
4.读取wav音频文件
自此,我们应该知道了wav 文件就是一段“文件头”+“音频二进制数据”。所以读写非常简单,如何读,如何写大致如下:
1. 写 wav 文件,其实就是先写入一个 wav 文件头,然后再继续写入音频二进制数据即可
2. 读 wav 文件,其实也就是先读一个 wav 文件头,然后再继续读出音频二进制数据即可
看了上面wav参数和RIFF的那么多介绍,我们在动手写代码之前,有两点是需要搞清楚:
-
wav 文件头中,有哪些是“变化的”,哪些是“不变的”?
比如:文件头开头的“RIFF”字符串就是“不变的”部分,而用来记录音频数据总长度的“Subchunk2Size”变量就是属于“变化的”部分,因为,再音频数据没有彻底全部写完之前,你是无法知道一共写入了多少字节的音频数据的,因此,这个部分,需要用一个变量记录起来,到全部写完之后,再使用 Java 的“RandomAccessFile”类,将文件指针跳转到“Subchunk2Size”字段,改写一下默认值即可。
-
如何把 int、short 变量与 byte[] 的转换
因为 wav 文件都是二进制的方式读写,因此,“WavFileHeader”类中定义的变量都需要转换为byte字节流,具体转换方法如下:
private fun intToByteArray(data: Int): ByteArray? {
return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()
}
private fun shortToByteArray(data: Short): ByteArray? {
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()
}
private fun byteArrayToShort(b: ByteArray): Short {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort()
}
private fun byteArrayToInt(b: ByteArray): Int {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt()
}
好了,最后附上完整的wav文件的读写代码如下:
- wav文件头的代码
package com.bnd.myaudioandvideo.wav
import java.nio.ByteBuffer
import java.nio.ByteOrder
class WavFileHeader {
var mChunkID = "RIFF"
var mChunkSize = 0
var mFormat = "WAVE"
var mSubChunk1ID = "fmt "
var mSubChunk1Size = 16
var mAudioFormat: Short = 1
var mNumChannel: Short = 1
var mSampleRate = 8000
var mByteRate = 0
var mBlockAlign: Short = 0
var mBitsPerSample: Short = 8
var mSubChunk2ID = "data"
var mSubChunk2Size = 0
constructor() {
}
constructor(sampleRateInHz: Int, bitsPerSample: Int, channels: Int) {
mSampleRate = sampleRateInHz
mBitsPerSample = bitsPerSample.toShort()
mNumChannel = channels.toShort()
mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8
mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()
}
private fun intToByteArray(data: Int): ByteArray? {
return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()
}
private fun shortToByteArray(data: Short): ByteArray? {
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()
}
private fun byteArrayToShort(b: ByteArray): Short {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort()
}
private fun byteArrayToInt(b: ByteArray): Int {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt()
}
}
- wav文件读取的代码
package com.bnd.myaudioandvideo.wav
import android.util.Log
import java.io.DataInputStream
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
class WavFileReader {
private var mDataInputStream: DataInputStream? = null
private var mWavFileHeader: WavFileHeader? = null
@Throws(IOException::class)
fun openFile(filepath: String?): Boolean {
if (mDataInputStream != null) {
closeFile()
}
mDataInputStream = DataInputStream(FileInputStream(filepath))
return readHeader()
}
@Throws(IOException::class)
fun closeFile() {
if (mDataInputStream != null) {
mDataInputStream!!.close()
mDataInputStream = null
}
}
fun getmWavFileHeader(): WavFileHeader? {
return mWavFileHeader
}
fun readData(buffer: ByteArray?, offset: Int, count: Int): Int {
if (mDataInputStream == null || mWavFileHeader == null) {
return -1
}
try {
val nbytes = mDataInputStream!!.read(buffer, offset, count)
return if (nbytes == -1) {
0
} else nbytes
} catch (e: IOException) {
e.printStackTrace()
}
return -1
}
private fun readHeader(): Boolean {
if (mDataInputStream == null) {
return false
}
val header = WavFileHeader()
val intValue = ByteArray(4)
val shortValue = ByteArray(2)
try {
header.mChunkID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()
Log.d(TAG, "Read file chunkID:" + header.mChunkID)
mDataInputStream!!.read(intValue)
header.mChunkSize = byteArrayToInt(intValue)
Log.d(TAG, "Read file chunkSize:" + header.mChunkSize)
header.mFormat = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()
Log.d(TAG, "Read file format:" + header.mFormat)
header.mSubChunk1ID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()
Log.d(TAG, "Read fmt chunkID:" + header.mSubChunk1ID)
mDataInputStream!!.read(intValue)
header.mSubChunk1Size = byteArrayToInt(intValue)
Log.d(TAG, "Read fmt chunkSize:" + header.mSubChunk1Size)
mDataInputStream!!.read(shortValue)
header.mAudioFormat = byteArrayToShort(shortValue)
Log.d(TAG, "Read audioFormat:" + header.mAudioFormat)
mDataInputStream!!.read(shortValue)
header.mNumChannel = byteArrayToShort(shortValue)
Log.d(TAG, "Read channel number:" + header.mNumChannel)
mDataInputStream!!.read(intValue)
header.mSampleRate = byteArrayToInt(intValue)
Log.d(TAG, "Read samplerate:" + header.mSampleRate)
mDataInputStream!!.read(intValue)
header.mByteRate = byteArrayToInt(intValue)
Log.d(TAG, "Read byterate:" + header.mByteRate)
mDataInputStream!!.read(shortValue)
header.mBlockAlign = byteArrayToShort(shortValue)
Log.d(TAG, "Read blockalign:" + header.mBlockAlign)
mDataInputStream!!.read(shortValue)
header.mBitsPerSample = byteArrayToShort(shortValue)
Log.d(TAG, "Read bitspersample:" + header.mBitsPerSample)
header.mSubChunk2ID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()
Log.d(TAG, "Read data chunkID:" + header.mSubChunk2ID)
mDataInputStream!!.read(intValue)
header.mSubChunk2Size = byteArrayToInt(intValue)
Log.d(TAG, "Read data chunkSize:" + header.mSubChunk2Size)
Log.d(TAG, "Read wav file success !")
} catch (e: Exception) {
e.printStackTrace()
return false
}
mWavFileHeader = header
return true
}
companion object {
private val TAG = WavFileReader::class.java.simpleName
private fun byteArrayToShort(b: ByteArray): Short {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).short
}
private fun byteArrayToInt(b: ByteArray): Int {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int
}
}
}
- wav文件写的代码
package com.bnd.myaudioandvideo.wav
import java.io.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
class WavFileWriter {
private var mFilepath: String? = null
private var mDataSize = 0
private var mDataOutputStream: DataOutputStream? = null
@Throws(IOException::class)
fun openFile(filepath: String?, sampleRateInHz: Int, channels: Int, bitsPerSample: Int): Boolean {
if (mDataOutputStream != null) {
closeFile()
}
mFilepath = filepath
mDataSize = 0
mDataOutputStream = DataOutputStream(FileOutputStream(filepath))
return writeHeader(sampleRateInHz, bitsPerSample, channels)
}
@Throws(IOException::class)
fun closeFile(): Boolean {
var ret = true
if (mDataOutputStream != null) {
ret = writeDataSize()
mDataOutputStream!!.close()
mDataOutputStream = null
}
return ret
}
fun writeData(buffer: ByteArray?, offset: Int, count: Int): Boolean {
if (mDataOutputStream == null) {
return false
}
mDataSize += try {
mDataOutputStream!!.write(buffer, offset, count)
count
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun writeHeader(sampleRateInHz: Int, channels: Int, bitsPerSample: Int): Boolean {
if (mDataOutputStream == null) {
return false
}
val header = WavFileHeader(sampleRateInHz, channels, bitsPerSample)
try {
mDataOutputStream!!.writeBytes(header.mChunkID)
mDataOutputStream!!.write(intToByteArray(header.mChunkSize), 0, 4)
mDataOutputStream!!.writeBytes(header.mFormat)
mDataOutputStream!!.writeBytes(header.mSubChunk1ID)
mDataOutputStream!!.write(intToByteArray(header.mSubChunk1Size), 0, 4)
mDataOutputStream!!.write(shortToByteArray(header.mAudioFormat), 0, 2)
mDataOutputStream!!.write(shortToByteArray(header.mNumChannel), 0, 2)
mDataOutputStream!!.write(intToByteArray(header.mSampleRate), 0, 4)
mDataOutputStream!!.write(intToByteArray(header.mByteRate), 0, 4)
mDataOutputStream!!.write(shortToByteArray(header.mBlockAlign), 0, 2)
mDataOutputStream!!.write(shortToByteArray(header.mBitsPerSample), 0, 2)
mDataOutputStream!!.writeBytes(header.mSubChunk2ID)
mDataOutputStream!!.write(intToByteArray(header.mSubChunk2Size), 0, 4)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun writeDataSize(): Boolean {
if (mDataOutputStream == null) {
return false
}
try {
val wavFile = RandomAccessFile(mFilepath, "rw")
wavFile.seek(WavFileHeader.Companion.WAV_CHUNKSIZE_OFFSET.toLong())
wavFile.write(intToByteArray(mDataSize + WavFileHeader.Companion.WAV_CHUNKSIZE_EXCLUDE_DATA), 0, 4)
wavFile.seek(WavFileHeader.Companion.WAV_SUB_CHUNKSIZE2_OFFSET.toLong())
wavFile.write(intToByteArray(mDataSize), 0, 4)
wavFile.close()
} catch (e: FileNotFoundException) {
e.printStackTrace()
return false
} catch (e: IOException) {
e.printStackTrace()
return false
}
return true
}
companion object {
private fun intToByteArray(data: Int): ByteArray {
return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()
}
private fun shortToByteArray(data: Short): ByteArray {
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()
}
}
}
5.总结
- WAVE数据文件假定的默认字节顺序为little-endian。使用big-endian字节排序方案写入的文件具有标识符RIFX而不是RIFF。
- 样本数据必须以偶数字节边界结尾。不管它是什么意思。
- 8位样本存储为0到255之间的无符号字节。16位样本存储为2的补码有符号整数,范围从-32768到32767。
- Wave数据流中可能还有其他子块。如果是这样,则每个将具有char [4] SubChunkID,以及无符号长SubChunkSize和SubChunkSize的数据量。
- RIFF代表 资源交换文件格式。
音频开发的知识点还是很多的,学习音频开发需要大家有足够的耐心,一步一个脚印的积累,只有这样才能把音频开发学好。下面推荐几个比较好的博主,希望对大家有所帮助。
- csdn博主:《雷神雷霄骅》
- 51CTO博客:《Jhuster的专栏》