wave文件格式详解
目录
第一节 wav格式scheme介绍
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 |
第二节 真实wav文件分析
实例分析:
下面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-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个字节。
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倍。
第三节 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包代码逻辑也有非常清晰的了解。
下一篇: C 求平均值