对于多媒体资源,一般都是以文件的形式存储在固化存储器中。Fatfs所支持的fat32为windows支持的文件系统,因此在嵌入式系统中采用Fatfs文件系统可极大地扩展系统的应用。例如,把计算机上图片,音频,视频,文本等资源直接拷贝到嵌入式系统中的固化存储器中,在系统中即可直接应用这些资源。把嵌入式系统中录制的音频、视频直接保存成一定的格式,在计算机上可直接播放处理,把传感器采集的数据保存成txt或dat文件,在计算机上通过处理生成数据曲线分析等。笔者此处就wav音频文件的播放与录音进行简单的介绍。

1. wav音频格式

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

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

RIFF结构的基本单元为chunk,它的结构如下:

  1. struct chunk {
  2. unsignedint id; /* 块标志 */
  3. unsignedint size; /* 块大小 */
  4. unsigned chardata[size]; /* 块内容 */
  5. }

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音频,还会有一个fact chunk,用以指明解压后音频数据的大小,对于PCM非压缩wav文件,并没有该chunk。音频数据保存在data chunk中,根据fmt chunk中指明的声道数以及采样位数,wav音频数据存放形式有不同的方式。

一个PCM格式的wav结构定义Wav.h如下:

  1. #ifndef __WAV_H__
  2. #define __WAV_H__
  3. #ifdef __cplusplus
  4. extern "C" {
  5. #endif
  6. //资源互换文件格式RIFF,树状结构,基本单位是chunk,整个文件由chunk构成
  7. typedef struct RIFF_HEADER {
  8. char Riff_ID[4];
  9. unsigned int Riff_Size;//记录整个RIFF文件的大小,除ID和Size这两个变量
  10. char Riff_Format[4];
  11. } RIFF_HEADER;
  12. typedef struct WAVE_FORMAT {
  13. unsigned short FormatTag; //声音的格式代号
  14. unsigned short Channels; //声音通道
  15. unsigned int SamplesPerSec; //采样率
  16. unsigned int AvgBytesPerSec; //=采样率*区块对其单位
  17. unsigned short BlockAlign; //区块对其单位=每个取样所需位数*声音通道/8
  18. unsigned short BitsPerSample; //每个取样所需位数
  19. } WAVE_FORMAT;
  20. typedef struct FMT_CHUNK {
  21. char Fmt_ID[4];
  22. unsigned int Fmt_Size;//记录fmt的大小
  23. WAVE_FORMAT WaveFormat;
  24. } FMT_CHUNK;
  25. typedef struct DATA_CHUNK {
  26. char Data_ID[4];
  27. unsigned int Data_Size;//记录data区的大小
  28. } DATA_CHUNK;
  29. typedef struct WAVE_HEADER {
  30. RIFF_HEADER RiffHeader;
  31. FMT_CHUNK FmtChunk;
  32. DATA_CHUNK DataChunk;
  33. } WAVE_HEADER;
  34. #ifdef __cplusplus
  35. }
  36. #endif
  37. #endif /*__WAV_H__*/

根据以上的wav结构定义,一个录音文件的wav文件头可如下定义:

  1. static WAVE_HEADER RecorderWaveHeader = {
  2. 'R', 'I', 'F', 'F',
  3. sizeof(WAVE_HEADER) - 8,//整个wave文件大小,初始化值
  4. 'W', 'A', 'V', 'E',
  5. 'f', 'm', 't', ' ',
  6. sizeof(FMT_CHUNK) - 8,
  7. 1,//编码方式,线性PCM编码
  8. 1,//单声道
  9. 10000,//采样率为10k
  10. 20000,//每个采样2个字节
  11. 2,//每个采样2个字节
  12. 16,//每个采样需16位
  13. 'd', 'a', 't', 'a',
  14. 0, //data长度初始化为0
  15. };

2. wav音频文件的播放或录音

sd卡由于其可插拔、灵活性好,通常应用于设备的扩展存储应用。计算机上wav音频等文件可轻易地拷贝到fat32格式的sd卡上,在嵌入式系统中要使用sd卡,首先需实现sd卡驱动,这在前面的章节有详细的介绍,此处不再详述。fat32文件的读写还需要相应文件系统的接口支持,此处选用Fatfs,对于不同的嵌入式系统,这是需要移植的部分,关于s3c2416下Fatfs文件系统的移植在前面章节有详细的介绍,此处不再详述。从wav文件读出音频数据后(播放),还需要把音频数据传输给声卡,声卡还原出声音模拟信号,即可听到声音。音频数据的处理需要用到音频编解码器,数据的传输也有一定的音频总线要求,因此还需要音频驱动的实现,这部分在前面的章节有详细的介绍,此处不再详述。

3. 应用实例

工程中利用串口对耳机音量进行加大、调小,对Mic录音进行灵敏度的调节,通过串口输入进行播放wav音频或开始录音。播放时实时显示播放进度并可按下’s’后停止播放,录音时实时显示录音wav文件的大小并可按下’s’后停止录音。

main.c的内容如下:

  1. #include"s3c2416.h"
  2. #include"UART0.h"
  3. #include"ff.h"
  4. #include"diskio.h"
  5. #include "RTC.h"
  6. #include"Wav.h"
  7. #include"IIS.h"
  8. #include"IIC.h"
  9. #include"WM8960.h"
  10. staticWAVE_HEADER RecorderWaveHeader = {
  11. 'R', 'I', 'F', 'F',
  12. sizeof(WAVE_HEADER) - 8,//整个wave文件大小
  13. 'W', 'A', 'V', 'E',
  14. 'f', 'm', 't', ' ',
  15. sizeof(FMT_CHUNK) - 8,
  16. 1,//编码方式,线性PCM编码
  17. 1,//单声道
  18. 10000,//采样率为10k
  19. 20000,//每个采样2个字节
  20. 2,//每个采样2个字节
  21. 16,//每个采样需16位
  22. 'd', 'a', 't', 'a',
  23. 0, //data长度初始化为0
  24. };
  25. // 音频数据缓存20KB
  26. unsigned charAudioBuffer[20*1024];
  27. int main()
  28. {
  29. FATFS fs;
  30. FIL file;
  31. FRESULT Res;
  32. unsigned int i;
  33. unsigned int BufferLen;
  34. unsigned char *pData;
  35. WAVE_HEADER WaveHeader;
  36. int ByteWrite, ByteRead;
  37. unsigned char State;
  38. unsigned char VolumeLevel;
  39. unsigned short Command;
  40. const char Path1[] = "test.wav";
  41. const char Path2[] = "1.wav";
  42. char FilePath[256];
  43. unsigned int Size = 0;
  44. unsigned int AudioSize = 0;
  45. unsigned int TotalSize = 0;
  46. RTC_Time Time = {
  47. 2014, 5, 22, 23, 00, 0, 5
  48. };
  49. RTC_Init(&Time); // RTC初始化
  50. Uart0_Init(); // 串口初始化
  51. IIC_Init(); //IIC初始化,音频芯片控制
  52. IIS_Init(); // IIS音频接口初始化
  53. WM8960_Init(); // 音频编解码器初始化
  54. RTC_GetTime(&Time); // 显示RTC时间
  55. Uart0_Printf("Time: %4d/%02d/%02d%02d:%02d:%02d\r\n", Time.Year,
  56. Time.Month, Time.Day, Time.Hour,Time.Min, Time.Sec);
  57. f_mount(&fs, "" , 0);
  58. ByteRead = 0;
  59. pData = (unsigned char *)0;
  60. for (i=0; i<sizeof(Path1); i++) {
  61. FilePath[i] = Path1[i];
  62. }
  63. State = 1; // 进入播放test.wav状态
  64. while(1) {
  65. switch (State) {
  66. case 0: // 操作选择
  67. WM8960_HeadphoneStop();
  68. WM8960_RecorderStop();
  69. IIS_TxPause();
  70. IIS_RxPause();
  71. Uart0_SendString("\r\nSelect:\r\n"
  72. "0: Play test.wav\r\n"
  73. "1: Play recording file\r\n"
  74. "2: Start recorder\r\n"
  75. "3: Recorder volume up\r\n"
  76. "4: Recorder volume down\r\n"
  77. "5: Player volume up\r\n"
  78. "6: Player volume down\r\n"
  79. );
  80. while(State == 0) {
  81. // 等待串口选择操作,阻塞型
  82. Command = Uart0_ReceiveByte();
  83. switch (Command) {
  84. case '0': // 播放test.wav
  85. for (i=0; i<sizeof(Path1);i++) {
  86. FilePath[i] = Path1[i];
  87. }
  88. State = 1; // 转到开始播放wav状态
  89. break;
  90. case '1': // 播放录音wav
  91. for (i=0; i<sizeof(Path2);i++) {
  92. FilePath[i] = Path2[i];
  93. }
  94. State = 1; // 转到开始播放wav状态
  95. break;
  96. case '2':
  97. State = 3; // 转到开始录音状态
  98. break;
  99. case '3': // Mic灵敏度增加
  100. VolumeLevel = WM8960_RecorderVolume(VolumeUp);
  101. Uart0_Printf("Recordervolume %d%%\r\n", VolumeLevel);
  102. break;
  103. case '4': // Mic灵敏度减小
  104. VolumeLevel =WM8960_RecorderVolume(VolumeDown);
  105. Uart0_Printf("Recorder volume%d%%\r\n", VolumeLevel);
  106. break;
  107. case '5': // 耳机音量增加
  108. VolumeLevel =WM8960_HeadphoneVolume(VolumeUp);
  109. Uart0_Printf("Player volume%d%%\r\n", VolumeLevel);
  110. break;
  111. case '6': // 耳机音量降低
  112. VolumeLevel =WM8960_HeadphoneVolume(VolumeDown);
  113. Uart0_Printf("Player volume%d%%\r\n", VolumeLevel);
  114. break;
  115. default:
  116. break;
  117. }
  118. }
  119. Uart0_SendString("\r\n");
  120. break;
  121. case 1: // 开始播放音频
  122. // 打开wav音频文件
  123. Res = f_open(&file, FilePath, FA_READ | FA_OPEN_EXISTING);
  124. if (Res != RES_OK) {
  125. Uart0_Printf("Open %s failed\r\n",FilePath);
  126. State = 0; // 进入到操作选择界面
  127. } else {
  128. // 读取wav音频文件头,获得音频采样率,位数,声道数信息
  129. Res = f_read(&file, (unsignedchar *)&WaveHeader,
  130. sizeof(WAVE_HEADER),(unsigned int *)&ByteRead);
  131. if (Res != RES_OK) {
  132. f_close(&file);
  133. Uart0_Printf("Read wavheader error\r\n");
  134. State = 0; // 进入到操作选择界面
  135. } else {
  136. // 读取一小段音频数据到缓存中
  137. Res = f_read(&file,(unsigned int *)AudioBuffer,
  138. sizeof(AudioBuffer), (unsignedint *)&ByteRead);
  139. if (Res != RES_OK) {
  140. Uart0_Printf("Read wavdata error\r\n");
  141. f_close(&file);
  142. State = 0; // 进入到操作选择界面
  143. } else {
  144. if (ByteRead <sizeof(AudioBuffer)) { // 文件到结尾
  145. // 已播放到文件的结尾,重定位到音频文件的开始
  146. Res = f_lseek(&file,sizeof(WAVE_HEADER));
  147. if (Res != RES_OK) {
  148. Uart0_Printf("f_lseek error\r\n");
  149. f_close(&file);
  150. State= 0; // 进入到操作选择界面
  151. break;
  152. }
  153. }
  154. // 根据wav文件头的音频信息初始化音频驱动的播放参数
  155. IIS_TxInit(WaveHeader.FmtChunk.WaveFormat.SamplesPerSec,
  156. WaveHeader.FmtChunk.WaveFormat.BitsPerSample,
  157. WaveHeader.FmtChunk.WaveFormat.Channels);
  158. // wav文件的总文件大小,bytes计
  159. TotalSize =WaveHeader.RiffHeader.Riff_Size;
  160. pData = AudioBuffer; // 播放指向音频缓存区
  161. // 把pData指向的数据写入音频缓存,最大允许写入
  162. // ByteRead字节,返回实际写入到音频缓存的字节数
  163. BufferLen = IIS_WriteBuffer(pData,ByteRead);
  164. ByteRead -= BufferLen; // 数据剩余字节数
  165. pData += BufferLen; // 数据下一次开始写入的位置
  166. Size = BufferLen; // 播放的长度
  167. AudioSize = 0; // 已播放的音频长度
  168. WM8960_HeadphoneStart(); // 打开耳机播放通道
  169. IIS_TxStart(); // IIS开始传输音频播放
  170. State= 2; // 转入正在播放音频状态
  171. Uart0_SendString("PlayingMusic, press 's' to stop"
  172. "playing at any time\r\n");
  173. Uart0_SendString("Playbackprogress: 00.0%");
  174. }
  175. }
  176. }
  177. break;
  178. case 2:// 正在播放音频
  179. if (ByteRead > 0) {
  180. // 返回值的高8位不为0,说明低8位键值有效,查询是否有串口输入,非阻塞
  181. Command = Uart0_Peek();
  182. if (Command & (0xff<<8)) {// 有按键按下
  183. if ((Command & 0xff) == 's'){ // 按下了's'
  184. f_close(&file);
  185. State = 0; // 返回到操作选择界面
  186. break;
  187. }
  188. }
  189. if (Size > 20*1024) { // 播放了20k大小的音频数据
  190. AudioSize += (Size>>10);// 累计己播放的总音频数据大小(KB)
  191. Size = 0;
  192. Uart0_SendString("\b\b\b\b\b");
  193. // 显示播放进度的百分比
  194. Uart0_Printf("%02d.%d%%",(AudioSize*100)/(TotalSize>>10),
  195. ((AudioSize*100)%(TotalSize>>10))*10/(TotalSize>>10));
  196. }
  197. // 连续写入音频数据到音频缓存中,实现连续播放
  198. BufferLen = IIS_WriteBuffer(pData,ByteRead);
  199. ByteRead -= BufferLen; // 数据剩余字节数
  200. pData += BufferLen; // 数据下一次开始写入的位置
  201. Size += BufferLen;
  202. } else { // 播放完缓存中的音频数据,再从sd卡加载下一段音频数据
  203. // 一段音频数据播放完后,从sd卡中加载下一段数据
  204. Res = f_read(&file, (unsignedchar *)AudioBuffer,
  205. sizeof(AudioBuffer),(unsigned int *)&ByteRead);
  206. if (Res != RES_OK) {
  207. Uart0_Printf("Read wav dataerror\r\n");
  208. f_close(&file);
  209. State = 0; // 进入到操作选择界面
  210. } else {
  211. pData = AudioBuffer; // 重定位到数据首位置
  212. if (ByteRead <sizeof(AudioBuffer)) {
  213. // 到文件结尾,文件重定位到开头,重播放
  214. Uart0_Printf("\r\n");
  215. Uart0_Printf("replay%s\r\n", FilePath);
  216. Uart0_SendString("Playbackprogress: 00.0%");
  217. Size = 0;
  218. AudioSize = 0;
  219. Res = f_lseek(&file,sizeof(WAVE_HEADER));
  220. if (Res != RES_OK) {
  221. Uart0_Printf("Replayaudio error\r\n");
  222. f_close(&file);
  223. State = 0; // 进入到操作选择界面
  224. }
  225. }
  226. }
  227. }
  228. break;
  229. case 3: // 开始录音
  230. // 创建录音保存1.wav文件
  231. Res = f_open(&file, "1.wav",FA_WRITE | FA_CREATE_ALWAYS);
  232. if (Res != RES_OK) {
  233. Uart0_Printf("Create 1.wavfailed\r\n");
  234. State = 0; // 进入到操作选择界面
  235. } else {
  236. // 写入wav文件头
  237. Res = f_write(&file, (unsignedchar *)&RecorderWaveHeader,
  238. sizeof(WAVE_HEADER), (unsignedint *)&ByteWrite);
  239. if (Res != RES_OK) {
  240. f_close(&file);
  241. Uart0_Printf("Write wavheader error\r\n");
  242. State = 0; // 进入到操作选择界面
  243. } else {
  244. // 初始化录音参数,采样率,采样位数,声道数
  245. IIS_RxInit(RecorderWaveHeader.FmtChunk.WaveFormat.SamplesPerSec,
  246. RecorderWaveHeader.FmtChunk.WaveFormat.BitsPerSample,
  247. RecorderWaveHeader.FmtChunk.WaveFormat.Channels);
  248. Size = 0;
  249. AudioSize = 0; // 总录音文件大小初始化0
  250. pData = AudioBuffer; // 指向录音缓存区
  251. ByteWrite = sizeof(AudioBuffer);// 一段音频缓存的大小
  252. WM8960_RecorderStart(); //WM8960打开录音通道
  253. IIS_RxStart(); // IIS开始接收录音数据
  254. State = 4; // 转到正在录音状态
  255. Uart0_SendString("Recording,press 's' to stop recording"
  256. "at any time\r\n");
  257. Uart0_SendString("Recording(KB): ");
  258. }
  259. }
  260. break;
  261. case 4:// 正在录音
  262. if (ByteWrite > 0) {
  263. // 返回值的高8位不为0,说明低8位键值有效,查询是否有串口输入,非阻塞
  264. Command = Uart0_Peek();
  265. if (Command & (0xff<<8)) {// 有按键按下
  266. if ((Command & 0xff) == 's'){ // 按下了's'
  267. // 停止录音,更改wav文件头文件大小
  268. f_lseek(&file, 0); // 定位到文件头
  269. // 数据大小改为录音的音频大小
  270. RecorderWaveHeader.DataChunk.Data_Size= AudioSize;
  271. // RIFF大小改为整个文件文件的大小
  272. RecorderWaveHeader.RiffHeader.Riff_Size=
  273. (sizeof(WAVE_HEADER)-8) + AudioSize;
  274. // 更改wav文件头信息
  275. f_write(&file, (unsignedchar *)&RecorderWaveHeader,
  276. sizeof(WAVE_HEADER),(unsigned int *)&ByteWrite);
  277. f_close(&file);
  278. State = 0; // 进入到操作选择界面
  279. break;
  280. }
  281. }
  282. if (Size > 20*1024) { // 记录了20k大小的音频数据
  283. AudioSize += Size; // 累计己播放的总音频数据大小
  284. Size = 0;
  285. Uart0_SendString("\b\b\b\b\b\b");
  286. // 显示已录音的文件大小
  287. Uart0_Printf("%6d",(AudioSize>>10));
  288. }
  289. // 从音频缓存中读取录音数据到pData中,最大允许读取ByteWrite
  290. // 字节大小,返回实际从音频缓存中读取的字节数
  291. BufferLen = IIS_ReadBuffer(pData,ByteWrite);
  292. ByteWrite -= BufferLen; // 剩余内存空间字节数
  293. pData += BufferLen; // 下一位读开始存入的内存位置
  294. Size += BufferLen;
  295. } else { // 缓存已满,写入缓存数据到sd卡中
  296. Res = f_write(&file, (unsignedchar *)&AudioBuffer,
  297. sizeof(AudioBuffer),(unsigned int *)&ByteWrite);
  298. if (Res != RES_OK) {
  299. f_close(&file);
  300. Uart0_Printf("Write 1.waverror\r\n");
  301. State = 0; // 进入到操作选择界面
  302. } else {
  303. pData = AudioBuffer;
  304. ByteWrite = sizeof(AudioBuffer);
  305. }
  306. }
  307. break;
  308. default:
  309. break;
  310. }
  311. }
  312. }

19_Fatfs下播放录音wav音频文件 - 图1

4. 附录

通过Fatfs的api函数,可以轻易读写windows下常见格式文件,这和windows/Linux下操作文件差异不大。播放对wav音频文件无特殊要求,可任意采样率、采样位数、单/双声道,插上耳机即能听到声音,录制wav音频对采样率、采样位数、声道数、录制长度等均没有任何限制,录制好的wav音频文件可直接在计算机上播放。虽然wav格式音频文件较占用存储空间,但其是无损的,音质在相同码率下远好于mp3等有损压缩音频文件。

Wav_GCC.rar,GCC下wav音频文件播放与录制工程,可直接make。

http://pan.baidu.com/s/1c05s2cg

Wav_MDK.rar,MDK下wav音频文件播放与录制工程

http://pan.baidu.com/s/1i33guiD

test.wav,wav播放测试音频文件,11.025k采样率、16位、单声道音乐,可通过音频格式转换软件生成wav音频文件。

http://pan.baidu.com/s/1eQzOErg