我正在尝试使用struct.unpack
从单个通道WAVE文件中解压缩数据。我想将数据存储在一个数组中并能够操作它(比如通过添加给定方差的噪声)。我已经提取了标题数据并将其存储在字典中,如下所示:
stHeaderFields['ChunkSize'] = struct.unpack('<L', bufHeader[4:8])[0]
stHeaderFields['Format'] = bufHeader[8:12]
stHeaderFields['Subchunk1Size'] = struct.unpack('<L', bufHeader[16:20])[0]
stHeaderFields['AudioFormat'] = struct.unpack('<H', bufHeader[20:22])[0]
stHeaderFields['NumChannels'] = struct.unpack('<H', bufHeader[22:24])[0]
stHeaderFields['SampleRate'] = struct.unpack('<L', bufHeader[24:28])[0]
stHeaderFields['ByteRate'] = struct.unpack('<L', bufHeader[28:32])[0]
stHeaderFields['BlockAlign'] = struct.unpack('<H', bufHeader[32:34])[0]
stHeaderFields['BitsPerSample'] = struct.unpack('<H', bufHeader[34:36])[0]
当我传入文件时,我得到以下输出:
NumChannels: 1
ChunkSize: 78476
BloackAlign: 0
Filename: foo.wav
ByteRate: 32000
BlockAlign: 2
AudioFormat: 1
SampleRate: 16000
BitsPerSample: 16
Format: WAVE
Subchunk1Size: 16
然后我尝试通过执行struct.unpack('<h', self.bufHeader[36:])[0]
来获取数据,但这样做会返回一个简单的整数值24932
。我不允许使用wave
库或其他任何与波有关的事情,因为我必须将其适应其他类型的信号。如何存储和操作实际波形数据?
编辑:
while chunk_reader < stHeaderFields['ChunkSize']:
data.append(struct.unpack('<H', bufHeader[chunk_reader:chunk_reader+stHeaderFields['BlockAlign']]))
答案 0 :(得分:2)
好的,我会尝试写一个完整的演练。
首先,将WAV(或更可能是RIFF)文件视为线性结构是一个常见的错误。它实际上是一棵树,每个元素都有一个4字节的标签,4字节长的数据和/或子元素,以及里面的某种数据。
WAV文件通常只有两个子元素('fmt'和'data'),但它也可能包含一些子元素('INAM','IART', 'ICMT'等)或其他一些要素。此外,对于块没有实际的订单要求,因此认为“数据”遵循“fmt”是不正确的,因为元数据可能会介于两者之间。
让我们看一下RIFF文件:
'RIFF'
|-- file type ('WAVE')
|-- 'fmt '
| |-- AudioFormat
| |-- NumChannels
| |-- ...
| L_ BitsPerSample
|-- 'LIST' (optional)
| |-- ... (other tags)
| L_ ... (other tags)
L_ 'data'
|-- sample 1 for channel 1
|-- ...
|-- sample 1 for channel N
|-- sample 2 for channel 1
|-- ...
|-- sample 2 for channel N
L_ ...
那么,您应该如何阅读WAV文件?好吧,首先你需要从文件开头读取4个字节,并确保它是RIFF
或RIFX
标记,否则它不是有效的RIFF文件。 RIFF
和RIFX
之间的区别是前者使用little-endian编码(并且在任何地方都受支持),而后者使用big-endian(实际上没有人支持它)。为简单起见,我们假设我们只处理little-endian RIFF文件。
接下来,您将读取根元素长度(以文件字节顺序排列)和以下文件类型。如果文件类型不是WAVE
,则它不是WAV文件,因此您可能放弃进一步处理。阅读完根元素后,您将开始阅读所有子元素并处理您感兴趣的元素。
阅读fmt
标题非常简单,您实际上是在代码中完成的。
数据样本通常表示为1,2,3或4个字节(同样,在文件字节序中)。最常见的格式是所谓的s16_le
(您可能已经在某些音频处理实用程序中看到了这样的命名,如ffmpeg),这意味着样本以小端显示为带符号的16位整数。其他可能的格式是u8
(8位样本是无符号数!),s24_le
,s32_le
。数据样本是交错的,因此即使对于多声道音频,也很容易在流中寻找任意位置。 注意:这仅对未压缩的WAV文件有效,如AudioFormat == 1所示。对于其他格式,数据样本可能有另一种布局。
让我们来看看一个简单的WAV阅读器:
stHeaderFields = dict()
rawData = None
with open("file.wav", "rb") as f:
riffTag = f.read(4)
if riffTag != 'RIFF':
print 'not a valid RIFF file'
exit(1)
riffLength = struct.unpack('<L', f.read(4))[0]
riffType = f.read(4)
if riffType != 'WAVE':
print 'not a WAV file'
exit(1)
# now read children
while f.tell() < 8 + riffLength:
tag = f.read(4)
length = struct.unpack('<L', f.read(4))[0]
if tag == 'fmt ': # format element
fmtData = f.read(length)
fmt, numChannels, sampleRate, byteRate, blockAlign, bitsPerSample = struct.unpack('<HHLLHH', fmtData)
stHeaderFields['AudioFormat'] = fmt
stHeaderFields['NumChannels'] = numChannels
stHeaderFields['SampleRate'] = sampleRate
stHeaderFields['ByteRate'] = byteRate
stHeaderFields['BlockAlign'] = blockAlign
stHeaderFields['BitsPerSample'] = bitsPerSample
elif tag == 'data': # data element
rawData = f.read(length)
else: # some other element, just skip it
f.seek(length, 1)
现在我们知道文件格式信息及其示例数据,因此我们可以解析它。如上所述,样本可能有任何大小,但现在让我们假设我们只处理16位样本:
blockAlign = stHeaderFields['BlockAlign']
numChannels = stHeaderFields['NumChannels']
# some sanity checks
assert(stHeaderFields['BitsPerSample'] == 16)
assert(numChannels * stHeaderFields['BitsPerSample'] == blockAlign * 8)
for offset in range(0, len(rawData), blockAlign):
samples = struct.unpack('<' + 'h' * numChannels, rawData[offset:offset+blockAlign])
# now samples contains a tuple with sample values for each channel
# (in case of mono audio, you'll have a tuple with just one element).
# you may store it in the array for future processing,
# change and immediately write to another stream, whatever.
现在您拥有rawData中的所有示例,您可以根据需要访问和修改它。使用Python的array()来有效地访问和修改数据可能很方便(但是在24位音频的情况下不会这样做,你需要编写自己的序列化和反序列化)。
完成数据处理(可能涉及升级或缩减每个样本的位数,更改通道数,声级操作等)之后,您只需编写一个新的RIFF
标题。数据长度(通常可以使用简化的公式36 + len(rawData)
计算),更改的fmt
标头和data
流。
希望这有帮助。