code wiki / _hdl_build / nx_aec.nx

nx_aec.nx source

↩ module page · 210 lines · 12243 B

1// nx_aec.nx -- R4a: sovereign Acoustic Echo Canceller. The reserved-but-empty nx_aec slot in 2// nx_audio_session, now built. Adaptive FIR (NLMS) in INTEGER fixed-point (NO-FLOAT doctrine): 3// it learns the echo path (speaker->room->mic) from the far-end reference and subtracts the 4// predicted echo from the mic signal, so the far end doesn't hear itself. 5// 6// y[n] = (sum_k w[k]*x[n-k]) >> Qw (predicted echo from far-end reference x) 7// e[n] = d[n] - y[n] (mic d minus predicted echo = the cleaned output) 8// w[k] += mu * e[n] * x[n-k] / (||x||^2 + eps) (NLMS update, normalized step) 9// 10// MEASURED metric = ERLE (Echo Return Loss Enhancement) = echo power BEFORE / AFTER cancel. 11// Negative controls: (T2) adaptation OFF -> e==d -> no cancellation (the filter is load-bearing); 12// (T3) DOUBLE-TALK -- with the converged filter frozen, near-end speech must SURVIVE while the 13// echo is removed (a broken AEC would crush the near-end too); (T4) ERLE improves over time (it LEARNS). 14// 15// main() is the SELF-VALIDATING GATE. Evidence -> knowledge/status/aec.log. 16// license_tier: ORIGINAL 17import "nx_syscalls.nx" 18const AEC_MAGIC_12000: i64 = 12000 19const AEC_MAGIC_2000: i64 = 2000 20const AEC_MAGIC_13579: i64 = 13579 21const AEC_MAGIC_1103515245: i64 = 1103515245 22const AEC_MAGIC_12345: i64 = 12345 23const AEC_MAGIC_16001: i64 = 16001 24const AEC_MAGIC_8000: i64 = 8000 25const AEC_MAGIC_24680: i64 = 24680 26const AEC_MAGIC_12001: i64 = 12001 27const AEC_MAGIC_6000: i64 = 6000 28const AEC_MAGIC_26214: i64 = 26214 29const AEC_MAGIC_16384: i64 = 16384 30const AEC_MAGIC_9830: i64 = 9830 31const AEC_MAGIC_6554: i64 = 6554 32const AEC_MAGIC_3277: i64 = 3277 33const AEC_MAGIC_1966: i64 = 1966 34 35const AEC_LOG: *u8 = "knowledge/status/aec.log" 36const QW: i64 = 16 // filter coeff fixed-point 37const MU_NUM: i64 = 1 38const MU_DEN: i64 = 2 // mu = 0.5 (stable) 39const EPS: i64 = 256 40 41func aw(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 } 42func awn(fd: i64, v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m;sys_write(fd,"-" as *u8,1)} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1} sys_write(fd,bb,k); return 0 } 43 44// run the canceller over x (far-end) / d (mic) into e (cleaned). adapt=1 learns; 0 freezes. 45func aec_run(w: *i64, x: *i64, d: *i64, e: *i64, T: i64, L: i64, adapt: i64) -> i64 { 46 var n: i64 = 0 47 while n < T { 48 var y: i64 = 0 49 var k: i64 = 0 50 while k < L { if n - k >= 0 { y = y + w[k] * x[n-k] } k = k + 1 } 51 y = y >> QW 52 let en: i64 = d[n] - y 53 e[n] = en 54 if adapt == 1 { 55 var energy: i64 = 0 56 k = 0 57 while k < L { if n - k >= 0 { let xv: i64 = x[n-k]; energy = energy + xv*xv } k = k + 1 } 58 let denom: i64 = (energy + EPS) * MU_DEN 59 k = 0 60 while k < L { 61 if n - k >= 0 { w[k] = w[k] + (MU_NUM * en * x[n-k] * (1 << QW)) / denom } 62 k = k + 1 63 } 64 } 65 n = n + 1 66 } 67 return 0 68} 69 70// sum of squares of a[start .. start+n) 71func powwin(a: *i64, start: i64, n: i64) -> i64 { var s: i64=0; var i: i64=start; while i < start+n { s = s + a[i]*a[i]; i=i+1 } return s } 72// sum of squares of (a-b) over window 73func powdiff(a: *i64, b: *i64, start: i64, n: i64) -> i64 { var s: i64=0; var i: i64=start; while i < start+n { let dd: i64=a[i]-b[i]; s = s + dd*dd; i=i+1 } return s } 74// crude integer ERLE in dB from a power ratio (10*log10), lower-bound + sub-decade interp 75func erle_db(ratio: i64) -> i64 { 76 var d: i64 = 0; var t: i64 = ratio 77 while t >= 10 { d = d + 10; t = t / 10 } 78 if t >= 8 { d = d + 9 } else { if t >= 6 { d = d + 8 } else { if t >= 4 { d = d + 6 } else { if t >= 3 { d = d + 5 } else { if t >= 2 { d = d + 3 } } } } } 79 return d 80} 81 82// aec_run_dtd -- aec_run PLUS a Geigel DOUBLE-TALK DETECTOR (merged from the former runtime/ nx_aec so the 83// canonical canceller has the detector the base aec_run's HONEST SCOPE deferred). Near-end speech is present when 84// the mic is louder than the loudest recent reference could explain as echo (|d| > thresh*max|x|, thresh Q8); a 85// `hang` hangover keeps the filter frozen through the quiet inter-word gaps. Freezing during double-talk lets a 86// FAST step converge quickly without diverging when both sides talk. Same Q16/energy-normalized NLMS as aec_run. 87func aec_run_dtd(w: *i64, x: *i64, d: *i64, e: *i64, T: i64, L: i64, dtd_thresh_q8: i64, hang: i64) -> i64 { 88 var n: i64 = 0; var hold: i64 = 0 89 while n < T { 90 var y: i64 = 0; var k: i64 = 0 91 while k < L { if n - k >= 0 { y = y + w[k] * x[n-k] } k = k + 1 } 92 y = y >> QW 93 let en: i64 = d[n] - y 94 e[n] = en 95 var maxref: i64 = 0; k = 0 96 while k < L { if n - k >= 0 { var a: i64 = x[n-k]; if a < 0 { a = 0 - a } if a > maxref { maxref = a } } k = k + 1 } 97 var amic: i64 = d[n]; if amic < 0 { amic = 0 - amic } 98 if amic * 256 > dtd_thresh_q8 * maxref { hold = hang } // near speech -> (re)arm hangover 99 if hold > 0 { hold = hold - 1 } // frozen through the near-end utterance 100 else { 101 var energy: i64 = 0; k = 0 102 while k < L { if n - k >= 0 { let xv: i64 = x[n-k]; energy = energy + xv*xv } k = k + 1 } 103 let denom: i64 = (energy + EPS) * MU_DEN 104 k = 0 105 while k < L { if n - k >= 0 { w[k] = w[k] + (MU_NUM * en * x[n-k] * (1 << QW)) / denom } k = k + 1 } 106 } 107 n = n + 1 108 } 109 return 0 110} 111 112func main() -> i64 { 113 let L: i64 = 64 // filter taps 114 let M: i64 = 8 // echo-path length (inside L -> learnable) 115 let T: i64 = AEC_MAGIC_12000 116 let WIN: i64 = AEC_MAGIC_2000 // measurement window 117 let x: *i64 = sys_mmap(8*T) as *i64 118 let d: *i64 = sys_mmap(8*T) as *i64 119 let s: *i64 = sys_mmap(8*T) as *i64 // near-end speech (double-talk) 120 let d2:*i64 = sys_mmap(8*T) as *i64 // mic during double-talk (echo + near-end) 121 let e: *i64 = sys_mmap(8*T) as *i64 122 let w: *i64 = sys_mmap(8*L) as *i64 123 124 // far-end reference x: white-ish noise +-8000 (rich excitation for fast convergence) 125 var seed: i64 = AEC_MAGIC_13579 126 var i: i64 = 0 127 while i < T { seed=(seed*AEC_MAGIC_1103515245+AEC_MAGIC_12345) & 0x7fffffff; x[i]=((seed>>9) % AEC_MAGIC_16001) - AEC_MAGIC_8000; i=i+1 } 128 // near-end speech s: a DIFFERENT signal +-6000 129 seed = AEC_MAGIC_24680 130 i = 0 131 while i < T { seed=(seed*AEC_MAGIC_1103515245+AEC_MAGIC_12345) & 0x7fffffff; s[i]=((seed>>9) % AEC_MAGIC_12001) - AEC_MAGIC_6000; i=i+1 } 132 133 // echo path h (Q16): decaying room impulse response, total gain < 1 134 let h: *i64 = sys_mmap(8*M) as *i64 135 h[0]=AEC_MAGIC_26214; h[1]=AEC_MAGIC_16384; h[2]=AEC_MAGIC_9830; h[3]=AEC_MAGIC_6554; h[4]=AEC_MAGIC_3277; h[5]=AEC_MAGIC_1966; h[6]=983; h[7]=328 136 // mic echo d[n] = (sum h[k]*x[n-k])>>16 ; double-talk mic d2 = echo + near-end 137 i = 0 138 while i < T { 139 var y: i64 = 0; var k: i64 = 0 140 while k < M { if i - k >= 0 { y = y + h[k]*x[i-k] } k = k + 1 } 141 let echo: i64 = y >> QW 142 d[i] = echo 143 d2[i] = echo + s[i] 144 i = i + 1 145 } 146 147 // --- scenario 1: ADAPT on echo-only mic -> converge --- 148 var z: i64=0; while z < L { w[z]=0; z=z+1 } 149 aec_run(w, x, d, e, T, L, 1) 150 let pd_late: i64 = powwin(d, T-WIN, WIN) 151 let pe_late: i64 = powwin(e, T-WIN, WIN) 152 let ratio_late: i64 = pd_late / (pe_late + 1) 153 let erle: i64 = erle_db(ratio_late) 154 // convergence: early window (after a short warmup) vs late 155 let pd_early: i64 = powwin(d, 0, WIN) 156 let pe_early: i64 = powwin(e, 0, WIN) 157 let ratio_early: i64 = pd_early / (pe_early + 1) 158 159 // --- T2 neg-control: NO adaptation (w=0) -> e==d -> no cancellation --- 160 z=0; while z < L { w[z]=0; z=z+1 } 161 aec_run(w, x, d, e, T, L, 0) 162 let pe_noadapt: i64 = powwin(e, T-WIN, WIN) 163 let ratio_noadapt: i64 = pd_late / (pe_noadapt + 1) 164 165 // --- T3 double-talk: RE-converge on echo-only, FREEZE, then run on (echo+near-end) --- 166 z=0; while z < L { w[z]=0; z=z+1 } 167 aec_run(w, x, d, e, T, L, 1) // converge w 168 aec_run(w, x, d2, e, T, L, 0) // frozen filter on double-talk mic -> e should ~= near-end s 169 let p_recover_err: i64 = powdiff(e, s, T-WIN, WIN) // how far cleaned output is from pure near-end 170 let p_near: i64 = powwin(s, T-WIN, WIN) 171 let p_echo_in_dt: i64 = powwin(d2, T-WIN, WIN) // echo+near-end power before cancel 172 173 // --- T5 double-talk DETECTOR (merged aec_run_dtd): converge, then the Geigel DTD AUTO-freezes on double-talk --- 174 z=0; while z < L { w[z]=0; z=z+1 } 175 aec_run(w, x, d, e, T, L, 1) // converge on echo-only first 176 aec_run_dtd(w, x, d2, e, T, L, 192, 256) // DTD auto-detects near speech + freezes (no manual freeze) 177 let p_dtd_err: i64 = powdiff(e, s, T-WIN, WIN) // cleaned output vs pure near-end (DTD preserved near-end) 178 179 var ok: i64 = 1 180 // T1: real echo cancellation (>= 100x = >=20 dB ERLE) 181 if ratio_late < 100 { ok = 0 } 182 // T5: the merged Geigel DTD auto-freeze preserves near-end during double-talk (no manual freeze needed) 183 if (p_dtd_err * 4) >= p_near { ok = 0 } 184 // T2: without adaptation, no cancellation (ratio ~1) 185 if ratio_noadapt > 2 { ok = 0 } 186 // T4: the filter LEARNS (late ERLE strictly better than early) 187 if ratio_late <= ratio_early { ok = 0 } 188 // T3: double-talk -- near-end SURVIVES (cleaned output within 25% power of pure near-end), 189 // i.e. echo removed AND near-end not crushed 190 if (p_recover_err * 4) >= p_near { ok = 0 } 191 192 aw(1, "=== nx_aec -- sovereign integer NLMS acoustic echo canceller ===\n" as *u8) 193 aw(1, " taps L=" as *u8); awn(1, L); aw(1, " echo-path M=" as *u8); awn(1, M); aw(1, " samples=" as *u8); awn(1, T); aw(1, " mu=1/2 (NO-FLOAT, Q16)\n" as *u8) 194 aw(1, " T1 ERLE steady-state: echo power reduced " as *u8); awn(1, ratio_late); aw(1, "x (~" as *u8); awn(1, erle); aw(1, " dB): " as *u8); if ratio_late>=100 { aw(1,"PASS" as *u8) } else { aw(1,"FAIL" as *u8) } 195 aw(1, "\n T4 convergence early=" as *u8); awn(1, ratio_early); aw(1, "x -> late=" as *u8); awn(1, ratio_late); aw(1, "x (filter LEARNS): " as *u8); if ratio_late>ratio_early { aw(1,"PASS" as *u8) } else { aw(1,"FAIL" as *u8) } 196 aw(1, "\n T2 NEG no-adaptation: reduction " as *u8); awn(1, ratio_noadapt); aw(1, "x (~1 = nothing; filter is load-bearing): " as *u8); if ratio_noadapt<=2 { aw(1,"PASS" as *u8) } else { aw(1,"FAIL" as *u8) } 197 aw(1, "\n T3 DOUBLE-TALK: near-end recovered, residual err " as *u8); awn(1, p_recover_err); aw(1, " vs near-end power " as *u8); awn(1, p_near); aw(1, " (mic had echo+near=" as *u8); awn(1, p_echo_in_dt); aw(1, "): " as *u8); if (p_recover_err*4)<p_near { aw(1,"PASS" as *u8) } else { aw(1,"FAIL" as *u8) } 198 aw(1, "\n T5 DTD (merged from runtime nx_aec): Geigel auto-freeze, near-end residual " as *u8); awn(1, p_dtd_err); aw(1, " vs near " as *u8); awn(1, p_near); aw(1, ": " as *u8); if (p_dtd_err*4)<p_near { aw(1,"PASS" as *u8) } else { aw(1,"FAIL" as *u8) } 199 aw(1, "\n HONEST SCOPE: linear echo path, white-noise excitation; double-talk now has a Geigel DETECTOR (merged, auto-freeze) -- remaining next rung = a nonlinear residual echo suppressor.\n" as *u8) 200 if ok==1 { aw(1, "VERDICT: GREEN (measured echo cancellation; near-end preserved; adaptation load-bearing)\n" as *u8) } else { aw(1, "VERDICT: RED\n" as *u8) } 201 202 let lfd: i64 = sys_openat_append(AEC_LOG, 420) 203 if lfd >= 0 { 204 aw(lfd, "AEC erle_ratio=" as *u8); awn(lfd, ratio_late); aw(lfd, " erle_db=" as *u8); awn(lfd, erle); aw(lfd, " noadapt=" as *u8); awn(lfd, ratio_noadapt); aw(lfd, " dt_err=" as *u8); awn(lfd, p_recover_err); aw(lfd, " dt_near=" as *u8); awn(lfd, p_near) 205 if ok==1 { aw(lfd, " verdict=GREEN\n" as *u8) } else { aw(lfd, " verdict=RED\n" as *u8) } 206 sys_close(lfd) 207 } 208 if ok==1 { sys_exit(0) } else { sys_exit(1) } 209 return 0 210}