数字噪音生成器的工作原理
基础:伪随机数生成
在 WhiteNoise.top 构建噪音引擎的工作中,我花了数百小时优化将数学随机性转化为逼真音频的管线。每个数字噪音生成器都从同一个基本组件开始:伪随机数生成器,简称 PRNG。PRNG 是一种算法,产生一个确定性的数字序列,根据统计测试看起来是随机的。来自硬件熵源的真随机数对实时音频来说太慢且不可预测,因此 PRNG 为所有数字噪音生成提供了实用的基础。
最适合音频工作的基本 PRNG 是线性同余生成器(LCG),它将每个值计算为前一个值的线性函数,对一个大常数取模。虽然速度快,LCG 有已知弱点:连续样本之间存在相关性,可能在噪音信号中产生可听到的伪影。在我的早期原型中,我使用了简单的 LCG,通过高质量耳机可以听到输出中微弱的周期性模式。这些模式在 FFT 分析中表现为窄频谱峰值,确认了 PRNG 的统计弱点渗入了音频领域。
对于量产级噪音,我切换到了 xorshift128+ 算法,这是大多数 JavaScript 引擎内部用于 Math.random() 的 PRNG。它的周期为 2 的 128 次方减 1,统计特性优秀,计算成本可忽略不计。当我对其输出运行 TestU01 随机性测试套件时,它通过了所有 SmallCrush 和 Crush 子测试。结果是一个没有可听伪影的噪音信号,其平坦频谱在可听范围内验证精度达到 0.3 dB 以内。
从随机数到音频采样
PRNG 产生数字,但音频系统需要特定振幅和采样率的波形样本。转换过程涉及缩放、分布整形和缓冲区管理。在我的实现中,PRNG 输出无符号 32 位整数,然后我将其归一化为负一到正一范围内的浮点值。这是 Web Audio API 和大多数其他音频框架中数字音频的标准振幅范围。
随机值的分布影响噪音的特性。均匀分布——范围内所有值等概率出现——产生的噪音振幅分布与表征模拟电路中热噪音的高斯分布略有不同。实际上差异很微妙:均匀白噪音的波峰因数约为 4.8 dB,而高斯白噪音的波峰因数理论上无界,不过实际中对于典型缓冲区长度约为 12 dB。对于大多数应用,均匀分布完全可接受且计算更简单。
当我需要高斯分布噪音时,我使用 Box-Muller 变换,将均匀分布的随机数对转换为高斯分布的值对。计算开销适中,大约使每个样本的成本翻倍,但结果是一个更接近模拟噪音源振幅统计特性的噪音信号。在聆听测试中,均匀白噪音和高斯白噪音之间的差异几乎听不出来,但高斯变体在某些需要关注振幅统计的信号处理应用中表现更好。
频谱整形:将白色变为彩色
白噪音是所有其他噪音颜色的原材料。转换通过数字滤波实现——对白噪音频谱施加频率相关的增益曲线。在我的噪音引擎中,我使用 IIR(无限脉冲响应)和 FIR(有限脉冲响应)滤波器实现频谱整形,各有不同优势。
对于粉红噪音(每倍频程衰减 3 分贝),我主要使用 Voss-McCartney 算法。该算法维护多个以不同速率更新的独立随机数生成器:一个每个样本更新一次,一个每两个样本更新一次,一个每四个样本更新一次,依此类推。将输出求和产生一个频谱近似理想粉红噪音斜率的信号。在我的实现中,我使用 16 个倍频程层,从低于 1 Hz 到高于 20 kHz 提供精确的频谱整形。相对于理想斜率的误差在可听范围内小于 0.5 dB。
对于棕噪音(每倍频程衰减 6 分贝),我使用简单的积分:每个输出样本是前一个输出加上一个新的白噪音样本(乘以一个小系数)。从数学上看,这是一个一阶 IIR 低通滤波器,在单位圆上有一个非常接近 1 的极点。这种方法的挑战是直流偏移:累加和可能随时间远离零点,最终超过削波阈值。我通过在 5 Hz 处添加一个非常温和的一阶高通滤波器来解决这个问题,它约束了偏移而不会可听地影响 20 Hz 以上的频谱。
对于高级用户请求的自定义频谱形状,我使用由级联二阶双二次滤波器组成的参量均衡器。每个段实现一种标准滤波器类型(峰值、低搁架、高搁架或陷波),带有用户可调的频率、增益和带宽参数。通过串联四到六个段,我可以近似用户可能想要的几乎任何平滑频谱形状,从温和的倾斜到复杂的多频段轮廓。
使用 Web Audio API 的实时生成
Web Audio API 在所有现代浏览器中可用,提供了无需任何服务器端处理即可实时生成和播放噪音的基础设施。在 WhiteNoise.top 的实现中,我使用三个主要的 Web Audio API 组件:AudioContext、ScriptProcessorNode(或其现代替代品 AudioWorkletNode)和 BiquadFilterNode。
AudioWorkletNode 是核心生成发生的地方。我注册了一个自定义的 AudioWorkletProcessor,它在独立于浏览器主线程的线程上运行,确保 UI 交互不会导致音频故障。处理器的 process() 方法被重复调用,输出缓冲区通常在 44.1 kHz 下每次调用 128 个样本。在此方法内,我使用 PRNG 生成白噪音样本,应用任何频谱整形,并将结果写入输出缓冲区。整个管线在现代硬件上每个缓冲区大约运行 0.01 毫秒,远在无故障播放所需的大约 3 毫秒期限内。
缓冲区管理对避免可听伪影至关重要。如果处理器填充缓冲区的时间太长,音频输出就会欠载,产生咔嗒或爆音。在我的压力测试中,我通过在主线程上添加计算负载(如大量 DOM 操作或大型 JSON 解析)来推动系统,并验证音频工作线程继续按时交付缓冲区。AudioWorklet API 提供的线程隔离对这种稳健性至关重要;旧的 ScriptProcessorNode 在主线程上运行,在繁重的 JavaScript 执行期间容易出现故障。
当所需滤波器是标准类型时,我也使用 Web Audio API 内置的 BiquadFilterNode 进行频谱整形。这些节点以优化的原生代码实现,比等效的 JavaScript 实现快得多。对于较简单配置的粉红噪音生成,我串联多个配置为低通滤波器的 BiquadFilterNode,精心选择频率和 Q 因子以近似理想的每倍频程负 3 分贝斜率。
优化与实际考量
性能优化是实时音频生成中持续关注的问题。在我的开发过程中,我实施了几种技术以在不牺牲质量的前提下最大化效率。第一种是预计算:对于不动态变化的噪音颜色,我在初始化期间生成一个大型噪音缓冲区(通常在 44.1 kHz 下为 10 秒,即 441,000 个样本),并在播放期间循环它。为了防止循环被听出来,我在循环边界使用交叉淡入淡出,使用升余弦窗口将缓冲区的最后 4,096 个样本与前 4,096 个样本混合。由此产生的循环在感知上是无缝的。
第二种优化是 JavaScript 内的 SIMD 式处理。虽然 JavaScript 在 AudioWorklet 上下文中不提供显式的 SIMD 指令,但我将内循环结构化为以四个样本为一组处理,使 JavaScript 引擎的 JIT 编译器能够应用自动向量化。在我的基准测试中,这种方法相比逐个样本处理获得了 15% 到 25% 的速度提升,具体取决于浏览器引擎。
第三个考量是内存管理。在音频处理期间分配和释放内存可能触发垃圾回收暂停,导致可听故障。我在初始化期间预分配所有缓冲区,并在整个会话中重复使用它们。process() 方法内不创建任何对象,所有中间值存储在预分配的类型化数组中。这种规范完全消除了 GC 相关的音频伪影。
功耗是另一个实际考量,尤其是对移动设备。在我对智能手机的测试中,简单噪音生成器的 CPU 负载可以忽略不计,通常不到百分之一。然而,保持音频输出活动会阻止设备进入深度睡眠状态,在长时间使用中可能消耗电池。我通过提供计时器功能来缓解这一问题——在用户指定的时间后停止生成器,并使用 Page Visibility API 在浏览器标签不在前台时暂停生成。
验证生成器输出质量
噪音生成器的好坏取决于其输出质量,而质量必须通过客观测量来验证。在我的质量保证流程中,我在向用户发布之前对每种生成器配置运行一套自动化测试。第一项测试是频谱平坦度:我捕获 60 秒的白噪音样本,计算平均 FFT 幅度,并验证没有频率区间偏离平均值超过 1 dB。对于整形噪音,我将测量的频谱与目标曲线进行比较,验证偏差小于 0.5 dB。
第二项测试是振幅分布。我计算样本值的直方图,并使用 Kolmogorov-Smirnov 检验将其与预期分布(无论是均匀还是高斯分布)进行比较。p 值高于 0.05 表明分布在 95% 置信水平上与预期形状匹配。
第三项测试是周期性检测。我计算 10 秒样本的自相关函数,并检查没有非零延迟的相关系数超过该长度随机过程的理论最大值。PRNG 中的周期性模式会以自相关函数中的峰值形式出现,即使它们听不到,也表明生成器中存在可能在信号处理应用中造成问题的缺陷。
最后,我用耳机进行主观聆听测试,检查咔嗒声、爆音、音调伪影以及长时间播放期间的电平或音色变化。自动化测试能捕获大多数问题,但人耳仍然是音频质量的终极裁判,我坚持在每种生成器变体上线之前亲自聆听。
参考资料
常见问题
数字噪音生成器能产生真正随机的输出吗?
不能。数字噪音生成器使用伪随机数生成器(PRNG),产生确定性的序列。然而,设计良好的 PRNG 产生的输出在所有实际音频应用中与真随机在统计上不可区分。
Web Audio API 为什么使用 AudioWorklet 而非 ScriptProcessorNode?
AudioWorklet 在独立于浏览器主线程的线程上运行,防止 UI 操作导致音频故障。ScriptProcessorNode 在主线程上运行,在繁重的 JavaScript 执行期间容易出现中断。ScriptProcessorNode 现已弃用。
噪音生成器中的咔嗒或爆音是什么原因?
咔嗒和爆音通常由缓冲区欠载引起——生成器无法及时填充输出缓冲区——或由 JavaScript 中的垃圾回收暂停引起。适当的缓冲区管理和内存预分配可以消除这些伪影。
噪音生成器使用多少 CPU?
简单的噪音生成器在现代硬件上通常使用不到百分之一的 CPU。主要的性能关注点是保持一致的时间控制以防止音频故障,而非整体 CPU 负载。
可以循环播放噪音采样而不实时生成吗?
可以,但你需要在循环边界应用交叉淡入淡出以防止可听的咔嗒声。在 44.1 kHz 下约 4,096 个样本的升余弦交叉淡入淡出可创建在感知上无缝的循环。