短时傅里叶变换(STFT)是音频信号处理中最常用的时频分析工具之一。与传统的傅里叶变换不同,STFT通过将长音频信号分割成短时帧序列,再对每一帧进行傅里叶变换,从而获得信号随时间变化的频率成分。
在C++中实现STFT需要理解几个关键概念:
时域与频域:音频信号在时域表现为振幅随时间变化的波形,而频域则展示不同频率成分的能量分布。STFT的核心价值在于同时保留时域和频域信息。
分帧(Framing):将连续的音频流切分为20-40ms的短时帧(典型采样率44.1kHz下约882-1764个样本)。这个时长选择基于人耳听觉特性(音素持续时间)和频率分辨率的需求。
加窗(Windowing):使用汉宁窗(Hann)或汉明窗(Hamming)等窗函数减少频谱泄漏。窗函数的选择直接影响频谱分析的准确性,汉宁窗在频率分辨率和幅值精度之间提供了良好平衡。
重叠(Overlap):帧间通常采用50%重叠(hop size=帧长/2)以确保时域连续性,避免信息丢失。更高的重叠率会增加计算量但能提升时域分辨率。
C++标准库不直接提供STFT实现,开发者面临三种选择:
FFTW3:C语言编写的高性能傅里叶变换库,MIT许可证。优势包括:
KissFFT:轻量级替代方案,更适合嵌入式环境。特点:
手动实现FFT:仅建议用于教学目的。实际工程中的主要问题:
重要提示:避免使用Eigen或Armadillo等线性代数库直接实现STFT。虽然它们提供FFT功能,但缺少窗函数管理和帧滑动逻辑,需要额外编写大量胶水代码。
以下是使用FFTW3实现STFT的典型工作流程:
cpp复制#include <fftw3.h>
// 1. 参数配置
const int frame_size = 1024; // 帧长
const int hop_size = 512; // 帧移
const int n_frames = (audio_length - frame_size) / hop_size + 1;
// 2. 内存预分配
double* in = (double*)fftw_malloc(sizeof(double) * frame_size);
fftw_complex* out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * (frame_size/2+1));
fftw_plan plan = fftw_plan_dft_r2c_1d(frame_size, in, out, FFTW_MEASURE);
// 3. 处理循环
for (int i = 0; i < n_frames; ++i) {
// 分帧
int offset = i * hop_size;
for (int j = 0; j < frame_size; ++j) {
in[j] = audio_data[offset + j] * hann_window[j]; // 加窗
}
// 执行FFT
fftw_execute(plan);
// 后处理(幅度谱、dB转换等)
process_spectrum(out, frame_size/2+1);
}
// 4. 资源释放
fftw_destroy_plan(plan);
fftw_free(in);
fftw_free(out);
关键注意事项:
fftw_malloc而非new/malloc确保内存对齐FFTW_MEASURE参数让FFTW寻找最优算法,首次运行较慢但后续变换更快分帧过程需要考虑边界处理。对于实时流式处理,通常采用环形缓冲区策略:
cpp复制class AudioBuffer {
private:
std::vector<double> buffer;
size_t write_pos = 0;
public:
void push_samples(const double* samples, size_t count) {
if (write_pos + count > buffer.size()) {
size_t first_part = buffer.size() - write_pos;
std::copy(samples, samples + first_part, buffer.begin() + write_pos);
std::copy(samples + first_part, samples + count, buffer.begin());
write_pos = count - first_part;
} else {
std::copy(samples, samples + count, buffer.begin() + write_pos);
write_pos += count;
}
}
bool get_frame(size_t frame_size, double* out_frame) {
// 检查是否有足够数据
if (available_samples() < frame_size) return false;
// 从write_pos向前取frame_size个样本(处理环回)
// ...
return true;
}
};
加窗操作需要预计算窗函数系数。汉宁窗的标准实现:
cpp复制std::vector<double> create_hann_window(size_t N) {
std::vector<double> window(N);
for (size_t n = 0; n < N; ++n) {
window[n] = 0.5 * (1 - cos(2 * M_PI * n / (N - 1)));
}
return window;
}
FFT输出需要转换为有物理意义的频谱表示:
幅度谱计算:
cpp复制void compute_magnitude(fftw_complex* fft_out, size_t N, double* magnitude) {
for (size_t i = 0; i < N; ++i) {
magnitude[i] = sqrt(fft_out[i][0]*fft_out[i][0] +
fft_out[i][1]*fft_out[i][1]);
}
}
dB转换:
cpp复制void to_decibel(double* magnitude, size_t N, double ref = 1.0) {
for (size_t i = 0; i < N; ++i) {
magnitude[i] = 20 * log10(std::max(magnitude[i], 1e-12) / ref);
}
}
频率轴映射:
cpp复制double bin_to_frequency(int bin, int fft_size, double sample_rate) {
return bin * sample_rate / fft_size;
}
fftw_plan_with_nthreads()开启多线程支持pthread_setschedparam)cpp复制#include <chrono>
auto start = std::chrono::high_resolution_clock::now();
// STFT处理...
auto end = std::chrono::high_resolution_clock::now();
double latency_ms = std::chrono::duration<double, std::milli>(end-start).count();
直流偏移(零频能量过高):
频谱泄漏:
频率分辨率不足:
FFT输出NaN/Inf:
幅度谱出现负值:
std::max(mag, 1e-12))以下是一个面向实时音频流的STFT处理器实现框架:
cpp复制class STFTProcessor {
public:
STFTProcessor(int frame_size, int hop_size, double sample_rate)
: frame_size_(frame_size),
hop_size_(hop_size),
window_(create_hann_window(frame_size)),
input_buffer_(frame_size * 2) // 双倍长度环形缓冲
{
in_ = (double*)fftw_malloc(sizeof(double) * frame_size);
out_ = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * (frame_size/2+1));
plan_ = fftw_plan_dft_r2c_1d(frame_size, in_, out_, FFTW_MEASURE);
}
~STFTProcessor() {
fftw_destroy_plan(plan_);
fftw_free(in_);
fftw_free(out_);
}
void process_samples(const double* samples, size_t count) {
input_buffer_.push_samples(samples, count);
while (input_buffer_.get_frame(frame_size_, in_)) {
// 加窗
for (int i = 0; i < frame_size_; ++i) {
in_[i] *= window_[i];
}
// FFT
fftw_execute(plan_);
// 后处理
std::vector<double> spectrum(frame_size_/2+1);
compute_magnitude(out_, spectrum.size(), spectrum.data());
to_decibel(spectrum.data(), spectrum.size());
// 通知观察者
if (spectrum_callback_) {
spectrum_callback_(spectrum);
}
// 移动读指针
input_buffer_.consume(hop_size_);
}
}
void set_spectrum_callback(std::function<void(const std::vector<double>&)> cb) {
spectrum_callback_ = cb;
}
private:
// ... 其他成员函数和变量
};
实际部署时,这个类可以与PortAudio或RtAudio等音频库配合使用,构建完整的实时频谱分析系统。