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 文件支持多种不同的比特率、采样率、多声道音频。
chunk 的格式如下:
| size | 内容 | 含义 |
|---|---|---|
| 4 bytes | ID | 如:RIFF,fmt,data |
| 4 bytes | chund size | 如标准的 fmt chunk 为 16 字节 |
| N bytes | data | 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。
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 字节。
struct DATA_CHUNK
{
char DataID[4]; //'d','a','t','a'
unsigned int DataSize;
};
| size | 内容 | 含义 |
|---|---|---|
| 4 bytes | DataID | data |
| 4 bytes | DataSize | 原始音频数据的大小 |
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 /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
wav 测试文件下载:https://samplelib.com/zh/sample-wav.html
在 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)
#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
#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
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。
具有ADC和DAC的低功耗单声道音频编解码器ES8311。
使用ESP32和MAX98357音频放大器芯片来播放音乐,效果令人惊叹!
本文介绍使用 ESP32 构建一个有趣的音频播放器,您只需在 ESP32 上连接一个额外的扬声器即可在其中播放音效。