4、音频效果节点(一)—— GainNode 与 BiquadFilterNode

音频处理的世界里,有两个节点你几乎天天都会碰到。一个是 GainNode,管音量;一个是 BiquadFilterNode,管频率。说白了,一个控制"响不响",一个控制"什么频段响"。

我个人习惯把这两个节点称为"音频处理的左右手"。很多复杂的效果器,比如均衡器、动态处理器,底层都是它们俩的组合。今天我们就彻底把它们搞明白。

GainNode 与 BiquadFilterNode 知识体系 音频源 GainNode gain.value 0 ~ 1(默认1) BiquadFilterNode type / frequency / Q / gain lowpass / highpass / bandpass 音频输出 实时调节参数(通过 AudioParam 接口) • gain.value = 0.5 • frequency.value = 1000 • Q.value = 1.0 • gain.value(滤波器增益)= -6 实战:构建一个简单的均衡器 • 低频段:BiquadFilterNode (lowshelf) + GainNode • 中频段:BiquadFilterNode (peaking) + GainNode • 高频段:BiquadFilterNode (highshelf) + GainNode • 总音量:GainNode

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);
小技巧:GainNode 的 gain 值范围是 0 到 1,但理论上可以超过 1。超过 1 会产生削波失真,听起来就是"爆音"。我一般把最大值限制在 1.2 以内,再高就加一个 DynamicsCompressorNode 做保护。

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%
实战经验:这个均衡器虽然简单,但已经能应付很多场景了。我在做播客录制工具时,就用类似的架构做了 10 段均衡器。核心思路是一样的——串联多个 BiquadFilterNode,每个负责一个频段。注意 Q 值要合理设置,避免频段之间互相干扰。

你可能会问:为什么不用并联?并联确实可以,但需要额外的 GainNode 做混合。串联更简单,而且相位失真更小。对于大多数应用场景,串联就足够了。