嵌入式音频开发笔记——PCM编码与WAV音频文件格式详解

PCM 编码原理详解

一、PCM 基本概念

PCM(Pulse Code Modulation,脉冲编码调制)是一种将模拟信号数字化的无损编码方式,包含三个核心步骤:

  1. ​采样(Sampling)​

    • 按照固定时间间隔(奈奎斯特频率)测量模拟信号的瞬时值
    • 例如:CD音频采样率为44.1kHz(人耳最高识别20kHz的2倍以上)
    • 公式:采样频率 ≥ 2 × 信号最高频率
  2. ​量化(Quantization)​

    • 将连续幅值离散化为有限个量化电平
    • 量化精度(位深):常见16位(65,536级)、24位(16,777,216级)、32位(4,294,967,296级)
    • 量化误差:实际值与量化值之差(引入"量化噪声")
  3. ​编码(Coding)​

    • 将量化值转换为二进制序列
    • 线性PCM:直接二进制映射
    • 编码公式:数字值 = (模拟值/满量程) × (2ⁿ-1)

二、关键技术特征

特性说明
信号保真度直接取决于采样率和量化位深
数据量未压缩原始数据:位深×声道数×采样率
动态范围每增加1比特提升约6dB信噪比
处理复杂度编码/解码只需ADC/DAC转换,几乎无计算负载

Waveform 音频文件结构(WAV 格式)

        PCM编码后的声音数据是需要保存的,WAVE文件常常用来保存PCM编码数据。WAVE文件是微软公司(Microsoft)开发的一种声音文件格式,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,WAVE文件默认打开工具是WINDOWS的媒体播放器。

1. RIFF文件格式标准

        WAVE文件是以微软RIFF格式为标准的,RIFF全称为资源互换文件格式(Resources Interchange File Format),是Windows下大部分多媒体文件遵循的一种文件结构。RIFF文件所包含的数据类型由该文件的扩展名来标识,能以RIFF格式存储的数据有很多:音频视频交错格式数据(.AVI)、波形格式数据(.WAV)、位图数据格式(.RDI)、MIDI格式数据(.RMI)、调色板格式(.PAL)、多媒体电影(.RMN)、动画光标(.ANI)等。
  如下代码所示的CK结构体是RIFF文件的基本单元,该基本单元也称 Chunk。其中ckID用于标识块中所包含的数据类型,其取值可有'RIFF'、'LIST'、'fmt '、'data'等;ckSize表示存储在ckData域中的数据长度(不包含ckID和ckSize的大小);ckData存储数据,数据以字节为单位存放,如果数据长度为奇数,则最后添加一个空字节。

/*
由于RIFF文件结构最初是由Microsoft和IBM为PC机所定义
RIFF文件是按照小端little-endian字节顺序写入的
*/
typedef unsigned long DWORD;
typedef unsigned char BYTE;
typedef DWORD         FOURCC;    // Four-character code

typedef struct { 
     FOURCC ckID;          // The unique chunk identifier 
     DWORD ckSize;         // The size of field <ckData> 
     BYTE ckData[ckSize];  // The actual data of the chunk 
} CK;

2. WAVE文件结构

        WAVE是Microsoft开发的一种音频文件格式,它符合上面提到的RIFF文件格式标准,可以看作是RIFF文件的一个具体实例。既然WAVE符合RIFF规范,其基本的组成单元也是Chunk。一个 WAVE文件 通常有三个Chunk以及一个可选Chunk,其在文件中的排列方式依次是:RIFF Chunk,Format Chunk,Fact Chunk(附加块,可选),Data Chunk,如下图所示:

WAV 文件基于 RIFF(Resource Interchange File Format) 结构,由多个区块(Chunks)组成,关键区块如下:

RIFF 头区块
  • Chunk ID:固定为 "RIFF"(4 字节)。
  • Chunk Size:文件总大小减去 8 字节(4 字节,小端序)。
  • Format:固定为 "WAVE"(4 字节)。
fmt 子区块
  • Subchunk ID:固定为 "fmt "(4 字节)。
  • Subchunk Size:一般为 16 字节(PCM 格式)(4 字节)。
  • Audio Format:编码格式(2 字节),如 PCM 为 1。
  • Num Channels:声道数(2 字节),如单声道为 1,立体声为 2。
  • Sample Rate:采样率(Hz,4 字节),如 44100。
  • Byte Rate:每秒字节数(4 字节),计算公式:
    SampleRate × NumChannels × BitsPerSample / 8
  • Block Align:每样本字节数(2 字节),计算公式:
    NumChannels × BitsPerSample / 8
  • Bits Per Sample:量化位数(2 字节),如 16。
data 子区块
  • Subchunk ID:固定为 "data"(4 字节)。
  • Subchunk Size:音频数据大小(4 字节)。
  • Data:连续的 PCM 样本数据,按声道交替存储(如立体声为 L-R-L-R…),小端序。

扩展说明

  • 非 PCM 格式:WAV 文件可支持 ADPCM、IEEE 浮点数等,需修改 Audio Format 字段。
  • 扩展区块:可能包含 "fact"(压缩格式必需)或 "LIST"(元信息)等子区块。

 示例代码

一、WAV文件头的定义

#pragma pack(push, 1) // 关键!确保编译器使用1字节对齐,无填充。
struct WAVHEADER
{
    /********** RIFF Chunk **********/
    char RiffName[4];     // <-> 'R' 'I' 'F' 'F'
    uint32_t nRiffLength; // <-> Total File Size - 8
    char WavName[4];      // <-> 'W' 'A' 'V' 'E'

    /********** fmt Chunk **********/
    char FmtName[4];       // <-> 'f' 'm' 't' ' ' (注意最后一个字节是空格' '的ASCII码0x20)
    uint32_t nFmtLength;   // <-> fmt块的数据部分长度 (应为16)
    /****** fmt Chunk Data *******/
    uint16_t nAudioFormat;     // <-> 1 (PCM)
    uint16_t nChannleNumber;   // <-> 声道数 (1, 2...)
    uint32_t nSampleRate;      // <-> 采样率
    uint32_t nBytesPerSecond;  // <-> 字节速率
    uint16_t nBytesPerSample;  // <-> 采样帧字节数 (== BlockAlign)
    uint16_t nBitsPerSample;   // <-> 每个采样的位数

    /********** data Chunk Header **********/
    char DATANAME[4];     // <-> 'd' 'a' 't' 'a'
    uint32_t nDataLength; // <-> 音频数据的长度
};
#pragma pack(pop) // 恢复默认对齐方式
  • 成员顺序​​: 结构体成员的顺序​​严格对应​​了WAV文件格式要求的块(RIFF, fmt, data)的顺序以及各块内部字段的顺序。
  • ​字段意义​​: 每个结构体成员变量名清晰反映了其对应的WAV文件字段的含义,命名规范合理(如nRiffLengthnSampleRate)。
  • ​字符数组 vs 字符串​​: 使用char[4](如RiffNameWavName等)存储4字符代码(FourCC)是最合适的。const char* 在这里不合适,因为它存储的是指针而非实际数据。memcpy("RIFF", ...)确保将4个有效字符复制进去,没有C风格字符串的终止符'\0',符合二进制格式要求。
  • ​整数类型选择​​: uint32_tuint16_t 严格对应了WAV格式中相应字段的大小(4字节和2字节)。
  • #pragma pack(push, 1)/pop​: 这是​​最关键​​的部分!
    • WAV文件格式要求字段​​紧密排列​​,中间不允许有字节填充(padding)。
    • C/C++编译器为了提高内存访问效率,通常会对结构体成员进行内存对齐(比如在uint16_t后面插入2个字节的填充,使下一个uint32_t对齐到4字节地址)。这会破坏与文件二进制布局的对应关系。
    • #pragma pack(1) 强制编译器按1字节对齐方式打包结构体,确保成员之间没有任何填充字节。push/pop确保只在定义这个结构体时改变对齐方式,避免影响其他代码。
  • 最常见的WAVE Format的Tag值定义

二、WAV文件结构体的初始化

WAVHEADER wavHeader;

void set_wav_header()
{
    memcpy(wavHeader.RiffName, "RIFF", 4);
    memcpy(wavHeader.WavName, "WAVE", 4);
    memcpy(wavHeader.FmtName, "fmt ", 4);
    memcpy(wavHeader.DATANAME, "data", 4);

    wavHeader.nFmtLength = 16;
    wavHeader.nAudioFormat = 1;
    wavHeader.nBitsPerSample = 32;
    wavHeader.nChannleNumber = 2;
    wavHeader.nSampleRate = mSetWaveSamplate;
    wavHeader.nBytesPerSample = wavHeader.nChannleNumber * wavHeader.nBitsPerSample / 8;
    wavHeader.nBytesPerSecond = wavHeader.nSampleRate * wavHeader.nBytesPerSample;
}

 三、WAVE文件数据解析

        当WAVE文件的头被解析成功后,下一步便是获取WAVE文件里的声音源数据,我们知道声音文件有单声道和多声道之分,对于单声道文件很好理解,声音数据就是按序排放。

        而如果是立体声(2声道)文件,那么左右声道的声音数据到底是怎么排放的呢?下面以一个示例立体声文件数据(仅分析前72bytes)进行解释:

offset(h)
00000000: 52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20
00000010: 10 00 00 00 01 00 02 00 22 56 00 00 88 58 01 00
00000020: 04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00
00000030: 24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6
00000040: 3c f2 24 f2 11 ce 1a 0d

        下图是这个72bytes数据解析图,从图中可以看到,左右声道的声音数据是按块(nBlockAlign指定)交替排放的。

四、WAVE文件实例分析

  WAVE文件格式我们都了解透彻了,下面我们尝试分析一个经典的WAVE文件:"Windows XP 启动.wav",让我们直接用二进制编辑器HxD打开它:

  按wave_head_t解析WAVE头可知,这段wave是44.1kHz/16bit双声道线性PCM码音频,实际音频数据总长度为1361076bytes(1361076(datasize)/176400(nAvgBytesPerSec)=7.7158秒),最后再用Adobe Audition(原Cool Edit)打开查看其波形图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值