第九章:音频处理与效果器 —— 让声音从"能听"变成"好听"

第七章介绍了 Web Audio API 的节点体系,第八章把 AnalyserNode 玩到了极致。这一章我们把重心放在音频信号处理本身——均衡器、动态压缩、混响、延迟、失真、合唱、镶边……每一种效果器背后都有清晰的信号处理逻辑,理解原理才能用好参数,调出真正好听的声音。


一、音频效果器的信号处理基础

在动手写代码之前,需要建立几个基础概念,它们贯穿本章所有效果器。

增益与分贝

人耳对音量的感知是对数的,因此音频领域普遍用**分贝(dB)**来描述增益变化:

$$ \text{dB} = 20 \times \log_{10}\left(\frac{A_{\text{out}}}{A_{\text{in}}}\right) $$

几个关键的 dB 值要记住:

  • +6dB ≈ 幅度翻倍
  • -6dB ≈ 幅度减半
  • 0dB = 无变化
  • -∞ dB = 静音

在 Web Audio API 中,GainNode.gain.value线性幅度比,不是 dB。转换关系:

// dB → 线性幅度
const linearGain = Math.pow(10, dB / 20);
 
// 线性幅度 → dB
const dB = 20 * Math.log10(linearGain);
 
// 工具函数
const dbToLinear = db => Math.pow(10, db / 20);
const linearToDb = linear => 20 * Math.log10(Math.max(linear, 1e-6));

干湿信号(Dry / Wet)

几乎所有效果器都有**干信号(Dry)湿信号(Wet)**的概念:

  • 干信号:未经处理的原始信号
  • 湿信号:经过效果器处理后的信号
  • 混合比(Mix):干湿信号的比例,0% 是纯干,100% 是纯湿
// 通用干湿混合节点工厂
function createDryWetMix(audioCtx, effectNode, wetAmount = 0.5) {
  const input   = audioCtx.createGain(); // 输入节点
  const dryGain = audioCtx.createGain();
  const wetGain = audioCtx.createGain();
  const output  = audioCtx.createGain(); // 输出节点
 
  dryGain.gain.value = 1 - wetAmount;
  wetGain.gain.value = wetAmount;
 
  input.connect(dryGain);
  input.connect(effectNode);
  effectNode.connect(wetGain);
  dryGain.connect(output);
  wetGain.connect(output);
 
  return {
    input,
    output,
    setMix(wet) {
      const t = audioCtx.currentTime;
      dryGain.gain.setTargetAtTime(1 - wet, t, 0.01);
      wetGain.gain.setTargetAtTime(wet, t, 0.01);
    },
  };
}

信号流图约定

本章所有效果器的信号流都遵循同一个结构:

[输入] → [前置增益] → [效果处理] → [干湿混合] → [输出]
                           ↑
                      [参数控制]

二、均衡器(Equalizer)

均衡器是最基础也最重要的音色塑造工具,通过提升或衰减特定频段来改变音色。

BiquadFilter 滤波器类型详解

Web Audio 的 BiquadFilterNode 实现了 7 种滤波器,每种对应不同的频率响应曲线:

lowpass(低通)         highpass(高通)        bandpass(带通)
    ████████╲               ╱████████              ╱██╲
    ────────→ Hz            ────────→ Hz           ────────→ Hz

peaking(峰值)         notch(陷波)           lowshelf(低架)
       ╭─╮                    ╰─╯              █████╲
    ───┴─┴──→ Hz           ───┬─┬──→ Hz        ──────→ Hz

highshelf(高架)
          ╱█████
    ──────→ Hz

参数均衡器(Parametric EQ)实现

专业均衡器通常有多个频段,每段可以独立调节频率、增益和 Q 值:

class ParametricEQ {
  constructor(audioCtx) {
    this.ctx = audioCtx;
    this.input  = audioCtx.createGain();
    this.output = audioCtx.createGain();
 
    // 定义 5 段均衡器
    this.bands = [
      { type: 'lowshelf',  freq: 80,   gain: 0, Q: 0.7,  label: '低频'  },
      { type: 'peaking',   freq: 250,  gain: 0, Q: 1.0,  label: '中低频' },
      { type: 'peaking',   freq: 1000, gain: 0, Q: 1.0,  label: '中频'  },
      { type: 'peaking',   freq: 4000, gain: 0, Q: 1.0,  label: '中高频' },
      { type: 'highshelf', freq: 8000, gain: 0, Q: 0.7,  label: '高频'  },
    ].map(band => {
      const node = audioCtx.createBiquadFilter();
      node.type            = band.type;
      node.frequency.value = band.freq;
      node.gain.value      = band.gain;
      node.Q.value         = band.Q;
      return { ...band, node };
    });
 
    // 串联所有滤波器
    let prev = this.input;
    for (const band of this.bands) {
      prev.connect(band.node);
      prev = band.node;
    }
    prev.connect(this.output);
  }
 
  // 设置某频段增益(dB)
  setBandGain(index, gainDb) {
    const band = this.bands[index];
    if (!band) return;
    band.node.gain.setTargetAtTime(
      Math.max(-24, Math.min(24, gainDb)),
      this.ctx.currentTime,
      0.01
    );
    band.gain = gainDb;
  }
 
  // 设置某频段频率(Hz)
  setBandFrequency(index, hz) {
    const band = this.bands[index];
    if (!band) return;
    band.node.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.01);
    band.freq = hz;
  }
 
  // 设置某频段 Q 值
  setBandQ(index, q) {
    const band = this.bands[index];
    if (!band) return;
    band.node.Q.setTargetAtTime(
      Math.max(0.1, Math.min(20, q)),
      this.ctx.currentTime,
      0.01
    );
    band.Q = q;
  }
 
  // 重置所有频段
  reset() {
    this.bands.forEach((_, i) => this.setBandGain(i, 0));
  }
 
  // 应用预设
  applyPreset(preset) {
    // preset: [{ gain, freq?, Q? }, ...]
    preset.forEach((p, i) => {
      if (p.gain !== undefined) this.setBandGain(i, p.gain);
      if (p.freq !== undefined) this.setBandFrequency(i, p.freq);
      if (p.Q    !== undefined) this.setBandQ(i, p.Q);
    });
  }
}

EQ 预设库

const EQ_PRESETS = {
  flat: [
    { gain: 0 }, { gain: 0 }, { gain: 0 }, { gain: 0 }, { gain: 0 }
  ],
  bass_boost: [
    { gain: 8 }, { gain: 4 }, { gain: 0 }, { gain: 0 }, { gain: 0 }
  ],
  vocal: [
    { gain: -2 }, { gain: 0 }, { gain: 5 }, { gain: 3 }, { gain: 1 }
  ],
  acoustic: [
    { gain: 4 }, { gain: 2 }, { gain: 0 }, { gain: 2 }, { gain: 4 }
  ],
  electronic: [
    { gain: 6 }, { gain: 2 }, { gain: -1 }, { gain: 2 }, { gain: 5 }
  ],
  classical: [
    { gain: 3 }, { gain: 0 }, { gain: 0 }, { gain: 0 }, { gain: 3 }
  ],
  loudness: [
    { gain: 6 }, { gain: 2 }, { gain: -1 }, { gain: 2 }, { gain: 6 }
  ],
};
 
// 使用
const eq = new ParametricEQ(audioCtx);
eq.applyPreset(EQ_PRESETS.bass_boost);

频率响应曲线可视化

均衡器 UI 通常需要绘制频率响应曲线,BiquadFilterNode 提供了 getFrequencyResponse() 方法:

function drawEQCurve(bands, canvas) {
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  const POINTS = 512;
 
  // 生成对数分布的频率点(20Hz ~ 20kHz)
  const freqArray = new Float32Array(POINTS);
  for (let i = 0; i < POINTS; i++) {
    freqArray[i] = 20 * Math.pow(1000, i / (POINTS - 1));
  }
 
  // 计算所有频段的综合频率响应
  const magResponse   = new Float32Array(POINTS).fill(1);
  const phaseResponse = new Float32Array(POINTS);
  const tempMag       = new Float32Array(POINTS);
  const tempPhase     = new Float32Array(POINTS);
 
  for (const band of bands) {
    band.node.getFrequencyResponse(freqArray, tempMag, tempPhase);
    for (let i = 0; i < POINTS; i++) {
      magResponse[i] *= tempMag[i]; // 各频段响应相乘(dB 相加)
    }
  }
 
  // 绘制
  ctx.clearRect(0, 0, W, H);
  ctx.strokeStyle = '#667eea';
  ctx.lineWidth = 2;
  ctx.beginPath();
 
  for (let i = 0; i < POINTS; i++) {
    const x = (i / POINTS) * W;
    // 将幅度比转换为 dB,映射到画布 Y 轴(±24dB 范围)
    const db = 20 * Math.log10(magResponse[i]);
    const y  = H / 2 - (db / 24) * (H / 2 - 10);
 
    i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
  }
  ctx.stroke();
 
  // 绘制 0dB 参考线
  ctx.strokeStyle = 'rgba(255,255,255,0.2)';
  ctx.lineWidth = 1;
  ctx.setLineDash([4, 4]);
  ctx.beginPath();
  ctx.moveTo(0, H / 2);
  ctx.lineTo(W, H / 2);
  ctx.stroke();
  ctx.setLineDash([]);
}

三、动态压缩器(Dynamics Compressor)

压缩器是混音中最重要的动态处理工具,它压缩音频的动态范围——让响的部分变小,配合 Makeup Gain 整体提升,使声音更饱满、更有力度。

压缩器五参数深度解析

class Compressor {
  constructor(audioCtx) {
    this.ctx  = audioCtx;
    this.node = audioCtx.createDynamicsCompressor();
    this.makeupGain = audioCtx.createGain();
 
    // 压缩器参数详解:
    // threshold(阈值):超过这个电平才触发压缩,单位 dBFS,范围 -100~0
    this.node.threshold.value = -24;
 
    // knee(拐点):软拐点宽度,值越大过渡越平滑,范围 0~40dB
    this.node.knee.value = 6;
 
    // ratio(压缩比):超出阈值部分的压缩比例,4:1 意味着超出 4dB 只输出 1dB
    this.node.ratio.value = 4;
 
    // attack(起音):信号超过阈值后,压缩器开始工作的时间,单位秒
    // 值越小响应越快(会影响瞬态),值越大保留更多瞬态感
    this.node.attack.value = 0.003;
 
    // release(释音):信号降到阈值以下后,压缩器停止工作的时间,单位秒
    // 值越小恢复越快(可能产生"泵浦"感),值越大恢复越平滑
    this.node.release.value = 0.25;
 
    // Makeup Gain:压缩后补偿增益,通常设为压缩量的一半左右
    this.makeupGain.gain.value = dbToLinear(6);
 
    // 连接:输入 → 压缩器 → makeup gain → 输出
    this.node.connect(this.makeupGain);
 
    this.input  = this.node;
    this.output = this.makeupGain;
  }
 
  // 获取当前增益压缩量(dB,负值)
  getReduction() {
    return this.node.reduction; // 只读属性,实时反映压缩量
  }
 
  // 应用压缩预设
  applyPreset(preset) {
    const t = this.ctx.currentTime;
    const p = preset;
    this.node.threshold.setTargetAtTime(p.threshold, t, 0.01);
    this.node.knee.setTargetAtTime(p.knee, t, 0.01);
    this.node.ratio.setTargetAtTime(p.ratio, t, 0.01);
    this.node.attack.setTargetAtTime(p.attack, t, 0.01);
    this.node.release.setTargetAtTime(p.release, t, 0.01);
    if (p.makeupGain !== undefined) {
      this.makeupGain.gain.setTargetAtTime(
        dbToLinear(p.makeupGain), t, 0.01
      );
    }
  }
}
 
// 压缩预设
const COMPRESSOR_PRESETS = {
  // 轻压缩:保留动态,轻微整形
  gentle: {
    threshold: -18, knee: 12, ratio: 2,
    attack: 0.01, release: 0.3, makeupGain: 2,
  },
  // 人声压缩:控制动态,突出存在感
  vocal: {
    threshold: -20, knee: 6, ratio: 4,
    attack: 0.005, release: 0.15, makeupGain: 4,
  },
  // 鼓组压缩:保留瞬态,压缩尾音
  drums: {
    threshold: -12, knee: 3, ratio: 6,
    attack: 0.001, release: 0.05, makeupGain: 3,
  },
  // 母带压缩:整体动态控制
  mastering: {
    threshold: -6, knee: 6, ratio: 2,
    attack: 0.01, release: 0.2, makeupGain: 1,
  },
  // 重度压缩(Limiter 效果)
  limiter: {
    threshold: -3, knee: 0, ratio: 20,
    attack: 0.001, release: 0.1, makeupGain: 0,
  },
};

压缩量表(GR Meter)

专业压缩器 UI 都有一个增益压缩量表,实时显示压缩器正在压缩多少 dB:

class GRMeter {
  constructor(compressorNode, canvas) {
    this.compressor = compressorNode;
    this.canvas     = canvas;
    this.ctx        = canvas.getContext('2d');
    this.animId     = null;
  }
 
  start() {
    const draw = () => {
      this.animId = requestAnimationFrame(draw);
      this._draw();
    };
    draw();
  }
 
  stop() { cancelAnimationFrame(this.animId); }
 
  _draw() {
    const reduction = Math.abs(this.compressor.reduction); // 0 ~ 40dB
    const { ctx, canvas } = this;
    const W = canvas.width, H = canvas.height;
 
    ctx.clearRect(0, 0, W, H);
 
    // 背景
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, W, H);
 
    // 压缩量条(从右往左,越长表示压缩越多)
    const maxDb  = 20;
    const ratio  = Math.min(reduction / maxDb, 1);
    const barW   = ratio * (W - 20);
 
    // 颜色:轻压缩绿色,重压缩红色
    const hue = 120 - ratio * 120; // 绿→黄→红
    ctx.fillStyle = `hsl(, 90%, 50%)`;
    ctx.fillRect(W - 10 - barW, H * 0.25, barW, H * 0.5);
 
    // 刻度线
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.font = '10px monospace';
    [0, 3, 6, 10, 20].forEach(db => {
      const x = W - 10 - (db / maxDb) * (W - 20);
      ctx.fillRect(x, 0, 1, H * 0.2);
      ctx.fillText(`-`, x - 8, H - 4);
    });
 
    // 当前压缩量数字
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 12px monospace';
    ctx.fillText(`GR: -dB`, 8, H / 2 + 4);
  }
}

四、混响(Reverb)

混响是最能改变空间感的效果器。Web Audio 提供两种混响实现方式:卷积混响(ConvolverNode)和算法混响(用延迟网络模拟)。

卷积混响(Convolution Reverb)

卷积混响通过与真实空间录制的**脉冲响应(IR)**做卷积运算,能完美重现真实空间的声学特性:

class ConvolutionReverb {
  constructor(audioCtx) {
    this.ctx      = audioCtx;
    this.convolver = audioCtx.createConvolver();
    this.preDelay  = audioCtx.createDelay(0.1); // 预延迟,增加空间感
    this.preDelay.delayTime.value = 0.02;        // 20ms 预延迟
 
    this.input  = audioCtx.createGain();
    this.output = audioCtx.createGain();
    const { dryGain, wetGain, setMix } = this._buildDryWet();
    this.setMix = setMix;
 
    // 信号流:输入 → 预延迟 → 卷积器 → 湿增益 → 输出
    this.input.connect(dryGain);
    this.input.connect(this.preDelay);
    this.preDelay.connect(this.convolver);
    this.convolver.connect(wetGain);
    dryGain.connect(this.output);
    wetGain.connect(this.output);
  }
 
  _buildDryWet() {
    const dryGain = this.ctx.createGain();
    const wetGain = this.ctx.createGain();
    dryGain.gain.value = 0.7;
    wetGain.gain.value = 0.3;
 
    const setMix = (wet) => {
      const t = this.ctx.currentTime;
      dryGain.gain.setTargetAtTime(1 - wet, t, 0.02);
      wetGain.gain.setTargetAtTime(wet, t, 0.02);
    };
    return { dryGain, wetGain, setMix };
  }
 
  // 从 URL 加载 IR 文件
  async loadIR(url) {
    const response    = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();
    this.convolver.buffer = await this.ctx.decodeAudioData(arrayBuffer);
    console.log('IR 加载完成:', url);
  }
 
  // 合成简单的 IR(无需外部文件)
  synthesizeIR(duration = 2, decay = 3.0, reverse = false) {
    const sampleRate = this.ctx.sampleRate;
    const length     = sampleRate * duration;
    const buffer     = this.ctx.createBuffer(2, length, sampleRate);
 
    for (let ch = 0; ch < 2; ch++) {
      const data = buffer.getChannelData(ch);
      for (let i = 0; i < length; i++) {
        // 指数衰减的白噪声 = 简单的混响 IR
        const t = i / sampleRate;
        data[i] = (Math.random() * 2 - 1) * Math.pow(Math.E, -decay * t);
      }
      if (reverse) data.reverse(); // 反向混响效果
    }
 
    this.convolver.buffer = buffer;
  }
 
  setPreDelay(ms) {
    this.preDelay.delayTime.setTargetAtTime(
      ms / 1000,
      this.ctx.currentTime,
      0.01
    );
  }
}

算法混响(Schroeder 混响网络)

用纯算法模拟混响,不需要 IR 文件,资源占用更低,但音色不如卷积混响自然:

class AlgorithmicReverb {
  constructor(audioCtx) {
    this.ctx = audioCtx;
 
    // Schroeder 混响:4 个并联梳状滤波器 + 2 个串联全通滤波器
    // 梳状滤波器延迟时间(毫秒)—— 互质,避免产生明显的重复回声
    const combDelays   = [29.7, 37.1, 41.1, 43.7];
    const allpassDelays = [5.0, 1.7];
 
    this.input  = audioCtx.createGain();
    this.output = audioCtx.createGain();
 
    // 并联梳状滤波器
    const combOutputs = combDelays.map(delayMs => {
      return this._createCombFilter(delayMs / 1000, 0.84);
    });
 
    // 合并梳状滤波器输出
    const combMerge = audioCtx.createGain();
    combMerge.gain.value = 0.25; // 4 路平均
 
    combOutputs.forEach(comb => {
      this.input.connect(comb.input);
      comb.output.connect(combMerge);
    });
 
    // 串联全通滤波器
    const allpass1 = this._createAllpassFilter(allpassDelays[0] / 1000, 0.7);
    const allpass2 = this._createAllpassFilter(allpassDelays[1] / 1000, 0.7);
 
    combMerge
      .connect(allpass1.input);
    allpass1.output
      .connect(allpass2.input);
    allpass2.output
      .connect(this.output);
  }
 
  // 梳状滤波器 = 延迟 + 反馈
  _createCombFilter(delayTime, feedback) {
    const delay    = this.ctx.createDelay(delayTime + 0.01);
    const feedGain = this.ctx.createGain();
    const input    = this.ctx.createGain();
    const output   = this.ctx.createGain();
 
    delay.delayTime.value = delayTime;
    feedGain.gain.value   = feedback;
 
    input.connect(delay);
    delay.connect(output);
    delay.connect(feedGain);
    feedGain.connect(delay); // 反馈回路
 
    return { input, output };
  }
 
  // 全通滤波器(保持频率响应平坦,只改变相位)
  _createAllpassFilter(delayTime, feedback) {
    const delay      = this.ctx.createDelay(delayTime + 0.01);
    const feedGain   = this.ctx.createGain();
    const forwardGain = this.ctx.createGain();
    const input      = this.ctx.createGain();
    const output     = this.ctx.createGain();
 
    delay.delayTime.value  = delayTime;
    feedGain.gain.value    = -feedback;
    forwardGain.gain.value = feedback;
 
    input.connect(delay);
    input.connect(forwardGain);
    delay.connect(feedGain);
    feedGain.connect(input);
    delay.connect(output);
    forwardGain.connect(output);
 
    return { input, output };
  }
}

五、延迟效果器(Delay)

延迟效果器将信号延迟一段时间后叠加回原信号,产生回声效果。通过调节延迟时间、反馈量和混合比,可以得到从单次回声到无限循环的各种效果。

class DelayEffect {
  constructor(audioCtx) {
    this.ctx = audioCtx;
 
    this.input    = audioCtx.createGain();
    this.output   = audioCtx.createGain();
    this.delay    = audioCtx.createDelay(5.0); // 最大延迟 5 秒
    this.feedback = audioCtx.createGain();     // 反馈增益
    this.wetGain  = audioCtx.createGain();
    this.dryGain  = audioCtx.createGain();
 
    // 低通滤波器(让回声逐渐变暗,更自然)
    this.toneFilter = audioCtx.createBiquadFilter();
    this.toneFilter.type            = 'lowpass';
    this.toneFilter.frequency.value = 4000;
 
    // 默认参数
    this.delay.delayTime.value = 0.375; // 375ms(对应 160BPM 的八分音符)
    this.feedback.gain.value   = 0.4;   // 40% 反馈
    this.dryGain.gain.value    = 0.7;
    this.wetGain.gain.value    = 0.3;
 
    // 信号路由
    // 干路:输入 → 干增益 → 输出
    this.input.connect(this.dryGain);
    this.dryGain.connect(this.output);
 
    // 湿路:输入 → 延迟 → 色调滤波 → 反馈回路 + 湿增益
    this.input.connect(this.delay);
    this.delay.connect(this.toneFilter);
    this.toneFilter.connect(this.feedback);
    this.toneFilter.connect(this.wetGain);
    this.feedback.connect(this.delay); // 反馈回路
    this.wetGain.connect(this.output);
  }
 
  // 按 BPM 同步延迟时间
  syncToBPM(bpm, noteValue = '8n') {
    const beatDuration = 60 / bpm; // 一拍的时长(秒)
    const noteMap = {
      '1n':  beatDuration * 4,   // 全音符
      '2n':  beatDuration * 2,   // 二分音符
      '4n':  beatDuration,       // 四分音符
      '8n':  beatDuration / 2,   // 八分音符
      '16n': beatDuration / 4,   // 十六分音符
      '4t':  beatDuration / 1.5, // 四分三连音
      '8t':  beatDuration / 3,   // 八分三连音
    };
    const delayTime = noteMap[noteValue] || beatDuration / 2;
    this.delay.delayTime.setTargetAtTime(
      Math.min(delayTime, 5.0),
      this.ctx.currentTime,
      0.01
    );
    return delayTime;
  }
 
  setFeedback(value) {
    // 反馈不能 >= 1,否则产生无限增益(啸叫)
    const safeValue = Math.max(0, Math.min(0.95, value));
    this.feedback.gain.setTargetAtTime(safeValue, this.ctx.currentTime, 0.01);
  }
 
  setDelayTime(seconds) {
    this.delay.delayTime.setTargetAtTime(
      Math.max(0.001, Math.min(5.0, seconds)),
      this.ctx.currentTime,
      0.01
    );
  }
 
  setMix(wet) {
    const t = this.ctx.currentTime;
    this.dryGain.gain.setTargetAtTime(1 - wet, t, 0.01);
    this.wetGain.gain.setTargetAtTime(wet, t, 0.01);
  }
 
  setTone(hz) {
    this.toneFilter.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.01);
  }
}

Ping-Pong 延迟

Ping-Pong 延迟是延迟效果的经典变体——回声在左右声道之间交替出现,产生空间感极强的立体声效果:

class PingPongDelay {
  constructor(audioCtx) {
    this.ctx = audioCtx;
 
    this.input    = audioCtx.createGain();
    this.output   = audioCtx.createGain();
 
    // 两个延迟节点,分别对应左右声道
    this.delayL   = audioCtx.createDelay(2.0);
    this.delayR   = audioCtx.createDelay(2.0);
    this.feedbackL = audioCtx.createGain();
    this.feedbackR = audioCtx.createGain();
 
    // 声道分离器和合并器
    this.splitter = audioCtx.createChannelSplitter(2);
    this.merger   = audioCtx.createChannelMerger(2);
 
    const delayTime = 0.25; // 250ms
    this.delayL.delayTime.value = delayTime;
    this.delayR.delayTime.value = delayTime;
    this.feedbackL.gain.value   = 0.5;
    this.feedbackR.gain.value   = 0.5;
 
    // Ping-Pong 路由:左延迟的输出反馈到右延迟,右延迟反馈到左延迟
    this.input.connect(this.delayL);
    this.delayL.connect(this.feedbackL);
    this.feedbackL.connect(this.delayR); // L → R
    this.delayR.connect(this.feedbackR);
    this.feedbackR.connect(this.delayL); // R → L(形成 Ping-Pong)
 
    // 左右声道分别输出到 merger
    this.delayL.connect(this.merger, 0, 0); // L → 左声道
    this.delayR.connect(this.merger, 0, 1); // R → 右声道
    this.merger.connect(this.output);
 
    // 干信号直通
    this.input.connect(this.output);
  }
 
  setDelayTime(seconds) {
    const t = this.ctx.currentTime;
    this.delayL.delayTime.setTargetAtTime(seconds, t, 0.01);
    this.delayR.delayTime.setTargetAtTime(seconds, t, 0.01);
  }
 
  setFeedback(value) {
    const safe = Math.max(0, Math.min(0.92, value));
    const t    = this.ctx.currentTime;
    this.feedbackL.gain.setTargetAtTime(safe, t, 0.01);
    this.feedbackR.gain.setTargetAtTime(safe, t, 0.01);
  }
}

六、失真效果器(Distortion)

失真通过对信号进行非线性变换(削波),产生丰富的谐波,是电吉他音色的灵魂,也广泛用于电子音乐制作。

波形整形曲线(Waveshaper Curves)

不同的传递函数曲线产生不同风格的失真:

class Distortion {
  constructor(audioCtx) {
    this.ctx       = audioCtx;
    this.input     = audioCtx.createGain();
    this.output    = audioCtx.createGain();
    this.waveshaper = audioCtx.createWaveShaper();
    this.preGain   = audioCtx.createGain();  // 前置增益(驱动量)
    this.postGain  = audioCtx.createGain();  // 后置增益(音量补偿)
 
    // 高通滤波器:去除直流偏移,让失真更干净
    this.dcFilter = audioCtx.createBiquadFilter();
    this.dcFilter.type            = 'highpass';
    this.dcFilter.frequency.value = 10;
 
    // 色调滤波器:控制失真音色的亮度
    this.toneFilter = audioCtx.createBiquadFilter();
    this.toneFilter.type            = 'lowpass';
    this.toneFilter.frequency.value = 3500;
 
    this.preGain.gain.value   = 3.0;  // 驱动量
    this.postGain.gain.value  = 0.4;  // 补偿失真后的音量增加
    this.waveshaper.oversample = '4x'; // 过采样减少混叠
 
    // 路由:输入 → 前置增益 → DC滤波 → 波形整形 → 色调 → 后置增益 → 输出
    this.input.connect(this.preGain);
    this.preGain.connect(this.dcFilter);
    this.dcFilter.connect(this.waveshaper);
    this.waveshaper.connect(this.toneFilter);
    this.toneFilter.connect(this.postGain);
    this.postGain.connect(this.output);
 
    // 默认使用软削波
    this.setType('soft', 100);
  }
 
  // 软削波(Soft Clipping)—— 温暖、自然,类似电子管过载
  _softClipCurve(amount) {
    const n = 256;
    const curve = new Float32Array(n);
    const k = amount;
    for (let i = 0; i < n; i++) {
      const x = (i * 2) / n - 1;
      curve[i] = ((Math.PI + k) * x) / (Math.PI + k * Math.abs(x));
    }
    return curve;
  }
 
  // 硬削波(Hard Clipping)—— 刺激、攻击性强,类似晶体管失真
  _hardClipCurve(amount) {
    const n = 256;
    const curve = new Float32Array(n);
    const threshold = 1 - amount / 1000;
    for (let i = 0; i < n; i++) {
      const x = (i * 2) / n - 1;
      curve[i] = Math.max(-threshold, Math.min(threshold, x * (1 / threshold));
    }
    return curve;
  }
 
  // 折叠失真(Foldback)—— 数字感强,产生复杂谐波
  _foldbackCurve(amount) {
    const n = 256;
    const curve = new Float32Array(n);
    const threshold = 0.5 + (1 - amount / 200) * 0.5;
    for (let i = 0; i < n; i++) {
      let x = (i * 2) / n - 1;
      // 超过阈值时折叠回来
      while (Math.abs(x) > threshold) {
        if (x > threshold)  x = 2 * threshold - x;
        if (x < -threshold) x = -2 * threshold - x;
      }
      curve[i] = x;
    }
    return curve;
  }
 
  // 比特压缩(Bit Crusher 近似)—— 复古数字音色
  _bitCrushCurve(bits = 4) {
    const n = 256;
    const curve = new Float32Array(n);
    const steps = Math.pow(2, bits);
    for (let i = 0; i < n; i++) {
      const x = (i * 2) / n - 1;
      curve[i] = Math.round(x * steps) / steps;
    }
    return curve;
  }
 
  setType(type, amount = 100) {
    const curves = {
      soft:     () => this._softClipCurve(amount),
      hard:     () => this._hardClipCurve(amount),
      foldback: () => this._foldbackCurve(amount),
      bitcrush: () => this._bitCrushCurve(Math.round(8 - amount / 14)),
    };
    this.waveshaper.curve = (curves[type] || curves.soft)();
  }
 
  setDrive(value) {
    // value: 1 ~ 10
    const t = this.ctx.currentTime;
    this.preGain.gain.setTargetAtTime(value, t, 0.01);
    // 驱动增大时降低后置增益补偿
    this.postGain.gain.setTargetAtTime(0.4 / Math.sqrt(value), t, 0.01);
  }
 
  setTone(hz) {
    this.toneFilter.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.01);
  }
}

七、合唱效果器(Chorus)

合唱效果通过将信号轻微延迟并叠加,模拟多个演奏者同时演奏的效果,让声音更丰满、有厚度。

class ChorusEffect {
  constructor(audioCtx) {
    this.ctx   = audioCtx;
    this.input  = audioCtx.createGain();
    this.output = audioCtx.createGain();
 
    // 合唱核心:LFO 调制的延迟
    // 使用 3 个声部,相位各差 120°,产生更丰满的效果
    this.voices = [0, 1, 2].map(i => this._createVoice(i * (Math.PI * 2 / 3)));
 
    const dryGain = audioCtx.createGain();
    dryGain.gain.value = 0.6;
    this.input.connect(dryGain);
    dryGain.connect(this.output);
 
    this.voices.forEach(voice => {
      this.input.connect(voice.input);
      voice.output.connect(this.output);
    });
  }
 
  _createVoice(lfoPhase) {
    const delay   = this.ctx.createDelay(0.05);
    const lfo     = this.ctx.createOscillator();
    const lfoGain = this.ctx.createGain();
    const voiceGain = this.ctx.createGain();
 
    // 基础延迟:15ms(合唱的典型延迟范围 10~25ms)
    delay.delayTime.value = 0.015;
 
    // LFO 调制延迟时间,产生音高微颤
    lfo.type            = 'sine';
    lfo.frequency.value = 0.8;  // 0.8Hz 调制速率
    lfoGain.gain.value  = 0.003; // 调制深度:±3ms
 
    // 设置 LFO 初始相位(通过 start 时间偏移模拟)
    lfo.connect(lfoGain);
    lfoGain.connect(delay.delayTime);
    lfo.start(this.ctx.currentTime - lfoPhase / (Math.PI * 2 * 0.8));
 
    voiceGain.gain.value = 0.33; // 3 个声部平均分配
    delay.connect(voiceGain);
 
    return { input: delay, output: voiceGain, lfo, lfoGain };
  }
 
  // 调制速率(Hz)
  setRate(hz) {
    this.voices.forEach(v => {
      v.lfo.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.01);
    });
  }
 
  // 调制深度(0~1)
  setDepth(value) {
    const depthMs = value * 0.006; // 最大 ±6ms
    this.voices.forEach(v => {
      v.lfoGain.gain.setTargetAtTime(depthMs, this.ctx.currentTime, 0.01);
    });
  }
}

八、镶边效果器(Flanger)

镶边与合唱原理相同,但延迟时间更短(0.5~10ms),反馈更强,产生独特的"金属扫频"音色:

class FlangerEffect {
  constructor(audioCtx) {
    this.ctx    = audioCtx;
    this.input   = audioCtx.createGain();
    this.output  = audioCtx.createGain();
 
    this.delay    = audioCtx.createDelay(0.02);
    this.lfo      = audioCtx.createOscillator();
    this.lfoGain  = audioCtx.createGain();
    this.feedback = audioCtx.createGain();
    this.wetGain  = audioCtx.createGain();
    this.dryGain  = audioCtx.createGain();
 
    // 镶边参数:比合唱更短的延迟,更强的反馈
    this.delay.delayTime.value = 0.003; // 3ms 基础延迟
    this.lfo.frequency.value   = 0.3;   // 0.3Hz 调制速率
    this.lfoGain.gain.value    = 0.002; // ±2ms 调制深度
    this.feedback.gain.value   = 0.6;   // 60% 反馈(镶边的关键)
    this.dryGain.gain.value    = 0.7;
    this.wetGain.gain.value    = 0.3;
 
    // LFO → 延迟时间
    this.lfo.connect(this.lfoGain);
    this.lfoGain.connect(this.delay.delayTime);
    this.lfo.start();
 
    // 信号路由(含反馈回路)
    this.input.connect(this.dryGain);
    this.input.connect(this.delay);
    this.delay.connect(this.feedback);
    this.feedback.connect(this.delay); // 反馈回路
    this.delay.connect(this.wetGain);
    this.dryGain.connect(this.output);
    this.wetGain.connect(this.output);
  }
 
  setRate(hz) {
    this.lfo.frequency.setTargetAtTime(hz, this.ctx.currentTime, 0.01);
  }
 
  setDepth(value) {
    this.lfoGain.gain.setTargetAtTime(value * 0.004, this.ctx.currentTime, 0.01);
  }
 
  setFeedback(value) {
    // 镶边反馈可以是负值(产生不同音色)
    const safe = Math.max(-0.95, Math.min(0.95, value));
    this.feedback.gain.setTargetAtTime(safe, this.ctx.currentTime, 0.01);
  }
}

九、音高移调(Pitch Shift)

Web Audio API 没有内置音高移调节点,需要用 AudioWorklet 结合**相位声码器(Phase Vocoder)**算法实现:

// pitch-shift-processor.js(AudioWorklet)
class PitchShiftProcessor extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return [{
      name: 'pitchFactor',
      defaultValue: 1.0,   // 1.0 = 原始音高,2.0 = 高八度,0.5 = 低八度
      minValue: 0.25,
      maxValue: 4.0,
      automationRate: 'k-rate',
    }];
  }
 
  constructor() {
    super();
    this.FRAME_SIZE  = 2048;
    this.HOP_SIZE    = 512;
    this.OVERLAP     = this.FRAME_SIZE / this.HOP_SIZE;
 
    // 输入/输出缓冲区
    this.inputBuffer  = new Float32Array(this.FRAME_SIZE);
    this.outputBuffer = new Float32Array(this.FRAME_SIZE);
    this.inputPtr     = 0;
    this.outputPtr    = 0;
 
    // 相位累积器
    this.lastPhase    = new Float32Array(this.FRAME_SIZE / 2 + 1);
    this.sumPhase     = new Float32Array(this.FRAME_SIZE / 2 + 1);
    this.outputAccum  = new Float32Array(this.FRAME_SIZE * 2);
 
    // 汉宁窗(减少频谱泄漏)
    this.window = new Float32Array(this.FRAME_SIZE);
    for (let i = 0; i < this.FRAME_SIZE; i++) {
      this.window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / this.FRAME_SIZE));
    }
  }
 
  process(inputs, outputs, parameters) {
    const input  = inputs[0][0];
    const output = outputs[0][0];
    const pitchFactor = parameters['pitchFactor'][0];
 
    if (!input) return true;
 
    // 将输入数据写入缓冲区
    for (let i = 0; i < input.length; i++) {
      this.inputBuffer[this.inputPtr++] = input[i];
 
      // 当积累了足够的数据(一帧)时,处理这一帧
      if (this.inputPtr >= this.HOP_SIZE) {
        this.inputPtr = 0;
        this._processFrame(pitchFactor);
      }
    }
 
    // 从输出缓冲区读取数据
    for (let i = 0; i < output.length; i++) {
      output[i] = this.outputBuffer[this.outputPtr++];
      if (this.outputPtr >= this.outputBuffer.length) {
        this.outputPtr = 0;
      }
    }
 
    return true;
  }
 
  _processFrame(pitchFactor) {
    // 简化版相位声码器:
    // 完整实现需要 FFT/IFFT,这里展示核心思路
    // 实际项目建议使用 soundtouch.js 或 rubberband-wasm 等成熟库
 
    const expFactor = pitchFactor;
    for (let i = 0; i < this.FRAME_SIZE; i++) {
      // 时域重采样(近似音高移调,有音质损失)
      const srcIdx = Math.floor(i / expFactor);
      if (srcIdx < this.inputBuffer.length) {
        this.outputBuffer[i] = this.inputBuffer[srcIdx] * this.window[i];
      }
    }
  }
}
 
registerProcessor('pitch-shift-processor', PitchShiftProcessor);

实际项目建议:完整的高质量音高移调算法(相位声码器)实现复杂,建议直接使用 soundtouch-js 或将 RubberBand 编译为 WebAssembly 使用,音质远优于简单的时域重采样。


十、整合:专业级效果链

把本章所有效果器整合成一个完整的信号处理链,模拟专业 DAW 的效果器插槽:

class EffectChain {
  constructor(audioCtx) {
    this.ctx    = audioCtx;
    this.input   = audioCtx.createGain();
    this.output  = audioCtx.createGain();
 
    // 实例化所有效果器
    this.eq         = new ParametricEQ(audioCtx);
    this.compressor = new Compressor(audioCtx);
    this.reverb     = new ConvolutionReverb(audioCtx);
    this.delay      = new DelayEffect(audioCtx);
    this.chorus     = new ChorusEffect(audioCtx);
    this.distortion = new Distortion(audioCtx);
 
    // 效果器槽位(有序,可启用/禁用)
    this.slots = [
      { name: 'distortion', effect: this.distortion, enabled: false },
      { name: 'eq',         effect: this.eq,         enabled: true  },
      { name: 'compressor', effect: this.compressor, enabled: true  },
      { name: 'chorus',     effect: this.chorus,     enabled: false },
      { name: 'delay',      effect: this.delay,      enabled: false },
      { name: 'reverb',     effect: this.reverb,     enabled: false },
    ];
 
    this._rebuildChain();
  }
 
  // 重新构建效果链(启用/禁用效果器后调用)
  _rebuildChain() {
    // 断开所有现有连接
    this.input.disconnect();
    this.slots.forEach(slot => slot.effect.output.disconnect());
 
    const activeSlots = this.slots.filter(s => s.enabled);
 
    if (activeSlots.length === 0) {
      // 没有启用的效果器,直通
      this.input.connect(this.output);
      return;
    }
 
    // 串联所有启用的效果器
    this.input.connect(activeSlots[0].effect.input);
    for (let i = 0; i < activeSlots.length - 1; i++) {
      activeSlots[i].effect.output.connect(activeSlots[i + 1].effect.input);
    }
    activeSlots[activeSlots.length - 1].effect.output.connect(this.output);
  }
 
  // 启用/禁用某个效果器
  toggleEffect(name, enabled) {
    const slot = this.slots.find(s => s.name === name);
    if (slot) {
      slot.enabled = enabled;
      this._rebuildChain();
    }
  }
 
  // 应用完整音效预设
  async applyPreset(preset) {
    // 启用/禁用效果器
    Object.entries(preset.effects || {}).forEach(([name, enabled]) => {
      this.toggleEffect(name, enabled);
    });
 
    // 设置 EQ
    if (preset.eq) {
      this.eq.applyPreset(preset.eq);
    }
 
    // 设置压缩器
    if (preset.compressor) {
      this.compressor.applyPreset(preset.compressor);
    }
 
    // 加载混响 IR
    if (preset.reverbIR) {
      await this.reverb.loadIR(preset.reverbIR);
      this.reverb.setMix(preset.reverbMix || 0.3);
    } else if (preset.reverbSynth) {
      this.reverb.synthesizeIR(
        preset.reverbSynth.duration,
        preset.reverbSynth.decay
      );
      this.reverb.setMix(preset.reverbMix || 0.3);
    }
 
    // 设置延迟
    if (preset.delay) {
      this.delay.setDelayTime(preset.delay.time || 0.375);
      this.delay.setFeedback(preset.delay.feedback || 0.4);
      this.delay.setMix(preset.delay.mix || 0.3);
    }
 
    console.log('预设已应用:', preset.name);
  }
 
  // 连接音频元素
  connectSource(audioEl) {
    const src = this.ctx.createMediaElementSource(audioEl);
    src.connect(this.input);
    return src;
  }
}
 
// ── 完整预设库 ─────────────────────────────────────────────
const EFFECT_PRESETS = {
  clean: {
    name: '清晰直通',
    effects: {
      distortion: false, eq: true, compressor: true,
      chorus: false, delay: false, reverb: false,
    },
    eq: EQ_PRESETS.flat,
    compressor: COMPRESSOR_PRESETS.gentle,
  },
 
  concert_hall: {
    name: '音乐厅',
    effects: {
      distortion: false, eq: true, compressor: true,
      chorus: false, delay: false, reverb: true,
    },
    eq: EQ_PRESETS.classical,
    compressor: COMPRESSOR_PRESETS.mastering,
    reverbSynth: { duration: 3.5, decay: 1.5 },
    reverbMix: 0.45,
  },
 
  pop_vocal: {
    name: '流行人声',
    effects: {
      distortion: false, eq: true, compressor: true,
      chorus: true, delay: true, reverb: true,
    },
    eq: EQ_PRESETS.vocal,
    compressor: COMPRESSOR_PRESETS.vocal,
    reverbSynth: { duration: 1.5, decay: 2.5 },
    reverbMix: 0.25,
    delay: { time: 0.25, feedback: 0.3, mix: 0.2 },
  },
 
  lo_fi: {
    name: 'Lo-Fi',
    effects: {
      distortion: true, eq: true, compressor: true,
      chorus: true, delay: false, reverb: true,
    },
    eq: [
      { gain: 3 }, { gain: -2 }, { gain: 1 },
      { gain: -4 }, { gain: -6 },
    ],
    compressor: COMPRESSOR_PRESETS.mastering,
    reverbSynth: { duration: 1.0, decay: 3.0 },
    reverbMix: 0.3,
  },
 
  rock_guitar: {
    name: '摇滚吉他',
    effects: {
      distortion: true, eq: true, compressor: true,
      chorus: false, delay: true, reverb: true,
    },
    eq: [
      { gain: 4 }, { gain: -2 }, { gain: 3 },
      { gain: 4 }, { gain: 2 },
    ],
    compressor: COMPRESSOR_PRESETS.drums,
    reverbSynth: { duration: 1.2, decay: 2.0 },
    reverbMix: 0.2,
    delay: { time: 0.375, feedback: 0.35, mix: 0.25 },
  },
};

使用方式

const audio  = document.getElementById('audio');
const chain  = new EffectChain(audioCtx);
chain.connectSource(audio);
chain.output.connect(audioCtx.destination);
 
// 应用预设
document.querySelectorAll('.preset-btn').forEach(btn => {
  btn.addEventListener('click', async () => {
    const presetName = btn.dataset.preset;
    await chain.applyPreset(EFFECT_PRESETS[presetName]);
  });
});
 
// 实时调节 EQ
document.querySelectorAll('.eq-slider').forEach((slider, i) => {
  slider.addEventListener('input', () => {
    chain.eq.setBandGain(i, parseFloat(slider.value));
  });
});
 
// 开关混响
document.getElementById('reverbToggle').addEventListener('change', (e) => {
  chain.toggleEffect('reverb', e.target.checked);
});

十一、本章知识图谱

流程图画布 · 115%
Mermaid 流程图加载中...

小结

音频效果器的本质是信号处理算法——均衡器是频域滤波,压缩器是动态范围控制,混响是卷积或延迟网络,失真是非线性变换,调制效果是参数随时间变化的延迟。理解了这些底层逻辑,你就不再是"调参数的人",而是真正能根据听感判断"该用什么效果、调哪个参数、往哪个方向调"的工程师。

Web Audio API 提供的节点已经足以构建专业级的效果链。BiquadFilterNode 的七种滤波器类型覆盖了绝大多数 EQ 需求;DynamicsCompressorNode 的五个参数对应真实压缩器的核心控制;ConvolverNode 配合高质量 IR 文件,混响效果甚至可以媲美专业插件;WaveShaperNode 的自定义曲线让失真风格完全可编程。更复杂的需求,AudioWorklet 可以在独立线程中运行任意 DSP 算法。