WAV是一种保存音频信息的文件格式,广泛应用于Windows及其应用程序中,如今主流的音频播放器都支持WAV音频文件的播放。

1. WAV音频格式

WAV是录音时用的标准Windows文件格式,文件扩展名为”.wav”,数据本身的格式为PCM或压缩型,它是由微软与IBM联合开发的用于音频数字存储的标准,采用RIFF文件格式结构。

RIFF全称资源互换文件格式,是Windows下大部分多媒体文件遵循的一种文件结构,除了本文所说的波形格式数据(.wav),采用RIFF格式结构的文件还有音频视频交错格式(.avi)、位图格式(.rdi)、MIDI格式(.rmi)、调色板格式(.pal)、多媒体电影(.rmn)、动画光标(.ani)。

RIFF结构的基本单元为chunk,每个chunk必须包含一个4字节的chunk id,一个4字节的数据大小和对应的chunk数据。它的结构如下:

struct chunk {
unsigned int  id; /*  块标志 */

unsignedint  size; /* 块大小*/

unsigned chardata[size]; /* 块内容 */

}

id为4个ascii字符组成,用来识别块中所包含的数据,如”RIFF”、”WAV ”、”data”、”fmt ”等,size是存储在data域中数据的长度,不包括id与size域的大小,data[size]为该块保存的数据,以字为单位排列。

WAV音频文件作为RIFF结构,其由若干个chunk组成,按照在文件中的位置包括:RIFF chunk,fmt chunk,fact chunk(可选),data chunk。所有RIFF结构文件均会首先包含RIFF chunk,并指明RIFF类型,此处为”WAVE”。WAV文件在fmt chunk中指明音频格式信息,例如采样位数、采样频率、声道数、编码方式等。对于压缩型WAV音频,如ADPCM、A律、U律等等,还会有一个fact chunk,用以指明解压后音频数据的大小,对于PCM非压缩WAV文件,并没有该chunk。音频数据保存在data chunk中,根据fmt chunk中指明的声道数以及采样位数,WAV音频数据存放形式有不同的方式。

一个简单的PCM格式WAV结构定义如下:

#define PCM_WAVE_FORMAT 1

typedef struct RIFF_HEADER {
       charRiffId[4];

       uint32_tRiffSize;

       charRiffFormat[4];

} RIFF_HEADER;



typedef struct WAVE_FORMAT {
       uint16_tFormatTag; //声音的格式代号

       uint16_tChannels; //声音通道

       uint32_tSamplesPerSec; //采样率

       uint32_tAvgBytesPerSec; //采样率*块对齐单位

       uint16_tBlockAlign; //块对齐单位=每个取样所需位数*声音通道/8

       uint16_tBitsPerSample; //每个取样所需位数

} WAVE_FORMAT;



typedef struct FMT_CHUNK {
       charFmtID[4];

       uint32_tFmtSize;

       WAVE_FORMATWaveFormat;

} FMT_CHUNK;



typedef struct DATA_CHUNK {
       charDataId[4];

       uint32_t  DataSize;

} DATA_CHUNK;



typedef struct WAVE_HEADER {
       RIFF_HEADERRiffHeader;

       FMT_CHUNK  FmtChunk;

       DATA_CHUNK       DataChunk;

} WAVE_HEADER;



static WAVE_HEADER WaveHeader = {
       'R','I', 'F', 'F',

       0,

       'W','A', 'V', 'E',

       'f','m', 't', ' ',

       16,

       PCM_WAVE_FORMAT,// PCM编码

       1, // 单声道

       0,// 采样率初始化0

       0,// 每秒字节流初始化0

       2,// 每个采样2字节

       16,// 采样16位

       'd','a', 't', 'a',

       0

};

2、WAV音频播放

WAV音频的播放涉及到音频驱动、SD卡读写文件的实现,可以参考前面的章节。播放实现主要流程如下:

a. 用f_open()打开SD卡里的WAV文件。

b. 用Wave_ReadHeader()函数解析WAV头,获取采样位数、采样频率、声道数等等音频格式,此处只支持PCM 16位音频格式。

int Wave_ReadHeader(FIL *File,WAVE_FORMAT *WaveFormat)

{
uint32_t ByteRead;

int DataBytes;

char Buffer[512];

char *pBuffer;



if (f_lseek(File, 0) != RES_OK) {
       return-1;

}

if (f_read(File, Buffer, sizeof(Buffer),&ByteRead) != RES_OK) {
       return-2;

}

if (!Wav_FindChunk(Buffer,"RIFF", sizeof(Buffer))) {
       return-3;

}

if (!Wav_FindChunk(Buffer,"WAVE", sizeof(Buffer))) {
       return-4;

}

pBuffer = Wav_FindChunk(Buffer,"fmt ", sizeof(Buffer));

if (!pBuffer) {
       return-5;

}

pBuffer += 8; // Move past "fmt", fmt size

memcpy(WaveFormat, pBuffer,sizeof(WAVE_FORMAT));

if (WaveFormat->FormatTag !=PCM_WAVE_FORMAT) {
       return-6;

}

pBuffer = Wav_FindChunk(Buffer,"data", sizeof(Buffer));

if (!pBuffer) {
       return-7;

}

pBuffer += 4; // Move past"data"

memcpy(&DataBytes, pBuffer,sizeof(uint32_t));

if (WaveFormat->BitsPerSample != 16){
       return-8;

}

return DataBytes;

}

c. 根据解析的音频格式,对I2S音频驱动初始化。

PRINTF("Playing %s\r\n",WavFilesList[FileIndex]);

PRINTF("Mode: %s\r\n",WaveFormat.Channels==1?"Mono":"Stereo");

PRINTF("Samplerate: %dHz\r\n", WaveFormat.SamplesPerSec);

PRINTF("Bitrate: %d bps\r\n",WaveFormat.AvgBytesPerSec*8);

PRINTF("Samples: %d\r\n",DataBytes / WaveFormat.BlockAlign);

I2S_SetSamplerate(WaveFormat.SamplesPerSec);

I2S_TxStart();

d. 采用双缓存(缓存0和缓存1)实现SD卡音频数据的不断读取,当任一个缓存空的时候,用f_read()从SD卡读取音频数据到空缓存中,如果缓存满,则等待音频帧数据播放完,然后把缓存中的数据清空到音频输出流中。

if (Playing &&(!BufferState.Buffer0Full || !BufferState.Buffer1Full)) {
       Res= f_read(&file, BufferState.Buffer[BufferState.WriteIndex],sizeof(BufferState.Buffer[0]), &ByteRead);

       if(Res != RES_OK) {
              f_close(&file);

              PRINTF("Readdata error\r\n");

              State= 0;

              break;   

       }

       if(ByteRead < sizeof(BufferState.Buffer[0])) {
              f_close(&file);// 文件结束

              Playing= 0; // 结束播放

       }

       if(BufferState.WriteIndex) {
              BufferState.Buffer1Full= 1;

              BufferState.WriteIndex= 0;

       }else {
              BufferState.Buffer0Full= 1;

              BufferState.WriteIndex= 1;

       }

}

SD卡读取的音频数据需要不断加载到音频输出缓存中,实现音频的连续播放。当I2S音频输出流播放完一帧后,就可以从准备好数据的双缓存中加载一帧的音频数据到输出帧中,直到这一缓存加载完,置缓存空,告知SD卡可以读取数据到这个空缓存。

if (WriteIndex != I2SState.TxReadIndex){
if (BufferState.ReadIndex <BUFFER_NUM*2) {
       if(!BufferState.Buffer0Full) {
              break;

       }

} else {
       if(!BufferState.Buffer1Full) {
              break;

       }

}



pBuffer = (int16_t *)BufferState.Buffer+ AUDIO_FRAME_SIZE*BufferState.ReadIndex;

if (WaveFormat.Channels == 1) {
       for(i=0; i<AUDIO_FRAME_SIZE; i++) {
              I2SState.TxBuffer[I2SState.TxWriteIndex][i]= ((int16_t *)pBuffer)[i];

       }

       TotalSize+= AUDIO_FRAME_SIZE * sizeof(int16_t);

       BufferState.ReadIndex++;

} else {
       for(i=0; i<AUDIO_FRAME_SIZE; i++) {
              I2SState.TxBuffer[I2SState.TxWriteIndex][i]= ((int32_t *)pBuffer)[i];

       }

       TotalSize+= AUDIO_FRAME_SIZE * sizeof(int32_t);

       BufferState.ReadIndex+= 2;

}



if (BufferState.ReadIndex ==BUFFER_NUM*2) {
       BufferState.Buffer0Full= 0;

} else if (BufferState.ReadIndex ==BUFFER_NUM*4) {
       BufferState.Buffer1Full= 0;

       BufferState.ReadIndex= 0;

}



I2SState.TxWriteIndex = WriteIndex;

if (WriteIndex >=AUDIO_NUM_BUFFERS-1) {
       WriteIndex= 0;

} else {
       WriteIndex++;

}



if (!Playing) {
       if(TotalSize >= DataBytes) {
              I2S_TxStop();

              PRINTF("Play over\r\n");

              State = 0;

       }

}

}

3、WAV音频录制

WAV音频的录制涉及到数字麦克风驱动、SD卡读写文件的实现,可以参考前面的章节。录音实现主要流程如下:

a. 用f_open()创建SD卡里的WAV录音文件。

b. 用f_lseek()开始从音频数据位置开始写入数据。16K采样率、单声道初始化数字麦克风。

if (f_lseek(&file,sizeof(WAVE_HEADER)) != RES_OK) {
       f_close(&file);

       State= 0;

       break;

}

PRINTF("Recordingsound.wav\r\n");

PRINTF("Mode: Mono\r\n");

PRINTF("Samplerate: 16000Hz\r\n");

PRINTF("Bitrate: %d bps\r\n",16000*2*8);

Dmic_Start();

c. 不断把麦克风录制的帧数据保存到空的双缓存中,当某一缓存填充满的时候,置位相应的缓存通道,告知SD卡可以把这一缓存通道的数据写入后清空。

if(DmicState.Event) {
pBuffer = (int16_t *)BufferState.Buffer+ AUDIO_FRAME_SIZE*BufferState.WriteIndex;

for (i=0; i<AUDIO_FRAME_SIZE; i++) {
       ((int16_t*)pBuffer)[i] = DmicState.Buffer[DmicState.ReadIndex][i];

}

BufferState.WriteIndex++;

if (BufferState.WriteIndex ==BUFFER_NUM*2) {
       BufferState.Buffer0Full= 1;

} else if (BufferState.WriteIndex == BUFFER_NUM*4){
       BufferState.Buffer1Full= 1;

       BufferState.WriteIndex= 0;

}

if(DmicState.ReadIndex >=AUDIO_NUM_BUFFERS-1) {
DmicState.ReadIndex=0;

}else {
DmicState.ReadIndex++;

}

DmicState.Event= 0;

}

用双缓存不断把录制的音频数据写入到SD卡,当双缓存中的某一缓存填充满,用f_write()把音频数据写入到SD卡,并清空这一缓存,告知麦克风可以把录制帧数据保存到这一空缓存中。

if ((BufferState.Buffer0Full ||BufferState.Buffer1Full)) {
       Res= f_write(&file, BufferState.Buffer[BufferState.ReadIndex],sizeof(BufferState.Buffer[0]), &ByteWrite);

       if(Res != RES_OK) {
              Dmic_Stop();

              f_close(&file);

              PRINTF("Writedata error\r\n");

              State= 0;

              break;   

       }

       if(BufferState.ReadIndex) {
              BufferState.Buffer1Full= 0;

              BufferState.ReadIndex= 0;

       }else {
              BufferState.Buffer0Full= 0;

              BufferState.ReadIndex= 1;

       }

       DataBytes += sizeof(BufferState.Buffer[0]);

}

d. 结束录制(通过按键)后,根据实际录制的音频数据大小,通过Wave_WriteHeader()更新WAV文件头。

int Wave_WriteHeader(FIL *File, uint32_tSamplerate, uint32_t DataBytes)

{
uint32_t ByteWrite;

if (f_lseek(File, 0) != RES_OK) {
       return-1;

}

WaveHeader.FmtChunk.WaveFormat.SamplesPerSec= Samplerate;

WaveHeader.FmtChunk.WaveFormat.AvgBytesPerSec= Samplerate * 2;

WaveHeader.DataChunk.DataSize =DataBytes;

WaveHeader.RiffHeader.RiffSize =DataBytes + sizeof(WaveHeader) - 8;

if (f_write(File, (uint8_t*)&WaveHeader, sizeof(WaveHeader), &ByteWrite) != RES_OK) {
       return-2;

}

return 0;

}

4. 附录

MDK工程,包含SD卡文件读写代码,I2S、数字麦克风音频录制播放驱动,WAV音频文件播放、录制的实现。
源码:https://pan.baidu.com/s/1c6kxdk