code wiki / (root) / nx_voiceprint.nx

nx_voiceprint.nx source

↩ module page · 127 lines · 4720 B

1// nx_voiceprint.nx -- VOICE-CLONE-001 rung 1: a sovereign SPEAKER-IDENTITY 2// descriptor ("voiceprint") extracted from a voice sample, plus a distance 3// metric that tells two voices apart. 4// 5// A voice is a SOURCE (glottal pulses -> pitch, F0) driving a FILTER (the 6// vocal tract -> formants). A speaker's IDENTITY lives mostly in the FILTER: 7// the same person at a different pitch is still that person. LPC captures the 8// vocal-tract filter exactly; its REFLECTION coefficients (PARCOR) are a 9// compact, bounded (|k|<1), distance-friendly description of that filter. 10// 11// The voiceprint = the average PARCOR vector over the VOICED frames of a 12// sample (voicing gated by nx_pitch) + the mean F0. Because we average only 13// voiced frames and identity rides in the PARCOR (not the pitch), the print is 14// pitch-invariant: the SAME speaker reading at 120 Hz vs 125 Hz prints nearly 15// identical, while a DIFFERENT vocal tract prints far away. That property is 16// the floor under voice DUPLICATION: capture who a speaker IS, separably from 17// what they happen to be saying or how high they happen to be speaking. 18// 19// Composes (no reinvention): nx_lpc_autocorr (R[0..p]) -> nx_lpc_levinson 20// (PARCOR k in Q30) ; nx_pitch (voiced-frame gate + F0). Integer-only, 21// patent-clean. 22// 23// Voiceprint buffer layout (caller-owned *i64, VP_WORDS words): 24// vp[0] = number of voiced frames averaged 25// vp[1] = mean F0 (Hz) 26// vp[2 .. 2+ORDER) = average reflection coeffs k[1..ORDER] in Q30 27// 28// license_tier: ORIGINAL 29// module: nishi-core.audio.voiceprint 30// depends: nishi-core.audio.pitch, nishi-core.voice.lpc 31// capability: AUDIO_SPEAKER_IDENTITY 32import "nx_syscalls_x86_64.nx" 33import "nx_lpc_autocorr.nx" 34import "nx_lpc_levinson.nx" 35import "nx_pitch.nx" 36 37const VP_ORDER: i64 = 10 38const VP_FRAME: i64 = 256 39const VP_HOP: i64 = 128 40const VP_FS: i64 = 8000 41const VP_MINLAG: i64 = 25 42const VP_MAXLAG: i64 = 114 43const VP_WORDS: i64 = 12 // 2 + VP_ORDER 44 45// read one i64 LE word from a *u8 buffer at word index i 46func _vp_i64(b: *u8, i: i64) -> i64 { 47 var v: i64 = 0 48 var j: i64 = 0 49 while j < 8 { 50 v = v | ((b[i * 8 + j]) << (j * 8)) 51 j = j + 1 52 } 53 return v 54} 55 56// nx_voiceprint_extract -- fill vp[0..VP_WORDS) from i16-LE mono PCM. 57// Returns the number of voiced frames averaged (0 = no usable voiced speech). 58func nx_voiceprint_extract(pcm: *u8, n_samples: i64, vp: *i64) -> i64 { 59 let r_buf: *u8 = sys_mmap((VP_ORDER + 1) * 8) 60 let k_buf: *u8 = sys_mmap(VP_ORDER * 8) 61 let a_buf: *u8 = sys_mmap(VP_ORDER * 8) 62 let e_buf: *u8 = sys_mmap(8) 63 let pout: *i64 = sys_mmap(16) as *i64 64 65 // accumulators 66 var sum_k: *i64 = sys_mmap(VP_ORDER * 8) as *i64 67 var ki: i64 = 0 68 while ki < VP_ORDER { sum_k[ki] = 0; ki = ki + 1 } 69 var nvoiced: i64 = 0 70 var sum_f0: i64 = 0 71 72 var start: i64 = 0 73 while start + VP_FRAME + VP_MAXLAG <= n_samples { 74 let frame_pcm: *u8 = (pcm as i64 + start * 2) as *u8 75 // voicing + F0 from the shared pitch primitive 76 let f0: i64 = nx_pitch_f0(frame_pcm, 0, VP_FRAME, VP_FS, VP_MINLAG, VP_MAXLAG, pout) 77 let voiced: i64 = nx_pitch_is_voiced(pout[0]) 78 if voiced == 1 { 79 let rc: i64 = nx_lpc_autocorr(frame_pcm, VP_FRAME, VP_ORDER, r_buf) 80 if rc == 0 { 81 let lr: i64 = nx_lpc_levinson(r_buf, VP_ORDER, k_buf, a_buf, e_buf) 82 if lr == 0 { 83 var i: i64 = 0 84 while i < VP_ORDER { 85 sum_k[i] = sum_k[i] + _vp_i64(k_buf, i) 86 i = i + 1 87 } 88 sum_f0 = sum_f0 + f0 89 nvoiced = nvoiced + 1 90 } 91 } 92 } 93 start = start + VP_HOP 94 } 95 96 vp[0] = nvoiced 97 if nvoiced > 0 { 98 vp[1] = sum_f0 / nvoiced 99 var i: i64 = 0 100 while i < VP_ORDER { 101 vp[2 + i] = sum_k[i] / nvoiced 102 i = i + 1 103 } 104 return nvoiced 105 } 106 // degenerate: no voiced frames -> zeroed print 107 vp[1] = 0 108 var z: i64 = 0 109 while z < VP_ORDER { vp[2 + z] = 0; z = z + 1 } 110 return 0 111} 112 113// nx_voiceprint_distance -- L2 distance between two voiceprints over the 114// PARCOR (vocal-tract) coordinates. Coeffs are Q30; we shift to Q15 before 115// squaring so the sum-of-squares stays well inside i64. Smaller = more alike. 116func nx_voiceprint_distance(a: *i64, b: *i64) -> i64 { 117 var acc: i64 = 0 118 var i: i64 = 0 119 while i < VP_ORDER { 120 let ka: i64 = a[2 + i] >> 15 121 let kb: i64 = b[2 + i] >> 15 122 let d: i64 = ka - kb 123 acc = acc + d * d 124 i = i + 1 125 } 126 return acc 127}