PCM 和 WAV 数据结构

  • 采样率
  • 数字音频常用的采样率
  • 位深度
  • PCM 音频数据
  • PCM 音频数据的存储
  • PCM 音频数据的参数
  • PCM 音频数据的处理
  • 降低某个声道的音量[^1]
  • PCM → WAV
  • WAV 格式定义
  • WAV 文件头解析
  • PCM → WAV 代码[^1]



采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。

 

wav采样率调整 python_采样率


A. 使原始声波扭曲的低采样率

B. 完全重现原始声波的高采样率

数字音频常用的采样率

采样率

品质级别

频率范围

11,025 Hz

较差的 AM 电台(低端多媒体)

0–5,512 Hz

22,050 Hz

接近 FM 电台(高端多媒体)

0–11,025 Hz

32,000 Hz

好于 FM 电台(标准广播采样率)

0–16,000 Hz

44,100 Hz

CD

0–22,050 Hz

48,000 Hz

标准 DVD

0–24,000 Hz

96,000 Hz

蓝光 DVD

0–48,000 Hz

位深度

位深度决定动态范围。采样声波时,为每个采样指定最接近原始声波振幅的振幅值。较高的位深度可提供更多可能的振幅值,产生更大的动态范围、更低的噪声基准和更高的保真度。

位深度

品质级别

振幅值

动态范围

8 位

电话

256

48 dB

16 位

音频 CD

65,536

96 dB

24 位

音频 DVD

16,777,216

144 dB

32 位

最佳

4,294,967,296

192 dB

位深度越高,提供的动态范围越大。

PCM 音频数据

PCM (Pulse Code Modulation) 也被称为脉冲编码调制。PCM 音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。

wav采样率调整 python_位深度_02

PCM 音频数据的存储

如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(有的时候也会采用 LRLRLR 方式存储,只是另一个声道的数据为 0),如果是双声道的话通常按照 LRLRLR 的方式存储,存储的时候还和机器的大小端有关。大端模式如下图所示:

wav采样率调整 python_采样率_03


PCM 音频数据是未经压缩的数据,所以通常都比较大,常见的 MP3 格式都是经过压缩的,128Kbps 的 MP3 压缩率可以达到 1:11

PCM 音频数据的参数

一般我们描述 PCM 音频数据的参数的时候有如下描述方式:

44100HZ 16bit stereo: 每秒钟有 44100 次采样, 采样数据用 16 位(2 字节)记录, 双声道(立体声)
22050HZ 8bit  mono: 每秒钟有 22050 次采样, 采样数据用 8 位(1 字节)记录, 单声道
48000HZ 32bit 51ch: 每秒钟有 48000 次采样, 采样数据用 32 位(4 字节浮点型)记录, 5.1 声道

44100Hz 指的是采样率,它的意思是每秒取样 44100 次。采样率越大,存储数字音频所占的空间就越大。

16bit 指的是采样精度,意思是原始模拟信号被采样后,每一个采样点在计算机中用 16 位(两个字节)来表示。采样精度越高越能精细地表示模拟信号的差异。

Stereo 指的是声道数,也即采样时用到的麦克风的数量,麦克风越多就越能还原真实的采样环境(当然麦克风的放置位置也是有规定的)。

一般来说 PCM 数据中的波形幅值越大,代表音量越大。

PCM 音频数据的处理

降低某个声道的音量1

因为对于 PCM 音频数据而言,它的幅值(即该采样点采样值的大小)代表音量的大小,所以我们可以通过减小某个声道的数据的值来实现降低某个声道的音量。

int pcm16le_half_volume_left( char *url )
{
    FILE *fp_in = fopen( url, "rb+" );
    FILE *fp_out = fopen( "output_half_left.pcm", "wb+" );
    unsigned char *sample = ( unsigned char * )malloc(4); // 一次读取一个sample,因为是2声道,所以是4字节 
    while ( !feof( fp_in ) ){
        fread( sample, 1, 4, fp_in );
        short* sample_num = ( short* )sample; // 转成左右声道两个short数据
        *sample_num = *sample_num / 2; // 左声道数据减半
        fwrite( sample, 1, 2, fp_out ); // L
        fwrite( sample + 2, 1, 2, fp_out ); // R
    }
    free( sample );
    fclose( fp_in );
    fclose( fp_out );
    return 0;
}

从源代码可以看出,本程序在读出左声道的 2 Byte 的取样值之后,将其转成了 C 语言中的一个 short 类型的变量。将该数值除以 2 之后写回到了 PCM 文件中。下图为输入 PCM 双声道音频采样数据的波形图。

wav采样率调整 python_WAV_04


下图为输出的左声道经过处理后的波形图。可以看出左声道的波形幅度降低了一半。

wav采样率调整 python_wav采样率调整 python_05

PCM → WAV

WAV 是 Microsoft 和 IBM 为 PC 开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。WAVE 文件通常只是一个具有单个 “WAVE” 块的 RIFF 文件,该块由两个子块(”fmt” 子数据块和 ”data” 子数据块),它的格式如下图所示:

wav采样率调整 python_wav采样率调整 python_06

WAV 格式定义

该格式的实质就是在 PCM 文件的前面加了一个文件头,每个字段的的含义如下:

typedef struct {
    char          ChunkID[4]; //内容为"RIFF"
    unsigned long ChunkSize;  //存储文件的字节数(不包含ChunkID和ChunkSize这8个字节)
    char          Format[4];  //内容为"WAVE“
} WAVE_HEADER;

typedef struct {
   char           Subchunk1ID[4]; //内容为"fmt"
   unsigned long  Subchunk1Size;  //存储该子块的字节数(不含前面的Subchunk1ID和Subchunk1Size这8个字节)
   unsigned short AudioFormat;    //存储音频文件的编码格式,例如若为PCM则其存储值为1。
   unsigned short NumChannels;    //声道数,单声道(Mono)值为1,双声道(Stereo)值为2,等等
   unsigned long  SampleRate;     //采样率,如8k,44.1k等
   unsigned long  ByteRate;       //每秒存储的bit数,其值 = SampleRate * NumChannels * BitsPerSample / 8
   unsigned short BlockAlign;     //块对齐大小,其值 = NumChannels * BitsPerSample / 8
   unsigned short BitsPerSample;  //每个采样点的bit数,一般为8,16,32等。
} WAVE_FMT;

typedef struct {
   char          Subchunk2ID[4]; //内容为“data”
   unsigned long Subchunk2Size;  //接下来的正式的数据部分的字节数,其值 = NumSamples * NumChannels * BitsPerSample / 8
} WAVE_DATA;
WAV 文件头解析

这里是一个 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

字段解析如下图:

wav采样率调整 python_位深度_07

PCM → WAV 代码1
int simplest_pcm16le_to_wave( const char *pcmpath, int channels, int sample_rate, const char *wavepath )
{ // 省去错误判断
    short pcmData;
    FILE* fp = fopen( pcmpath, "rb" );
    FILE* fpout = fopen( wavepath, "wb+" );
    
    // 填充 WAVE_HEADER
    WAVE_HEADER pcmHEADER;
    memcpy( pcmHEADER.ChunkID, "RIFF", strlen( "RIFF" ) );
    memcpy( pcmHEADER.Format, "WAVE", strlen( "WAVE" ) );
    fseek( fpout, sizeof( WAVE_HEADER ), 1 );
    
    //填充 WAVE_FMT 
    WAVE_FMT pcmFMT;
    pcmFMT.SampleRate = sample_rate;
    pcmFMT.ByteRate = sample_rate * sizeof( pcmData );
    pcmFMT.BitsPerSample = 8 * sizeof( pcmData );
    memcpy( pcmFMT.Subchunk1ID, "fmt ", strlen( "fmt " ) );
    pcmFMT.Subchunk1Size = 16;
    pcmFMT.BlockAlign = channels * sizeof( pcmData );
    pcmFMT.NumChannels = channels;
    pcmFMT.AudioFormat = 1;
    fwrite( &pcmFMT, sizeof( WAVE_FMT ), 1, fpout );

    //填充 WAVE_DATA;
    WAVE_DATA pcmDATA;
    memcpy( pcmDATA.Subchunk2ID, "data", strlen( "data" ) );
    pcmDATA.Subchunk2Size = 0;
    fseek( fpout, sizeof( WAVE_DATA ), SEEK_CUR );
    fread( &m_pcmData, sizeof( short ), 1, fp );
    while ( !feof( fp ) ) {
         pcmDATA.dwSize += 2;
         fwrite( &m_pcmData, sizeof( short ), 1, fpout );
         fread( &m_pcmData, sizeof( short ), 1, fp );
    }
    
    int headerSize = sizeof( pcmHEADER.Format ) + sizeof( WAVE_FMT ) + sizeof( WAVE_DATA ); // 36
    pcmHEADER.ChunkSize = headerSize + pcmDATA.Subchunk2Size;

    rewind( fpout );
    fwrite( &pcmHEADER, sizeof( WAVE_HEADER ), 1, fpout );
    fseek( fpout, sizeof( WAVE_FMT ), SEEK_CUR );
    fwrite( &pcmDATA, sizeof( WAVE_DATA ), 1, fpout );
    fclose( fp );
    fclose( fpout );
    return 0;
}