本文处理常见的刚需——降噪。通话里的键盘声、直播里的风扇声、低光视频的噪点,都是信号淹没在噪声里。端侧降噪的难点是实时性:音频一帧 20ms 内要出结果,视频一帧 33ms 内要出结果。
1、你遇到的场景
“用户在地铁里开语音通话,对面只听到轰隆隆。视频聊天在暗光环境,画面全是雪花点。想加个降噪,传统算法(谱减法、维纳滤波)降完人声发闷、金属音严重;上深度学习模型,手机跑不动实时……”
降噪的两难:
- 降得多 ≠ 听感好 — 降噪太狠,人声失真、音乐变成”电话音”
- 实时预算 — 音频 48kHz 采样,20ms 一帧,模型只能算 5ms 左右
- 场景泛化 — 地铁、咖啡馆、办公室的噪声谱完全不同
2、两条降噪路线
| 维度 | 语音降噪 | 视频降噪 |
|---|---|---|
| 输入 | 单/多声道 PCM | YUV/RGB 帧序列 |
| 主流模型 | RNNoise / 频域掩码 DNN | 时域+空域 CNN(如 FastDVDnet) |
| 帧预算 | 20ms/帧 | 33ms/帧 |
| 关键指标 | 信噪比 SNR + 主观 MOS | PSNR + 主观观感 |
核心洞察: 语音降噪在频域做最划算(噪声和语音在频谱上可分),视频降噪要时域+空域一起做(噪点随时间抖、空间上散)。两者共性:都用「掩码」——估计一个 0~1 的增益,乘回原信号,而不是直接生成干净信号。
语音降噪管线(频域掩码):
PCM → 分帧加窗 → STFT(FFT) → DNN估计掩码 → 掩码×频谱 → iSTFT → 干净PCM
(噪声大处≈0,语音处≈1)
3、语音降噪:RNNoise 风格频域掩码
# 训练侧:频域掩码网络(Claude Code 生成)
# 输入:一帧的幅度谱(161 个频点);输出:每个频点的增益 mask
import torch
import torch.nn as nn
class MaskNet(nn.Module):
"""输入 (batch, 161),输出 (batch, 161) 的增益,0~1"""
def __init__(self):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(161, 256), nn.ReLU(),
nn.Linear(256, 256), nn.ReLU(),
nn.Linear(256, 161), nn.Sigmoid(),
)
def forward(self, mag): # mag: 帧幅度谱
return self.fc(mag) # mask: 0~1 增益
# 参数量 ~150K,量化后不到 200KB,实时毫无压力
4、端侧实时语音降噪
// AudioDenoiser.swift (iOS)
// 实时语音降噪:AVAudioEngine 回调里逐帧处理
final class AudioDenoiser {
private let engine: MLInferenceEngine
private let fft: FFTProcessor // 自实现 STFT/iSTFT
private let window: [Float] // 汉宁窗
init() throws {
engine = try CoreMLEngine(bundle: "masknet")
fft = FFTProcessor(fftSize: 320) // 20ms @16kHz
window = Self.hann(320)
}
/// 输入一帧 PCM(320 采样点),输出降噪后 PCM
func denoise(_ frame: [Float]) -> [Float] {
// 1. 分帧加窗 → FFT → 幅度谱
let mag = fft.magnitude(applyWindow: frame, window: window)
// 2. DNN 估计 mask
let mask = try! engine.predict(MLTensor(shape: [1, 161], data: bytes(mag)))
// 3. mask × 幅度谱(相位保持不变)→ iSTFT → 干净帧
let cleanMag = zip(mag, maskFloats).map(*)
return fft.inverse(cleanMag, phase: fft.phase)
}
}
// AudioDenoiser.kt (Android)
// AudioRecord 读到的 PCM 实时降噪
class AudioDenoiser(private val engine: MLInferenceEngine) {
fun denoise(frame: ShortArray, out: ShortArray) {
// ShortArray → Float 幅度谱 → mask → 干净 PCM
val mag = stft.magnitude(frame)
val mask = engine.predict(MLTensor(intArrayOf(1, 161), floatToBytes(mag)))
val clean = FloatArray(161) { i -> mag[i] * mask[i] }
stft.inverse(clean, out)
}
}
5、视频降噪:时域 + 空域
视频降噪用相邻帧的时域信息,噪点随机分布,多帧平均能压掉噪声又保住细节。
帧 t-2 帧 t-1 帧 t ← 输入多帧
│ │ │
└───┬───┴───┬───┘
▼ ▼
时域对齐(运动补偿)→ 空域 CNN 去噪 → 干净帧 t
# 训练侧:3 帧输入的视频降噪网络(FastDVDnet 思路)
# 输入 (batch, 3帧, H, W),输出干净帧 t
class VideoDenoiseNet(nn.Module):
def __init__(self):
super().__init__()
# 时域融合:3 帧在通道维拼接后过卷积
self.enc = nn.Sequential(
nn.Conv2d(9, 32, 3, padding=1), nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(),
nn.Conv2d(32, 3, 3, padding=1),
)
def forward(self, frames): # frames: 3帧×3通道
return self.enc(frames)
💡 实测取舍:视频降噪在低光场景效果最明显,但 3 帧输入意味着要缓存 2 帧历史,且算量翻倍。低端机可退化成单帧空域降噪,保住实时性。
6、踩坑记录
| # | 问题 | 现象 | 根因 | 修复 |
|---|---|---|---|---|
| 1 | 人声发闷 | 降噪后声音”堵” | mask 过狠,高频语音被压 | mask 下限设 0.1,保留残留 |
| 2 | 金属音/水声 | 背景有”叮叮”声 | 相位不连续 + 帧间不重叠 | 帧间 50% 重叠 + overlap-add |
| 3 | 延迟累积 | 通话延迟越来越高 | 帧缓冲没有对齐 | 处理帧长 = 采集帧长,别攒帧 |
| 4 | 音乐被吃掉 | 放音乐时全变噪 | 降噪把音乐当噪声 | 检测到音乐类信号时旁路降噪 |
| 5 | 视频降噪拖影 | 运动物体后面有”鬼影” | 时域对齐不准 | 运动补偿 + 运动大时减时域权重 |
| 6 | 第一秒有噪声 | 刚开始没降噪 | 模型加载慢 | 预加载 + 首帧 warmup |
学习和提升音视频开发技术,欢迎你加入我们的知识星球

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。