Possible swapped physical frequency labels for feature[4]/feature[5] in WebRTC VAD filterbank

38 views
Skip to first unread message

YUNIST

unread,
Aug 12, 2026, 2:05:03 AM (2 days ago) Aug 12
to discuss-webrtc
I have been analyzing the classic WebRTC VAD filterbank implementation, especially the recursive SplitFilter() used in vad_filterbank.c.

I think there may be a spectral-orientation issue in the upper branch of the filterbank, causing the physical 2–3 kHz and 3–4 kHz bands to be swapped relative to the comments and feature indices.

I would like to confirm whether my multirate analysis is correct, or whether there is some property of the WebRTC all-pass polyphase filterbank that I am missing.

The relevant structure is approximately:

    8 kHz input
    0–4 kHz
        |
        | SplitFilter()
        |
        +-------------------+
        |                   |
      LP                  HP
    0–2 kHz             2–4 kHz
    lp_120              hp_120
                           |
                           | SplitFilter()
                           |
                     +-----+-----+
                     |           |
                   lp_60       hp_60

The WebRTC source labels the second split approximately as:

    in_ptr = hp_120;       // [2000 - 4000] Hz.
    hp_out_ptr = hp_60;    // [3000 - 4000] Hz.
    lp_out_ptr = lp_60;    // [2000 - 3000] Hz.

and later stores:

    hp_60 -> features[5]   // claimed 3000–4000 Hz
    lp_60 -> features[4]   // claimed 2000–3000 Hz

However, I think the first high branch is spectrally inverted after decimation by 2.

For an 8 kHz input sinusoid:

    x[n] = exp(j * Omega * n)

after decimation by 2, the output frequency is:

    Omega' = 2 * Omega  (mod 2*pi)

For the original upper band:

    2 kHz < f < 4 kHz

we have:

    pi/2 < Omega < pi

therefore:

    pi < 2*Omega < 2*pi

which maps to negative discrete-time frequencies:

    Omega' = 2*Omega - 2*pi

For a real-valued signal, the corresponding positive-frequency magnitude is therefore reversed.

In Hz, the mapping becomes:

    f_local = 4000 - f_original

Examples:

    original 2.0 kHz -> local 2.0 kHz
    original 2.5 kHz -> local 1.5 kHz
    original 3.0 kHz -> local 1.0 kHz
    original 3.5 kHz -> local 0.5 kHz
    original 4.0 kHz -> local 0 Hz

So although hp_120 contains energy originating from the original 2–4 kHz band, its local frequency orientation at the new 4 kHz sample rate is reversed.

The second SplitFilter() operates on hp_120 as a normal 4 kHz sampled signal.

Therefore, locally it separates:

    lp_60 -> local 0–1 kHz
    hp_60 -> local 1–2 kHz

Mapping these local frequencies back to the original 8 kHz input gives:

    local 0–1 kHz
        -> original 3–4 kHz

    local 1–2 kHz
        -> original 2–3 kHz

Therefore I obtain:

    lp_60 -> original 3–4 kHz
    hp_60 -> original 2–3 kHz

which is the opposite of the comments in vad_filterbank.c.

A simple tone test also appears to support this interpretation:

    2.5 kHz input tone
        -> after the first high branch: local 1.5 kHz
        -> should therefore dominate the second-stage HP output
        -> hp_60

    3.5 kHz input tone
        -> after the first high branch: local 0.5 kHz
        -> should therefore dominate the second-stage LP output
        -> lp_60

I do not see any explicit spectral-orientation correction in SplitFilter(), such as:

    (-1)^n modulation
    spectrum reversal
    swapping the child bands
    complex conjugation

The implementation appears to be only:

    even samples -> all-pass A0
    odd samples  -> all-pass A1

    HP = upper - lower
    LP = upper + lower

with a factor-of-two reduction in sample rate.

My current interpretation is therefore:

    feature[0] -> approximately 80–250 Hz
    feature[1] -> approximately 250–500 Hz
    feature[2] -> approximately 500–1000 Hz
    feature[3] -> approximately 1000–2000 Hz

    feature[4] -> actually approximately 3000–4000 Hz
    feature[5] -> actually approximately 2000–3000 Hz

rather than the documented ordering:

    feature[4] -> 2000–3000 Hz
    feature[5] -> 3000–4000 Hz

There is another related question.

The WebRTC global VAD uses the spectrum weights:

    {6, 8, 10, 12, 14, 16}

These appear to increase monotonically with channel index, which suggests that the implementation may have intended the channel indices to correspond to increasing physical frequency.

If feature[4] and feature[5] are physically reversed, then in actual frequency order the last two weights would effectively be:

    2000–3000 Hz -> 16
    3000–4000 Hz -> 14

rather than 14 and 16.

However, I do not know how the original GMM parameters and spectrum weights were trained or tuned.

So I think there are three separate questions:

1. Is the spectral inversion analysis above correct?

2. If so, are the comments and the physical frequency labels of feature[4] and feature[5] incorrect?

3. Were the GMM parameters and spectrum weights tuned using the actual filter paths, in which case this may only be a documentation/semantic issue, or were they designed under the assumption that feature[4] is 2–3 kHz and feature[5] is 3–4 kHz?

I would especially appreciate confirmation from someone familiar with critically sampled all-pass / QMF filterbanks or the history of the WebRTC VAD implementation.

I am not claiming yet that the VAD classification itself is broken. The actual runtime feature paths and GMM parameters may have been tuned together for many years. My main concern is whether the physical frequency interpretation of the top two bands is reversed.

Note: English is not my native language, so I used AI assistance to translate and improve the wording of this post. I have reviewed the technical content and am posting it to ask for independent verification.
Reply all
Reply to author
Forward
0 new messages