解包单通道波形数据并将其存储在数组中
Posted
技术标签:
【中文标题】解包单通道波形数据并将其存储在数组中【英文标题】:Unpacking mono channel wave data and storing it in an array 【发布时间】:2014-12-09 01:49:43 【问题描述】:我正在尝试使用 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']]))
【问题讨论】:
您读取的值有误。24932
实际上是0x6164
,也就是ad
,是data
签名的前半部分(反向字节序)。数据块跟随 WAV 标头,具有 4 字节签名 (data
) 和 4 字节长度(以字节为单位),后跟实际样本的数据。所以在读取样本之前需要验证数据块签名和读取数据长度。
知道实际数据偏移量和长度后,以BlockAlign
字节为单位读取它,将每个块解压缩为一个元组(具有NumChannels
元素,每个BitsPerSample
位),然后做任何你想做的事。
我根据您所说的尝试编辑了我的问题。它只是给了我一个包含许多24932
值的数组。我读错了吗?你能扩展一下你的答案吗?
【参考方案1】:
好的,我会尝试写一个完整的演练。
首先,将 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 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
流。
希望这会有所帮助。
【讨论】:
帮助很大。一个问题是最后一行samples = struct.unpack ...
应该添加到样本中,这会覆盖第一个索引。
samples
这是循环内的一个局部变量,下面的评论说明你可以用它做任何你想做的事情。例如,您可以在循环之前声明一个列表:stream = list()
,然后在循环中将样本附加到它:stream.append(samples)
(或stream.append(samples[0])
,如果您只需要一个通道)。
啊,我明白了。再次感谢!
我是否使用struct.pack()
来保存我编辑的波形文件?以上是关于解包单通道波形数据并将其存储在数组中的主要内容,如果未能解决你的问题,请参考以下文章
QT5.14串口调试助手:上位机接收数据解析数据帧+多通道波形显示+数据保存为csv文件