4. 流式处理核心思想
音频处理的世界里,有两个节点你几乎天天都会碰到。一个是 GainNode,管音量;一个是 BiquadFilterNode,管频率。说白了,一个控制"响不响",一个控制"什么频段响"。
我个人习惯把这两个节点称为"音频处理的左右手"。很多复杂的效果器,比如均衡器、动态处理器,底层都是它们俩的组合。今天我们就彻底把它们搞明白。
4.1 GainNode:音量控制的正确姿势
GainNode 是 Web Audio API 里最基础的节点。它的核心属性就一个——gain,一个 AudioParam 对象。你给它赋值,音量就变了。
但这里有个坑。我在项目中遇到过好几次,新手直接写 gainNode.gain = 0.5,结果发现没效果。为什么?因为 gain 是 AudioParam,不是普通属性。你得用 .value 或者 setValueAtTime 来赋值。
// 正确写法
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5; // 直接设置
// 或者用自动化方法
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
4.2 淡入淡出效果
淡入淡出是音频编辑里最常用的效果之一。用 GainNode 实现起来非常简单——利用 linearRampToValueAtTime 做平滑过渡。
// 淡入:从 0 到 1,持续 2 秒
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 2);
// 淡出:从 1 到 0,持续 3 秒
gainNode.gain.setValueAtTime(1, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 3);
嗯,这里要注意:linearRampToValueAtTime 是线性变化。如果你想要更自然的听觉感受,可以用 exponentialRampToValueAtTime。人耳对音量的感知是对数级的,指数变化听起来更平滑。
exponentialRampToValueAtTime 做淡入,结果起始值设成了 0。指数变化不允许从 0 开始!会抛异常。正确做法是从一个极小值开始,比如 0.001。
4.3 BiquadFilterNode:频率的雕刻刀
BiquadFilterNode 是 Web Audio API 里最强大的滤波器节点。它支持多种滤波器类型,每种类型都有不同的频率响应特性。
| 滤波器类型 | type 值 | 用途 | 关键参数 |
|---|---|---|---|
| 低通 | lowpass | 保留低频,衰减高频 | frequency, Q |
| 高通 | highpass | 保留高频,衰减低频 | frequency, Q |
| 带通 | bandpass | 保留特定频段 | frequency, Q |
| 低架 | lowshelf | 提升/衰减低频段 | frequency, gain |
| 高架 | highshelf | 提升/衰减高频段 | frequency, gain |
| 峰值 | peaking | 提升/衰减特定频点 | frequency, Q, gain |
| 陷波 | notch | 消除特定频点 | frequency, Q |
| 全通 | allpass | 改变相位,不改变幅度 | frequency, Q |
你想想看,这 8 种滤波器基本覆盖了所有常见的音频处理场景。低通和高通用来做频段分割,峰值和架式滤波器用来做均衡器,陷波滤波器用来消除特定频率的噪音。
4.4 滤波器参数的实时调节
BiquadFilterNode 有三个核心参数可以实时调节:frequency、Q、gain。它们都是 AudioParam,所以调节方式和 GainNode 一样。
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1000; // 截止频率 1000Hz
filter.Q.value = 1.0; // 品质因数
// 实时调节
filter.frequency.setValueAtTime(2000, audioCtx.currentTime);
filter.Q.linearRampToValueAtTime(5, audioCtx.currentTime + 1);
- frequency:截止频率或中心频率,单位 Hz。范围 10 到采样率的一半。
- Q:品质因数,控制滤波器的"尖锐程度"。Q 值越大,频带越窄,共振越明显。
- gain:仅对 lowshelf、highshelf、peaking 有效,单位 dB。范围 -40 到 40。
我记得有一次做语音降噪,需要动态调整滤波器的频率。用户说话时,滤波器要跟着音调走。用 setValueAtTime 配合 requestAnimationFrame 就能实现实时追踪。效果还不错。
4.5 实战:构建一个简单的均衡器
现在我们把 GainNode 和 BiquadFilterNode 结合起来,做一个三频段均衡器。低频、中频、高频,每个频段都可以独立调节增益。
class SimpleEqualizer {
constructor(audioCtx) {
this.audioCtx = audioCtx;
// 输入节点
this.input = audioCtx.createGain();
// 低频段:低架滤波器
this.lowFilter = audioCtx.createBiquadFilter();
this.lowFilter.type = 'lowshelf';
this.lowFilter.frequency.value = 250;
this.lowFilter.gain.value = 0;
// 中频段:峰值滤波器
this.midFilter = audioCtx.createBiquadFilter();
this.midFilter.type = 'peaking';
this.midFilter.frequency.value = 1000;
this.midFilter.Q.value = 1.0;
this.midFilter.gain.value = 0;
// 高频段:高架滤波器
this.highFilter = audioCtx.createBiquadFilter();
this.highFilter.type = 'highshelf';
this.highFilter.frequency.value = 4000;
this.highFilter.gain.value = 0;
// 总音量控制
this.masterGain = audioCtx.createGain();
this.masterGain.gain.value = 1.0;
// 连接:输入 → 低架 → 峰值 → 高架 → 总音量 → 输出
this.input.connect(this.lowFilter);
this.lowFilter.connect(this.midFilter);
this.midFilter.connect(this.highFilter);
this.highFilter.connect(this.masterGain);
}
// 连接输出
connect(destination) {
this.masterGain.connect(destination);
}
// 调节低频增益(dB)
setLowGain(db) {
this.lowFilter.gain.setValueAtTime(db, this.audioCtx.currentTime);
}
// 调节中频增益(dB)
setMidGain(db) {
this.midFilter.gain.setValueAtTime(db, this.audioCtx.currentTime);
}
// 调节高频增益(dB)
setHighGain(db) {
this.highFilter.gain.setValueAtTime(db, this.audioCtx.currentTime);
}
// 调节总音量
setMasterVolume(volume) {
this.masterGain.gain.setValueAtTime(volume, this.audioCtx.currentTime);
}
}
// 使用示例
const audioCtx = new AudioContext();
const eq = new SimpleEqualizer(audioCtx);
// 连接音频源
const source = audioCtx.createMediaElementSource(audioElement);
source.connect(eq.input);
eq.connect(audioCtx.destination);
// 调节参数
eq.setLowGain(3); // 低频提升 3dB
eq.setMidGain(-2); // 中频衰减 2dB
eq.setHighGain(1); // 高频提升 1dB
eq.setMasterVolume(0.8); // 总音量 80%
你可能会问:为什么不用并联?并联确实可以,但需要额外的 GainNode 做混合。串联更简单,而且相位失真更小。对于大多数应用场景,串联就足够了。