WAV 音频格式学习

WAV 是最常见的声音文件格式之一,是微软公司专门为 Windows 开发的一种标准数字音频文件,该文件能记录各种单声道或立体声的声音信息,并能保证声音不失真。但 WAV 文件有一个致命的缺点,就是它所占用的磁盘空间太大。它符合资源互换文件格式(RIFF)规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。Wave 格式支持 MSADPCM、CCITT A 律、CCITT μ 律和其他压缩算法,支持多种音频位数、采样频率和声道,是 PC 机上最为流行的声音文件格式;但其文件尺寸较大,多用于存储简短的声音片段。

一、RIFF 规范

资源交换档案标准(Resource Interchange File Format) (RIFF) 是一种把资料储存在被标记的区块(tagged chunks)中的档案格式(meta-format)。 它是在 1991 年时,由 Microsoft 和 IBM 提出。它是 Electronic Arts 1985 提出的 Interchange File Format 的翻版。这两种标准的唯一不同处是在多位元整数的储存方式。 RIFF 使用的是小端序,这是 IBM PC 使用的处理器 80x86 所使用的格式,而 IFF 储存整数的方式是使用大端序,这是 Amiga 和 Apple Macintosh 电脑使用的处理器可处理的整数型态。

RIFF 文件所包含的数据类型由该文件的扩展名来标识,能以 RIFF 格式存储的数据有:

• 音频视频交错格式数据 .AVI

• 波形格式数据 .WAV

• 位图数据格式 .RDI

• MIDI格式数据 .RMI

• 调色板格式 .PAL

• 多媒体电影 .RMN

• 动画光标 .ANI

• 其他的RIFF文件 .BND

RIFF 文件由不同数量的 chunk 组成,下面来学习其定义。

1.1 chunk

chunk(区块)是 RIFF 文件的基本单元,RIFF 文件由不同数量的 chunk 组成,每个 chunk 由“标识符”、“数据大小”和“数据”三个部分组成,“标识符”和“数据大小”都是占用 4 个字节空间,chunk 的基本结构如下所示:

struct chunk
{
    u32  ID; //标识符
    u32  Size; //数据大小
    u8   Data[Size]; //数据
};

ID:标识符由 4 个 ASCII 字符组成,用以识别块中所包含的数据。如:‘RIFF’,‘LIST’,'fmt '(最后一个字符为空格),‘data’,'WAV ','AVI’等,这种文件结构最初是由 Microsoft 和 IBM 为 PC 机所定义,所以 RIFF 文件是按照小端字节顺序写入的。

Size:块数据大小,存储在 Data 域中的数据长度,不包含 ID 和 Size 的大小。

Data:块数据,数据以字为单位存放,如果数据长度为奇数(字节为单位),则最后添加一个空字节。

chunk 是可以嵌套的,但是只有块标识符为’RIFF’或者’LIST’的 chunk 才能包含其他的 chunk。

1.2 FourCC

FourCC(Four Character Codes)是一个 4 字节 32 位的标识符,通常用来标识文件的数据格式。例如,在音视频播放器中,可以通过文件的 FourCC 来决定调用哪种 CODEC 进行音视频的解码。例如:DIV3、DIV4、DIVX 和 H264 等,对于音频则有:WAV、MP3 等。FourCC 是 4 个 ASCII 字符,不足四个字符的则在最后补充空格(不是空字符)。比如,FourCC fmt,实际上是’f’ ‘m’ ‘t’ ’ '。

1.3 RIFF chunk 和 LIST chunk

块标识符 ID 为’RIFF’的 chunk 是比较特殊的,每一个 RIFF 文件首先存放的必须是一个 RIFF chunk,并且只能有一个标识符为’RIFF’的 chunk。RIFF chunk 的数据域的起始位置是一个 4 字节的 FormType(FourCC 格式),用于标识 RIFF chunk 数据域中所包含的 chunk 的数据类型。紧接着 FormType 之后的数据域的内容则是 RIFF chunk 所包含的 subchunk。一个简单的 RIFF chunk 的示意图如下所示:

在这里插入图片描述

上图中的 RIFF chunk 包含有两个 subchunk,可以看出 RIFF chunk 的数据域首先是 4 字节的 FormType,接着是两个 subchunk,每一个 subchunk 又包含有自己的标识符、数据域的大小以及数据域。

除了 RIFF chunk 可以嵌套其他的 chunk 外,另一个可以包含 subchunk 的就是 LIST chunk,其示意图如下所示:
在这里插入图片描述

上图中,首先 RIFF 文件必须的 RIFF chunk,其数据域又包含有两个 subchunk,其中一个 subchunk 的类型为’LIST’,该 LIST chunk 又包含了两个 subchunk。

RIFF chunk 和 LIST chunk 的基本结构如下所示:

struct chunk
{
    u32 ID; //标识符: 'RIFF'或者'LIST'
    u32 Size; //数据大小
    struct ChunkData { //数据
        u32 Type; //包含的 subchunk 的数据类型, 与上面图中的 FormType 和 ListType 对应
        u8  Data[Size-4]; //包含的 subchunk
    };
};

备注:一个 RIFF 文件的总大小为:RIFF chunk 的 Size + 8,这里的 8 是 ID 和 Size 所占用的空间。

二、WAV 格式

WAV 文件是在 PC 机平台上很常见的、最经典的多媒体音频文件,最早于 1991 年 8 月出现在 Windows3.1 操作系统上,文件扩展名为 WAV,是 WaveForm 的简写,也称为波形文件,可直接存储声音波形,还原的波形曲线十分逼真。WAV 文件格式简称 WAV 格式是一种存储声音波形的数字音频格式,是由微软公司和 IBM 联合设计的,经过了多次修订,可用于 Windows、Macintosh、Linux 等多种操作系统。WAV 支持多种音频数字、取样频率和声道,标准格式化的 WAV 文件和 CD 格式一样,也是 44.1kHz 的取样频率,16 位量化数字,因此声音文件质量和 CD 相差无几。

WAV 的特点如下:真实记录自然声波形,基本无数据压缩,数据量大。一般来说,由 WAV 文件还原而成的声音的音质取决于声音卡采样样本的尺寸,采样频率越高,音质就越好,但开销就越大,WAV 文件也就越大。

最基本的 WAVE 文件是 PCM(脉冲编码调制)格式的,这种文件直接存储采样的声音数据没有经过任何的压缩,是声卡直接支持的数据格式,要让声卡正确播放其它被压缩的声音数据,就应该先把压缩的数据解压缩成 PCM 格式,然后再让声卡来播放。

声源发出的声波通过话筒被转换成连续变化的电信号,经过放大、抗混叠滤波后,按固定的频率进行采样,每个样本是在一个采样周期内检测到的电信号幅度值;接下来将其由模拟电信号量化为由二进制数表示的积分值;最后编码并存储为音频流数据。常见的声音文件主要有两种,分别对应于单声道(11.025 KHz 采样率、8 Bit 的采样值)和双声道(44.1 KHz 采样率、16 Bit 的采样值)。采样率是指:声音信号在“模→数”转换过程中单位时间内采样的次数。采样值是指每一次采样周期内声音模拟信号的积分值。

对于单声道声音文件,采样数据为 8 位的整数(char 0x00~0xFF);而对于双声道立体声声音文件,每次采样数据为一个 16 位的整数(short),高八位和低八位分别代表左右两个声道。在单声道 WAVE 文件中,声道 0 代表左声道,声道 1 代表右声道。在多声道 WAVE 文件中,样本是交替出现的。

2.1 PCM

脉冲编码调制(Pulse Code Modulation,PCM)就是把一个时间连续,取值连续的模拟信号变换成时间离散,取值离散的数字信号后在信道中传输。脉冲编码调制就是对模拟信号先抽样,再对样值幅度量化,编码的过程。

抽样,就是对模拟信号进行周期性扫描,把时间上连续的信号变成时间上离散的信号,抽样必须遵循奈奎斯特抽样定理。该模拟信号经过抽样后还应当包含原信号中所有信息,也就是说能无失真的恢复原模拟信号。它的抽样速率的下限是由抽样定理确定的。

量化,就是把经过抽样得到的瞬时值将其幅度离散,即用一组规定的电平,把瞬时抽样值用最接近的电平值来表示,通常是用二进制表示。

量化误差:量化后的信号和抽样信号的差值。量化误差在接收端表现为噪声,称为量化噪声。 量化级数越多误差越小,相应的二进制码位数越多,要求传输速率越高,频带越宽。

为使量化噪声尽可能小而所需码位数又不太多,通常采用非均匀量化的方法进行量化。 非均匀量化根据幅度的不同区间来确定量化间隔,幅度小的区间量化间隔取得小,幅度大的区间量化间隔取得大。

一个模拟信号经过抽样量化后,得到已量化的脉冲幅度调制信号,它仅为有限个数值。

编码,就是用一组二进制码组来表示每一个有固定电平的量化值。然而,实际上量化是在编码过程中同时完成的,故编码过程也称为模/数变换,可记作 A/D。

2.2 WAV

WAVE 文件虽然可以压缩,但一般都使用不压缩的格式。下面是 WAV 格式详细说明。

endianfield nameSizedescription
bigChunkID4文件头标识,一般就是" RIFF" 四个字母
littleChunkSize4整个数据文件的大小,不包括上面 ID 和 Size 本身
bigFormat4一般就是 “WAVE” 四个字母
bigSubChunk1ID4格式说明块,本字段一般就是 "fmt "
littleSubChunk1Size4本数据块的大小,不包括 ID 和 Size 字段本身
littleAudioFormat2存储音频文件的编码格式,例如若为PCM则其存储值为1,若为其他非PCM格式的则有一定的压缩。
littleNumChannels2声道数,单通道(Mono)值为1,双通道(Stereo)值为2等等
littleSampleRate4采样率
littleByteRate4比特率,每秒存储的bit数,其值=SampleRate * NumChannels * BitsPerSample/8
littleBlockAlign2数据块对齐单元,值=NumChannels * BitsPerSample/8
littleBitsPerSample2采样时模数转换的分辨率,每个采样点的bit数,一般为8、16和32等
bigSubChunk2ID4真正的声音数据块,本字段一般是 “data”
littleSubChunk2Size4本数据块的大小,不包括 ID 和 Size 字段本身,其值=NumSamples * NumChannels * BitsPerSample/8
littleDataN音频的采样数据

对于 Data 块,根据声道数和采样率的不同情况,布局如下:

PCM 数据类型采样采样
8 Bit 单声道声道0声道0
8 Bit 双声道声道0声道1
16 Bit 单声道声道0低位,声道0高位声道0低位,声道0高位
16 Bit 双声道声道0低位,声道0高位声道1低位,声道1高位

下面是一段给 PCM 加头变为 WAV 的 Kotlin 代码。

/**
 * 文件名 PCMHandler
 * 描  述 PCM 数据处理成 mkv、mp4 可直接打包的格式
 * 作  者 lhw
 * 时  间 2020/10/22 11:40
 */
class PCMHandler {
    companion object {
        public const val WAV_HEADER_SIZE = 44
        private val RIFF = "RIFF".toByteArray(Charsets.US_ASCII)
        private val WAVE = "WAVE".toByteArray(Charsets.US_ASCII)
        private val FMT = "fmt ".toByteArray(Charsets.US_ASCII)
        private val DATA = "data".toByteArray(Charsets.US_ASCII)

        private val WAV_HEADER = ByteArray(WAV_HEADER_SIZE)

    }

    fun makeFreeScaleWAVHeader(pcmDataLen: Int): ByteArray {
        return makeWAVHeader(pcmDataLen, 8000, 1, 16)
    }

    private fun makeWAVHeader(
        pcmDataLen: Int,
        sampleRate: Int,
        numChannels: Short,
        bitsPerSample: Short
    ): ByteArray {
        // ChunkID 内容为"RIFF"
        System.arraycopy(RIFF, 0, WAV_HEADER, 0, RIFF.size)
        // ChunkSize 存储文件的字节数(不包含ChunkID和ChunkSize这8个字节)
        fillSizeByPos(4, pcmDataLen + (WAV_HEADER_SIZE - 8))
        // Format 内容为"WAVE"
        System.arraycopy(WAVE, 0, WAV_HEADER, 8, WAVE.size)
        // Subchunk1ID 内容为"fmt "
        System.arraycopy(FMT, 0, WAV_HEADER, 12, FMT.size)
        // Subchunk1Size 存储该子块的字节数(不含前面的Subchunk1ID和Subchunk1Size这8个字节)
        fillSizeByPos(16, 16)
        // AudioFormat 存储音频文件的编码格式,例如若为PCM则其存储值为1,若为其他非PCM格式的则有一定的压缩。
        fillSizeByPos(20, 1.toShort())
        // NumChannels 通道数,单通道(Mono)值为1,双通道(Stereo)值为2,等等
        fillSizeByPos(22, numChannels)
        // SampleRate 采样率,如8k,44.1k等
        fillSizeByPos(24, sampleRate)
        // ByteRate 每秒存储的bit数,其值=SampleRate * NumChannels * BitsPerSample/8
        val byteRate = sampleRate * numChannels * bitsPerSample / 8
        fillSizeByPos(28, byteRate)
        // BlockAlign 块对齐大小,其值=NumChannels * BitsPerSample/8
        val blockAlign = numChannels * bitsPerSample / 8
        fillSizeByPos(32, blockAlign.toShort())
        // BitsPerSample 每个采样点的bit数,一般为8,16,32等。
        fillSizeByPos(34, bitsPerSample)
        // Subchunk2ID 内容为“data”
        System.arraycopy(DATA, 0, WAV_HEADER, 36, DATA.size)
        // Subchunk2Size 内容为接下来的正式的数据部分的字节数,其值=NumSamples * NumChannels * BitsPerSample/8
        fillSizeByPos(40, pcmDataLen)

        return WAV_HEADER
    }

    private fun fillSizeByPos(startPos: Int, size: Int, data: ByteArray = WAV_HEADER) {
        // 小端
        data[startPos] = (size and 0xff).toByte()
        data[startPos + 1] = (size shr 8 and 0xff).toByte()
        data[startPos + 2] = (size shr 16 and 0xff).toByte()
        data[startPos + 3] = (size shr 24 and 0xff).toByte()
    }

    private fun fillSizeByPos(startPos: Int, size: Short, data: ByteArray = WAV_HEADER) {
        // 小端
        data[startPos] = (size.toInt() and 0xff).toByte()
        data[startPos + 1] = (size.toInt() shr 8 and 0xff).toByte()
    }
}
tyyj90 CSDN认证博客专家 安卓全栈开发
Android高级开发工程师,熟悉App开发,熟悉Framework定制开发,略了解Linux内核。开发使用的语言涉及Java、Kotlin、C/C++、Python和汇编NEON指令集。
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值