AtomGit Flutter 鸿蒙客户端:高斯噪声模拟自然音效的数学原理
雨声不是"随机噪声"——它是经过精确滤波和调制的信号。本文揭示 E-Brufen 中四种自然音效背后的数学原理和算法实现。
一、为什么高斯噪声而非均匀噪声?
1.1 Box-Muller 变换
final _rand = Random();
double _gauss() {
final u1 = _rand.nextDouble(); // [0, 1) 均匀分布
final u2 = _rand.nextDouble(); // [0, 1) 均匀分布
return sqrt(-2 * log(max(u1, 0.0001))) * cos(2 * pi * u2);
}
这是经典的 Box-Muller 变换——将两个独立的 [0,1) 均匀随机数转换为标准正态分布 N(0,1)。
1.2 为什么自然音效需要高斯分布?
均匀噪声 (Random.nextDouble)
├─ 频谱:所有频率等概率
├─ 听感:刺耳的"静电声"
└─ 原因:自然界不存在等概率的频谱分布
高斯噪声 (Box-Muller)
├─ 频谱:能量集中在均值附近
├─ 听感:柔和的"沙沙声"
└─ 原因:自然界的大多数随机过程遵循正态分布
★ Insight ─────────────────────────────────────
自然界的声音之所以"自然",是因为它们遵循中心极限定理——大量独立随机因素的叠加趋于正态分布。雨滴落下的声音是成千上万个小水滴的叠加 → 高斯分布。直接使用均匀分布的Random.nextDouble()生成的声音听起来"不自然",根因就在这里。─────────────────────────────────────────────────
二、雨声:高通滤波 + 低频包络
2.1 算法流程
高斯白噪声 → 低通滤波(LPF) → raw - LPF×0.5 = 高通 → × 双正弦包络 → 雨声
× 随机"大水滴" (0.3%)
2.2 完整实现

List<int> _generateRain(String path, int sr, int durSec) {
final n = sr * durSec;
final samples = <int>[];
double lpf = 0; // 一阶低通滤波器记忆
for (var i = 0; i < n; i++) {
final t = i / sr;
// 包络:两个不同频率正弦波的乘积 → 不规则的疏密变化
final envelope = 0.6 + 0.4 * sin(2 * pi * 0.07 * t) * sin(2 * pi * 0.13 * t);
final raw = _gauss();
lpf = 0.3 * raw + 0.7 * lpf; // 一阶 IIR 低通:y[n] = 0.3x[n] + 0.7y[n-1]
final high = raw - lpf * 0.5; // 高通 = 原始 - 低通分量
var val = high * envelope * 0.5;
// 偶尔的"大水滴"——模拟雨滴打在屋檐/窗户上的声音
if (_rand.nextDouble() < 0.003) {
val += _gauss() * 1.8;
}
samples.add((val * 18000).toInt());
}
_writeWav(path, sr, samples);
return samples;
}
2.3 参数解析
| 参数 | 值 | 作用 |
|---|---|---|
lpf 系数 (0.3, 0.7) |
y[n] = 0.3x[n] + 0.7y[n-1] | 截止频率约 1kHz |
| 包络 sin(0.07t)×sin(0.13t) | ~14s 和 ~7.7s 周期 | "时大时小"的雨势变化 |
| 大水滴概率 | 0.3% | 平均每秒 6-7 个突出滴声 |
三、海浪:布朗噪声(1/f² 频谱)
3.1 布朗噪声的数学定义
布朗噪声 = 白噪声的积分(累积和):
brown[n] = brown[n-1] + white[n] × 步长
频谱特性:power ∝ 1/f²,即频率每翻一倍,能量降为 1/4。这产生了极其低沉、隆隆的声音。
3.2 实现
List<int> _generateOcean(String path, int sr, int durSec) {
final n = sr * durSec;
final samples = <int>[];
double brown = 0;
for (var i = 0; i < n; i++) {
final t = i / sr;
brown += _gauss() * 0.02; // 积分步长 = 0.02
brown = brown.clamp(-3.0, 3.0); // 防止数值漂移
// 海浪包络:两个慢波相乘
final wave1 = (sin(2 * pi * 0.08 * t) + 1) / 2; // 12.5s 周期
final wave2 = (sin(2 * pi * 0.05 * t + 1.5) + 1) / 2; // 20s 周期
final envelope = 0.2 + 0.8 * wave1 * wave2;
samples.add((brown * envelope * 0.7 * 25000).toInt());
}
_writeWav(path, sr, samples);
return samples;
}
★ Insight ─────────────────────────────────────
包络使用两个不同频率、不同相位的正弦波相乘而非相加——这是关键。相加产生的波形规则可预测(“听几秒就知道下一个浪什么时候来”),而相乘产生不规则的复合波(“浪高浪低没有固定规律”)。真正的海浪正是如此——受月球引力、风速、海底地形等多因素影响,永远不会呈现完美的周期性。─────────────────────────────────────────────────
3.3 参数分析
| 参数 | 值 | 效果 |
|---|---|---|
| 积分步长 | 0.02 | 控制"隆隆"的强度 |
| clamp 范围 | [-3.0, 3.0] | 防止长时间积分漂移导致失真 |
| wave1 频率 | 0.08 Hz | 主浪周期 ~12.5 秒 |
| wave2 频率 | 0.05 Hz | 次浪周期 ~20 秒 |
| 相位偏移 | 1.5 rad | 避免两波同步叠加 |
四、篝火:稀疏事件模型
4.1 三层声音结构
篝火声音模型
├─ 底层:持续低频底噪 (base) → 火焰的"身体"
├─ 中层:4% 概率的噼啪脉冲 (2-22样本) → 小木柴爆裂
└─ 顶层:0.2% 概率的大爆裂 (2.5×振幅) → 大木块崩裂
4.2 实现
List<int> _generateFire(String path, int sr, int durSec) {
final n = sr * durSec;
final samples = <int>[];
for (var i = 0; i < n; i++) {
final base = _gauss() * 0.15; // 持续低频底噪
var crackle = 0.0;
if (_rand.nextDouble() < 0.04) { // 4% 噼啪概率
final crackLen = 2 + _rand.nextInt(20); // 2-22 样本
final crackAmp = 0.3 + _rand.nextDouble() * 1.2;
for (var j = 0; j < crackLen && i + j < n; j++) {
final decay = 1 - (j / crackLen);
samples.add(((base + crackAmp * decay * (_gauss() * 2)) * 14000).toInt());
}
i += crackLen; // 跳过已生成的样本
continue;
}
// 偶尔的大块爆裂
if (_rand.nextDouble() < 0.002) {
crackle = _gauss() * 2.5;
}
samples.add(((base + crackle) * 0.8 * 18000).toInt());
}
return samples;
}
4.3 稀疏事件模型的关键参数
| 参数 | 含义 | 调大效果 |
|---|---|---|
0.04 |
噼啪概率 | 更"活跃"的篝火 |
2-22 样本 |
噼啪持续时长 | 噼啪声更"长" |
decay = 1 - j/crackLen |
线性衰减 | 控制噼啪声的"脆"度 |
0.002 |
大爆裂概率 | 更多的"砰"声 |
五、森林:调频(FM)鸟鸣合成
5.1 为什么用 FM 合成?
真正的鸟鸣不是纯音——鸟类的鸣管(syrinx)通过肌肉调节产生频率微变。在合成中,这通过调频(Frequency Modulation)实现:
carrier(t) = sin(2π × fc × t) // 载波(基频)
modulator(t) = 300 × sin(2π × 0.01 × t) // 调制信号
bird(t) = sin(2π × (fc + modulator(t)) × t) // FM 结果
5.2 实现
List<int> _generateForest(String path, int sr, int durSec) {
final n = sr * durSec;
final samples = <int>[];
double lpf = 0;
for (var i = 0; i < n; i++) {
final raw = _gauss();
lpf = 0.3 * raw + 0.7 * lpf; // 中频环境噪声
final ambience = lpf * 0.25;
if (_rand.nextDouble() < 0.015) { // 1.5% 鸟鸣概率
final birdFreq = 1200 + _rand.nextDouble() * 2000; // 1.2kHz - 3.2kHz
final birdLen = 150 + _rand.nextInt(400); // 150-550 样本
for (var j = 0; j < birdLen && i + j < n; j++) {
final decay = 1 - (j / birdLen);
final freq = birdFreq + 300 * sin(2 * pi * 0.01 * j); // FM!
final chirp = sin(2 * pi * freq * (i + j) / sr) * decay * 0.35;
samples.add(((ambience * 0.6 + chirp) * 14000).toInt());
}
i += birdLen;
continue;
}
samples.add((ambience * 18000).toInt());
}
return samples;
}
六、四种算法的频谱对比
频率 (Hz) 20 50 100 200 500 1000 2000 5000 10000
─────────────────────────────────────────────────────────────
雨声 (高通) ░ ░ ░░ ░░░ ████ ███ ██ ░ ·
海浪 (1/f²) ████ ██ █ ░ ░ · · · ·
篝火 (混合) ██ ██ █░ █░ ░░ ░ · · ·
森林 (中频) ░ █ ██ ███ ██ ░ ░░ · ·
- 雨声在高频段能量集中 → 清脆感
- 海浪在极低频能量集中 → 低沉感
- 篝火在中低频有持续能量 + 高频随机尖峰 → 温暖 + 噼啪
- 森林在中频有持续能量 + 高频调频啁啾 → 宁静 + 鸟鸣
七、总结:噪声 → 滤波 → 调制
四种自然音效,本质上都是噪声 + 滤波器 + 调制的排列组合:
| 音效 | 噪声源 | 滤波器 | 调制方式 |
|---|---|---|---|
| 雨声 | 高斯白噪声 | 高通 (raw - LPF) | 双正弦包络 + 随机脉冲 |
| 海浪 | 布朗噪声 (积分) | 自然低频 (1/f²) | 双波相乘包络 |
| 篝火 | 高斯噪声 | 低频保留 | 概率事件 + 衰减包络 |
| 森林 | 高斯噪声 | 中频 (LPF) | FM 啁啾 + 衰减包络 |
理解这三个要素,你就能合成任何自然声——雷声(低频 + 稀疏强脉冲)、溪流(中频 + 密集小脉冲)、蝉鸣(高频正弦 + AM 调制)。
作者简介:E-Brufen Dev,Flutter & 鸿蒙开发者。本文中的信号处理知识参考了 Julius O. Smith 的《Physical Audio Signal Processing》,项目地址:AtomGit - E-Brufen。
更多推荐



所有评论(0)