Wav文件格式解析

参考资料:
https://en.wikipedia.org/wiki/WAV
http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
http://www.lightlink.com/tjweber/StripWav/Canon.html

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的格式就容我之后再补充了

       

评论

此博客中的热门博文

转一下关于Fuck的用法

远程记录OpenWRT日志

用OpenWRT打造自动翻墙路由器(详解篇)