解包单通道波形数据并将其存储在数组中

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('&lt;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 个字节,并确保它是RIFFRIFX 标记,否则它不是一个有效的 RIFF 文件。 RIFFRIFX 之间的区别在于前者使用小端编码(并且到处都支持),而后者使用大端编码(几乎没有人支持它)。为简单起见,假设我们只处理 little-endian RIFF 文件。

接下来,您读取根元素长度(以文件字节顺序)和以下文件类型。如果文件类型不是WAVE,则它不是 WAV 文件,因此您可能会放弃进一步处理。读取根元素后,开始读取所有子元素并处理您感兴趣的。

阅读fmt 标头非常简单,您实际上已经在代码中完成了。

数据样本通常表示为 1、2、3 或 4 个字节(同样,在文件字节序中)。最常见的格式是所谓的s16_le(您可能在一些音频处理实用程序如 ffmpeg 中看到过这样的命名),这意味着样本以小端序的有符号 16 位整数呈现。其他可能的格式是u8(8 位样本是无符号数字!)、s24_les32_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文件

我如何使用具有 2 个以上通道的 NAudio 录制到波形文件?

Mongoose - 数组中的 Discord 保存通道

将多通道 PyAudio 转换为 NumPy 数组

如何在java中将Wav文件拆分为通道?

分别对立体声信号的两个通道应用 FFT?