读写wav格式文件
读写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
#pragma once
typedef char Int8;
typedef short Int16;
typedef long Int32;
typedef long long Int64;
typedef unsigned char UInt8;
typedef unsigned short UInt16;
typedef unsigned long UInt32;
typedef unsigned long long UInt64;
typedef UInt8 Byte;
typedef UInt16 Word;
typedef UInt32 DWord;
typedef UInt64 QWord;
typedef struct
{
DWord D1;
Word D2;
Word D3;
Byte D4[8];
} Guid;
typedef union
{
DWord dw;
char chr[4];
} FourCC;
wavfmt.h
#pragma once
#include "types.h"
typedef struct
{
FourCC id; // 区块类型
DWord size; // 区块大小(不包括id和size字段的大小)
} RIFFChunkHeader;
typedef struct
{
FourCC id; // 必须是 "RIFF"
DWord size; // 文件大小(字节数)-8
FourCC type; // 必须是 "WAVE"
} RIFFHeader;
/* 下面这些格式字段的具体含义上篇文章都有说明 */
typedef struct
{
Word FormatTag;
Word Channels;
DWord SampleRate;
DWord BytesRate;
Word BlockAlign;
Word BitsPerSample;
} WaveFormat;
typedef struct
{
Word FormatTag;
Word Channels;
DWord SampleRate;
DWord BytesRate;
Word BlockAlign;
Word BitsPerSample;
Word ExSize;
} WaveFormatEx;
typedef struct
{
Word FormatTag;
Word Channels;
DWord SampleRate;
DWord BytesRate;
Word BlockAlign;
Word BitsPerSample;
Word ExSize;
Word ValidBitsPerSample;
DWord ChannelMask;
Guid SubFormat;
} WaveFormatExtensible;
读取wav文件相对麻烦一些,我们先从写入开始吧。
写一个wav文件
一般我们需要写入wav文件的情况就是将PCM数据封装起来,所以我们需要一段原始PCM数据。获得PCM数据的方法有很多,比如可以用麦克风录制一段声音,但是这个要留到后面讲DirectSound的时候,所以这一次,我们自己创建一段PCM数据,并把它写入到文件,用现有的播放器来播放试听效果。
创建一段PCM数据
众所周知,声音是物体震动发出的,记录声音的方式就是把振幅值随时间的变化曲线记录下来,但是由于计算机是以离散的方式存储数据的,所以我们需要每过一定的时间间隔就记录一次振幅并量化,这样存储下来的数据就是PCM数据。这个PCM数据是没有任何信息的,你用不同的速度播放效果是不一样的,所以我们需要同时拥有采样率、量化位数等信息才能正确播放,而wav格式就存储了这些必要数据。
用来生成波形的设备叫振荡器(oscillator),当其生成的频率在20HZ-20kHZ范围内就可以让扬声器播放出声音(能不能听见接近两端频率的声音取决于多种因素),由于奈奎斯特采样定理,这个采样率至少为该频率的两倍。
因为声音记录下来的数据是波形,所以这里我们用sin函数生成一段正弦波数据,作为我们的PCM数据。由于数据存储的方式,为了生成这个数据,我们需要同时设置采样率和频率。
本文我们将以44100HZ的采样率生成一段10秒钟的1000HZ的正弦波,单声道,量化位数16位。
typedef struct
{
double increase; // 相位步进
double phase; // 当前相位
double gain; // 增益
} oscillator;
void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
{
osc->increase = TWOPI * frequency / sample_rate;
osc->gain = gain;
osc->phase = 0;
}
// 获取下一个采样点
double osc_next(oscillator *osc)
{
double sample = sin(osc->phase) * osc->gain;
osc->phase += osc->increase;
if (osc->phase > TWOPI)
osc->phase -= TWOPI;
return sample;
}
这段代码实现了一个简单的正弦波振荡器。
解释一下,其原理是这个公式:
\]
\(t\)是时刻,\(A\)是振幅,\(f\)是频率,但是由于在计算机这里时间不能是连续的,所以我们就把公式改成:
\]
用\(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采样的方法如下:
oscillator osc;
init_osc(&osc, 44100, 1000, 0.25);
short *buffer = (short *)malloc(441000 * sizeof(short));
for (int i = 0; i < 441000; i++)
buffer[i] = 32767 * osc_next(&osc);
// ...
free(buffer);
这里init_osc第四个参数gain设为0.25(等效于约-12dB)是为了播放的时候声音不要太大,不然1kHZ正弦波的声音还是很刺耳难听的。
写入到文件
写入文件的方法就简单了,依次按照RIFF文件头,fmt块,data块的顺序写入文件即可。
完整代码如下,使用gcc直接编译即可,无需链接任何库。
#include "wavfmt.h"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define TWOPI (2*3.1415926535897932)
typedef struct
{
double increase;
double phase;
double gain;
} oscillator;
void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
{
osc->increase = TWOPI * frequency / sample_rate;
osc->gain = gain;
osc->phase = 0;
}
double osc_next(oscillator *osc)
{
double sample = sin(osc->phase) * osc->gain;
osc->phase += osc->increase;
if (osc->phase > TWOPI)
osc->phase -= TWOPI;
return sample;
}
#define BUFFER_LENGTH 441000
int main()
{
// RIFF header
RIFFHeader riff;
strncpy((char *)&riff.id, "RIFF", 4);
riff.size = 4 + sizeof(RIFFChunkHeader) * 2 + sizeof(WaveFormat) + BUFFER_LENGTH * sizeof(short); // 计算实际大小
strncpy((char *)&riff.type, "WAVE", 4);
// Format header
RIFFChunkHeader fmt_header;
strncpy((char *)&fmt_header.id, "fmt ", 4);
fmt_header.size = sizeof(WaveFormat);
// Format
WaveFormat fmt;
fmt.FormatTag = 1;
fmt.Channels = 1;
fmt.BitsPerSample = 16;
fmt.SampleRate = 44100;
fmt.BlockAlign = 2;
fmt.BytesRate = 44100 * 2;
// Data header
RIFFChunkHeader data_header;
strncpy((char *)&data_header.id, "data", 4);
data_header.size = BUFFER_LENGTH * sizeof(short);
// Generate PCM
oscillator osc;
init_osc(&osc, 44100, 1000, 0.25);
short *buffer = (short *)malloc(BUFFER_LENGTH * sizeof(short));
for (int i = 0; i < BUFFER_LENGTH; i++)
buffer[i] = 32767 * osc_next(&osc);
// Write to file
FILE *f = fopen("sin_1khz.wav", "wb");
fwrite(&riff, sizeof(RIFFHeader), 1, f); // 写入RIFF头
fwrite(&fmt_header, sizeof(RIFFChunkHeader), 1, f); // 写入fmt头
fwrite(&fmt, sizeof(WaveFormat), 1, f); // 写入fmt内容
fwrite(&data_header, sizeof(RIFFChunkHeader), 1, f); // 写入data头
fwrite(buffer, 2, BUFFER_LENGTH, f); // 写入实际数据
free(buffer);
fclose(f);
}
运行程序后会在当前工作目录下生成一个"sin_1khz.wav"的文件,用播放器播放就可以听到嘟~~~的声音了。
读取wav文件
实际上读取wav文件也不难,只要按照区块的标准一个个查找就行了,一般fmt块就是第一个块,而data块则有可能夹在中间,所以我们需要循环读取区块,找出fmt和data这两个块就可以了。
当然这样只适合读取标准PCM编码或者IEEE浮点格式的wav文件,对于其他格式的文件并不支持(需要例如fact块),但是一般这样就足够了。
对于这个过程,我们只关心以下几点就可以了
- fmt块的内容
- 数据在文件中的位置
- 数据的大小
为了方便编码以及使用,读取wav文件的代码使用c++实现
也没什么很复杂的,稍微注意一点细节即可,这个前文其实提到过。
wavread.h
#pragma once
#include <stdio.h>
#include <string.h>
#include "wavfmt.h"
class WaveReader
{
private:
FILE *f;
WaveFormatExtensible fmtext;
Int64 data_pos; // 实际音频数据在文件中的位置
Int64 data_size; // 文件中音频数据的大小
Int64 read_size; // 当前已经读取的音频数据大小
bool find_fmt(); // 用于查找文件中的"fmt "块
bool find_data(); // 用于查找文件中的"data"块
public:
WaveReader();
~WaveReader();
bool open_file(char *filename); // 打开文件
void close_file(); // 关闭文件
const WaveFormatExtensible &get_fmtext(); // 返回fmtext的引用
// 把size个字节的音频数据读到buffer缓冲区
// 返回实际读取的字节数,如果已经读取完了返回-1
int read_data(void *buffer, DWord size);
void reset(); // 重置读取指针,即重新从data_pos的位置读取
};
wavread.cpp
#include "wavread.h"
bool WaveReader::find_fmt()
{
RIFFChunkHeader hd;
long size = 0; // 读取一次Header实际读到的大小,用来判断是否读取完毕
do {
size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
if (hd.size % 2 == 1) // 如果块大小为奇数则需要对齐
hd.size++;
// 判断当前块的ID是不是"fmt "
if (strncmp((char *)&hd.id, "fmt ", 4) != 0)
fseek(f, hd.size, SEEK_CUR); // 不是直接跳过这个块
else
break;
} while (size >= 8);
if (size < 8) // 实际读取不足8字节一般说明到了文件末尾
return false;
// 假设文件的format块小于等于sizeof(WaveFormatExtensible)
// 因为有极少数的格式是有自己的标准的,其尺寸大于微软的WaveFormatExtensible
fread(&fmtext, 1, hd.size, f);
// 判断文件格式是否是PCM或者IEEE编码,也就是FormatTag是1或者3
// 否则是编码过的格式,需要解码,我们不支持这种格式
if (fmtext.FormatTag == 0xFFFE) {
if (fmtext.SubFormat.D1 != 1 && fmtext.SubFormat.D1 != 3)
return false;
}
else if (fmtext.FormatTag != 1 && fmtext.FormatTag != 3) {
return false;
}
return true;
}
bool WaveReader::find_data()
{
RIFFChunkHeader hd;
long size = 0; // 同上
do {
size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
if (hd.size % 2 == 1) // 查找data块过程中这个更加重要
hd.size++; // 因为如果是8bit或者24bit单声道的文件可能不对齐2字节
if (strncmp((char *)&hd.id, "data", 4) != 0) // 同上
fseek(f, hd.size, SEEK_CUR);
else
break;
} while (size >= 8);
if (size < 8)
return false;
fgetpos(f, &data_pos); // 获取实际数据的位置
data_size = hd.size; // 该块的大小即为数据的大小
return true;
}
WaveReader::WaveReader()
{
memset(&fmtext, 0, sizeof(WaveFormatExtensible));
f = NULL;
read_size = 0;
}
WaveReader::~WaveReader()
{
if (f)
fclose(f);
}
bool WaveReader::open_file(char *filename)
{
f = fopen(filename, "rb");
if (f) {
RIFFHeader riff;
fread(&riff, 1, sizeof(RIFFHeader), f); // 读取文件的RIFF头
if (strncmp((char *)&riff.id, "RIFF", 4) != 0) // 判断是不是RIFF文件
return false;
if (strncmp((char *)&riff.type, "WAVE", 4) != 0) // 判断是不是WAVE文件
return false;
// 按照规范,fmt块是第一个块
if (!find_fmt()) // 先找fmt块
return false;
if (!find_data()) // 再找data块
return false;
return true;
}
return false;
}
void WaveReader::close_file()
{
// 清理和初始化必要内容
if (f)
fclose(f);
memset(&fmtext, 0, sizeof(WaveFormatExtensible));
f = NULL;
data_pos = 0;
data_size = 0;
read_size = 0;
}
const WaveFormatExtensible &WaveReader::get_fmtext()
{
return fmtext;
}
int WaveReader::read_data(void *buffer, DWord size)
{
if (read_size >= data_size) { // 已经读完所有数据了
memset(buffer, 0, size); // 缓冲区置0
return -1;
}
int result;
// 已经读取的加上要读取的小于实际的数据大小,说明还没到末尾
if (read_size + size < data_size) {
result = fread(buffer, 1, size, f);
read_size += result; // 累加实际读取的
}
// 否则说明还没有读取的字节数小于需要读取的字节数,即将读完
else {
memset(buffer, 0, size); // 全部清0处理
result = fread(buffer, 1, data_size - read_size, f); // 读取剩下的
read_size = data_size;
}
return result;
}
void WaveReader::reset()
{
read_size = 0;
fseek(f, data_pos, SEEK_SET); // 回到数据开始的地方
}
这段代码可以读取大部分的PCM和IEEE格式的wav文件,只需调用open_file()打开文件,read_data()读取数据,close_file()关闭文件,其他各种见具体示例代码。
不过目前只能读取数据,还没有实现播放,播放API有好多,比较老的比如waveXxx系列,DirectSound,最新的是WASAPI。
目前已经更新了用DirectSound和WASAPI播放声音的教程。
补充内容
关于代码段
short *buffer = malloc(BUFFER_LENGTH * sizeof(short));
经过测试发现使用纯C语言编译器如gcc是完全可以的,因为C语言标准支持这种用法,但是C++标准不支持这种用法,需要使用类型转换,即
short *buffer = (short *)malloc(BUFFER_LENGTH * sizeof(short));
不过呢,对C++来说,用new更好一点,原来的代码是用C的,不过为了与后来的内容一致,现在代码库已经全部改成C++了,所以这一段代码也就改成了
short *buffer = new short[BUFFER_LENGTH];
这样应该是最合理的了。
更新记录
- 2023-01-19:新增gcc 8.5.0链接,新增振荡器原理介绍,添加更多代码注释。
读写wav格式文件的更多相关文章
- JAVA用geotools读写shape格式文件
转自:http://toplchx.iteye.com/blog/1335007 JAVA用geotools读写shape格式文件 (对应geotools版本:2.7.2) (后面添加对应geotoo ...
- Android音频: 怎样使用AudioTrack播放一个WAV格式文件?
翻译 By Long Luo 原文链接:Android Audio: Play a WAV file on an AudioTrack 译者注: 1. 因为这是技术文章,所以有些词句使用原文,表达更准 ...
- wav格式文件、pcm数据
wav格式文件是常见的录音文件,是声音波形文件格式之一,wav 文件由文件头和数据体两部分组成. 文件头是我们在做录音保存到文件的时候,要存储的文件的说明信息,播放器要通过文件头的相关信息去读取数据播 ...
- 使用Spark读写CSV格式文件(转)
原文链接:使用Spark读写CSV格式文件 CSV格式的文件也称为逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号.在本文中的CSV格 ...
- 将PCM格式存储成WAV格式文件
将PCM格式存储成WAV格式文件 WAV比PCM多44个字节(在文件头位置多) 摘自:https://blog.csdn.net/u012173922/article/details/78849076 ...
- WAV格式文件无损合并&帧头数据体解析(python)(原创)
一,百度百科 WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频 ...
- 音频文件解析(一):WAV格式文件头部解析
WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源. 文 ...
- [VB.NET][C#]WAV格式文件头部解析
简介 WAV 为微软开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源. 第一节 文件头 ...
- 如何用python读写CSV 格式文件
工作中经常会碰到读写CSV文件的情况.记录下,方便自己以后查询并与大家一起分享: 写CSV文件方法一: import csv #导入CSV with open("D:\eg ...
- linux下alsa架构音频驱动播放wav格式文件
#include<stdio.h> #include<stdlib.h> #include <string.h> #include <alsa/asoundl ...
随机推荐
- 基于python的数学建模---传染病六个模型
六个模型的区别 SI-Model import scipy.integrate as spi import numpy as np import matplotlib.pyplot as plt # ...
- IDEA项目下out与target目录的区别详解
IDEA项目下out与target目录的区别详解 一.目录主要区别: out存放的是该项目下所有Module(模块)的编译结果. target存放的是单个Module的编译结果. 二.目录详解 out ...
- Iris_data_analysis
SVM调用实例--鸢尾花 任务描述: 构建一个模型,根据鸢尾花的花萼和花瓣大小将其分为三种不同的品种. 数据集: 每一行数据由4个特征值及1个目标值组成,4个特征值分别为:萼片长度.萼片宽度.花瓣长度 ...
- docker部署项目
@ 目录 前言 一.下载安装docker: 1.前提工作 1.1 查看linux版本 1.2 yum包更新到最新 1.3 安装工具包 1.4 设置yum源并更新yum包索引 2.安装docker 2. ...
- 云原生架构(二)环境搭建(Mac上安装Docker+Kubernetes+Istio一条龙)
一.背景 Istio 项目由 Google 和 IBM 的团队与 Lyft 的 Envoy 团队合作启动.它已经完全在 GitHub 上公开开发.目前已经是"Service Mesh服务网格 ...
- MySQL中这14个牛逼的功能,惊艳到我了!!!
前言 我最近几年用MYSQL数据库挺多的,发现了一些非常有用的小玩意,今天拿出来分享到大家,希望对你会有所帮助. 1.group_concat 在我们平常的工作中,使用group by进行分组的场景, ...
- Vuex从入门到精通
一.vuex介绍 目标 什么是Vuex 为什么学习Vuex 通信方案 组件关系 数据通信 父子关系 父传子:props : 子传父:$emit 非父子关系 vuex (一种组件通信方案) Vuex是什 ...
- 14、java.sql.SQLException: 无效的列类型: 1111 报错问题解决
转载自 在项目中利用Mybatis做持久化框架,XXXMapper.xml中insert语句做插入时,偶然会遇到一些报错或者说是问题,如下: 一.报错日志: nested exception is o ...
- CH9120/CH9121 WCH-ETH透传芯片(持续更新)
网络变压器中心抽头: 如果使用网络变压器,变压器的中心抽头需要看PHY芯片时电流型还是电压型. 如果是电压型,则需要通过一个电容直接接到GND. 如果时电流型的PHY,那么就需要根据PHY芯片来看,从 ...
- Jmeter——循环控制器中实现Counter计数器的次数重置
近期在使用Jmeter编写个辅助测试的脚本,用到了多个Loop Controller和Counter. 当时想的思路就是三个可变的数量值,使用循环实现:但第三个可变值的数量次数,是基于第二次循环中得到 ...