nx_aec_q15.nx source
↩ module page · 69 lines · 4196 B
1// nx_aec.nx -- sovereign ACOUSTIC ECHO CANCELLATION (the echoCancellation feature Zoom/Discord/Meet have): the
2// mic picks up the near-end speaker PLUS an echo of the far-end audio coming out of the speakers; AEC learns the
3// echo path with an adaptive NLMS filter over the far-end REFERENCE signal and subtracts the predicted echo, so the
4// other side hears your voice without their own voice bouncing back. Pure Nishi DSP (the browser only ferries raw
5// PCM). w[] = filter taps in Q15 (persist across calls, start zeroed). license_tier: ORIGINAL
6
7// process n samples: mic[] = near-speech + echo; ref[] = far-end reference. Writes the echo-cancelled signal to
8// out[] and adapts w[L] in place (NLMS, step in Q15 ~ mu*32768).
9func aec_run(mic: *i64, ref: *i64, n: i64, L: i64, step_q15: i64, w: *i64, out: *i64) -> i64 {
10 var i: i64 = 0
11 while i < n {
12 // predicted echo = sum_k w[k]*ref[i-k] (Q15 -> descale)
13 var echo: i64 = 0
14 var k: i64 = 0
15 while k < L { if i - k >= 0 { echo = echo + w[k] * ref[i - k] } k = k + 1 }
16 echo = echo / 32768
17 let err: i64 = mic[i] - echo // echo removed -> the near-end voice
18 out[i] = err
19 // reference power (NLMS normalization)
20 var power: i64 = 1
21 k = 0
22 while k < L { if i - k >= 0 { let r: i64 = ref[i - k]; power = power + r * r } k = k + 1 }
23 // NLMS tap update: w[k] += step * err * ref[i-k] / power
24 k = 0
25 while k < L { if i - k >= 0 { w[k] = w[k] + (step_q15 * err * ref[i - k]) / power } k = k + 1 }
26 i = i + 1
27 }
28 return 0
29}
30
31// aec_run_dtd -- AEC with a Geigel DOUBLE-TALK DETECTOR. The trouble with plain NLMS: when BOTH sides speak at once
32// (double-talk), the near-end voice corrupts the error signal and the filter mis-adapts (diverges). The fix is to
33// FREEZE adaptation whenever near-end speech is present. Geigel's test: near speech is present when the mic sample is
34// louder than the loudest recent reference could explain as echo -- |mic| > thresh * max_k|ref[i-k]| (thresh in Q8,
35// e.g. 192 = 0.75 for an echo path attenuated to <= ~0.75x). This lets us use a FAST step (quick convergence when the
36// far-end talks alone) WITHOUT diverging during double-talk -- strictly better than a slow step. A HANGOVER (hang
37// samples) keeps the filter frozen through the quiet inter-word gaps of a near-end utterance -- without it, a single
38// instant where near speech and echo cancel looks "quiet" and the filter wrongly resumes adapting mid-utterance.
39// Research: Geigel DTD (Duttweiler), normalized-cross-correlation DTD, hangover timing. license_tier: ORIGINAL
40func aec_run_dtd(mic: *i64, ref: *i64, n: i64, L: i64, step_q15: i64, w: *i64, out: *i64, dtd_thresh_q8: i64, hang: i64) -> i64 {
41 var i: i64 = 0
42 var hold: i64 = 0 // hangover counter: >0 -> recently heard near speech, stay frozen
43 while i < n {
44 var echo: i64 = 0
45 var k: i64 = 0
46 while k < L { if i - k >= 0 { echo = echo + w[k] * ref[i - k] } k = k + 1 }
47 echo = echo / 32768
48 let err: i64 = mic[i] - echo
49 out[i] = err
50 // Geigel double-talk detector: loudest |ref| over the filter window
51 var maxref: i64 = 0
52 k = 0
53 while k < L { if i - k >= 0 { var a: i64 = ref[i - k]; if a < 0 { a = 0 - a } if a > maxref { maxref = a } } k = k + 1 }
54 var amic: i64 = mic[i]
55 if amic < 0 { amic = 0 - amic }
56 if amic * 256 > dtd_thresh_q8 * maxref { hold = hang } // mic louder than echo could explain -> near speech, (re)arm hangover
57 if hold > 0 {
58 hold = hold - 1 // inside a near-end utterance -> FREEZE adaptation
59 } else { // far-end alone -> adapt
60 var power: i64 = 1
61 k = 0
62 while k < L { if i - k >= 0 { let r: i64 = ref[i - k]; power = power + r * r } k = k + 1 }
63 k = 0
64 while k < L { if i - k >= 0 { w[k] = w[k] + (step_q15 * err * ref[i - k]) / power } k = k + 1 }
65 }
66 i = i + 1
67 }
68 return 0
69}