nx_audio_enhance.nx source
↩ module page · 80 lines · 3489 B
1// nx_audio_enhance.nx -- sovereign AUDIO ENHANCEMENT for the call mic path: AGC (auto gain control -> boosts a
2// quiet/bad mic to a healthy level, with a hard limiter so it never clips) + an adaptive VAD NOISE GATE (estimates
3// the noise floor, attenuates frames that are just background hiss between speech). This is the "top-of-the-line
4// audio even with a bad mic" path, pure Nishi -- the browser only ferries raw PCM; ALL DSP is here. Operates on
5// signed PCM samples in an i64 array (-32767..32767). Spectral subtraction via nx_fft = the next rung (in-speech
6// denoise); this rung delivers the two biggest bad-mic wins (level + background gating). license_tier: ORIGINAL
7
8const AE_FULL: i64 = 32767
9
10// integer sqrt (Newton) for RMS without an FPU
11func ae_isqrt(v: i64) -> i64 {
12 if v <= 0 { return 0 }
13 var x: i64 = v
14 var y: i64 = (x + 1) / 2
15 var go: i64 = 1
16 while go == 1 { if y < x { x = y; y = (x + v / x) / 2 } else { go = 0 } }
17 return x
18}
19// RMS level of n samples
20func ae_rms(s: *i64, n: i64) -> i64 {
21 if n <= 0 { return 0 }
22 var sum: i64 = 0
23 var i: i64 = 0
24 while i < n { sum = sum + s[i] * s[i]; i = i + 1 }
25 return ae_isqrt(sum / n)
26}
27// mean-square energy of a frame [base, base+FL)
28func ae_frame_energy(s: *i64, base: i64, FL: i64) -> i64 {
29 var sum: i64 = 0
30 var i: i64 = 0
31 while i < FL { let v: i64 = s[base + i]; sum = sum + v * v; i = i + 1 }
32 return sum / FL
33}
34// AGC: scale the signal toward target_rms, capped at max_gain (x100) so a bad mic is boosted but background
35// noise isn't blown up; hard-limited to +/-FULL so it never clips. returns the applied gain (x100).
36func ae_agc(s: *i64, n: i64, target_rms: i64, max_gain100: i64) -> i64 {
37 let rms: i64 = ae_rms(s, n)
38 if rms < 1 { return 100 } // silence -> leave it
39 var g100: i64 = target_rms * 100 / rms
40 if g100 > max_gain100 { g100 = max_gain100 }
41 if g100 < 10 { g100 = 10 }
42 var i: i64 = 0
43 while i < n {
44 var v: i64 = s[i] * g100 / 100
45 if v > AE_FULL { v = AE_FULL }
46 if v < 0 - AE_FULL { v = 0 - AE_FULL }
47 s[i] = v
48 i = i + 1
49 }
50 return g100
51}
52// adaptive noise gate: attenuate frames whose energy is well BELOW the average (background hiss between speech).
53// MEAN-relative (not floor-relative) so a uniform/steady signal is never gated -- only frames < mean/div are noise.
54// returns the number of frames gated.
55func ae_noise_gate(s: *i64, n: i64, FL: i64, div: i64, att100: i64) -> i64 {
56 let nf: i64 = n / FL
57 if nf <= 0 { return 0 }
58 var sum: i64 = 0
59 var f: i64 = 0
60 while f < nf { sum = sum + ae_frame_energy(s, f*FL, FL); f = f + 1 }
61 let mean: i64 = sum / nf
62 var gated: i64 = 0
63 f = 0
64 while f < nf {
65 let e: i64 = ae_frame_energy(s, f*FL, FL)
66 if e * div < mean { // frame well below average -> background noise -> attenuate
67 var i: i64 = f * FL
68 let endi: i64 = i + FL
69 while i < endi { s[i] = s[i] * att100 / 100; i = i + 1 }
70 gated = gated + 1
71 }
72 f = f + 1
73 }
74 return gated
75}
76// full enhancement: gate the background first, then AGC-boost the (now cleaner) speech.
77func ae_enhance(s: *i64, n: i64, FL: i64, target_rms: i64, max_gain100: i64) -> i64 {
78 ae_noise_gate(s, n, FL, 4, 12) // frames below mean/4 energy -> background noise -> attenuate to 12%
79 return ae_agc(s, n, target_rms, max_gain100)
80}