我发现假设所有PCM wav音频文件在样本开始前都有44个字节的头数据是危险的。虽然这很常见,但许多应用程序(例如ffmpeg)将生成带有46个字节的头的wav文件。在处理时忽略这个事实会导致文件损坏且无法读取。但是,如何检测头实际上有多长呢?
显然有一种方法可以解决这个问题,但我搜索并没有找到太多关于此的讨论。许多音频项目都假设44或46(根据作者自己的上下文而定)。
我发现假设所有PCM wav音频文件在样本开始前都有44个字节的头数据是危险的。虽然这很常见,但许多应用程序(例如ffmpeg)将生成带有46个字节的头的wav文件。在处理时忽略这个事实会导致文件损坏且无法读取。但是,如何检测头实际上有多长呢?
显然有一种方法可以解决这个问题,但我搜索并没有找到太多关于此的讨论。许多音频项目都假设44或46(根据作者自己的上下文而定)。
//
// 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规范指出子块可以以任何顺序出现。甚至我知道一些主要的音频系统也会犯这个错误,没有考虑到这一点。
因此,如果你想查找某个特定的块,请循环检查每个描述符,直到找到你要找的那个。
00000000000000001000000010000001
(十进制32897)可以分成四个字节,00000000
、00000000
、10000000
和10000001
。使用位移的代码接受字节并从中创建32位整数,通过将每个字节移到适当的位置,然后使用按位OR结合它们。&0xFF
部分是Java特定的,并在这里解释。 - Radiodef关键是查看"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;
}
}
WAV文件的唯一规则是FMT块在DATA块之前。除此之外,在DATA块之前和之后会找到你不知道的块。必须读取每个块的标头以跳过并查找下一个块。
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位可以正常工作。
(新添加)块大小经常出现奇数大小。主要在制作24位单声道文件时出现。规范不清楚,但块大小指定实际数据长度(奇数值),并在块后和下一个块的开始之前添加填充字节(零)。因此,块始终从偶数边界开始,但块大小本身存储为实际奇数值。
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;
块中也可以包含块。在这种情况下,“父”块将具有一个“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;
// 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;