Wav音频文件格式详解

Wav是RIFF的一种音频格式,所以开始的4个字节一定是RIFF这4个char,接下来4个字节是接下来的文件的总长度,加上之前的RIFF占了8字节,所以文件目测总长度应该是这个字节的数值+8,在Wav中所有的数值类的值都是int,字节序为little endian,接下来4个字节是WAVE这个4个Char,接下来4个字节是fmt 这4个Char,是的,是4个,后面有个空格别漏了,但是,也有例外,如果这个地方出现的不是fmt 这个字符串,那么可能还会有别的

信息继续被放在了文件头,但是它一定是如下的格式:

  • 4个字节的信息头名称,我这里见过的有(JUNK和FAKE)
  • 4个字节的附加信息头长度(int)

对应长度的chunk信息

在这之后一定还会出现fmt 这4个char的组合,即回到正轨

接下来4个字节是整个Wave文件fmt头的长度, 类型是int,可选择的是16、18或者40

接下来4个字节是WAVE文件的编码方式,类型是int,一般来说是1、3、6、7或者65534,1代表PCM,3代表IEEE浮点,6代表8-bit ITU-T G.711 A-law,8代表8-bit ITU-T G.711 μ-law,65534代表在后面的subFormat中定义,除了1之外,后面的都没有见过实例,也无法得知它的实际是个什么样子了,PCM也是用得最多的

接下来2个字节是音频的声道数量,类型是short,1=单声道,2=stereo=立体声,其他多声道的可能也有,但是我暂时没看到实例

接下来4个字节是采样率数值,类型是int,一般都是44100,代表一秒钟记录多少个信息

接下来4个字节是一个能自己算出来的数值了,类型为int,它应该被理解为每秒钟有多少个byte,它的数值等于采样率 * 单位Chunk长度(楼下这个值)

接下来2个字节同样是一个能自己算出来的数值,类型为short,它应该被理解为单位Chunk的长度,它的数值等于声道数 * 位深 / 8

接下来2个字节是位深,类型是short,可以理解为这是一个声音在某个极短的时间是一个浮点表示的,用多少位的浮点来表示这个时候的声音,就是位深,理论上位深越深越越能够还原声音的原本信息,但是太多也就超过人耳的上限了就是,一般都是24、32

如果fmt头长度超过了16,那么这里应该会有其他的信息,剩余的长度就是fmt头长度的数值 – 16,具体的我也是没见过实例的,只有这个可能性列举在楼下:

如果上面那个PCM的值取到了非1的值,那么这里应该需要多出一个如下的信息头:

  • 4个字节的信息头名称,应该是fact这4个char
  • 4个字节的信息头总长度,int

对应长度的chunk详细信息

接下来4个字节又是标识符,char类型,应该等于’data’,接下来4个字节是int类型,标志data的总长度,然后剩下的就全是data了

最后可能会出现的是Wav文件的Meta信息,比如一些编辑器会把图片、Artist之类的放在文件末尾,同样是以4个字节的开头介绍、4字节的长度信息、对应长度的具体信息这种模式存放的

继续补全信息,说完了以上的理想状态,再说说实际中怎么用,用Python来解析这个其实很容易:

with open(self.__wave_file, 'rb') as wav:
    flag = wav.read(4)
    if flag != 'RIFF':
        return 0
    file_length, = struct.unpack('i', wav.read(4))
    print(file_length)

这样其实没有太多的难度,但是如果你用Java写,就很蛋疼了,首先,因为Java没有太好用的像Python一样的struct类,你只能把它变成ByteArray,但是变成了ByteArray之后,又面临两个问题:

一、ByteArray默认是有符号的,而我们需要无符号的数据

二、如果你强制转Int,大部分时候比如ByteBuffer.toInt()按照的是大端序去做转换的,但是不巧的是Wav文件用的是小端序,注意这俩坑点之后就简单了

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class WavHeader {

    public static void HeaderParser(String fileUri) throws IOException {
        RandomAccessFile wavFile;
        try {
            wavFile = new RandomAccessFile(fileUri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return;
        }
        byte[] buffer4 = new byte[4];
        wavFile.read(buffer4);
        assert byteArrayToString(buffer4).equals("RIFF");
        // 向后跳到获取sample,这个一般是44100,比较好确认解析正确
        wavFile.seek(12);
        wavFile.read(buffer4);
        while (!byteArrayToString(buffer4).equals("fmt ")) {
            // 一般来说头文件不会超过500字节的,也可以通过文件大小限制来避免空指针,这里图简单
            if (wavFile.getFilePointer() + 4 >= 500) {
                wavFile.close();
                return;
            }
            wavFile.read(buffer4);
            wavFile.seek(byteArrayToInt(buffer4));
            wavFile.read(buffer4);
        }
        wavFile.seek(wavFile.getFilePointer() + 8);
        wavFile.read(buffer4);
        assert byteArrayToIntUseBuffer(buffer4) == 44100;
        assert byteArrayToInt(buffer4) == 44100;
    }

    private static String byteArrayToString(byte[] byteArray) {
        return new String(byteArray);
    }

    // 两种不同的方式来获取Int值,这里为原始手段
    private static int byteArrayToInt(byte[] byteArray) {
        int total = 0;
        for (int i = 0; i < byteArray.length; i++) {
            total += (byteArray[i] & 0xFF) << (i * 8);
        }
        return total;
    }

    // 显得很高级的手段,其实原理相同
    private static int byteArrayToIntUseBuffer(byte[] bytes) {
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        return buffer.getInt();
    }
}

然后文件内部data的格式就容我之后再补充了。

转载需保留链接来源:软件玩家 » Wav音频文件格式详解

赞 (0)

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

感谢您的支持!

微信扫一扫打赏