第 40 章 均衡器(EQ)与音效处理

本章目标

  • 实现多段 biquad 峰值/低架/高架滤波器。
  • 把 EQ 插入到解码流之后、混响之前。
  • UI:10 段 ±12dB 滑块 + 预设(流行 / 摇滚 / 人声)。
  • 可选:压缩、限幅(Limiter)、立体声增强。

一、Biquad 滤波器

Biquad 是音频 DSP 的工作马。一段 RBJ Cookbook 的实现:

// core/audio/dsp/biquad.rs
pub struct Biquad {
    b0: f32, b1: f32, b2: f32, a1: f32, a2: f32,
    z1: f32, z2: f32,
}

impl Biquad {
    pub fn peaking(fs: f32, f0: f32, q: f32, gain_db: f32) -> Self {
        let a = 10f32.powf(gain_db / 40.0);
        let w0 = 2.0 * std::f32::consts::PI * f0 / fs;
        let alpha = w0.sin() / (2.0 * q);
        let b0 = 1.0 + alpha * a;
        let b1 = -2.0 * w0.cos();
        let b2 = 1.0 - alpha * a;
        let a0 = 1.0 + alpha / a;
        let a1 = -2.0 * w0.cos();
        let a2 = 1.0 - alpha / a;
        Self { b0: b0/a0, b1: b1/a0, b2: b2/a0, a1: a1/a0, a2: a2/a0, z1: 0.0, z2: 0.0 }
    }

    pub fn process(&mut self, x: f32) -> f32 {
        let y = self.b0 * x + self.z1;
        self.z1 = self.b1 * x - self.a1 * y + self.z2;
        self.z2 = self.b2 * x - self.a2 * y;
        y
    }

    pub fn reset(&mut self) { self.z1 = 0.0; self.z2 = 0.0; }
}

二、EQ Graph

pub struct Equalizer {
    bands: Vec<(f32, Biquad, Biquad)>, // (freq, left, right)
    enabled: bool,
    fs: f32,
}

impl Equalizer {
    pub const DEFAULT_BANDS: [f32; 10] = [31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0];

    pub fn new(fs: f32) -> Self {
        let bands = Self::DEFAULT_BANDS.iter()
            .map(|&f| (f, Biquad::peaking(fs, f, 1.0, 0.0), Biquad::peaking(fs, f, 1.0, 0.0)))
            .collect();
        Self { bands, enabled: false, fs }
    }

    pub fn set_gains(&mut self, gains_db: &[f32]) {
        assert_eq!(gains_db.len(), self.bands.len());
        let fs = self.fs;
        for (i, &g) in gains_db.iter().enumerate() {
            let f = self.bands[i].0;
            self.bands[i].1 = Biquad::peaking(fs, f, 1.0, g);
            self.bands[i].2 = Biquad::peaking(fs, f, 1.0, g);
        }
    }

    pub fn process(&mut self, buf: &mut [f32]) {
        if !self.enabled { return; }
        // 立体声交错 L R L R
        for frame in buf.chunks_exact_mut(2) {
            let (l, r) = (frame[0], frame[1]);
            let mut yl = l; let mut yr = r;
            for (_, bl, br) in self.bands.iter_mut() {
                yl = bl.process(yl);
                yr = br.process(yr);
            }
            frame[0] = yl; frame[1] = yr;
        }
    }
}

三、挂到播放链

在 decoder 输出后、发给 output 前:

if let Some(eq) = self.eq.as_mut() {
    eq.process(&mut samples);
}
self.output.write(&samples);

EQ 线程安全:包 Arc<Mutex<Equalizer>>,或者让 pump 线程独占、通过 PlayerCmd::SetEq(Vec<f32>) 更新。

四、预设

pub fn preset(name: &str) -> Vec<f32> {
    match name {
        "flat"    => vec![0.0; 10],
        "rock"    => vec![5.0, 3.0, -1.0, -2.0, -1.0, 1.0, 2.0, 4.0, 5.0, 6.0],
        "pop"     => vec![-1.0, -1.0, 0.0, 2.0, 3.0, 3.0, 1.0, 0.0, -1.0, -2.0],
        "vocal"   => vec![-3.0, -2.0, -1.0, 1.0, 3.0, 4.0, 3.0, 2.0, 1.0, 0.0],
        "bass"    => vec![6.0, 5.0, 4.0, 2.0, 1.0, 0.0, -1.0, -2.0, -2.0, -2.0],
        _         => vec![0.0; 10],
    }
}

五、Limiter 防削波

EQ 加到 +12dB 可能削波(clip)。加一个软限幅:

pub fn limiter(samples: &mut [f32], threshold: f32) {
    for s in samples {
        if *s > threshold { *s = threshold + (*s - threshold).tanh() * (1.0 - threshold); }
        else if *s < -threshold { *s = -threshold + (*s + threshold).tanh() * (1.0 - threshold); }
    }
}

六、前端 UI

const BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];

export function EqPanel() {
  const [gains, setGains] = useState<number[]>(Array(10).fill(0));
  const [enabled, setEnabled] = useState(false);

  useEffect(() => { commands.eqSet(gains, enabled); }, [gains, enabled]);

  function loadPreset(name: string) {
    commands.eqPreset(name).then(setGains);
  }

  return (
    <div className="p-4 bg-surface-1 rounded-lg">
      <div className="flex justify-between mb-3">
        <Switch checked={enabled} onChange={setEnabled} label="启用 EQ" />
        <Select onChange={loadPreset} options={["flat","rock","pop","vocal","bass"]} />
      </div>
      <div className="flex gap-2 items-end h-40">
        {BANDS.map((f, i) => (
          <div key={f} className="flex flex-col items-center gap-1 flex-1">
            <input type="range" min={-12} max={12} step={0.5} value={gains[i]} orient="vertical"
                   onChange={e => {
                     const v = Number(e.target.value);
                     setGains(prev => prev.map((g, j) => j === i ? v : g));
                   }}
                   className="h-32" />
            <span className="text-xs text-text-tertiary">{f >= 1000 ? `${f/1000}k` : f}</span>
            <span className="text-xs">{gains[i] > 0 ? "+" : ""}{gains[i].toFixed(1)}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

七、性能与细节

  • 切歌时 EQ reset() 防止残余滤波。
  • 采样率变化时重新创建 Biquad。
  • EQ 应作用在重采样后(output 的 fs),否则预设不对。

本章小结

  • Biquad 一段干净实现 → 搭 10 段 EQ。
  • 预设覆盖 80% 需求,专业用户可自定义。
  • Limiter 不是可选:有 EQ 就得有它。

动手时刻

  • 拉高低频 +6dB,听人声和鼓的变化。
  • 选"摇滚"预设,对比关闭 EQ 的效果。

下一章:国际化 i18n 与字体加载。