13、音频特效:GainNode音量控制、BiquadFilterNode滤波器、ConvolverNode混响效果

音频特效这块,说实话是WebRTC里最容易被忽略但又最有意思的部分。很多人觉得能采集到声音、能传出去就行了,但你想啊,用户听到的干巴巴的声音和经过处理的音效,体验差距可不是一星半点。

我个人习惯把音频节点想象成一个个效果器盒子。你从麦克风采集到的原始音频流,就像一根水管里的水,流经不同的盒子,每个盒子加点料,最后出来的就是调过味的声音。今天咱们就聊三个最常用的盒子:GainNode(音量控制)、BiquadFilterNode(滤波器)、ConvolverNode(混响效果)。

核心知识点速览

  • GainNode:控制音频信号的增益(说白了就是音量大小)
  • BiquadFilterNode:对音频频率进行过滤(低通、高通、带通等)
  • ConvolverNode:模拟空间混响效果(让声音听起来像在某个房间里)
音频特效节点体系 音频源 MediaStream GainNode 音量控制 gain.value BiquadFilter 滤波器 type/frequency Convolver 混响效果 impulse response 音频输出 destination 音频流依次经过各节点,每个节点可独立控制或组合使用

13.1 GainNode:最基础的音量控制

GainNode 是 Web Audio API 里最简单的节点之一。它的作用就是调整音频信号的增益值。说白了,你调音量旋钮时,背后就是这个节点在工作。

我在项目中遇到过一个问题:用户用手机浏览器打开页面,麦克风采集到的声音特别小。后来发现是默认的 gain 值没设置好。其实 GainNode 的 gain 属性是一个 AudioParam,默认值是 1,表示原始音量不变。你可以把它设成 0 到 1 之间来降低音量,或者大于 1 来放大音量。

小提示:gain.value 设为 0 就是静音,设为 2 就是放大一倍。但别设太大,超过 3 就容易出现削波失真,声音会破掉。

// 创建 GainNode
const audioContext = new AudioContext();
const gainNode = audioContext.createGain();

// 获取麦克风流
navigator.mediaDevices.getUserMedia({ audio: true })
  .then(stream => {
    const source = audioContext.createMediaStreamSource(stream);
    // 连接:源 → 增益节点 → 输出
    source.connect(gainNode);
    gainNode.connect(audioContext.destination);
    
    // 设置音量到 50%
    gainNode.gain.value = 0.5;
  });

// 动态调节音量(比如滑块控制)
function setVolume(value) {
  gainNode.gain.value = value;
}

你想想看,这个节点虽然简单,但实际场景里特别实用。比如做视频会议时,某个参会者声音太大,你可以单独把他的流接一个 GainNode 然后调低音量。或者做直播时,背景音乐和主播人声需要不同的音量比例,也是靠 GainNode 来平衡。

13.2 BiquadFilterNode:给声音做"整形"

BiquadFilterNode 就比 GainNode 有意思多了。它本质上是一个二阶滤波器,可以让你控制哪些频率的声音通过,哪些被削弱或阻断。

我记得有一次做语音降噪的 demo,环境里有低频的空调嗡嗡声。当时我就用了一个高通滤波器,把 200Hz 以下的频率切掉,效果立竿见影。虽然不能完全消除噪音,但人声清晰度提升了一大截。

BiquadFilterNode 支持多种滤波器类型,常用的有这些:

类型 说明 典型应用场景
lowpass 低通滤波,保留低频,衰减高频 模拟电话音效、去除高频噪音
highpass 高通滤波,保留高频,衰减低频 去除低频轰鸣声、空调噪音
bandpass 带通滤波,只保留某个频段 语音频段提取(300Hz-3.4kHz)
notch 陷波滤波,阻断某个窄频段 去除特定频率的啸叫
peaking 峰值滤波,增强或削弱某个频段 均衡器调节
// 创建 BiquadFilterNode
const filterNode = audioContext.createBiquadFilter();

// 设置为低通滤波器
filterNode.type = 'lowpass';
// 设置截止频率为 1000Hz
filterNode.frequency.value = 1000;
// Q 值控制共振峰,默认 1
filterNode.Q.value = 1;

// 连接:源 → 滤波器 → 输出
source.connect(filterNode);
filterNode.connect(audioContext.destination);

// 动态改变截止频率(比如做扫频效果)
function sweepFilter(freq) {
  filterNode.frequency.setValueAtTime(freq, audioContext.currentTime);
}

注意:frequency 和 Q 值都是 AudioParam,你可以用 setValueAtTime 或 linearRampToValueAtTime 来做平滑过渡。直接赋值虽然也行,但会有突变,听起来会有"咔嗒"声。

我曾经踩过一个坑:在移动端 Safari 上,BiquadFilterNode 的 frequency 值如果设得太低(比如低于 20Hz),会导致整个音频链路卡死。后来我加了个保护逻辑,确保 frequency 不低于 50Hz。嗯,这里要注意,不同浏览器的实现细节还是有差异的。

13.3 ConvolverNode:让声音"有空间感"

ConvolverNode 是这三个节点里最"高级"的一个。它通过卷积运算,把一段音频和一段"脉冲响应"(Impulse Response,简称 IR)结合起来,模拟出声音在特定空间里的混响效果。

说白了,你录一段干声,然后用 ConvolverNode 加载一个大教堂的 IR 文件,出来的声音就像真的在大教堂里录的一样。这个效果在音乐制作、语音场景模拟里非常常用。

我在项目中做过一个虚拟会议室的 demo,给每个参会者分配不同的混响效果。有人在小会议室,有人在大礼堂,听起来就很有空间层次感。用户反馈说"感觉真的在同一个房间里开会",这就是 ConvolverNode 的功劳。

// 创建 ConvolverNode
const convolverNode = audioContext.createConvolver();

// 加载脉冲响应文件(.wav 或 .mp3)
fetch('path/to/impulse-response.wav')
  .then(response => response.arrayBuffer())
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
  .then(audioBuffer => {
    convolverNode.buffer = audioBuffer;
  });

// 连接:源 → 混响 → 输出
source.connect(convolverNode);
convolverNode.connect(audioContext.destination);

// 如果想控制干湿比例(原始声和混响声的混合)
// 需要创建一个干声通道和湿声通道
const dryGain = audioContext.createGain();
const wetGain = audioContext.createGain();
dryGain.gain.value = 0.5;  // 干声 50%
wetGain.gain.value = 0.5;  // 湿声 50%

source.connect(dryGain);
dryGain.connect(audioContext.destination);

source.connect(convolverNode);
convolverNode.connect(wetGain);
wetGain.connect(audioContext.destination);

实用建议:IR 文件可以自己录制(在目标空间里放一个气球爆破声并录音),也可以从网上找免费的 IR 库。常用的有:小房间、大礼堂、浴室、隧道等。文件格式最好是 16bit 44.1kHz 的 WAV,采样率和你的 AudioContext 保持一致。

13.4 三个节点组合使用

实际项目中,很少只用单个节点。我习惯把它们串起来用,形成一个音频处理链。比如:

  1. 先用 BiquadFilterNode 做高通滤波,切掉低频噪音
  2. 再用 GainNode 调整音量到合适大小
  3. 最后用 ConvolverNode 加一点混响,让声音更自然
// 组合使用示例
const source = audioContext.createMediaStreamSource(stream);
const filterNode = audioContext.createBiquadFilter();
const gainNode = audioContext.createGain();
const convolverNode = audioContext.createConvolver();

// 配置滤波器:高通 200Hz
filterNode.type = 'highpass';
filterNode.frequency.value = 200;

// 配置音量:80%
gainNode.gain.value = 0.8;

// 加载混响 IR(假设已加载)
convolverNode.buffer = impulseResponseBuffer;

// 连接链:源 → 滤波器 → 增益 → 混响 → 输出
source.connect(filterNode);
filterNode.connect(gainNode);
gainNode.connect(convolverNode);
convolverNode.connect(audioContext.destination);

你想想看,这个链的顺序其实有讲究。如果把混响放在滤波器前面,那滤波也会作用于混响声,效果会不一样。我个人习惯把滤波器放在最前面,先"清理"原始信号,再加效果。当然,具体怎么排,还是要看你的需求。

避坑指南:我曾经把 GainNode 放在 ConvolverNode 后面,结果混响的尾音被放大了,听起来很不自然。后来改成 GainNode 在混响之前,只控制干声的音量,混响的湿声单独用一个 GainNode 控制,这样干湿比例就灵活多了。

好了,这三个节点讲完了。GainNode 是最基础的,BiquadFilterNode 是"整形"的,ConvolverNode 是"造空间"的。把它们组合起来,你就能做出很多有意思的音频效果。下次你在做 WebRTC 应用时,不妨试试给用户的语音加点"料",体验会完全不一样。


公众号:蓝海资料掘金营,微信deep3321