读写wav格式文件

本文所有相关代码(包括未来的)均可在该代码库找到

https://gitcode.net/PeaZomboss/learnaudios

本文代码在MinGW-w64 gcc/g++和MSVC(vs2022)环境下编译测试通过。

MinGW gcc/g++可以在以下链接下载:

https://github.com/niXman/mingw-builds-binaries/releases

https://sourceforge.net/projects/mingw-w64/files/

https://github.com/niXman/mingw-builds-binaries/releases/tag/8.5.0-rt_v10-rev0

以上链接中:

第一个版本较新(gcc 12.x),没有历史遗留问题的话选这个最好

第二个版本(8.1.0的)较老(现在不推荐)

第三个是第二个的最新修订版(8.5.0),也是gcc 8.x系列的最终版,再用8.1.0的话建议更新。


上次讲到了wav格式的组织存储方式,现在我们根据其格式进行wav文件的读写操作。

在此之前先将上篇文章部分关于wav格式的内容整理成头文件在这里贴出:

types.h

  1. #pragma once
  2. typedef char Int8;
  3. typedef short Int16;
  4. typedef long Int32;
  5. typedef long long Int64;
  6. typedef unsigned char UInt8;
  7. typedef unsigned short UInt16;
  8. typedef unsigned long UInt32;
  9. typedef unsigned long long UInt64;
  10. typedef UInt8 Byte;
  11. typedef UInt16 Word;
  12. typedef UInt32 DWord;
  13. typedef UInt64 QWord;
  14. typedef struct
  15. {
  16. DWord D1;
  17. Word D2;
  18. Word D3;
  19. Byte D4[8];
  20. } Guid;
  21. typedef union
  22. {
  23. DWord dw;
  24. char chr[4];
  25. } FourCC;

wavfmt.h

  1. #pragma once
  2. #include "types.h"
  3. typedef struct
  4. {
  5. FourCC id; // 区块类型
  6. DWord size; // 区块大小(不包括id和size字段的大小)
  7. } RIFFChunkHeader;
  8. typedef struct
  9. {
  10. FourCC id; // 必须是 "RIFF"
  11. DWord size; // 文件大小(字节数)-8
  12. FourCC type; // 必须是 "WAVE"
  13. } RIFFHeader;
  14. /* 下面这些格式字段的具体含义上篇文章都有说明 */
  15. typedef struct
  16. {
  17. Word FormatTag;
  18. Word Channels;
  19. DWord SampleRate;
  20. DWord BytesRate;
  21. Word BlockAlign;
  22. Word BitsPerSample;
  23. } WaveFormat;
  24. typedef struct
  25. {
  26. Word FormatTag;
  27. Word Channels;
  28. DWord SampleRate;
  29. DWord BytesRate;
  30. Word BlockAlign;
  31. Word BitsPerSample;
  32. Word ExSize;
  33. } WaveFormatEx;
  34. typedef struct
  35. {
  36. Word FormatTag;
  37. Word Channels;
  38. DWord SampleRate;
  39. DWord BytesRate;
  40. Word BlockAlign;
  41. Word BitsPerSample;
  42. Word ExSize;
  43. Word ValidBitsPerSample;
  44. DWord ChannelMask;
  45. Guid SubFormat;
  46. } WaveFormatExtensible;

读取wav文件相对麻烦一些,我们先从写入开始吧。

写一个wav文件

一般我们需要写入wav文件的情况就是将PCM数据封装起来,所以我们需要一段原始PCM数据。获得PCM数据的方法有很多,比如可以用麦克风录制一段声音,但是这个要留到后面讲DirectSound的时候,所以这一次,我们自己创建一段PCM数据,并把它写入到文件,用现有的播放器来播放试听效果。

创建一段PCM数据

众所周知,声音是物体震动发出的,记录声音的方式就是把振幅值随时间的变化曲线记录下来,但是由于计算机是以离散的方式存储数据的,所以我们需要每过一定的时间间隔就记录一次振幅并量化,这样存储下来的数据就是PCM数据。这个PCM数据是没有任何信息的,你用不同的速度播放效果是不一样的,所以我们需要同时拥有采样率、量化位数等信息才能正确播放,而wav格式就存储了这些必要数据。

用来生成波形的设备叫振荡器(oscillator),当其生成的频率在20HZ-20kHZ范围内就可以让扬声器播放出声音(能不能听见接近两端频率的声音取决于多种因素),由于奈奎斯特采样定理,这个采样率至少为该频率的两倍。

因为声音记录下来的数据是波形,所以这里我们用sin函数生成一段正弦波数据,作为我们的PCM数据。由于数据存储的方式,为了生成这个数据,我们需要同时设置采样率和频率。

本文我们将以44100HZ的采样率生成一段10秒钟的1000HZ的正弦波,单声道,量化位数16位。

  1. typedef struct
  2. {
  3. double increase; // 相位步进
  4. double phase; // 当前相位
  5. double gain; // 增益
  6. } oscillator;
  7. void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
  8. {
  9. osc->increase = TWOPI * frequency / sample_rate;
  10. osc->gain = gain;
  11. osc->phase = 0;
  12. }
  13. // 获取下一个采样点
  14. double osc_next(oscillator *osc)
  15. {
  16. double sample = sin(osc->phase) * osc->gain;
  17. osc->phase += osc->increase;
  18. if (osc->phase > TWOPI)
  19. osc->phase -= TWOPI;
  20. return sample;
  21. }

这段代码实现了一个简单的正弦波振荡器。

解释一下,其原理是这个公式:

\[f(t)=Asin(2\pi ft)
\]

\(t\)是时刻,\(A\)是振幅,\(f\)是频率,但是由于在计算机这里时间不能是连续的,所以我们就把公式改成:

\[x(n)=Asin(\frac{2\pi fn}{N}),n\in [0,1,2,...]
\]

用\(n\)来表示当前采样点,\(N\)表示采样率,\(\frac{n}{N}\)就是时间,这样上下两个公式就对的上了,而第二个公式是离散的。

那么对应一下代码,\(A\)就是增益,\(\frac{2\pi f}{N}\)就是相位步进了,因为\(n\)是每次+1的,而由于浮点数是存在误差的,加上\(sin\)函数的周期是\(2\pi\),所以用"if (osc->phase > TWOPI) osc->phase -= TWOPI;"这段代码来使相位始终保持在\(0-2\pi\),也就是画圈圈。

生成PCM采样的方法如下:

  1. oscillator osc;
  2. init_osc(&osc, 44100, 1000, 0.25);
  3. short *buffer = (short *)malloc(441000 * sizeof(short));
  4. for (int i = 0; i < 441000; i++)
  5. buffer[i] = 32767 * osc_next(&osc);
  6. // ...
  7. free(buffer);

这里init_osc第四个参数gain设为0.25(等效于约-12dB)是为了播放的时候声音不要太大,不然1kHZ正弦波的声音还是很刺耳难听的。

写入到文件

写入文件的方法就简单了,依次按照RIFF文件头,fmt块,data块的顺序写入文件即可。

完整代码如下,使用gcc直接编译即可,无需链接任何库。

  1. #include "wavfmt.h"
  2. #include <math.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #define TWOPI (2*3.1415926535897932)
  7. typedef struct
  8. {
  9. double increase;
  10. double phase;
  11. double gain;
  12. } oscillator;
  13. void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
  14. {
  15. osc->increase = TWOPI * frequency / sample_rate;
  16. osc->gain = gain;
  17. osc->phase = 0;
  18. }
  19. double osc_next(oscillator *osc)
  20. {
  21. double sample = sin(osc->phase) * osc->gain;
  22. osc->phase += osc->increase;
  23. if (osc->phase > TWOPI)
  24. osc->phase -= TWOPI;
  25. return sample;
  26. }
  27. #define BUFFER_LENGTH 441000
  28. int main()
  29. {
  30. // RIFF header
  31. RIFFHeader riff;
  32. strncpy((char *)&riff.id, "RIFF", 4);
  33. riff.size = 4 + sizeof(RIFFChunkHeader) * 2 + sizeof(WaveFormat) + BUFFER_LENGTH * sizeof(short); // 计算实际大小
  34. strncpy((char *)&riff.type, "WAVE", 4);
  35. // Format header
  36. RIFFChunkHeader fmt_header;
  37. strncpy((char *)&fmt_header.id, "fmt ", 4);
  38. fmt_header.size = sizeof(WaveFormat);
  39. // Format
  40. WaveFormat fmt;
  41. fmt.FormatTag = 1;
  42. fmt.Channels = 1;
  43. fmt.BitsPerSample = 16;
  44. fmt.SampleRate = 44100;
  45. fmt.BlockAlign = 2;
  46. fmt.BytesRate = 44100 * 2;
  47. // Data header
  48. RIFFChunkHeader data_header;
  49. strncpy((char *)&data_header.id, "data", 4);
  50. data_header.size = BUFFER_LENGTH * sizeof(short);
  51. // Generate PCM
  52. oscillator osc;
  53. init_osc(&osc, 44100, 1000, 0.25);
  54. short *buffer = (short *)malloc(BUFFER_LENGTH * sizeof(short));
  55. for (int i = 0; i < BUFFER_LENGTH; i++)
  56. buffer[i] = 32767 * osc_next(&osc);
  57. // Write to file
  58. FILE *f = fopen("sin_1khz.wav", "wb");
  59. fwrite(&riff, sizeof(RIFFHeader), 1, f); // 写入RIFF头
  60. fwrite(&fmt_header, sizeof(RIFFChunkHeader), 1, f); // 写入fmt头
  61. fwrite(&fmt, sizeof(WaveFormat), 1, f); // 写入fmt内容
  62. fwrite(&data_header, sizeof(RIFFChunkHeader), 1, f); // 写入data头
  63. fwrite(buffer, 2, BUFFER_LENGTH, f); // 写入实际数据
  64. free(buffer);
  65. fclose(f);
  66. }

运行程序后会在当前工作目录下生成一个"sin_1khz.wav"的文件,用播放器播放就可以听到嘟~~~的声音了。

读取wav文件

实际上读取wav文件也不难,只要按照区块的标准一个个查找就行了,一般fmt块就是第一个块,而data块则有可能夹在中间,所以我们需要循环读取区块,找出fmt和data这两个块就可以了。

当然这样只适合读取标准PCM编码或者IEEE浮点格式的wav文件,对于其他格式的文件并不支持(需要例如fact块),但是一般这样就足够了。

对于这个过程,我们只关心以下几点就可以了

  • fmt块的内容
  • 数据在文件中的位置
  • 数据的大小

为了方便编码以及使用,读取wav文件的代码使用c++实现

也没什么很复杂的,稍微注意一点细节即可,这个前文其实提到过。

wavread.h

  1. #pragma once
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "wavfmt.h"
  5. class WaveReader
  6. {
  7. private:
  8. FILE *f;
  9. WaveFormatExtensible fmtext;
  10. Int64 data_pos; // 实际音频数据在文件中的位置
  11. Int64 data_size; // 文件中音频数据的大小
  12. Int64 read_size; // 当前已经读取的音频数据大小
  13. bool find_fmt(); // 用于查找文件中的"fmt "块
  14. bool find_data(); // 用于查找文件中的"data"块
  15. public:
  16. WaveReader();
  17. ~WaveReader();
  18. bool open_file(char *filename); // 打开文件
  19. void close_file(); // 关闭文件
  20. const WaveFormatExtensible &get_fmtext(); // 返回fmtext的引用
  21. // 把size个字节的音频数据读到buffer缓冲区
  22. // 返回实际读取的字节数,如果已经读取完了返回-1
  23. int read_data(void *buffer, DWord size);
  24. void reset(); // 重置读取指针,即重新从data_pos的位置读取
  25. };

wavread.cpp

  1. #include "wavread.h"
  2. bool WaveReader::find_fmt()
  3. {
  4. RIFFChunkHeader hd;
  5. long size = 0; // 读取一次Header实际读到的大小,用来判断是否读取完毕
  6. do {
  7. size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
  8. if (hd.size % 2 == 1) // 如果块大小为奇数则需要对齐
  9. hd.size++;
  10. // 判断当前块的ID是不是"fmt "
  11. if (strncmp((char *)&hd.id, "fmt ", 4) != 0)
  12. fseek(f, hd.size, SEEK_CUR); // 不是直接跳过这个块
  13. else
  14. break;
  15. } while (size >= 8);
  16. if (size < 8) // 实际读取不足8字节一般说明到了文件末尾
  17. return false;
  18. // 假设文件的format块小于等于sizeof(WaveFormatExtensible)
  19. // 因为有极少数的格式是有自己的标准的,其尺寸大于微软的WaveFormatExtensible
  20. fread(&fmtext, 1, hd.size, f);
  21. // 判断文件格式是否是PCM或者IEEE编码,也就是FormatTag是1或者3
  22. // 否则是编码过的格式,需要解码,我们不支持这种格式
  23. if (fmtext.FormatTag == 0xFFFE) {
  24. if (fmtext.SubFormat.D1 != 1 && fmtext.SubFormat.D1 != 3)
  25. return false;
  26. }
  27. else if (fmtext.FormatTag != 1 && fmtext.FormatTag != 3) {
  28. return false;
  29. }
  30. return true;
  31. }
  32. bool WaveReader::find_data()
  33. {
  34. RIFFChunkHeader hd;
  35. long size = 0; // 同上
  36. do {
  37. size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
  38. if (hd.size % 2 == 1) // 查找data块过程中这个更加重要
  39. hd.size++; // 因为如果是8bit或者24bit单声道的文件可能不对齐2字节
  40. if (strncmp((char *)&hd.id, "data", 4) != 0) // 同上
  41. fseek(f, hd.size, SEEK_CUR);
  42. else
  43. break;
  44. } while (size >= 8);
  45. if (size < 8)
  46. return false;
  47. fgetpos(f, &data_pos); // 获取实际数据的位置
  48. data_size = hd.size; // 该块的大小即为数据的大小
  49. return true;
  50. }
  51. WaveReader::WaveReader()
  52. {
  53. memset(&fmtext, 0, sizeof(WaveFormatExtensible));
  54. f = NULL;
  55. read_size = 0;
  56. }
  57. WaveReader::~WaveReader()
  58. {
  59. if (f)
  60. fclose(f);
  61. }
  62. bool WaveReader::open_file(char *filename)
  63. {
  64. f = fopen(filename, "rb");
  65. if (f) {
  66. RIFFHeader riff;
  67. fread(&riff, 1, sizeof(RIFFHeader), f); // 读取文件的RIFF头
  68. if (strncmp((char *)&riff.id, "RIFF", 4) != 0) // 判断是不是RIFF文件
  69. return false;
  70. if (strncmp((char *)&riff.type, "WAVE", 4) != 0) // 判断是不是WAVE文件
  71. return false;
  72. // 按照规范,fmt块是第一个块
  73. if (!find_fmt()) // 先找fmt块
  74. return false;
  75. if (!find_data()) // 再找data块
  76. return false;
  77. return true;
  78. }
  79. return false;
  80. }
  81. void WaveReader::close_file()
  82. {
  83. // 清理和初始化必要内容
  84. if (f)
  85. fclose(f);
  86. memset(&fmtext, 0, sizeof(WaveFormatExtensible));
  87. f = NULL;
  88. data_pos = 0;
  89. data_size = 0;
  90. read_size = 0;
  91. }
  92. const WaveFormatExtensible &WaveReader::get_fmtext()
  93. {
  94. return fmtext;
  95. }
  96. int WaveReader::read_data(void *buffer, DWord size)
  97. {
  98. if (read_size >= data_size) { // 已经读完所有数据了
  99. memset(buffer, 0, size); // 缓冲区置0
  100. return -1;
  101. }
  102. int result;
  103. // 已经读取的加上要读取的小于实际的数据大小,说明还没到末尾
  104. if (read_size + size < data_size) {
  105. result = fread(buffer, 1, size, f);
  106. read_size += result; // 累加实际读取的
  107. }
  108. // 否则说明还没有读取的字节数小于需要读取的字节数,即将读完
  109. else {
  110. memset(buffer, 0, size); // 全部清0处理
  111. result = fread(buffer, 1, data_size - read_size, f); // 读取剩下的
  112. read_size = data_size;
  113. }
  114. return result;
  115. }
  116. void WaveReader::reset()
  117. {
  118. read_size = 0;
  119. fseek(f, data_pos, SEEK_SET); // 回到数据开始的地方
  120. }

这段代码可以读取大部分的PCM和IEEE格式的wav文件,只需调用open_file()打开文件,read_data()读取数据,close_file()关闭文件,其他各种见具体示例代码。

不过目前只能读取数据,还没有实现播放,播放API有好多,比较老的比如waveXxx系列,DirectSound,最新的是WASAPI。

目前已经更新了用DirectSound和WASAPI播放声音的教程。

补充内容

关于代码段

  1. short *buffer = malloc(BUFFER_LENGTH * sizeof(short));

经过测试发现使用纯C语言编译器如gcc是完全可以的,因为C语言标准支持这种用法,但是C++标准不支持这种用法,需要使用类型转换,即

  1. short *buffer = (short *)malloc(BUFFER_LENGTH * sizeof(short));

不过呢,对C++来说,用new更好一点,原来的代码是用C的,不过为了与后来的内容一致,现在代码库已经全部改成C++了,所以这一段代码也就改成了

  1. short *buffer = new short[BUFFER_LENGTH];

这样应该是最合理的了。

更新记录

  • 2023-01-19:新增gcc 8.5.0链接,新增振荡器原理介绍,添加更多代码注释。

读写wav格式文件的更多相关文章

  1. JAVA用geotools读写shape格式文件

    转自:http://toplchx.iteye.com/blog/1335007 JAVA用geotools读写shape格式文件 (对应geotools版本:2.7.2) (后面添加对应geotoo ...

  2. Android音频: 怎样使用AudioTrack播放一个WAV格式文件?

    翻译 By Long Luo 原文链接:Android Audio: Play a WAV file on an AudioTrack 译者注: 1. 因为这是技术文章,所以有些词句使用原文,表达更准 ...

  3. wav格式文件、pcm数据

    wav格式文件是常见的录音文件,是声音波形文件格式之一,wav 文件由文件头和数据体两部分组成. 文件头是我们在做录音保存到文件的时候,要存储的文件的说明信息,播放器要通过文件头的相关信息去读取数据播 ...

  4. 使用Spark读写CSV格式文件(转)

    原文链接:使用Spark读写CSV格式文件 CSV格式的文件也称为逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号.在本文中的CSV格 ...

  5. 将PCM格式存储成WAV格式文件

    将PCM格式存储成WAV格式文件 WAV比PCM多44个字节(在文件头位置多) 摘自:https://blog.csdn.net/u012173922/article/details/78849076 ...

  6. WAV格式文件无损合并&帧头数据体解析(python)(原创)

    一,百度百科 WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频 ...

  7. 音频文件解析(一):WAV格式文件头部解析

    WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源. 文 ...

  8. [VB.NET][C#]WAV格式文件头部解析

    简介 WAV 为微软开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源. 第一节 文件头 ...

  9. 如何用python读写CSV 格式文件

    工作中经常会碰到读写CSV文件的情况.记录下,方便自己以后查询并与大家一起分享: 写CSV文件方法一: import csv          #导入CSV with open("D:\eg ...

  10. linux下alsa架构音频驱动播放wav格式文件

    #include<stdio.h> #include<stdlib.h> #include <string.h> #include <alsa/asoundl ...

随机推荐

  1. selenium被某些网页检测不允许正常访问、登录等,解决办法

    网站通过什么方式检测 function b() { return "$cdc_asdjflasutopfhvcZLmcfl_"in u || d.webdriver } 通过上方的 ...

  2. i春秋时间

    打开题目就是一段php代码 大致的意思是 ------------------------------------------------------------------------------- ...

  3. Training: Encodings I

    原题链接:http://www.wechall.net/challenge/training/encodings1/index.php 根据题目信息貌似是让我们用这个JPK来解码,我们先点击JPK去下 ...

  4. 初探Java安全之JavaAgent

    About Java Agent Java Agent的出现 在JDK1.5版本开始,Java增加了Instrumentation(Java Agent API)和JVMTI(JVM Tool Int ...

  5. (GDB) GDB调试技巧,调试命令

    调试时查看依赖DSO pidof tvm_rpc_server cat /proc/<pid_of_tvm_rpc_server>/maps 子进程调试 1.vscode -- launc ...

  6. 【Devexpres】spreadsheetControl自动列宽

    Worksheet worksheet = this.spreadsheetControl1.ActiveWorksheet; worksheet.Import(datatable, true, 0, ...

  7. 5 STL-string

    ​ 重新系统学习c++语言,并将学习过程中的知识在这里抄录.总结.沉淀.同时希望对刷到的朋友有所帮助,一起加油哦!  生命就像一朵花,要拼尽全力绽放!死磕自个儿,身心愉悦! 写在前面,本篇章主要介绍S ...

  8. tp6 requset获取参数的方式

    第一种:获取全部参数的值 request()->param() 1 第二种:获取排除某些字段的值,即获取其他值 request()->except(['serverToken','logi ...

  9. 解决manjaro无法连接github问题

    修改/etc/hosts文件 1.查看连接ip地址: https://ping.chinaz.com 2.在hosts文件下增加: vim /etc/hosts 需要管理员权限 140.82.113. ...

  10. 【Java面试指北】Exception Error Throwable 你分得清么?

    读本篇文章之前,如果让你叙述一下 Exception Error Throwable 的区别,你能回答出来么? 你的反应是不是像下面一样呢? 你在写代码时会经常 try catch(Exception ...