FXLMS 与主动降噪系统架构

主动降噪(ANC, Active Noise Control)的核心思路是用扬声器发出一个与噪声相位相反、幅值相同的声波,通过相消干涉抵消掉目标区域的噪声。落地到硬件系统,需要解决两个问题:用什么传感器感知噪声,以及用哪种控制策略生成抗噪声。这两点决定了 ANC 系统的架构分类——前馈、反馈和混合式。

前馈式 ANC

前馈式 ANC 用一只参考麦克风置于噪声进入方向,提前采集噪声源信号送给 DSP 处理,再由扬声器输出抗噪声。在人耳附近另设一只误差麦克风,监测残余噪声并反馈给自适应算法做权重修正。

信号链路:

1
2
3
噪声源 → 参考麦克风 → 自适应滤波器 W(z) → 扬声器 → 人耳
                          ↑                      ↓
                  误差麦克风 ← 残余噪声

下图展示了前馈 ANC 的完整信号流,包含参考麦克风、自适应滤波器、次级路径和误差反馈环路:

mermaid
flowchart TD
    SRC["噪声源"] --> REF["参考麦克风"]
    REF --> W["自适应滤波器<br/>W(z)"]
    W --> SPK["扬声器"]
    SPK -->|"次级路径 S(z)"| EAR["人耳"]
    EAR --> EMIC["误差麦克风"]
    EMIC -->|"e(n) 反馈"| W

    classDef src fill:#f44336,color:#fff
    classDef mic fill:#2196F3,color:#fff
    classDef proc fill:#9C27B0,color:#fff
    classDef out fill:#4CAF50,color:#fff
    class SRC src
    class REF,EMIC mic
    class W proc
    class SPK,EAR out

前馈架构能提前感知噪声,对宽带随机噪声的抑制效果最好。它的前提是参考麦克风与噪声源之间有稳定的声学耦合——如果用户转头、佩戴松动,次级路径变了,抵消效果就会下降。这也是前馈方案最敏感的地方:次级路径 $S(z)$ 的变化直接反映到误差信号里,自适应滤波器必须持续跟踪。

消费级场景中,前馈麦克风往往开在耳机外壳上,专门采集环境噪声;误差麦克风藏在耳罩内部靠近耳道的位置。

反馈式 ANC

反馈式 ANC 不需要参考麦克风。它只靠误差麦克风采集耳罩内的残余噪声,把整个系统当作一个负反馈环路来处理。

结构上的优势很直接:省一个麦克风,硬件成本和结构设计都简化。但代价是反馈系统只能对已经传到耳朵的噪声做事后补偿,对突发噪声的反应天然滞后。反馈式对周期性窄带噪声(发动机轰鸣、变压器嗡鸣)效果突出,因为这类噪声有规律,反馈环路能很快锁定相位。然而反馈环路天然存在稳定性问题——设计不当容易自激振荡。

单从控制理论角度看,反馈式 ANC 等价于一个高增益负反馈系统,开环传递函数的相位裕度必须留够。

混合式 ANC

混合式是前馈和反馈的结合:既保留外壳上的参考麦克风,也保留耳罩内的误差麦克风。

高端消费降噪耳机(AirPods Pro、Sony WH-1000XM 系列)清一色走混合路线。前馈通道负责宽带环境噪声(街道、风噪),反馈通道专攻耳罩内部残留的低频窄带噪声。两套滤波器独立运行,在 DSP 里叠加输出。

代价也很明确:两套自适应滤波器、两套次级路径辨识,计算量和内存占用几乎翻倍。对嵌入式平台来说,功耗和实时性是需要审慎评估的约束。

三种架构的对比示意:

mermaid
flowchart TD
    subgraph 前馈
        F1["参考麦克风"] --> F2["W(z)"] --> F3["扬声器"]
        F4["误差麦克风"] --> F2
    end
    subgraph 反馈
        B1["误差麦克风"] --> B2["控制器"] --> B3["扬声器"]
        B3 --> B1
    end
    subgraph 混合
        H1["参考麦克风"] --> H2["前馈 W₁(z)"] --> H5["Σ"] --> H6["扬声器"]
        H3["误差麦克风"] --> H4["反馈 W₂(z)"] --> H5
        H6 --> H3
    end

多通道 ANC

立体声耳机天然需要两个独立的 ANC 通道——左右耳各一套。通道间存在声学串扰(左声道扬声器可能被右耳误差麦克风拾取),多通道系统必须用矩阵形式的滤波器结构解耦。每个通道都有自己的自适应滤波器和误差麦克风,各自的参考信号可能来自对应的参考麦克风。

FXLMS 算法详解

标准 LMS 自适应滤波器假设误差信号可以直接用于权重更新。但在 ANC 系统中,扬声器到误差麦克风之间存在次级路径 $S(z)$——包含 DAC、功放、扬声器、声学腔体、误差麦克风和 ADC 的完整链路。这个路径引入的幅相畸变如果不做补偿,LMS 会收敛到次优解,甚至直接发散。

FXLMS(Filtered-x LMS)的修正就是把次级路径的影响纳入权重更新路径。它不是直接用参考信号 $x(n)$,而是先用次级路径的估计模型 $\hat{S}(z)$ 对 $x(n)$ 做一次滤波,得到 $x’(n)$,再用 $x’(n)$ 参与权重更新。

推导很简单。定义:

  • $x(n)$: 参考信号
  • $y(n) = \mathbf{w}^T(n) \mathbf{x}(n)$: 滤波器输出
  • $d(n)$: 目标信号(噪声在误差麦克风处的声学响应)
  • $e(n) = d(n) - s(n) * y(n)$: 误差信号($s(n)$ 是次级路径冲激响应)

权重更新方程:

$$ \mathbf{w}(n+1) = \mathbf{w}(n) + \mu \cdot e(n) \cdot \mathbf{x}’(n) $$

其中 $\mathbf{x}’(n) = \hat{s}(n) * \mathbf{x}(n)$ 是滤波后的参考信号向量。

FXLMS 的四步执行流程

下图展示了 FXLMS 算法的内部数据流,突出显示了 filtered-x 路径:

mermaid
flowchart TD
    X["参考信号 x(n)"] --> W["主滤波器 W(z)<br/>y = w^T · x"]
    W --> OUT["输出到扬声器"]
    X --> SHAT["次级路径估计 Ŝ(z)<br/>x' = ŝ * x"]
    SHAT --> XF["滤波后参考信号 x'(n)"]
    EMIC["误差信号 e(n)"] --> UP["权重更新<br/>w += μ · e · x'"]
    XF --> UP
    UP --> W

    classDef input fill:#2196F3,color:#fff
    classDef filter fill:#9C27B0,color:#fff
    classDef key fill:#f44336,color:#fff
    classDef output fill:#4CAF50,color:#fff
    class X,EMIC input
    class W,SHAT,UP filter
    class XF key
    class OUT output

每次采样周期,FXLMS 执行以下 4 步:

步骤①:滤波参考信号

$$ x’(n) = \hat{s}(n) * x(n) $$

为什么需要"再滤一次"?普通 LMS 的权重更新假设误差信号的梯度方向直接可用,但 ANC 系统中误差信号 $e(n)$ 经过了次级路径 $S(z)$ 的畸变。如果不做补偿,权重更新的梯度方向是错的——算法在一个扭曲的坐标系里优化。FXLMS 用 $\hat{S}(z)$ 对参考信号预滤波,把梯度方向"校正"回来。

步骤②:滤波器输出

$$ y(n) = \mathbf{w}^T(n)\mathbf{x}(n) $$

标准 FIR 滤波,和普通 LMS 完全一样。

步骤③:误差信号

$$ e(n) = d(n) - s(n) * y(n) $$

注意这里用的是真实的次级路径 $s(n)$(实际物理路径),不是估计值 $\hat{s}(n)$。$d(n)$ 是噪声通过被动路径传到误差麦克风的信号,$s(n) * y(n)$ 是扬声器输出经次级路径到达误差麦克风的信号。

步骤④:权重更新

$$ \mathbf{w}(n+1) = \mathbf{w}(n) + \mu \cdot e(n) \cdot \mathbf{x}’(n) $$

关键区别:这里用的是 滤波后的参考信号 $x’(n)$ 而不是原始 $x(n)$。如果用 $x(n)$ 替换 $x’(n)$,FXLMS 就退化为普通 LMS,在 ANC 系统中无法收敛。

💡 数值例子:假设次级路径 $S(z)$ 引入 0.5ms 的群延迟(16kHz 采样率下约 8 个采样点)。如果不做 filtered-x 补偿,权重更新的梯度方向滞后了 0.5ms。对 1kHz 的噪声(周期 1ms)来说,这相当于半周期的相位差——权重更新方向几乎与正确方向正交,算法基本无法收敛。

次级路径离线辨识

$\hat{S}(z)$ 的系数通常在 ANC 开始之前通过离线辨识获得。操作方式:用扬声器播放白噪声,同时用误差麦克风采集响应,再用 LMS 辨识出路径的 FIR 系数。辨识完成后固定下来,在运行时不再更新。

下面的收敛曲线展示了 LMS 辨识过程中误差如何随迭代下降——从初始的大误差快速收敛到稳态:

次级路径辨识收敛
1.00.750.500.250.00255075100迭代次数误差初始阶段收敛阶段稳态阶段

下面是一个完整的 FXLMS 实现。重点关注 xf_buf 的作用——它是 FXLMS 区别于普通 LMS 的核心。

c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

#define W_ORDER     64
#define S_ORDER     32
#define MU_W        0.0005f
#define MU_S        0.001f

typedef struct {
    float w[W_ORDER];
    float x_buf[W_ORDER];
    float xf_buf[W_ORDER];      // 滤波后参考信号缓冲区
    uint32_t x_ptr;
    float s_hat[S_ORDER];
    float s_buf[S_ORDER];
    uint32_t s_ptr;
    float s_model[S_ORDER];
    float y_buf[S_ORDER];
    uint32_t y_ptr;
    uint8_t mode;
} FXLMS_Controller;

void fxlms_init(FXLMS_Controller *fx) {
    memset(fx, 0, sizeof(FXLMS_Controller));
    fx->mode = 0;
}

// 次级路径离线辨识
void fxlms_identify_secondary_path(FXLMS_Controller *fx,
                                    float *white_noise,
                                    float *error_signal,
                                    uint32_t len) {
    for (uint32_t n = 0; n < len; n++) {
        fx->y_buf[fx->y_ptr] = white_noise[n];
        float y_est = 0.0f;
        for (uint32_t i = 0; i < S_ORDER; i++) {
            uint32_t idx = (fx->y_ptr + S_ORDER - i) % S_ORDER;
            y_est += fx->s_model[i] * fx->y_buf[idx];
        }
        float e_s = error_signal[n] - y_est;
        for (uint32_t i = 0; i < S_ORDER; i++) {
            uint32_t idx = (fx->y_ptr + S_ORDER - i) % S_ORDER;
            fx->s_model[i] += MU_S * e_s * fx->y_buf[idx];
        }
        fx->y_ptr = (fx->y_ptr + 1) % S_ORDER;
    }
    memcpy(fx->s_hat, fx->s_model, sizeof(fx->s_model));
    fx->mode = 1;
}

// FXLMS 核心处理
float fxlms_process(FXLMS_Controller *fx, float x_ref, float e_mic) {
    fx->x_buf[fx->x_ptr] = x_ref;
    float y = 0.0f;
    for (uint32_t i = 0; i < W_ORDER; i++) {
        uint32_t idx = (fx->x_ptr + W_ORDER - i) % W_ORDER;
        y += fx->w[i] * fx->x_buf[idx];
    }
    // 计算滤波后参考信号 x'(n)
    fx->s_buf[fx->s_ptr] = x_ref;
    float x_filtered = 0.0f;
    for (uint32_t i = 0; i < S_ORDER; i++) {
        uint32_t idx = (fx->s_ptr + S_ORDER - i) % S_ORDER;
        x_filtered += fx->s_hat[i] * fx->s_buf[idx];
    }
    fx->xf_buf[fx->x_ptr] = x_filtered;

    // 权重更新用滤波后的参考信号
    for (uint32_t i = 0; i < W_ORDER; i++) {
        uint32_t idx = (fx->x_ptr + W_ORDER - i) % W_ORDER;
        fx->w[i] += MU_W * e_mic * fx->xf_buf[idx];
    }

    fx->x_ptr = (fx->x_ptr + 1) % W_ORDER;
    fx->s_ptr = (fx->s_ptr + 1) % S_ORDER;
    return y;
}

代码中 xf_buf 是 FXLMS 的灵魂。fxlms_process 每次被调用时,先用 s_hat 对当前参考信号 x_ref 滤波得到 x_filtered,存入 xf_buf;权重更新时从 xf_buf 取数据,而非从 x_buf。这行代码:

c
1
fx->w[i] += MU_W * e_mic * fx->xf_buf[idx];

就是 FXLMS 与普通 LMS 的唯一区别,也是整个主动降噪算法能稳定收敛的关键。