发布时间:2025-12-10 11:48:01 浏览次数:9
目录
第一节 wav格式scheme介绍
第二节 真实wav文件分析
第三节 python读取wav文件
wav格式,是微软开发的一种文件格式规范,整个文件分为两部分。第一部分是“总文件头”,就包括两个信息,chunkID,其值为“RIFF”,占四个字节;ChunkSize,其值是整个wav文件除去chunkID和ChunkSize,后面所有文件大小的字节数,占四个字节。
第二部分是Format,其值为“wave”,占四个字节。它包括两个子chunk,分别是“fmt ”和“data”。在fmt子chunk中定义了该文件格式的参数信息,对于音频而言,包括:采样率、通道数、位宽、编码等等;data部分是“数据块”,即一帧一帧的二进制数据,对于音频而言,就是原始的PCM数据。
图一 wav格式文件示意图
表一 wav格式字段说明
| Offset | Size | Name | Description |
| 0 | 4 | ChunkID | ASCII码“0x52494646”对应字母“RIFF” |
| 4 | 4 | ChunkSize | 块大小是指除去ChunkID与ChunkSize的剩余部分有多少字节数据。注意:小尾字节序数。 |
| 8 | 4 | Format | ASCII码“0x57415645”对应字母“WAVE”。该块由两个子块组成,一个“fmt”块用于详细说明数据格式,一个“data”块包含实际的样本数据。 |
| 12 | 4 | Subchunk1ID | ASCII码“0x666d7420”对应字母“fmt ”。 |
| 16 | 4 | Subchunk1Size | 如果文件采用PCM编码,则该子块剩余字节数为16。 |
| 20 | 2 | AudioFormat | 如果文件采用PCM编码(线性量化),则AudioFormat=1。AudioFormat代表不同的压缩方式,表二说明了相应的压缩方式。 |
| 22 | 2 | NumChannels | 声道数,单声道(Mono)为1,双声道(Stereo)为2。 |
| 24 | 4 | SampleRate | 取样率,例:44.1kHz,48kHz。 |
| 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 |
实例分析:
下面notepad++打开一个wav的文件,看看里面十六进制内容。开始四个字节从小到大分别是0x52H、0x49H、0x46H、0x46H,分别对应ASCII的R、I、F、F字符。
根据第一节的格式介绍,其后四个字节是整个文件除去ChunkID、ChunkSize的大小,这里是00003224H,十进制为12836。从偏移字节地址8开始的四个字节为Format字段,内容为0x57415645,其对应的就是ASCII字符W、A、V、E。
0c~0f字节内容表示subchunk1ID的值“fmt(空格)”。
10H~13H值为ox00000010,十进制为16,表明该字块剩余占空间大小,14H-15H值为0001H,表明当前编码PCM。
16H-17H,表明通道数,为1;18H-1bH表明采样率,0x00001f40,十进制为8000。紧接着四个字节表明字节率,每个采样点占两个字节,那么该值为0x00003e80,十进制为16000。20H-21H对应BlockAlign,表示每个样本点占字节数0x0002,两个字节;22H-23H表明每个样本点占bit为数,0x0010,16个,与前一个值两个字节对应上。该音频文件是PCM格式,那么fmt子chunk在10H-13H为16,表明到该头字段结束处的剩余字节数为16,从14H到23H(包括23H字节)正好是16个字节。
24H-27H的值为ASCII字符d、a、t、a。其后四个字节为小端方式存储的数据“0x00003200”,表明音频数据段的大小为0x00003200,十进制为12800。从2c的a8H到322b的00H结束,正好是3200H个字节。
再看一个双声道的实例。下图是一个双声道的音频文件。
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倍。
读取上面给的第一个示例wav文件
# coding: utf-8import wavefrom typing import Any, Dictimport numpy as npnchannels = 0sampwidth = 0framerate = 0def _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, frameratef = 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)#将字符串转化为intif __name__ == "__main__":filename = "data/20200423152348/0.wav"_ReadWave(filename)Import wave包,通过open函数读取wav文件。
输出结果:
时常: 0.8channels: 1sampewidth: 2framerate: 8000framenumber: 6400len of strdata: 12800 WAVE_FORMAT_PCM = 0x0001_array_fmts = None, 'b', 'h', None, 'i'import audioopimport structimport sysfrom chunk import Chunkfrom collections import namedtupleclass 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.modeelse: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 = Noneself._soundpos = 0self._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 = 0self._data_chunk = Nonewhile 1:self._data_seek_needed = 1try:chunk = Chunk(self._file, bigendian = 0)except EOFError:breakchunkname = chunk.getname()if chunkname == b'fmt ':self._read_fmt_chunk(chunk)self._fmt_chunk_read = 1elif chunkname == b'data':if not self._fmt_chunk_read:raise Error('data chunk before fmt chunk')self._data_chunk = chunkself._nframes = chunk.chunksize // self._framesizeself._data_seek_needed = 0breakchunk.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._framesizeif pos:self._data_chunk.seek(pos, 0)self._data_seek_needed = 0if 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 datadef _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) // 8else:raise Error('unknown format: %r' % (wFormatTag,))self._framesize = self._nchannels * self._sampwidthself._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 structself.closed = Falseself.align = align # whether to align to word (2-byte) if bigendian:strflag = '>'else:strflag = '<'self.file = fileself.chunkname = file.read(4)if len(self.chunkname) < 4:raise EOFErrortry:self.chunksize = struct.unpack_from(strflag+'L', file.read(4))[0]except struct.error:raise EOFErrorif inclheader:self.chunksize = self.chunksize - 8 # subtract headerdef 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_readif size > self.chunksize - self.size_read:size = self.chunksize - self.size_readdata = 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包代码逻辑也有非常清晰的了解。