如何检测WAV文件是否具有44或46字节的文件头?

19

我发现假设所有PCM wav音频文件在样本开始前都有44个字节的头数据是危险的。虽然这很常见,但许多应用程序(例如ffmpeg)将生成带有46个字节的头的wav文件。在处理时忽略这个事实会导致文件损坏且无法读取。但是,如何检测头实际上有多长呢?

显然有一种方法可以解决这个问题,但我搜索并没有找到太多关于此的讨论。许多音频项目都假设44或46(根据作者自己的上下文而定)。


5
我有很多WAV文件,其中数据的起始位置可能完全不同:也许距离文件开头数百个字节,谁知道呢? WAV块头实际上很容易解析,您没有解析它们的借口。 - Dietrich Epp
3
译文:确实,解析标头没有任何借口,但是有很多关于如何解析标头的错误信息。在Google上搜索“wav解析器”,很多排名靠前的内容都假设标头长度为44个字节,而且没有进行讨论。Stack Overflow只提供了一些指示,但并未详细说明。我试图引起人们对这个问题的关注,以便下一个遇到困惑的人来查找时能够得到更好的帮助。 - Matt J.
我一直觉得斯坦福大学计算机音乐与声学研究中心的WAVE PCM音频文件格式页面是这类事情的有用资源。 - Sheridan
4个回答

32
您应该检查所有的头数据以查看实际大小。广播波格式文件将包含一个更大的扩展子块。来自Pro Tools的WAV和AIFF文件具有更多未记录的扩展块以及音频后面的数据。如果您想确切地知道采样数据的开始和结束位置,您需要实际查找数据块(对于WAV文件为“data”,对于AIFF文件为“SSND”)。
作为回顾,所有WAV子块都符合以下格式:
Subchunk描述符(4字节) Subchunk大小(4字节整数,小端) Subchunk数据(大小为Subchunk大小)
这很容易处理。您只需要读取描述符,如果不是您要查找的描述符,则读取数据大小并跳到下一个。一个简单的Java例程如下:
//
// Quick note for people who don't know Java well:
// 'in.read(...)' returns -1 when the stream reaches
// the end of the file, so 'if (in.read(...) < 0)'
// is checking for the end of file.
//
public static void printWaveDescriptors(File file)
        throws IOException {
    try (FileInputStream in = new FileInputStream(file)) {
        byte[] bytes = new byte[4];

        // Read first 4 bytes.
        // (Should be RIFF descriptor.)
        if (in.read(bytes) < 0) {
            return;
        }

        printDescriptor(bytes);

        // First subchunk will always be at byte 12.
        // (There is no other dependable constant.)
        in.skip(8);

        for (;;) {
            // Read each chunk descriptor.
            if (in.read(bytes) < 0) {
                break;
            }

            printDescriptor(bytes);

            // Read chunk length.
            if (in.read(bytes) < 0) {
                break;
            }

            // Skip the length of this chunk.
            // Next bytes should be another descriptor or EOF.
            int length = (
                  Byte.toUnsignedInt(bytes[0])
                | Byte.toUnsignedInt(bytes[1]) << 8
                | Byte.toUnsignedInt(bytes[2]) << 16
                | Byte.toUnsignedInt(bytes[3]) << 24
            );
            in.skip(Integer.toUnsignedLong(length));
        }

        System.out.println("End of file.");
    }
}

private static void printDescriptor(byte[] bytes)
        throws IOException {
    String desc = new String(bytes, "US-ASCII");
    System.out.println("Found '" + desc + "' descriptor.");
}

例如,这里有一个随机的WAV文件:

找到'RIFF'描述符。
找到'bext'描述符。
找到'fmt '描述符。
找到'minf'描述符。
找到'elm1'描述符。
找到'data'描述符。
找到'regn'描述符。
找到'ovwf'描述符。
找到'umid'描述符。
文件结束。

值得注意的是,这里的'fmt '和'data'都合法地出现在其他块之间,因为Microsoft的RIFF规范指出子块可以以任何顺序出现。甚至我知道一些主要的音频系统也会犯这个错误,没有考虑到这一点。

因此,如果你想查找某个特定的块,请循环检查每个描述符,直到找到你要找的那个。


Radiodef,感谢您的评论! 我以前从未使用过位移,并且在网络上找不到实际用例。您能否解释一下这个表达式的作用以及为什么要在此处使用位移? <code> (bytes [0]&0xFF) |(bytes [1]&0xFF)<< 8 |(bytes [2]&0xFF)<< 16 |(bytes [3]&0xFF)<< 24 </ code> 提前致谢! - Roman M
它将4个字节转换为32位整数。例如,32位整数00000000000000001000000010000001(十进制32897)可以分成四个字节,00000000000000001000000010000001。使用位移的代码接受字节并从中创建32位整数,通过将每个字节移到适当的位置,然后使用按位OR结合它们。&0xFF部分是Java特定的,并在这里解释 - Radiodef

13

关键是查看"Subchunk1Size",它是在头文件的第16个字节开始的4个字节整数。在一个普通的44字节wav文件中,这个整数将是16 [10, 0, 0, 0]。如果是46字节的头文件,则该整数将是18 [12, 0, 0, 0],如果有额外的可扩展元数据(罕见情况),则可能会更高。

额外的数据本身(如果存在)从第36个字节开始。

因此,用于检测头文件长度的简单C#程序如下:

static void Main(string[] args)
{
    byte[] bytes = new byte[4];
    FileStream fileStream = new FileStream(args[0], FileMode.Open, FileAccess.Read);
    fileStream.Seek(16, 0);
    fileStream.Read(bytes, 0, 4);
    fileStream.Close();
    int Subchunk1Size = BitConverter.ToInt32(bytes, 0);

    if (Subchunk1Size < 16)
        Console.WriteLine("This is not a valid wav file");
    else
        switch (Subchunk1Size)
        {
            case 16:
                Console.WriteLine("44-byte header");
                break;
            case 18:
                Console.WriteLine("46-byte header");
                break;
            default:
                Console.WriteLine("Header contains extra data and is larger than 46 bytes");
                break;
        }
}

4
除了Radiodef的优秀回复外,我想补充3件不太明显的事情。
  1. WAV文件的唯一规则是FMT块在DATA块之前。除此之外,在DATA块之前和之后会找到你不知道的块。必须读取每个块的标头以跳过并查找下一个块。

  2. FMT块通常是16字节和18字节变体,但规范实际上也允许超过18字节。 如果FMT块的标头大小字段大于16,则第17和18字节还指定了有多少额外字节,因此如果它们都为零,则会得到与16字节相同的18字节FMT块。 只需要读取FMT块的前16个字节并解析它们就可以了,忽略其他内容。 为什么这很重要? - 现在已经不太重要了,但Windows XP的媒体播放器可以播放16位WAV文件,但仅当FMT块是Extended(18+ byte)版本时才能播放24位WAV文件。曾经有很多投诉说“Windows无法播放我的24位WAV文件”,但如果它有18个字节的FMT块,它就可以... Microsoft在Windows 7早期的某个时候修复了这个问题,所以现在16字节FMT文件中的24位可以正常工作。

  3. (新添加)块大小经常出现奇数大小。主要在制作24位单声道文件时出现。规范不清楚,但块大小指定实际数据长度(奇数值),并在块后和下一个块的开始之前添加填充字节(零)。因此,块始终从偶数边界开始,但块大小本身存储为实际奇数值。


0
我想要添加一些我发现的“fmt”块中的复杂性的摘要。如果你处理WAV文件,了解如何解析这个块是一个很好的主意,而且你应该准备好应对一些怪癖。
首先,这是RIFF文件中块的一般格式,比如.wav文件:
typedef struct _wav_chunk {
    char      id[4];          // 4-byte ID
    uint32_t  sz;             // Size of chunk data (excl. header)
  //char     *data;           // Chunk data, of length 'sz'
  //char      pad;            // Padding byte
} wav_chunk;

块紧随其后。4个字节的可打印ASCII字符,如果需要的话,在右侧用空格填充,而且只能在右侧填充。32位大小字段,按小端序排列。然后是块的任意数据,长度为'sz'字节。最小可能的块长度为8字节,其中sz == 0。
下一个块将紧随其后,尽管需要注意的是,块应始终对齐到16位边界。因此,如果块的大小是奇数,必须在末尾填充一个字节。但是,预计会违反这个规则。我会在紧接的下一个字节和再下一个字节处寻找块,以防万一。您可能需要使用一些启发式方法,确保ID全部为ASCII字符,不以空格或0x00开头,并且后面跟着四个看起来像32位整数的字节 -- 第一个字段要么非零,要么所有四个字段都是零。就是这样的事情。

块中也可以包含块。在这种情况下,“父”块将具有一个“sz”字段,该字段容纳所有“子”块的总大小,因此即使您不知道它是什么,也可以跳过它,并且它应该是分层的。如果您通过其ID了解该块,您将知道要进入该块,通常是通过在父块的“sz”字段之后找到一个子块ID。

好的,那么让我们来看一下“fmt”块本身的定义:

typedef struct _wav_fmt_chunk {
    uint16_t  samp_format;    // PCM: 0x01, Float: 0x03, Ext: 0xFFFE
    uint16_t  channels;       // 1 or 2, usually
    uint32_t  samp_rate;      // E.g., 44100
    uint32_t  bytes_sec;      // Data rate, bytes/sec
    uint16_t  blocksz;        // Size of one sample, in bytes
    uint16_t  bits_samp;      // Bits per sample (e.g. 8,16,24..)
    uint16_t  extsz;          // Extension size (0 or more bytes)
    uint16_t  valid_bps;      // Valid bits per sample
    uint32_t  chan_mask;      // Channel mapping
    uint16_t  codec;          // Extended format codec (GUID [0:1])
    char      guid[14];       // Remaining 14 byte of GUID
} wav_fmt_chk;

16字节版本只包括前六个字段(bits_samp之后没有内容)。18字节版本包括'extsz'字段,但其值为0。可扩展格式中,extsz字段的值为22,该值用于使用此结构中的其余字段。这描述了更复杂的采样格式和编解码器(例如MP3帧或旧的ADPCM格式等)。
通常,大小字段是冗余的,并且应该等于描述采样格式的字段的乘积,如下所示:
// Round bits_samp up to a whole number of bytes
bytes_per_sample = (fmt.bits_samp / 8);

if (fmt.bits_samp % 8) {
    bytes_per_sample++;
}

// Blocksz -- size of one sample, in bytes
fmt.blocksz = fmt.channels * bytes_per_sample;

// Bytes_sec -- data rate in bytes/sec
fmt.bytes_sec = fmt.blocksz * fmt.samp_rate;

我马上会处理其他字段。
注意,bits_samp字段可能不是字节数的整数倍,比如18位和20位的样本。在这种情况下,你必须向上取整到最接近的整数倍字节数。
一个标准的16位、44.1kHz、立体声PCM波形文件可以只使用16字节的简单fmt块。规范建议或要求扩展版本(至少18字节)用于超过16位的位深度,用于任何非PCM样本格式,包括浮点数,当使用高于2(立体声)的通道数或通道映射不是"mono"或"stereo"时,以及当样本被填充时,例如将24位样本存储在32位块中。
现在,有很多文件违反了这些规则,各种实现对它们的处理结果也各不相同!
例如,在一个简单的头部中,看到24位PCM或浮点样本是非常常见的,但从技术上讲是不正确的。一些旧的音频编辑器(如Cool Edit)会在32位块中使用24位样本,但由于没有办法描述这种打包方式(这是在扩展的fmt块设计之前),它只会将bits_samp设置为24。你需要将blocksz字段与计算得出的块对齐进行比较,以查看差异。
这个最后的例子特别重要,因为修订后的规范规定,bits_samp始终是用于存储一个样本的实际位数,即使并非所有这些位都被使用。因此,添加了valid_bps,进一步说明了有多少位是有效的。(填充位应始终位于最低有效位。)
这是微妙而重要的一点:如果你处理的文件有一个18字节(或更大)的fmt块,bits_samp应准确地描述一个样本块的大小,如果需要,使用valid_bps来传达有多少位是有效的。但是,如果你处理的是一个16字节的fmt块,你应该准备将bits_samp解释为更像是valid_bps,并从(blocksz / channels)计算出真实的样本大小。
chan_mask字段是一个位图,描述了交错采样应该路由到哪些已知的扬声器位置。采样应该按照位图的顺序进行路由。如果chan_mask == 0,则将单声道文件路由到L+R,立体声文件路由到L、R,多声道文件路由到音频设备的输出,顺序由音频驱动程序定义(ch1到output1,ch2到output2,依此类推)。
codec字段是format字段的一种重复,当format == 0xFFFE(可扩展)时使用。实际上,该字段是16字节GUID的前两个字节,因此另一种有效的解释是删除该字段,并将GUID变为一个16字节的字符数组。
guid字段包含剩余的14个字节,对于众所周知的编解码器来说,通常为0x00 0x00 0x00 0x00 0x00 0x10 0x00 0x80 0x00 0x00 0xaa 0x00 0x38 0x9b 0x71。
可能会有诱惑只做最低限度的解释fmt块,但如果你这样做,你应该非常小心,只有在格式为PCM,fmt块大小为16或18时才继续,并验证内容看起来合理。
即使不考虑各种非PCM格式(这是另一个问题),正确地完成这个任务也可能会变得非常混乱。老实说,这总是有点碰运气。最初,规范并不够灵活,导致一些“创造性”的解决方案在实际应用中变得常见。你遇到的大多数文件应该很简单,即使规范仍然存在令人惊讶的歧义,你也不应该在解释基本块时遇到太多麻烦。不过,明智的做法肯定是运行一些合理性检查(自己计算blocksz和bytes_sec,并确保它们与头部中的值相等),以验证文件的合规性以及你的代码的正确性。
现在,还要考虑到实际上可以将样本数据存储在多个数据块中,并使用播放列表来对它们进行排序(以及一些静音块 - 技术上应该重复上一个有效的样本值,而不一定是0)。还有64位扩展来克服4GB的限制。我是否提到过,甚至还有一种用于大端编码的RIFX格式...?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接