WAV文件格式解析及处理 | 您所在的位置:网站首页 › ppt录制音频声音很小怎么办 › WAV文件格式解析及处理 |
RIFF file format
RIFF全称为资源互换文件格式(Resources Interchange File Format),是Windows下大部分多媒体文件遵循的一种文件结构。RIFF文件所包含的数据类型由该文件的扩展名来标识 ChunkRIFF文件结构可以看作是树状结构,其基本构成是称为"块"(Chunk)的单元,每个块有"标志符"、"数据大小"及"数据"所组成 public static class Chunk { //4个字节 public String chunkId; //4个字节。指的是 data的长度 public int dataSize; public byte[] data; } chunkId 4字节,用以标识块中所包含的数据。如:RIFF,LIST,fmt,data,WAV,AVI等。RIFF文件是按照小端 little-endian字节顺序写入的。 dataSize 存储在data域中的数据长度 data 包含数据,数据以字为单位存放,如果数据长度为奇数(字节为单位),则最后添加一个空字节。chunk是可以嵌套的,但是只有块标志为RIFF或者LIST的chunk才能包含其他的chunk。 RIFF chunk标志为RIFF的chunk是比较特殊的,每一个RIFF文件首先存放的必须是一个RIFF chunk,并且只能有这一个标志为RIFF的chunk。RIFF的数据域的起始位置是一个4字节码(FOURCC),用于标识其数据域中chunk的数据类型;紧接着数据域的内容则是包含的subchunk,如下图 RIFF chunk这是一个RIFF chunk中包含有两个subchunk,可以看出RIFF chunk的数据域首先是是4字节的 Form Type,接着是两个subchunk,每一个subchun有包含有自己的标识、数据域的大小以及数据域。 除了RIFF cunk可以嵌套其他的chunk外,另一个可以有subchunk的就是LIST chunk。 image上图中,首先是RIFF文件必须的RIFF chunk,其数据域又包含有两个subchunk,其中一个subchunk的类型为LIST,该LIST chunk又包含了两个subchunk。 FourCCFourCC 全称为Four-Character Codes,是一个4字节32位的标识符,通常用来标识文件的数据格式。例如,在音视频播放器中,可以通过 文件的FourCC来决定调用那种CODEC进行视音频的解码。例如:DIV3,DIV4,DIVX,H264等,对于音频则有:WAV,MP3等。对于上面的RIFF文件,则有:RIFF,WAVE,fmt,data等。FourCC是4个ASCII字符,不足四个字符的则在最后补充空格(不是空字符)。比如,FourCC fmt,实际上是'f' 'm' 't' ' '。 WAVWAV 是Microsoft开发的一种音频文件格式,它符合上面提到的RIFF文件格式标准,可以看作是RIFF文件的一个具体实例。既然WAV符合RIFF规范,其基本的组成单元也是chunk。一个WAV文件通常有三个chunk以及一个可选chunk,其在文件中的排列方式依次是:RIFF chunk,Format chunk,Fact chunk(附加块,可选),Data chunk。 image.png一个WAV文件,首先是一个RIFF chunk;RIFF chunk又包含有Format chunk,Data chunk以及可选的Fact chunk。各个chunk中字段的意义如下: RIFF chunk id size data 'R' 'I' 'F' 'F' 其data字段中数据的大小 字节数 包含其他的chunk Format chunk id size data 'f' 'm' 't' ' ' 见下面Chunk Size 见下面Chunk Data chunk size数据字段包含数据的大小。如无扩展块,则值为16;有扩展块,则值为= 16 + 2字节扩展块长度 + 扩展块长度或者值为18(只有扩展块的长度为2字节,值为0) chunk Data存放音频格式、声道数、采样率等信息 format_tag 2字节,表示音频数据的格式。如值为1,表示使用PCM格式。 channels 2字节,声道数。值为1则为单声道,为2则是双声道。 samples_per_sec 采样率,主要有22.05KHz,44.1kHz和48KHz。 bytes_per sec 音频的码率,每秒播放的字节数。samples_per_sec * channels * bits_per_sample / 8,可以估算出使用缓冲区的大小 block_align 数据块对齐单位,一次采样的大小,值为声道数 * 量化位数 / 8,在播放时需要一次处理多个该值大小的字节数据。 bits_per_sample 音频sample的量化位数,有16位,24位和32位等。 cbSize 扩展区的长度 扩展块内容22字节,具体介绍,后面补充。 Fact chunk**(option) id size 采样总数 'f' 'a' 'c' 't' 数据域的长度,4(最小值为4) 采样总数 (每个声道)采用压缩编码的WAV文件,必须要有Fact chunk,该块中只有一个数据,为每个声道的采样总数。 Data chunk id size data 'd' 'a' 't' 'a' 数据域的长度 具体的音频数据存放在这里 补充 Format chunk 中的编码方式在Format chunk中,除了有音频的数据的采样率、声道等音频的属性外,另一个比较主要的字段就是format_tag,该字段表示音频数据是以何种方式编码存放的。其具体的取值可以为以下: 格式代码 格式名称 fmt 块长度 fact 块 1(0x0001) PCM/非压缩格式 16 2(0x0002 Microsoft ADPCM 18 √ 3(0x0003) IEEE float 18 √ 6(0x0006) ITU G.711 a-law 18 √ 7(0x0007) ITU G.711 μ-law 18 √ 49(0x0031) GSM 6.10 20 √ 64(0x0040) ITU G.721 ADPCM √ 65,534(0xFFFE) 见子格式块中的编码格式 40 关于扩展格式块当WAV文件使用的不是PCM编码方式是,就需要扩展格式块,它是在基本的Format chunk又添加一段数据。该数据的前两个字节,表示的扩展块的长度。紧接其后的是扩展的数据区,含有扩展的格式信息,其具体的长度取决于压缩编码的类型。当某种编码方式(如 ITU G.711 a-law)使扩展区的长度为0,扩展区的长度字段还必须保留,只是其值设置为0。 扩展区的各个字节的含义如下: size 2字节 扩展区的数据长度 ,可以为0或22 valid_bits_per_sample 2字节 有效的采样位数,最大值为采样字节数 * 8。可以使用更灵活的量化位数,通常音频sample的量化位数为8的倍数,但是使用了WAVE_FORMAT_EXTENSIBLE时,量化的位数有扩展区中的valid bits per sample来描述,可以小于Format chunk中制定的bits per sample。 channle mask 4字节 声道掩码 sub format 16字节 GUID,include the data format code,数据格式码。在Format chunk中的format_tag设置为0xFFFE时,表示使用扩展区中的sub_format来决定音频的数据的编码方式。在以下几种情况下必须要使用WAVE_FORMAT_EXTENSIBLE PCM数据的量化位数大于16 音频的采样声道大于2 实际的量化位数不是8的倍数 存储顺序和播放顺序不一致,需要指定从声道顺序到声卡播放顺序的映射情况。 Data chunkData块中存放的是音频的采样数据。每个sample按照采样的时间顺序写入,对于使用多个字节的sample,使用小端模式存放(低位字节存放在低地址,高位字节存放在高地址)。对于多声道的sample采用交叉存放的方式。例如:立体双声道的sample存储顺序为:声道1的第一个sample,声道2的第一个sample;声道1的第二个sample,声道2的第二个sample;依次类推....。对于PCM数据,有以下两种的存储方式: 单声道,量化位数为8,使用偏移二进制码 除上面之外的,使用补码方式存储。 实例分析 普通的WAV image.png RIFF块由上面的介绍可知,由RIFF格式固定的。包括RIFF、Size和FOURCC RIFF RIFF.pngSize Size.png因为是小端的顺序。实际上的十六进制数应该是 “00077090”,转为487568。这个数值+8,就是文件的长度。 WAVE WAVE.png Format chunkChunkId "fmt "。和上面标识的一样。是4个字节,不足补“ ” image.pngChunk Size image.png因为是小端的顺序。实际上的十六进制数应该是 “00000010”,为16。就是后续的Data的长度。 Chunk Data fmt chunk中的chunk data就是包含有该视频的信息。 Chunk Data.png 名称 偏移地址 字节数 端序 内容 当前值 AudioFormat 0x08 2Byte 小端 音频格式 1,PCM音频数据的值为1。则当前没有Fact chunk NumChannels 0x0A 2Byte 小端 声道数 2,表示音频数据的声道数,1:单声道,2:双声道。 SampleRate 0x0C 4Byte 小端 采样率 44100 ByteRate 0x10 4Byte 小端 每秒数据字节数 176400。SampleRate * NumChannels * BitsPerSample / 8 BlockAlign 0x14 2Byte 小端 数据块对齐 4。NumChannels * BitsPerSample / 8 BitsPerSample 0x16 2Byte 小端 采样位数 采样深度16bit。8:8bit,16:16bit,32:32bit Data因为是PCM的数据格式,所以直接就到了data 标识'data' data.png 音频数据的长度Size Size.png 名称 偏移地址 字节数 端序 内容 当前值 ID 0x00 4Byte 大端 'data' (0x64617461) “0x77000”,转为十进制为 487424 。 Size 0x04 4Byte 小端 N 等于 ByteRate * seconds ,约为2.7秒。 Data 0x08 NByte 小端 音频数据 ... 总结 头部大小 通常的WAV,以PCM为数据格式的,基本上头部就如上面的结构。头部的SIZE为固定的44, 通常对WAV音频进行处理时,会直接写死这个头部的Offset。 排查一次WAV处理中的杂音情况但是在实际处理的过程中,遇到了下面这样的WAV HEADER。头部的长度不同,导致后续的音频处理中出现了杂音的情况。排查之后,才发现是因为头部大小不同导致。 特殊一点的WAV由Adobe Premiere Pro CC 创建的WAV。 image.png 它包含有LIST Chunk。而且fmt chunk的size为18。 wav list.png因为有LIST,导致上面通常写死的HEAD_SIZE 44出现错误。 这个时候重新去计算这个HEAD_SIZE就可以了。 LIST CHUNK CHUNK ID CHUNK ID为“LIST” CHUNK SIZE 可以看到为0x58,十进制为88。 计算HEAD_SIZE private static int getHeadSize(RandomAccessFile srcFis) throws IOException { int offset = 0; //riff getChunkId(srcFis); offset += 4; //length getChunkSize(srcFis); offset += 4; //wave getChunkId(srcFis); offset += 4; //fmt getChunkId(srcFis); offset += 4; //fmt length int skipLength = getChunkSize(srcFis); offset += 4; byte[] skipBytes = new byte[skipLength]; srcFis.read(skipBytes); offset += skipLength; String chunkId = getChunkId(srcFis); offset += 4; while (!chunkId.equals("data")) { skipLength = getChunkSize(srcFis); offset += 4; skipBytes = new byte[skipLength]; srcFis.read(skipBytes); offset += skipLength; chunkId = getChunkId(srcFis); offset += 4; } offset += 4; System.out.println("headSize="+offset); return offset; } private static int getChunkSize(RandomAccessFile srcFis) throws IOException { byte[] formatSize = new byte[4]; srcFis.read(formatSize); int fisrt8 = formatSize[0] & 0xFF; int fisrt16 = formatSize[1] & 0xFF; int fisrt24 = formatSize[2] & 0xFF; int fisrt32 = formatSize[3] & 0xFF; int chunkSize = fisrt8 | (fisrt16 8) & 0xff); header[6] = (byte) ((totalDataLen >> 16) & 0xff); header[7] = (byte) ((totalDataLen >> 24) & 0xff); header[8] = 'W';//WAVE header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; //FMT Chunk header[12] = 'f'; // 'fmt ' header[13] = 'm'; header[14] = 't'; header[15] = ' ';//过渡字节 //数据大小 header[16] = 16; // 4 bytes: size of 'fmt ' chunk header[17] = 0; header[18] = 0; header[19] = 0; //编码方式 10H为PCM编码格式 header[20] = 1; // format = 1 header[21] = 0; //通道数 header[22] = (byte) channels; header[23] = 0; //采样率,每个通道的播放速度 header[24] = (byte) (sampleRate & 0xff); header[25] = (byte) ((sampleRate >> 8) & 0xff); header[26] = (byte) ((sampleRate >> 16) & 0xff); header[27] = (byte) ((sampleRate >> 24) & 0xff); //音频数据传送速率,采样率*通道数*采样深度/8 header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); // 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数 header[32] = (byte) (channels * 16 / 8); header[33] = 0; //每个样本的数据位数 header[34] = 16; header[35] = 0; //Data chunk header[36] = 'd';//data header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totalAudioLen & 0xff); header[41] = (byte) ((totalAudioLen >> 8) & 0xff); header[42] = (byte) ((totalAudioLen >> 16) & 0xff); header[43] = (byte) ((totalAudioLen >> 24) & 0xff); return header; } 替换和插入音频 计算两个采样点的位置。偏移头部的大小,讲两个采样点之间的数据,替换成想要的音频。 重新写入修改之后的头部。因为数据长度修改。里面的RIFF块ChunkSize和data块的长度由当前的长度做对应修改。 public static void replaceAudioWithSame(Audio srcAudio, Audio coverAudio, Audio outAudio, float srcStartTime) { String srcWavePath = srcAudio.getPath(); String coverWavePath = coverAudio.getPath(); int sampleRate = srcAudio.getSampleRate(); int channels = srcAudio.getChannel(); int bitNum = srcAudio.getBitNum(); RandomAccessFile srcFis = null; RandomAccessFile coverFis = null; RandomAccessFile newFos = null; String tempOutPcmPath = srcWavePath + ".tempPcm"; try { //创建输入流 srcFis = new RandomAccessFile(srcWavePath, "rw"); coverFis = new RandomAccessFile(coverWavePath, "rw"); newFos = new RandomAccessFile(tempOutPcmPath, "rw"); int srcHeadSize = getHeadSize(srcFis); int coverHeadSize = getHeadSize(coverFis); final int srcStartPos = getPositionFromWave(srcStartTime, sampleRate, channels, bitNum); final int coverStartPos = 0; final int coverEndPos = (int) coverFis.length() - coverHeadSize; //复制源音频srcStartTime时间之前的数据 //跳过头文件数据 srcFis.seek(srcHeadSize); copyData(srcFis, newFos, srcStartPos); //复制覆盖音频指定时间段的数据 //跳过指定位置数据 coverFis.seek(coverHeadSize + coverStartPos); int copyCoverSize = coverEndPos - coverStartPos; float volume = coverAudio.getVolume(); copyData(coverFis, newFos, copyCoverSize); //复制srcStartTime时间后的源文件数据 final int srcStartAddCoverPosition = getPositionFromWave(srcStartTime + ((float) coverAudio.getTimeMillis()) / 1000, sampleRate, channels, bitNum); final long srcFileSize = srcFis.length() - srcHeadSize; int remainSize = (int) (srcFileSize - srcStartAddCoverPosition); if (remainSize > 0) { // coverFis.seek(WAVE_HEAD_SIZE + coverStartPos); srcFis.seek(srcHeadSize + srcStartAddCoverPosition); copyData(srcFis, newFos, remainSize); } } catch (Exception e) { e.printStackTrace(); return; } finally { //关闭输入流 if (srcFis != null) { try { srcFis.close(); } catch (IOException e) { e.printStackTrace(); } } if (coverFis != null) { try { coverFis.close(); } catch (IOException e) { e.printStackTrace(); } } if (newFos != null) { try { newFos.close(); } catch (IOException e) { e.printStackTrace(); } } } // 删除源文件, // new File(srcWavePath).delete(); // 转换临时文件为源文件 AudioEncodeUtil.convertPcm2Wav(tempOutPcmPath, outAudio.getPath(), sampleRate, channels, bitNum); //删除临时文件 new File(tempOutPcmPath).delete(); } 参考RIFF和WAVE音频文件格式 WAV文件格式详解 wav文件格式分析与详解 |
CopyRight 2018-2019 实验室设备网 版权所有 |