nx_audio_enhance.nx source
↩ module page · 74 lines · 3345 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
8import "nx_vecmath.nx"
9const AE_FULL: i64 = 32767
10
11// integer sqrt (Newton) for RMS without an FPU
12func ae_isqrt(v: i64) -> i64 { return vm_isqrt(v) }
13// RMS level of n samples
14func ae_rms(s: *i64, n: i64) -> i64 {
15 if n <= 0 { return 0 }
16 var sum: i64 = 0
17 var i: i64 = 0
18 while i < n { sum = sum + s[i] * s[i]; i = i + 1 }
19 return ae_isqrt(sum / n)
20}
21// mean-square energy of a frame [base, base+FL)
22func ae_frame_energy(s: *i64, base: i64, FL: i64) -> i64 {
23 var sum: i64 = 0
24 var i: i64 = 0
25 while i < FL { let v: i64 = s[base + i]; sum = sum + v * v; i = i + 1 }
26 return sum / FL
27}
28// AGC: scale the signal toward target_rms, capped at max_gain (x100) so a bad mic is boosted but background
29// noise isn't blown up; hard-limited to +/-FULL so it never clips. returns the applied gain (x100).
30func ae_agc(s: *i64, n: i64, target_rms: i64, max_gain100: i64) -> i64 {
31 let rms: i64 = ae_rms(s, n)
32 if rms < 1 { return 100 } // silence -> leave it
33 var g100: i64 = target_rms * 100 / rms
34 if g100 > max_gain100 { g100 = max_gain100 }
35 if g100 < 10 { g100 = 10 }
36 var i: i64 = 0
37 while i < n {
38 var v: i64 = s[i] * g100 / 100
39 if v > AE_FULL { v = AE_FULL }
40 if v < 0 - AE_FULL { v = 0 - AE_FULL }
41 s[i] = v
42 i = i + 1
43 }
44 return g100
45}
46// adaptive noise gate: attenuate frames whose energy is well BELOW the average (background hiss between speech).
47// MEAN-relative (not floor-relative) so a uniform/steady signal is never gated -- only frames < mean/div are noise.
48// returns the number of frames gated.
49func ae_noise_gate(s: *i64, n: i64, FL: i64, div: i64, att100: i64) -> i64 {
50 let nf: i64 = n / FL
51 if nf <= 0 { return 0 }
52 var sum: i64 = 0
53 var f: i64 = 0
54 while f < nf { sum = sum + ae_frame_energy(s, f*FL, FL); f = f + 1 }
55 let mean: i64 = sum / nf
56 var gated: i64 = 0
57 f = 0
58 while f < nf {
59 let e: i64 = ae_frame_energy(s, f*FL, FL)
60 if e * div < mean { // frame well below average -> background noise -> attenuate
61 var i: i64 = f * FL
62 let endi: i64 = i + FL
63 while i < endi { s[i] = s[i] * att100 / 100; i = i + 1 }
64 gated = gated + 1
65 }
66 f = f + 1
67 }
68 return gated
69}
70// full enhancement: gate the background first, then AGC-boost the (now cleaner) speech.
71func ae_enhance(s: *i64, n: i64, FL: i64, target_rms: i64, max_gain100: i64) -> i64 {
72 ae_noise_gate(s, n, FL, 4, 12) // frames below mean/4 energy -> background noise -> attenuate to 12%
73 return ae_agc(s, n, target_rms, max_gain100)
74}