C++中使用Essentia实现STFT/ISTFT
最近在做一个项目,需要将音频送入 AI 模型进行处理。整个流程包括:
-
从
.wav
文件中加载音频; -
进行 短时傅里叶变换(STFT);
-
将变换结果输入模型;
-
使用 逆STFT(ISTFT) 重建回时域信号。
作为一个长期从事图像方向的 CVer,我对音频领域相对陌生。在调研后发现,和计算机视觉相比,优质的音频处理库屈指可数。而手撸 STFT / ISTFT 又繁琐、容易出错。
最终,我选择了开源音频分析库 Essentia,其功能强大、API 结构清晰,非常适合科研和快速原型验证。本文将分享如何在 C++ 中基于 Essentia 实现 STFT / ISTFT,并完成音频的重建。
🔍 什么是 Essentia?
Essentia 是由巴塞罗那庞培法布拉大学音乐技术小组(MTG)开发的开源 C++ 音频分析库,发布于 AGPLv3 许可下。
Essentia 提供了一套丰富的算法模块,覆盖以下能力:
-
音频 I/O 和预处理;
-
数字信号处理(DSP)基础块;
-
声学和音乐特征提取(如光谱、音调、节奏、情绪等);
-
Python 包装与 Vamp 插件支持,便于快速原型与可视化。
虽然资料较少、使用门槛稍高,但其结构良好,非常适合做科研实验或工业应用中的音频前处理部分。
🛠 编译 Essentia(推荐 Docker 环境)
手动编译步骤如下:
git clone https://github.com/MTG/essentia.gitcd essentiapackaging/build_3rdparty_static_debian.sh
⚠️ 如果第三方依赖下载失败,可以手动修改 packaging/debian_3rdparty/
下的对应脚本,将源码下载到指定目录。
接着执行:
./waf configure --with-static-examples./waf
如果你希望跳过编译,可以从我上传的打包资源中下载使用。
📦 在 C++ 中使用 STFT
AlgorithmFactory& factory = standard::AlgorithmFactory::instance();// 1. 加载音频Algorithm* loader = factory.create(\"MonoLoader\", \"filename\", \"test.wav\", \"sampleRate\", 16000);vector audio;loader->output(\"audio\").set(audio);loader->compute();delete loader;
设置参数:
const int frameSize = 320; const int hopSize = 80; // 25% overlap
STFT 流程:
Algorithm* frameCutter = factory.create(\"FrameCutter\", \"frameSize\", frameSize, \"hopSize\", hopSize);Algorithm* windowing = factory.create(\"Windowing\", \"type\", \"hann\", \"normalized\", false);Algorithm* fft = factory.create(\"FFT\", \"size\", frameSize);frameCutter->input(\"signal\").set(audio);vector frame, windowedFrame;vector<complex> fftFrame;frameCutter->output(\"frame\").set(frame);windowing->input(\"frame\").set(frame);windowing->output(\"frame\").set(windowedFrame);fft->input(\"frame\").set(windowedFrame);fft->output(\"fft\").set(fftFrame);vector<vector<complex>> stftResult;while (true) { frameCutter->compute(); if (frame.empty()) break; windowing->compute(); fft->compute(); stftResult.push_back(fftFrame);}
🔁 实现 ISTFT
由于 Essentia 并未提供完整的 Overlap-Add 封装,我们需要仿照 librosa 逻辑手动实现。
Algorithm* ifft = factory.create(\"IFFT\", \"size\", frameSize);vector window = computeLibrosaHann(frameSize);Real windowSum = 0.0;for (int i = 0; i < hopSize; ++i) { windowSum += window[i] * window[i];}const Real compensation = 1.0 / windowSum;vector reconstructedAudio(originalLength, 0.0);vector ifftOutputFrame(frameSize);for (int i = 0; i input(\"fft\").set(stftResult[i]); ifft->output(\"frame\").set(ifftOutputFrame); ifft->compute(); int pos = i * hopSize; for (int n = 0; n < frameSize && pos + n < reconstructedAudio.size(); ++n) { reconstructedAudio[pos + n] += ifftOutputFrame[n] * window[n] * compensation; }}
🧹 后处理与音频保存
removeDCOffset(reconstructedAudio);conservativeNormalize(reconstructedAudio, 0.99);// 保存音频Algorithm* writer = factory.create(\"MonoWriter\", \"filename\", \"./reconstructed.wav\", \"sampleRate\", 16000);writer->input(\"audio\").set(reconstructedAudio);writer->compute();delete writer;
🧪 代码完整示例
#include #include #include #include #include #include #include #include using namespace std;using namespace essentia;using namespace essentia::standard;// Librosa风格的汉宁窗计算vector computeLibrosaHann(int size) { vector window(size); for (int i = 0; i < size; ++i) { window[i] = sin(M_PI * i / (size - 1)) * sin(M_PI * i / (size - 1)); } return window;}// 移除直流偏移void removeDCOffset(vector& audio) { Real dcOffset = accumulate(audio.begin(), audio.end(), 0.0) / audio.size(); for (auto& sample : audio) { sample -= dcOffset; }}// 保守归一化void conservativeNormalize(vector& audio, Real targetPeak = 0.99) { Real peak = *max_element(audio.begin(), audio.end(), [](Real a, Real b) { return abs(a) 1e-6) { // 避免除以0 for (auto& sample : audio) { sample *= (targetPeak / abs(peak)); } }}int main() { // 初始化Essentia essentia::init(); AlgorithmFactory& factory = standard::AlgorithmFactory::instance(); // 1. 加载音频 Algorithm* loader = factory.create(\"MonoLoader\", \"filename\", \"/work/000002.wav\", \"sampleRate\", 16000); vector audio; loader->output(\"audio\").set(audio); loader->compute(); delete loader; cout << \"Loaded audio with \" << audio.size() << \" samples\" <input(\"signal\").set(audio); vector frame, windowedFrame; frameCutter->output(\"frame\").set(frame); windowing->input(\"frame\").set(frame); windowing->output(\"frame\").set(windowedFrame); vector<complex> fftFrame; fft->input(\"frame\").set(windowedFrame); fft->output(\"fft\").set(fftFrame); vector<vector<complex>> stftResult; // STFT处理循环 while (true) { frameCutter->compute(); if (frame.empty()) break; windowing->compute(); fft->compute(); stftResult.push_back(fftFrame); } delete frameCutter; delete windowing; delete fft; cout << \"STFT completed. Frames: \" << stftResult.size() << \", Bins: \" << (stftResult.empty() ? 0 : stftResult[0].size()) << endl; // 4. ISTFT处理(librosa风格) Algorithm* ifft = factory.create(\"IFFT\", \"size\", frameSize); // 计算窗函数和补偿因子 vector window = computeLibrosaHann(frameSize); Real windowSum = 0.0; for (int i = 0; i < hopSize; ++i) { windowSum += window[i] * window[i]; } const Real compensation = 1.0 / windowSum; // 重建音频初始化 vector reconstructedAudio(originalLength, 0.0); vector ifftOutputFrame(frameSize); // 处理每一帧 for (int i = 0; i input(\"fft\").set(stftResult[i]); ifft->output(\"frame\").set(ifftOutputFrame); ifft->compute(); // 重叠相加(librosa风格) int pos = i * hopSize; for (int n = 0; n < frameSize && pos + n < reconstructedAudio.size(); ++n) { reconstructedAudio[pos + n] += ifftOutputFrame[n] * window[n] * compensation; } } delete ifft; // 5. 后处理 removeDCOffset(reconstructedAudio); conservativeNormalize(reconstructedAudio, 0.99); // 6. 结果验证 // 计算RMS能量比 auto computeRMS = [](const vector& x) { return sqrt(accumulate(x.begin(), x.end(), 0.0, [](Real sum, Real val) { return sum + val*val; }) / x.size()); }; Real originalRMS = computeRMS(audio); Real reconstructedRMS = computeRMS(reconstructedAudio); cout << \"Volume ratio (reconstructed/original): \" << reconstructedRMS / originalRMS <input(\"audio\").set(reconstructedAudio); writer->compute(); delete writer; essentia::shutdown(); return 0;}
🧩 CMake 配置示例
Essentia 依赖众多第三方库,下面是一个完整的 CMakeLists.txt
配置参考:
cmake_minimum_required(VERSION 3.10)project(gcrn_cpp)set(CMAKE_CXX_STANDARD 17)file(GLOB_RECURSE CORE_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/source/*.cpp)include_directories(${CMAKE_CURRENT_LIST_DIR}/3rdparty)link_directories(${CMAKE_CURRENT_LIST_DIR}/libs/)# 设置 PKG_CONFIG 路径set(ENV{PKG_CONFIG_PATH} \"/essentia-master/packaging/debian_3rdparty/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}\")find_package(PkgConfig REQUIRED)# 依赖库查找pkg_check_modules(SWRESAMPLE REQUIRED libswresample)pkg_check_modules(AVCODEC REQUIRED libavcodec)pkg_check_modules(AVFORMAT REQUIRED libavformat)pkg_check_modules(SAMPLERATE REQUIRED samplerate)pkg_check_modules(FFTW3 REQUIRED fftw3f)pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)pkg_check_modules(TAGLIB REQUIRED taglib)pkg_check_modules(YAML REQUIRED yaml-0.1)pkg_check_modules(EIGEN3 REQUIRED eigen3)include_directories(${EIGEN3_INCLUDE_DIRS})link_directories(${SAMPLERATE_LIBRARY_DIRS})add_executable(gcrn_cpp main.cpp ${CORE_SOURCE_FILES})target_link_libraries(gcrn_cpp PRIVATE essentia ${CHROMAPRINT_LIBRARIES} ${SWRESAMPLE_LIBRARIES} ${SAMPLERATE_LIBRARIES} ${AVFORMAT_LIBRARIES} ${AVCODEC_LIBRARIES} ${AVUTIL_LIBRARIES} ${FFTW3_LIBRARIES} ${TAGLIB_LIBRARIES} ${YAML_LIBRARIES} pthread dl m z)
📚 参考资料
-
官方仓库:https://github.com/MTG/essentia
-
FAQ 页面:Frequently Asked Questions — Essentia 2.1-beta6-dev documentation
-
Librosa STFT 文档:https://librosa.org/doc/main/generated/librosa.stft.html