资讯动态

音频去噪C++实现:谱减法核心原理与完整代码解析

发布时间:2026/10/20 3:23:29 来源:尧图企业网站定制
简介这是一套面向音频处理初学者的C去噪示例工程围绕谱减法和滤波器等经典方法展开详细讲解噪声估计、阈值处理与后处理流程适合在语音识别、音频分析、音乐处理等场景中学习降噪算法。压缩包共十七个文件包含六个头文件、三个C语言源文件、三个C语言源文件以及Visual Studio的解决方案与工程配置、音频读写模块另附一段带噪语音样本整包仅三十九KB体量轻巧便于阅读。代码注释详尽从读取音频、FFT变换到去噪输出形成完整闭环可帮助开发者快速理解算法实现与改造要点。已有两千一百八十一人浏览学习适合希望掌握音频处理基础并快速上手的开发者参考。工程结构清晰亦可作为课程设计或项目复用的基础模块。1. 音频去噪C代码一段注释清晰的降噪实现能当什么用拿到一段会议录音背景里空调风机和键盘敲击声混成一团网上现成的降噪工具跑完不是人声发闷就是冒出一股“水声”。如果你也在找能自己控制效果的去噪方案这份音频去噪C代码会省掉不少事它是纯 C 写的谱减降噪实现注释从分帧、FFT 到重叠相加每一步都标得清楚无外部依赖编译完直接对 WAV 文件跑一遍就能得到降噪后的音频。适合想把语音降噪接进自己工具链的开发者也适合第一次接触音频处理、想看懂每一行在干什么的读者。无论是处理录音素材还是为实时音频设备做离线算法验证这份代码都能当起点。2. 技术选型与工程结构为什么是谱减法源码目录怎么排2.1 为什么选谱减法而不是深度学习模型去噪这条路有挺多走法RNN 降噪模型、维纳滤波、谱减法、小波去噪。这份代码选的是谱减法理由很现实——它可读性强、无训练依赖、参数透明。谱减法的核心逻辑用一句话能讲清噪声在一段录音里是相对平稳的那么把带噪语音频谱的幅度减去噪声频谱的幅度剩下的就是相对干净的语音。具体到实现链路是分帧、加窗、FFT 转到频域、估计噪声功率谱、逐频点做谱减、IFFT 转回时域、重叠相加还原完整信号。选择它而不是深度学习模型主要是为了“可解释”和“能改”。深度学习降噪效果好但一上来就是训练集、模型格式、推理框架做成工程少说要牵扯几个大依赖。谱减法整个算法的主体就是几十行数学运算任何一个频点的增益是怎么算出来的都可以单独打印出来看。对于教学和二次开发这种透明感太重要了。2.2 工程目录与模块职责拿到源码后先别急着编译把目录结构过一遍。工程按模块拆分大致长这样audio-denoise/ ├── CMakeLists.txt ├── README.md ├── src/ │ ├── main.cpp # 入口解析命令行参数、串起处理流程 │ ├── wav_io.h │ ├── wav_io.cpp # WAV 文件读取与写出只支持 16bit PCM │ ├── denoiser.h │ ├── denoiser.cpp # 核心去噪器噪声估计、谱减、重叠相加 │ └── fft.h │ └── fft.cpp # 自带的基 2 FFT要求帧长是 2 的幂 └── test/ ├── noisy.wav # 带噪示例 └── clean.wav # 参考输出样例模块职责分得很清楚wav_io管文件解析fft管频谱变换denoiser管业务算法main负责把这三块串起来。这是我个人非常喜欢的拆分方式——算法部分完全不感知文件格式FFT 实现也可以被单独替换成更快的第三方库。有一点值得注意FFT 模块是自己实现的基 2 算法这意味着帧长必须是 2 的幂比如 512、1024、2048否则fft内部的分治逻辑会直接越界。后面编译跑的时候如果改了帧长参数务必确认这个约束。以下是核心模块的职责参数表。模块职责对外接口wav_io解析 16bit PCM 单声道 WAV写出结果LoadWav/SaveWavfft时域频域互转原地计算Forward/Inversedenoiser噪声估计、谱减、加窗与重叠相加ProcessFile/EstimateNoisemain参数解析、组装调用链命令行入口3. 核心实现拆解一帧音频从进入到输出的完整链路3.1 分帧加窗与噪声功率谱估计音频去噪不是一口气把整段文件处理完而是切成固定长度的帧逐帧变换、逐帧处理。完整链路是读取 WAV → 取前若干帧估计噪声 → 分帧加窗 → FFT → 谱减增益 → IFFT → 重叠相加 → 写出 WAV。帧长默认取 1024对应 44.1kHz 采样率下大约 23 毫秒。这个量级在语音去噪里比较常见太短了频率分辨率不够频域里分不清噪声和语音太长了语音会被当成平稳信号处理听起来像“慢动作”。帧移取一半也就是 512配合汉宁窗做 50% 重叠后面合成时能天然抵消窗函数造成的幅度调制。噪声估计用的是最朴素也最有效的做法拿音频开头的 N 帧当纯噪声来算平均功率谱。大多数录音在正式人声之前都会有一段几十到几百毫秒的环境底噪代码默认取前 10 帧。核心逻辑如下static const int NOISE_FRAMES 10; // 用开头 10 帧估计噪声底 void Denoiser::EstimateNoise(const int16_t* pcm, int total_samples) { const int frame_size 1024; // 逐帧累积噪声功率最后取平均得到噪声功率谱 for (int n 0; n NOISE_FRAMES; n) { float frame[frame_size]; for (int i 0; i frame_size; i) { int idx n * frame_size i; if (idx total_samples) break; float x pcm[idx] / 32768.0f; // 整型 PCM 转浮点 frame[i] x * Hann(i, frame_size); // 加窗压制频谱泄漏 } fft_.Forward(frame, spec_re_, spec_im_); // 复用缓冲区避免每帧分配 for (int k 0; k half_n_; k) { noise_power_[k] spec_re_[k] * spec_re_[k] spec_im_[k] * spec_im_[k]; } } for (int k 0; k half_n_; k) { noise_power_[k] / NOISE_FRAMES; } }逻辑说明EstimateNoise先把开头 10 帧做加窗和 FFT再把每个频点的功率累加后取平均。spec_re_和spec_im_是类内复用的缓冲区每次 FFT 的结果直接覆盖写避免高频次内存分配带来的性能抖动这个习惯在流式处理里尤其重要。half_n_是帧长一半加一因为实数信号的幅度谱关于中点共轭对称只保留前一半频点就够了。参数说明NOISE_FRAMES控制噪声估计的时长10 帧约 230 毫秒。如果录音开头没有纯净噪声段这个值会导致噪声估计完全错误后面第 5 章会细说。32768.0f是把 16bit 整型 PCM 归一化到 [-1, 1] 浮点区间。3.2 谱减增益计算alpha、beta 与频谱下限拿到噪声功率谱之后对每一帧做谱减。这里用的是带“过减因子”的改进谱减法直接用估计噪声去减容易在频谱上留下随机尖峰听感就是断断续续的“音乐噪声”。把减除量放大到alpha倍残余噪声会被压得更低。void Denoiser::SpectralSubtract() { const float alpha 2.0f; // 过减因子越大降噪越狠语音损伤越大 const float beta 0.05f; // 频谱下限避免个别频点被完全挖空 for (int k 0; k half_n_; k) { float x_power spec_re_[k] * spec_re_[k] spec_im_[k] * spec_im_[k]; float n_power noise_power_[k]; // 功率域做减法换算成幅度增益并钳到下限 float gain 1.0f - alpha * (n_power / (x_power 1e-6f)); gain std::max(gain, beta); gain sqrtf(gain); spec_re_[k] * gain; spec_im_[k] * gain; } }逻辑说明代码先算当前帧的功率x_power和估计噪声功率n_power的比值用 1 减去alpha倍的比值得到增益最后把增益下限钳到beta并开平方。之所以开平方是因为做减法的对象是功率而乘回到频谱上的是幅度增益二者是平方关系。参数说明alpha默认 2.0意思是噪声估计的 2 倍都要被减掉。调大能让底噪更干净但语音的轻声部分会先被削掉调小则保留更多语音细节。beta默认 0.05相当于允许每个频点最多衰减到原来的 5%防止增益变成负值。增益为负意味着相位翻转 180 度听感上就是“破音”“金属声”。1e-6f是防除零保护避免静音帧里x_power为 0 时出现除零崩溃。3.3 重叠相加还原时域信号频域处理完要 IFFT 回到时域再把一帧一帧的结果拼回完整音频。直接拼接会有帧边界跳变必须重叠相加。代码选的是 50% 重叠的汉宁窗分析时加一次汉宁窗合成时直接叠加窗函数平方和近似恒定不需要额外归一化。void Denoiser::OverlapAdd(int16_t* output, int out_len) { for (int i 0; i frame_size_; i) { int idx frame_index_ * hop_size_ i; if (idx out_len) break; out_buf_[idx] synth_frame_[i]; // synth_frame_ 是 IFFT 后的时域帧 } frame_index_; }逻辑说明frame_index_记录当前是第几帧hop_size_是帧移 512因此第 N 帧的输出位置从N * 512开始写。每个样本点会被前后两帧各贡献一次幅度自然互补。这里的原理是汉宁窗的“常数重叠相加”特性50% 重叠时两个窗函数在重叠区的平方和接近常量所以不需要再做归一化除法。如果改成 75% 重叠或者换矩形窗这条性质就不成立了输出会出现明显的“哇哇”包络抖动。这是重叠相加最容易写错的地方也是很多网上代码跑起来有杂音的根源。4. 编译与运行从源码到第一次跑通4.1 编译方式与工程约定工程使用 CMake 构建依赖只有标准库和 C11 以上的编译器不涉及第三方音频库。在 Linux 或 macOS 下按常规流程编译cmake -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j编译产物是名为denoise的可执行文件。源码里没有硬编码路径噪音输入输出都由命令行参数指定。Windows 下可以用 Visual Studio 的 CMake 支持直接打开CMakeLists.txt或者用 MinGW 跑同样的命令。编译前建议确认两件事一是编译器版本代码里用了std::max之类的基础 STL只要不是十年以上的古董编译器都没问题二是帧长默认值 1024如果你想改成 2048 或 4096记得帧长必须是 2 的幂否则fft.cpp里基 2 的分治循环会访问越界这种错误在 Release 模式下非常难排查。4.2 命令行参数与调参影响编译完成后处理一段音频的完整命令如下./build/denoise test/noisy.wav test/out.wav --alpha 2.0 --beta 0.05其中test/noisy.wav是输入test/out.wav是输出--alpha和--beta是对应第 3 章里的过减因子与频谱下限。下面是本项目支持的主要参数参数默认值作用调节方向--alpha2.0过减因子控制降噪强度底噪大就调高人声发闷就调低--beta0.05频谱下限防止频点被挖空音乐噪声明显时可适当调高--noise-frames10用于噪声估计的起始帧数开头噪声段短则调小--frame-size1024FFT 帧长须为 2 的幂语音场景保持默认即可参数说明--alpha和--beta是听感调参里最重要的两个旋钮。alpha从 1.5 调到 3.0底噪会从“能听到但不大”变成“几乎消失”同时人声的高频齿音也会随之损失。beta的作用更隐蔽它不改变整体降噪深度只保证没有频点被彻底归零让残余噪声听起来像“柔和的底噪”而不是“断续的吱吱声”。4.3 怎么判断降噪结果有没有效跑完第一版输出别急着拿去交差先做三轮验证。第一轮听整体人声可懂度是否下降底噪是否还在“抢戏”第二轮看波形在 Audacity 里打开输入输出对比静音段——被压下去的应该是噪声底而不是语音的包络第三轮看频谱图test/clean.wav是参考样例如果输出在 1kHz 以上的频段被削成一片空白说明alpha调大了人声的齿音和辅音已经没了。我一般还会跑一个“静音输入”测试直接用一段几秒的纯底噪录音做输入降噪后如果输出波形接近一条直线说明噪声估计和谱减工作正常如果输出还有明显的起伏说明噪声估计不准或者beta设置过高。这个测试脚本对后续改代码特别有用。5. 避坑清单音频去噪项目最常见的五个翻车现场5.1 处理完人声发闷像隔了一层被子现象底噪确实下去了但人声也跟着发闷说句话像在隔壁房间录的齿音和唇音几乎消失。原因alpha过高时谱减会把语音功率偏低的频段当成噪声一起减掉。语音的辅音和高频谐波能量本身就不高在噪声掩蔽下更弱过减之后直接低于beta于是被压到几乎听不见。解决把alpha降到 1.51.8或者把beta从 0.05 提到 0.1。经验值是当你能明显听出“人声发闷”时先动beta因为提升频谱下限等于保留一部分高频底噪作为掩盖人声细节的“底垫”。5.2 去噪之后出现“音乐噪声”或“水声”现象听感上底噪变成一阵一阵的咝咝声给人一种“水下说话”的感觉。原因相邻帧的频谱各自独立做谱减噪声残留的频点位置在帧间随机跳变听感就是忽大忽小的离散音调也就是常说的 musical tone。解决两个方向。一是把alpha提上去让残余噪声整体降到听不见的水平二是引入频谱平滑常见做法是对增益做一阶递归平滑gain[k] 0.7 * gain[k] 0.3 * prev_gain[k]让相邻帧的增益缓慢变化消除帧间跳变感。这个改动量很小但效果立竿见影是二次开发里最值得先做的事情之一。5.3 WAV 头解析报错输出全是爆音现象程序没崩但输出文件打开后全是“咔咔啪啪”的爆音或者读入的音频速度不对、音调不对。原因WAV 文件头部不一定是固定的 44 字节。很多工具会在data块前插入JUNK、LIST、FACT等扩展块如果代码里写死offset 44就会把扩展块的内容当成 PCM 数据读取。另外位深、声道数不匹配也会出现类似问题。解决解析 WAV 头时按块结构读取先找fmt块确认位深和声道数再找data块拿到真实的偏移和数据长度。本项目只支持 16bit PCM 单声道遇到 24bit 或浮点格式会直接报错而不是乱读。如果你手里只有双声道文件最省事的方式是用音频工具先转成单声道再喂给程序。5.4 音频开头和结尾出现咔嗒声现象整段音频处理完整体正常唯独开头和结尾各有一声“咔嗒”像静电脉冲。原因重叠相加要求每个样本点被前后两帧完整覆盖。音频开头的第一帧只有一次加窗贡献没有前一帧来补足窗函数的斜坡会直接暴露在波形上听感就是咔嗒。结尾同理。解决处理时把信号整体延后一帧或者处理完把首尾各半帧强制置零。更通用的做法是在开头补一段与帧长等长的零样本处理完再裁掉同样长度。这样所有帧都处于“有前一帧也有后一帧”的完整重叠状态边界咔嗒自然消失。5.5 噪声段混进人声导致人声被反向挖掉现象人声连续的部分被削掉一整块听起来像“抽风”但底噪确实很干净。原因EstimateNoise固定取前 10 帧平均。如果录音开头根本没有纯噪声段比如人声从第一毫秒就开始了那么“噪声功率谱”里混入了大量语音能量。谱减时用这个错误的噪声谱去减等于把人声当噪声挖掉。解决加一个检测逻辑在前 N 帧里逐帧计算能量取能量最小的几帧做噪声估计而不是无条件取前 10 帧。代码上只需要在EstimateNoise里多存一个最小值维护复杂度几乎不变但适用场景直接从“录音开头必须有静音”扩展到任意起始位置。6. 二次开发思路把文件式去噪改成可复用模块6.1 把去噪器抽象成类接入流式输入原工程是“读完整文件 → 处理 → 写完整文件”的离线模式。要把它接入实时链路第一个改造是用流式输入替换文件循环。核心思路是维护一个内部缓冲攒够一帧就处理一帧处理完输出hop_size个样本然后滑动缓冲。void StreamingDenoise(std::spanconst int16_t chunk) { // 新数据追加到内部缓冲尾部 internal_buf_.insert(internal_buf_.end(), chunk.begin(), chunk.end()); // 凑够一帧就处理一次输出 hop_size 个样本 while (internal_buf_.size() frame_size_) { ProcessFrame(internal_buf_.data()); // 输出从 processed_frames_ * hop_size_ 开始的 hop_size_ 个样本 EmitOutput(processed_frames_ * hop_size_, hop_size_); // 滑动缓冲丢弃已处理的 hop_size_ 个样本 internal_buf_.erase(internal_buf_.begin(), internal_buf_.begin() hop_size_); processed_frames_; } }逻辑说明internal_buf_始终保留frame_size_ - hop_size_个历史样本保证下一帧的分析窗完整。ProcessFrame内部就是第 3 章里的加窗、FFT、谱减、IFFT 一条链。重叠相加的输出位置用全局帧序号计算与缓冲区的滑动无关这样流式版本和离线版本的重建逻辑完全一致。改造完成后验证方法很简单拿同一个 WAV 文件分别用离线版和流式版跑一遍逐样本对比输出。两者误差应接近浮点精度任何超过千分之一的差异都说明滑动缓冲或帧序号算错了。从那以后我每次改完音频链路都会强制走一遍“离线 vs 流式对比”这个流程这个习惯帮我拦下了至少三次隐蔽的边界 bug。6.2 下一步可以做的三件小事第一件是做自适应alpha。按帧估算信噪比信噪比高时降低减除量信噪比低时加大减除量能显著缓解“人声发闷”和“音乐噪声”这对矛盾。第二件是把谱减换成维纳滤波增益公式从1 - alpha * (N/X)变成SNR / (SNR 1)改动集中在SpectralSubtract一个函数里效果通常比谱减更柔和。第三件是加一个轻量 VAD把纯噪声段找出来动态更新噪声谱前面 5.5 节的问题就能彻底解决。做完这三件事这个工程基本就从一个“能跑的示例”变成了“能用的降噪组件”了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑