欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

wave文件格式详解

程序员文章站 2022-07-15 23:46:16
...

目录

 

第一节 wav格式scheme介绍

第二节 真实wav文件分析

第三节 python读取wav文件


第一节 wav格式scheme介绍

wav格式,是微软开发的一种文件格式规范,整个文件分为两部分。第一部分是“总文件头”,就包括两个信息,chunkID,其值为“RIFF”,占四个字节;ChunkSize,其值是整个wav文件除去chunkID和ChunkSize,后面所有文件大小的字节数,占四个字节。

第二部分是Format,其值为“wave”,占四个字节。它包括两个子chunk,分别是“fmt ”和“data”。在fmt子chunk中定义了该文件格式的参数信息,对于音频而言,包括:采样率、通道数、位宽、编码等等;data部分是“数据块”,即一帧一帧的二进制数据,对于音频而言,就是原始的PCM数据。

wave文件格式详解

图一 wav格式文件示意图

表一 wav格式字段说明

Offset

Size

Name

Description

0

4

ChunkID

ASCII码“0x52494646”对应字母“RIFF

4

4

ChunkSize

块大小是指除去ChunkIDChunkSize的剩余部分有多少字节数据。注意:小尾字节序数。 

8

4

Format

ASCII码“0x57415645”对应字母“WAVE”。该块由两个子块组成,一个“fmt”块用于详细说明数据格式,一个“data”块包含实际的样本数据。

12

4

Subchunk1ID

ASCII码“0x666d7420”对应字母“fmt ”。

16

4

Subchunk1Size

如果文件采用PCM编码,则该子块剩余字节数为16

20

2

AudioFormat

如果文件采用PCM编码(线性量化),则AudioFormat=1AudioFormat代表不同的压缩方式,表二说明了相应的压缩方式。

22

2

NumChannels

声道数,单声道(Mono)为1,双声道(Stereo)为2

24

4

SampleRate

取样率,例:44.1kHz48kHz

28

4

ByteRate

传输速率,单位:Byte/s

32

2

BlockAlign

一个样点(包含所有声道)的字节数。

34

2

BitsPerSample

每个样点对应的位数。

 

2

ExtraParamSize

如果采用PCM编码,该值不存在。

 

X

ExtraParams

用于存储其他参数。如果采用PCM编码,该值不存在。

36

4

Subchunk2ID

ASCII码“0x64617461”对应字母 data”。

40

4

Subchunk2Size

实际样本数据的大小(单位:字节)。

44

*

Data

实际的音频数据

 

表二 wave支持格式

AudioFormat

Description

0 (0x0000)

Unknown

1 (0x0001)

PCM/uncompressed

2 (0x0002)

Microsoft ADPCM

6 (0x0006)

ITU G.711 a-law

7 (0x0007)

ITU G.711 µ-law

17 (0x0011)

IMA ADPCM

20 (0x0016)

ITU G.723 ADPCM (Yamaha)

49 (0x0031)

ITU G.721 ADPCM

80 (0x0050)

MPEG

65,536 (0xFFFF)

Experimental

第二节 真实wav文件分析

实例分析:

下面notepad++打开一个wav的文件,看看里面十六进制内容。开始四个字节从小到大分别是0x52H、0x49H、0x46H、0x46H,分别对应ASCII的R、I、F、F字符。

wave文件格式详解

根据第一节的格式介绍,其后四个字节是整个文件除去ChunkID、ChunkSize的大小,这里是00003224H,十进制为12836。从偏移字节地址8开始的四个字节为Format字段,内容为0x57415645,其对应的就是ASCII字符W、A、V、E。

wave文件格式详解

0c~0f字节内容表示subchunk1ID的值“fmt(空格)”。

10H~13H值为ox00000010,十进制为16,表明该字块剩余占空间大小,14H-15H值为0001H,表明当前编码PCM。

wave文件格式详解

16H-17H,表明通道数,为1;18H-19H表明采样率,0x00001f40,十进制为8000。紧接着四个字节表明字节率,每个采样点占两个字节,那么该值为ox00003e80,十进制为16000。20H-21H对应BlockAlign,表示每个样本点占字节数0x0002,两个字节;22H-23H表明每个样本点占bit为数,0x0010,16个,与前一个值两个字节对应上。该音频文件是PCM格式,那么fmt子chunk在10H-13H为16,表明到该头字段结束处的剩余字节数为16,从14H到23H(包括23H字节)正好是16个字节。

wave文件格式详解

24H-27H的值为ASCII字符d、a、t、a。其后四个字节为小端方式存储的数据“0x00003200”,表明音频数据段的大小为0x00003200,十进制为12800。从2c的a8H到322b的00H结束,正好是3200H个字节。

wave文件格式详解

再看一个双声道的实例。下图是一个双声道的音频文件。

wave文件格式详解

16H-17H,表明通道数,为2;18H-19H表明采样率,0x0000ac44,十进制为44100,表明当前wav录音文件的采样率是44.1K。紧接着四个字节表明字节率,每个采样点占两个字节,且是双声道,那么该值为ox0002b110,十进制为176400(44100*4)。20H-21H对应BlockAlign,表示每个样本点占字节数0x0004,4个字节,它是每个采样点占2个字节*双声道;22H-23H表明每个采样点占bit位数,0x0010,16位。

注释:采样点占bit位:是对波形进行离散采样,表示深度分8位、16位、24位等,不考虑声道数目;样本点占字节数得考虑声道数,双声道是单声道的2倍。

第三节 python读取wav文件

读取上面给的第一个示例wav文件

# coding: utf-8

import wave
from typing import Any, Dict
import numpy as np

nchannels = 0
sampwidth = 0
framerate = 0

def _WriteWav(fp: str, data: Dict) -> None:
    # 打开WAV文档
    f = wave.open(fp, "wb")
    # 配置声道数、量化位数和取样频率
    f.setnchannels(nchannels)
    f.setsampwidth(sampwidth)
    f.setframerate(framerate)
    # 将wav_data转换为二进制数据写入文件
    f.writeframes(data)
    f.close()

def _ReadWave(wav_path: str) -> None:
    global nchannels, sampwidth, framerate
    f = wave.open(wav_path, 'rb')
    params = f.getparams()
    nchannels, sampwidth, framerate, nframes = params[:4]
    print("时常:", float(nframes / framerate))
    print("channels:", nchannels)
    print("sampewidth:", sampwidth)
    print("framerate:", framerate)
    print("framenumber:", nframes)
    strData = f.readframes(nframes)  # 读取音频,字符串格式
    print("len of strdata: ", len(strData))
    waveData = np.fromstring(strData, dtype=np.int16)#将字符串转化为int

if __name__ == "__main__":
    filename = "data/20200423152348/0.wav"
    _ReadWave(filename)

Import wave包,通过open函数读取wav文件。

输出结果:

时常: 0.8
channels: 1
sampewidth: 2
framerate: 8000
framenumber: 6400
len of strdata:  12800
WAVE_FORMAT_PCM = 0x0001

_array_fmts = None, 'b', 'h', None, 'i'

import audioop
import struct
import sys
from chunk import Chunk
from collections import namedtuple


class Wave_read:
  

class Wave_write:
    

def open(f, mode=None):
   

wave包主要包含三部分,函数open、类Wave_read、类Wave_write。

其中open的代码如下:

def open(f, mode=None):
    if mode is None:
        if hasattr(f, 'mode'):
            mode = f.mode
        else:
            mode = 'rb'
    if mode in ('r', 'rb'):
        return Wave_read(f)
    elif mode in ('w', 'wb'):
        return Wave_write(f)
    else:
        raise Error("mode must be 'r', 'rb', 'w', or 'wb'")

最终返回write或者read的对象,根据mode参数来选择。所以在_ReadWave函数中f = wave.open(path, ‘rb’),表明f是一个Wave_read对象。对wave文件的读取主要在Wave_read中。下面看看该类的主要代码。

class Wave_read:   

    def initfp(self, file):
        self._convert = None
        self._soundpos = 0
        self._file = Chunk(file, bigendian = 0)
        if self._file.getname() != b'RIFF':
            raise Error('file does not start with RIFF id')
        if self._file.read(4) != b'WAVE':
            raise Error('not a WAVE file')
        self._fmt_chunk_read = 0
        self._data_chunk = None
        while 1:
            self._data_seek_needed = 1
            try:
                chunk = Chunk(self._file, bigendian = 0)
            except EOFError:
                break
            chunkname = chunk.getname()
            if chunkname == b'fmt ':
                self._read_fmt_chunk(chunk)
                self._fmt_chunk_read = 1
            elif chunkname == b'data':
                if not self._fmt_chunk_read:
                    raise Error('data chunk before fmt chunk')
                self._data_chunk = chunk
                self._nframes = chunk.chunksize // self._framesize
                self._data_seek_needed = 0
                break
            chunk.skip()
        if not self._fmt_chunk_read or not self._data_chunk:
            raise Error('fmt chunk and/or data chunk missing')


    def readframes(self, nframes):
        if self._data_seek_needed:
            self._data_chunk.seek(0, 0)
            pos = self._soundpos * self._framesize
            if pos:
                self._data_chunk.seek(pos, 0)
            self._data_seek_needed = 0
        if nframes == 0:
            return b''
        data = self._data_chunk.read(nframes * self._framesize)
        if self._sampwidth != 1 and sys.byteorder == 'big':
            data = audioop.byteswap(data, self._sampwidth)
        if self._convert and data:
            data = self._convert(data)
        self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth)
        return data

    def _read_fmt_chunk(self, chunk):
        wFormatTag, self._nchannels, self._framerate, dwAvgBytesPerSec, wBlockAlign = struct.unpack_from('<HHLLH', chunk.read(14))
        if wFormatTag == WAVE_FORMAT_PCM:
            sampwidth = struct.unpack_from('<H', chunk.read(2))[0]
            self._sampwidth = (sampwidth + 7) // 8
        else:
            raise Error('unknown format: %r' % (wFormatTag,))
        self._framesize = self._nchannels * self._sampwidth
        self._comptype = 'NONE'
        self._compname = 'not compressed'



主要包括三个函数,其他细节省略,可以自己去看wave的源码,比较好理解。initfp()函数进行了大部分的格式处理。包括对头文件的解析和数据的提取。通过不断的构建chunk对象把wave格式的文件进行解包。在第一次构建chunk时,读取总的RIFF字符和ChunkSize,然后确认“wave”字符,最后循环读取子chunk,分别是fmt和data,具体可以看下面chunk的代码。主要处理逻辑在__init__( )初始化函数和read( )函数中。

class Chunk:
    def __init__(self, file, align=True, bigendian=True, inclheader=False):
        import struct
        self.closed = False
        self.align = align      # whether to align to word (2-byte) 
        if bigendian:
            strflag = '>'
        else:
            strflag = '<'
        self.file = file
        self.chunkname = file.read(4)
        if len(self.chunkname) < 4:
            raise EOFError
        try:
            self.chunksize = struct.unpack_from(strflag+'L', file.read(4))[0]
        except struct.error:
            raise EOFError
        if inclheader:
            self.chunksize = self.chunksize - 8 # subtract header

    def read(self, size=-1):
        if self.closed:
            raise ValueError("I/O operation on closed file")
        if self.size_read >= self.chunksize:
            return b''
        if size < 0:
            size = self.chunksize - self.size_read
        if size > self.chunksize - self.size_read:
            size = self.chunksize - self.size_read
        data = self.file.read(size)
        self.size_read = self.size_read + len(data)
        if self.size_read == self.chunksize and \
           self.align and \
           (self.chunksize & 1):
            dummy = self.file.read(1)
            self.size_read = self.size_read + len(dummy)
        return data

细心的对着第一节和第二节的内容,来解读源码会比较容易理解,对wave文件格式以及python的wave包代码逻辑也有非常清晰的了解。