实战:音频波形生成(WAV格式+正弦波合成)
音频处理,听起来挺高大上对吧?其实说白了,就是跟一堆数字打交道。你想想看,计算机里哪有什么声音,只有0和1。我们今天要做的,就是把这些数字变成耳朵能听懂的波形。
我个人习惯把这类问题拆成三步:搞懂格式 → 生成数据 → 写入文件。WAV格式虽然老,但胜在简单,特别适合用来理解音频的底层原理。
WAV文件结构速览
WAV文件本质上就是一个带头的二进制块。我刚开始学的时候,总觉得这玩意儿很神秘,后来发现其实就是几个固定字段拼在一起。
| 偏移量 | 长度(字节) | 内容 | 说明 |
|---|---|---|---|
| 0 | 4 | "RIFF" | 文件标识 |
| 4 | 4 | 文件大小-8 | 整个文件减去前8字节 |
| 8 | 4 | "WAVE" | 格式标识 |
| 12 | 4 | "fmt " | 子块标识(注意有空格) |
| 16 | 4 | 16 | 子块1大小(PCM格式固定16) |
| 20 | 2 | 1 | 音频格式(1=PCM) |
| 22 | 2 | 声道数 | 1=单声道,2=立体声 |
| 24 | 4 | 采样率 | 如44100 |
| 28 | 4 | 字节率 | 采样率×声道数×位深/8 |
| 32 | 2 | 块对齐 | 声道数×位深/8 |
| 34 | 2 | 位深 | 如16 |
| 36 | 4 | "data" | 数据子块标识 |
| 40 | 4 | 数据大小 | 采样数×声道数×位深/8 |
| 44 | ... | 音频数据 | 原始采样值 |
嗯,这里要注意:"fmt "后面那个空格不是笔误,WAV规范就是这么写的。我曾经因为这个空格没写对,折腾了整整一个下午,最后发现是字符串长度问题。
正弦波合成的数学原理
正弦波公式很简单:y(t) = A * sin(2π * f * t)。其中A是振幅,f是频率,t是时间。
但在数字世界里,时间t不是连续的,而是离散的采样点。假设采样率是44100Hz,那么每秒钟就有44100个点。第n个采样点对应的时间就是 t = n / 采样率。
所以代码里实际用的是:sample[n] = A * sin(2π * f * n / 采样率)。
代码实现:从零写出WAV文件
下面这段代码,我建议你一行一行看。它完整地演示了如何生成一个440Hz的正弦波,并保存为16位单声道WAV文件。
#include <iostream>
#include <fstream>
#include <vector>
#include <cmath>
#include <cstdint>
// WAV文件头结构
struct WAVHeader {
char riff[4] = {'R', 'I', 'F', 'F'};
uint32_t fileSize;
char wave[4] = {'W', 'A', 'V', 'E'};
char fmt[4] = {'f', 'm', 't', ' '};
uint32_t fmtSize = 16;
uint16_t audioFormat = 1; // PCM
uint16_t numChannels = 1; // 单声道
uint32_t sampleRate = 44100;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample = 16;
char data[4] = {'d', 'a', 't', 'a'};
uint32_t dataSize;
};
void generateSineWave(const std::string& filename,
double frequency,
double duration,
uint32_t sampleRate = 44100,
uint16_t bitsPerSample = 16) {
// 计算总采样数
uint32_t numSamples = static_cast<uint32_t>(sampleRate * duration);
uint32_t dataSize = numSamples * (bitsPerSample / 8);
// 准备WAV头
WAVHeader header;
header.byteRate = sampleRate * header.numChannels * (bitsPerSample / 8);
header.blockAlign = header.numChannels * (bitsPerSample / 8);
header.dataSize = dataSize;
header.fileSize = 36 + dataSize; // 整个文件减去前8字节
// 生成采样数据
std::vector<int16_t> samples(numSamples);
double amplitude = 30000; // 避免削波,留点余量
double twoPiF = 2.0 * M_PI * frequency;
for (uint32_t i = 0; i < numSamples; ++i) {
double t = static_cast<double>(i) / sampleRate;
samples[i] = static_cast<int16_t>(amplitude * sin(twoPiF * t));
}
// 写入文件
std::ofstream outFile(filename, std::ios::binary);
if (!outFile) {
std::cerr << "无法打开文件: " << filename << std::endl;
return;
}
outFile.write(reinterpret_cast<const char*>(&header), sizeof(header));
outFile.write(reinterpret_cast<const char*>(samples.data()), dataSize);
outFile.close();
std::cout << "生成成功: " << filename << std::endl;
std::cout << "采样数: " << numSamples << std::endl;
std::cout << "时长: " << duration << " 秒" << std::endl;
}
int main() {
// 生成440Hz标准音,持续2秒
generateSineWave("output.wav", 440.0, 2.0);
return 0;
}
多频率叠加:生成更丰富的音色
单一正弦波听起来很单调,就像老式电话的拨号音。实际乐器发出的声音,都是基频加上一系列泛音。我们可以把多个频率的正弦波叠加起来,模拟出更复杂的音色。
// 生成带泛音的正弦波
std::vector<int16_t> generateHarmonic(double frequency,
uint32_t numSamples,
uint32_t sampleRate) {
std::vector<int16_t> samples(numSamples, 0);
double baseAmplitude = 20000;
// 基频 + 前4个泛音
double harmonics[] = {1.0, 2.0, 3.0, 4.0, 5.0};
double amplitudes[] = {1.0, 0.5, 0.3, 0.2, 0.1};
for (int h = 0; h < 5; ++h) {
double freq = frequency * harmonics[h];
double amp = baseAmplitude * amplitudes[h];
double twoPiF = 2.0 * M_PI * freq;
for (uint32_t i = 0; i < numSamples; ++i) {
double t = static_cast<double>(i) / sampleRate;
samples[i] += static_cast<int16_t>(amp * sin(twoPiF * t));
}
}
return samples;
}
你看,代码其实不复杂。核心就是循环里做加法。但要注意:多个波形叠加后,振幅可能会超过32767。我建议你先算一下理论最大值,或者生成后做一次归一化处理。
知识体系与核心逻辑
下面这张图,帮你理清整个音频波形生成的核心脉络:
避坑指南与经验之谈
做这个项目,有几个坑我踩过,你注意一下:
- 字节序问题:WAV格式使用小端字节序。如果你在ARM或PowerPC上跑,记得检查字节序。我曾在嵌入式设备上吃过这个亏,生成的WAV文件在PC上播放全是噪音。
- 文件大小计算:WAV头里的
fileSize字段是整个文件大小减去8,不是整个文件大小。这个细节很容易搞错。 - 采样率选择:44100Hz是CD音质,22050Hz是电话音质。如果你只是测试,用8000Hz就够了,文件小很多。
- 内存管理:生成长时间音频时,采样数据可能很大。比如44.1kHz、16位立体声,1分钟就是10MB左右。建议分段写入,不要一次性全放内存里。
说实话,音频编程入门并不难。你只要搞懂了WAV头的结构,再加上正弦波公式,就能生成任何你想要的声音。从简单的单音,到复杂的和弦,再到完整的音乐片段——这条路是通的。
我建议你从440Hz开始,这是标准音A4。然后试试523Hz(C5)、659Hz(E5),把它们叠加起来就是一个大三和弦。听着自己代码生成的声音,那种成就感,嗯,挺特别的。
公众号:蓝海资料掘金营,微信deep3321