17

我发现在采样开始之前假设所有 PCM wav 音频文件都有 44 字节的标头数据是很危险的。尽管这很常见,但许多应用程序(例如 ffmpeg)将生成带有 46 字节标头的 wav,并且在处理时忽略这一事实将导致文件损坏且无法读取。但是你怎么能检测到标题实际上有多长呢?

显然有一种方法可以做到这一点,但我搜索并发现对此的讨论很少。根据作者自己的上下文,许多音频项目假设为 44(或相反,46)。

4

3 回答 3

28

您应该检查所有标题数据以查看实际大小。广播波形格式文件将包含更大的扩展子块。Pro Tools 中的 WAV 和 AIFF 文件具有更多未记录的扩展块以及音频之后的数据。如果您想确定样本数据的开始和结束位置,您需要实际查找数据块(WAV 文件的“数据”和 AIFF 的“SSND”)。

作为评论,所有 WAV 子块都符合以下格式:

子块描述符(4 个字节)
    子块大小(4 字节整数,小端序)
    子块数据(大小为子块大小)

这很容易处理。您需要做的就是读取描述符,如果不是您要查找的描述符,请读取数据大小并跳到下一个。执行此操作的简单 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”描述符。
找到“数据”描述符。
找到“regn”描述符。
找到“ovwf”描述符。
找到“umid”描述符。
文件结束。

值得注意的是,这里的“fmt”和“数据”都合法地出现在其他块之间,因为微软的 RIFF 规范说子块可以以任何顺序出现。即使是我所知道的一些主要音频系统也会出现这种错误并且没有考虑到这一点。

因此,如果您想找到某个块,请遍历文件检查每个描述符,直到找到您要查找的那个。

于 2013-11-15T01:00:26.373 回答
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;
        }
}
于 2013-11-15T00:43:15.900 回答
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 块是扩展(18+ 字节)版本时才能播放 24 位 WAV 文件。曾经有很多抱怨说“Windows 不播放我的 24 位 WAV 文件”,但如果它有一个 18 字节的 FMT 块,它会......微软在 Windows 7 早期的某个时候修复了这个问题,

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

于 2016-10-19T02:46:31.193 回答