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}