Android音频开发(五)如何存储和解析最简单的音频wav文件

我们大家都知道,无论是文字、图像还是声音,都必须以一定的格式来组织和存储起来,然后其它的软件再以相同的协议规则,相应的格式才能去打开解析这一段数据,例如,对于原始的图像数据,我们常见的格式有 YUV、Bitmap,而对于音频来说,最简单常见的格式就是 wav 格式了。今天我们简单的介绍一下如何简单的解析解析最简单的音频数据-wav文件。

在前面几篇主要介绍了如何利用android系统的 API 来完成原始音频信号的采集和播放,今天就讲解如何在 Android 平台上,将采集到的 PCM 音频数据保存到 wav 文件,同时,也介绍如何读取和解析 wav 文件。

在解析wav文件时,我们要先来看看wav文件的组成。

1.wav文件的组成

在这里插入图片描述
我们可以简单的分析一下这个 wav 格式头,它主要分为三个部分:

  1. 第一部分

    属于最“顶层”的信息块,通过“ChunkID”来表示这是一个 “RIFF”格式的文件,通过“Format”填入 “WAVE”来标识这是一个 wav 文件。而“ChunkSize”则记录了整个 wav 文件的字节数。

  2. 第二部分
    属于“fmt”信息块,主要记录了本 wav 音频文件的详细音频参数信息,例如:通道数、采样率、位宽等等

  3. 第三部分
    属于“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文件中的数据的示例是:

  1. 音频/视频交错数据(.AVI)
  2. 波形数据(.WAV)
  3. 位图数据(.RDI)
  4. MIDI信息(.RMI)
  5. 调色板(.PAL)
  6. 多媒体电影(.RMN)
  7. 动画光标(.ANI)
  8. 一捆其他RIFF文件(.BND)

注意:在这一点上,AVI文件是使用当前RIFF规范完全实现的唯一RIFF文件类型。尽管已经实现了WAV文件,但是这些文件非常简单,它们的开发人员通常在构建它们时会使用较旧的规范。

4.读取wav音频文件

自此,我们应该知道了wav 文件就是一段“文件头”+“音频二进制数据”。所以读写非常简单,如何读,如何写大致如下:

1. 写 wav 文件,其实就是先写入一个 wav 文件头,然后再继续写入音频二进制数据即可
2. 读 wav 文件,其实也就是先读一个 wav 文件头,然后再继续读出音频二进制数据即可

看了上面wav参数和RIFF的那么多介绍,我们在动手写代码之前,有两点是需要搞清楚:

  1. wav 文件头中,有哪些是“变化的”,哪些是“不变的”?

    比如:文件头开头的“RIFF”字符串就是“不变的”部分,而用来记录音频数据总长度的“Subchunk2Size”变量就是属于“变化的”部分,因为,再音频数据没有彻底全部写完之前,你是无法知道一共写入了多少字节的音频数据的,因此,这个部分,需要用一个变量记录起来,到全部写完之后,再使用 Java 的“RandomAccessFile”类,将文件指针跳转到“Subchunk2Size”字段,改写一下默认值即可。

  2. 如何把 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文件的读写代码如下:

  1. 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()
    }
}
  1. 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
        }
    }
}
  1. 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代表 资源交换文件格式。

音频开发的知识点还是很多的,学习音频开发需要大家有足够的耐心,一步一个脚印的积累,只有这样才能把音频开发学好。下面推荐几个比较好的博主,希望对大家有所帮助。

  1. csdn博主:《雷神雷霄骅》
  2. 51CTO博客:《Jhuster的专栏》

猜你喜欢

转载自blog.csdn.net/ljx1400052550/article/details/114532136