一文搞懂 wav 文件格式

WAV 是微软公司开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format) 文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。

wav 概述

WAV 是微软公司开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format) 文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。

构成 RIFF 文件的基本单位称之为块(chunk)。每个 RIFF 文档是由若干个块构成。每个块(chunk)由块标识、块长度及数据等三部分所组成。

wav 文件由若干个 RIFF chunk 构成,分别为: RIFF WAVE Chunk,Format Chunk,Fact Chunk(可选),Data Chunk。另外,文件中还可能包含一些可选的区块,如:Fact chunk、Cue points chunk、Playlist chunk、Associated data list chunk 等。

基本 WAV 文件头(包括 RIFF Chunk 和 fmt Subchunk)的最小长度为 44 字节(12 字节的 RIFF Chunk 加上 16 字节的 fmt Subchunk 加上 4 字节的 data Subchunk ID 和 4 字节的 data Subchunk Size)。

WAV 文件可以包含额外的信息,如 'fact'、'list'、'JUNK' 等其他子块,这将增加文件头的总长度。在没有这些额外子块的情况下,最小文件头长度为 44 字节。当涉及到实际的音频数据时,'data' 子块的大小将决定整个文件的大小。

RIFF 文件是按照 little-endian 字节顺序写入的。

一文搞懂 wav 文件格式


wav 文件支持多种不同的比特率、采样率、多声道音频。

块解析

chunk 的格式如下:

size 内容 含义
4 bytes ID 如:RIFF,fmt,data
4 bytes chund size 如标准的 fmt chunk 为 16 字节
N bytes data chunk 的内容

RIFF chunk

typedef struct
{
    char ChunkID[4]; //'R','I','F','F'
    unsigned int ChunkSize;
    char Format[4]; //'W','A','V','E'
} riff_chunk;
size 内容 含义
4 bytes ChunkID RIFF
4 bytes ChunkSize 从下一个字段首地址开始到文件末尾的总字节数。该字段的数值加 8 为当前文件的实际长度
4 bytes Format WAVE

其中 ChunkSize 代表的是整个 file_size 的大小减去 ChunkID 和 ChunkSize 的大小,即 file_size = ChunkSize + 8。

fmt chunk

typedef struct
{
    char FmtID[4];
    unsigned int FmtSize;
    unsigned short FmtTag;
    unsigned short FmtChannels;
    unsigned int SampleRate;
    unsigned int ByteRate;
    unsigned short BlockAilgn;
    unsigned short BitsPerSample;
} fmt_chunk;
size 内容 含义
4 bytes FmtID "fmt "
4 bytes FmtSize fmt chunk 的大小,一般有 16/18/20/22/40 字节 (也有超过 40 字节的情况),超过 16 字节部分为扩展块
2 bytes FmtTag 编码格式代码,其值见常见编码格式表,如果上述取值为 16,则此值通常为 1,代表该音频的编码方式是 PCM 编码
2 bytes FmtChannels 声道数目,1 代表单声道,2 代表双声道
4 bytes SampleRate 采样频率,8/11.025/12/16/22.05/24/32/44.1/48/64/88.2/96/176.4/192 kHZ4 bytesByteRate 传输速率,每秒的字节数,计算公式为:SampleRate * FmtChannels * BitsPerSample/8
2 bytes BlockAilgn 块对齐,告知播放软件一次性需处理多少字节,公式为: BitsPerSample*FmtChannels/8
2 bytes BitsPerSample 采样位数,一般有8/16/24/32/64,值越大,对声音的还原度越高

常见编码格式

格式编码 格式名称 fmt 块长度 fact 块
0x01 PCM / 非压缩格式 16
0x02 Microsoft ADPCM 18
0x03 IEEE float 18
0x06 ITU G.711 a-law 18√
0x07 ITU G.711 μ-law 18√
0x031 GSM 6.10 20
0x040 ITU G.721 ADPCM
0xFFFE 见子格式块中的编码格式 40

现在常见的 wav 文件常采用 PCM 编码,所以 fmt 块的长度为 16 字节。

data chunk

struct DATA_CHUNK
{
    char DataID[4]; //'d','a','t','a'
    unsigned int DataSize;
};
size 内容 含义
4 bytes DataID data
4 bytes DataSize 原始音频数据的大小

wav 文件分析工具

mediainfo

Linux 平台下使用 mediainfo 分析 wav 文件

$ sudo apt-get install mediainfo
$ mediainfo /home/share/samba/sample-15s.wav 
General
Complete name                            : /home/share/samba/sample-15s.wav
Format                                   : Wave
File size                                : 3.23 MiB
Duration                                 : 19 s 174 ms
Overall bit rate mode                    : Constant
Overall bit rate                         : 1 411 kb/s

Audio
Format                                   : PCM
Format settings                          : Little / Signed
Codec ID                                 : 1
Duration                                 : 19 s 174 ms
Bit rate mode                            : Constant
Bit rate                                 : 1 411.2 kb/s
Channel(s)                               : 2 channels
Sampling rate                            : 44.1 kHz
Bit depth                                : 16 bits
Stream size                              : 3.23 MiB (100%)

hd 工具查看原始数据

hd /home/share/samba/sample-15s.wav -n 128
00000000  52 49 46 46 24 9c 33 00  57 41 56 45 66 6d 74 20  |RIFF$.3.WAVEfmt |
00000010  10 00 00 00 01 00 02 00  44 ac 00 00 10 b1 02 00  |........D.......|
00000020  04 00 10 00 64 61 74 61  00 9c 33 00 0f f8 0f f8  |....data..3.....|
00000030  f3 fb f3 fb 66 0a 66 0a  49 09 49 09 83 fc 83 fc  |....f.f.I.I.....|
00000040  fd fd fd fd b2 06 b2 06  b1 03 b1 03 86 02 86 02  |................|
00000050  f5 0c f5 0c 85 0d 85 0d  fc 02 fc 02 8f 06 8f 06  |................|
00000060  d2 12 d2 12 11 0e 11 0e  02 01 02 01 a9 02 a9 02  |................|
00000070  9b 09 9b 09 2e 05 2e 05  4c 02 4c 02 53 09 53 09  |........L.L.S.S.|
00000080
  • 52 49 46 46:对应的 ASCII 字符为 RIFF
  • 24 9c 33 00:ChunkSize,对应的十六进制是 0x339c24=3382308 + 8 和上面的文件大小一致
  • 57 41 56 45:对应的 ASCII 字符为 WAVE
  • 66 6d 74 20:对应的 ASCII 字符为 fmt
  • 10 00 00 00:FmtSize 0x10=16 代表 PCM 编码方式
  • 01 00:对应为 1,代表 PCM 编码方式
  • 02 00:通道个数,通道数为 2
  • 44 ac 00 00:采样频率 0xac44=44100=44.1KHz
  • 10 b1 02 00:传输速率,转化为十六进制为: 0x2b110=176400 通过此值可以计算该音频的时长:3382308/176400 = 19.174s
  • 04 00:数据对齐单位
  • 10 00:采样位数 0x10=16
  • 64 61 74 61:对应的 ASCII 字符为 data
  • 00 9c 33 00:对应该音频的 raw 数据的大小,转化为十六进制为 0x339c00=3382272,此值等于 0x339c24 - 44
wav 测试文件下载:https://samplelib.com/zh/sample-wav.html

播放 wav 文件

在 linux 下可以使用 aplay 或者 ffplay 命令播放 wav 文件

$ aplay sample-15s.wav
Playing WAVE 'sample-15s.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
# 或
$ ffplay -ar 44100 -channels 2 -f s16le -i sample-15s.wav
ffplay version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2003-2021 the FFmpeg developers
  built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)

使用 C 语言读取 wav 文件

wav.h

#ifndef __WAV_H__
#define __WAV_H__

#define debug(fmt...) do \
            { \
                printf("[%s::%d] ", __func__, __LINE__);\
                printf(fmt); \
            } while(0)

/* RIFF WAVE file struct.
 * For details see WAVE file format documentation 
 * (for example at http://www.wotsit.org).
 */
typedef struct WAV_HEADER_S
{
    char            riffType[4];    //4byte,资源交换文件标志:RIFF   
    unsigned int    riffSize;       //4byte,从下个地址到文件结尾的总字节数 
    char            waveType[4];    //4byte,wave文件标志:WAVE   
    char            formatType[4];  //4byte,波形文件标志:FMT  
    unsigned int        formatSize;     //4byte,音频属性(compressionCode,numChannels,sampleRate,bytesPerSecond,blockAlign,bitsPerSample)所占字节数
    unsigned short  compressionCode;//2byte,编码格式(1-线性pcm-WAVE_FORMAT_PCM,WAVEFORMAT_ADPCM)
    unsigned short  numChannels;    //2byte,通道数
    unsigned int    sampleRate;     //4byte,采样率
    unsigned int    bytesPerSecond; //4byte,传输速率
    unsigned short  blockAlign;     //2byte,数据块的对齐
    unsigned short  bitsPerSample;  //2byte,采样精度
    char            dataType[4];    //4byte,数据标志:data
    unsigned int    dataSize;       //4byte,从下个地址到文件结尾的总字节数,即除了wav header以外的pcm data length
} WAV_HEADER;

typedef struct WAV_INFO_S
{
  WAV_HEADER    header;
  FILE          *fp;
  unsigned int  channelMask;
} WAV_INFO;

#endif

wav.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "wav.h"

/* func     : endian judge
 * return   : 0-big-endian othes-little-endian
 */
int IS_LITTLE_ENDIAN(void) 
{
    int __dummy = 1;
    return ( *( (unsigned char*)(&(__dummy) ) ) );
}

unsigned int readHeader(void *dst, signed int size, signed int nmemb, FILE *fp) 
{
    unsigned int n, s0, s1, err;
    unsigned char tmp, *ptr;

    if ((err = fread(dst, size, nmemb, fp)) != nmemb) 
    {
        return err;
    }
    if (!IS_LITTLE_ENDIAN() && size > 1) 
    {
        //debug("big-endian \n");
        ptr = (unsigned char*)dst;
        for (n = 0; n<nmemb; n++) 
        {
            for (s0 = 0, s1 = size - 1; s0 < s1; s0 ++, s1 --) 
            {
                tmp = ptr[s0];
                ptr[s0] = ptr[s1];
                ptr[s1] = tmp;
            }
            ptr += size;
        }
    }
    else
    {
        //debug("little-endian \n");
    }

    return err;
}

void dumpWavInfo(WAV_INFO wavInfo)
{
    debug("compressionCode:%d \n",wavInfo.header.compressionCode);
    debug("numChannels:%d \n",wavInfo.header.numChannels);
    debug("sampleRate:%d \n",wavInfo.header.sampleRate);
    debug("bytesPerSecond:%d \n",wavInfo.header.bytesPerSecond);
    debug("blockAlign:%d \n",wavInfo.header.blockAlign);
    debug("bitsPerSample:%d \n",wavInfo.header.bitsPerSample);

}

int wavInputOpen(WAV_INFO *pWav, const char *filename)
{
    signed int offset;
    WAV_INFO *wav = pWav ;

    if (wav == NULL) 
    {
      debug("Unable to allocate WAV struct.\n");
      goto error;
    }

    wav->fp = fopen(filename, "rb");
    if (wav->fp == NULL) 
    {
      debug("Unable to open wav file. %s\n", filename);
      goto error;
    }

    /* RIFF 标志符判断 */
    if (fread(&(wav->header.riffType), 1, 4, wav->fp) != 4) 
    {
      debug("couldn't read RIFF_ID\n");
      goto error;  /* bad error "couldn't read RIFF_ID" */
    }

    if (strncmp("RIFF", wav->header.riffType, 4)) 
    {
        debug("RIFF descriptor not found.\n") ;
        goto error;
    }
    debug("Find RIFF \n");

    /* Read RIFF size. Ignored. */
    readHeader(&(wav->header.riffSize), 4, 1, wav->fp);
    debug("wav->header.riffSize:%d \n",wav->header.riffSize);

    /* WAVE 标志符判断 */
    if (fread(&wav->header.waveType, 1, 4, wav->fp) !=4) 
    {
        debug("couldn't read format\n");
        goto error;  /* bad error "couldn't read format" */
    }

    if (strncmp("WAVE", wav->header.waveType, 4)) 
    {
        debug("WAVE chunk ID not found.\n") ;
        goto error;
    }
    debug("Find WAVE \n");

    /* fmt 标志符判断 */
    if (fread(&(wav->header.formatType), 1, 4, wav->fp) != 4) 
    {
        debug("couldn't read format_ID\n");
        goto error;  /* bad error "couldn't read format_ID" */
    }

    if (strncmp("fmt", wav->header.formatType, 3)) 
    {
        debug("fmt chunk format not found.\n") ;
        goto error;
    }
    debug("Find fmt \n");

    readHeader(&wav->header.formatSize, 4, 1, wav->fp);  // Ignored
    debug("wav->header.formatSize:%d \n",wav->header.formatSize);

    /* read  info */
    readHeader(&(wav->header.compressionCode), 2, 1, wav->fp);
    readHeader(&(wav->header.numChannels), 2, 1, wav->fp);
    readHeader(&(wav->header.sampleRate), 4, 1, wav->fp);
    readHeader(&(wav->header.bytesPerSecond), 4, 1, wav->fp);
    readHeader(&(wav->header.blockAlign), 2, 1, wav->fp);
    readHeader(&(wav->header.bitsPerSample), 2, 1, wav->fp);

    offset = wav->header.formatSize - 16;

    /* Wav format extensible */
    if (wav->header.compressionCode == 0xFFFE) 
    {
        static const unsigned char guidPCM[16] = {
            0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
            0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
        };
        unsigned short extraFormatBytes, validBitsPerSample;
        unsigned char guid[16];
        signed int i;

        /* read extra bytes */
        readHeader(&(extraFormatBytes), 2, 1, wav->fp);
        offset -= 2;

        if (extraFormatBytes >= 22) 
        {
            readHeader(&(validBitsPerSample), 2, 1, wav->fp);
            readHeader(&(wav->channelMask), 4, 1, wav->fp);
            readHeader(&(guid), 16, 1, wav->fp);

            /* check for PCM GUID */
            for (i = 0; i < 16; i++) if (guid[i] != guidPCM[i]) break;
            if (i == 16) wav->header.compressionCode = 0x01;

            offset -= 22;
        }
    }
    debug("wav->header.compressionCode:%d \n",wav->header.compressionCode);

    /* Skip rest of fmt header if any. */
    for (;offset > 0; offset--) 
    {
        fread(&wav->header.formatSize, 1, 1, wav->fp);
    }

    do 
    {
        /* Read data chunk ID */
        if (fread(wav->header.dataType, 1, 4, wav->fp) != 4) 
        {
            debug("Unable to read data chunk ID.\n");
            free(wav);
            goto error;
        }
        /* Read chunk length. */
        readHeader(&offset, 4, 1, wav->fp);

        /* Check for data chunk signature. */
        if (strncmp("data", wav->header.dataType, 4) == 0) 
        {
            debug("Find data \n");
            wav->header.dataSize = offset;
            break;
        }

        /* Jump over non data chunk. */
        for (;offset > 0; offset--) 
        {
            fread(&(wav->header.dataSize), 1, 1, wav->fp);
        }
    } while (!feof(wav->fp));
    debug("wav->header.dataSize:%d \n",wav->header.dataSize);

    /* return success */
    return 0;

/* Error path */
error:
    if (wav) 
    {
      if (wav->fp) 
      {
        fclose(wav->fp);
        wav->fp = NULL;
      }
      //free(wav);
    }
    return -1; 
}

int main(int argc,char **argv)
{
    WAV_INFO wavInfo;
    char fileName[128];
    if(argc < 2 || strlen(&argv[1][0]) >= sizeof(fileName))
    {
        debug("argument error !!! \n");
        return -1 ;
    }
    debug("size : %d \n",sizeof(WAV_HEADER));

    strcpy(fileName,argv[1]);

    wavInputOpen(&wavInfo, fileName);
    return 0;
}

来源:https://zhuanlan.zhihu.com/p/1899435737633490578

- 本文内容来自网络,如有侵权,请联系本站处理。

12-28   阅读(21)   评论(0)
 标签: 创客 音频

涨知识
FreeRTOS

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。

评论:
相关文章
音频编解码器ES8311中文规格书

具有ADC和DAC的低功耗单声道音频编解码器ES8311。


ESP32 使用MAX98357 播放MP3

使用ESP32和MAX98357音频放大器芯片来播放音乐,效果令人惊叹!


基于ESP32构建的音频播放器

本文介绍使用 ESP32 构建一个有趣的音频播放器,您只需在 ESP32 上连接一个额外的扬声器即可在其中播放音效。