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