nx_voice_read.nx source
↩ module page · 82 lines · 3598 B
1// nx_voice_read.nx -- VOICE-CLONE-001 rung 5-FUSE: a CLONED VOICE READING WORDS.
2//
3// Fuses the two halves of the arc:
4// * rung 5a (nx_phoneme_synth) provides ARTICULATION -- WHAT is said (the
5// phoneme sequence, the formant trajectory over time).
6// * a parametric SPEAKER IDENTITY provides WHO says it -- a consistent
7// vocal-tract-length warp (every formant scaled by warp_num/warp_den) plus
8// the speaker's F0. Scaling all formants by one factor is VTLN (vocal-tract
9// length normalization): it preserves the formant RATIOS that make /a/ an
10// /a/, while shifting the whole spectrum to a different-sized tract -- i.e.
11// a different person saying the same word.
12//
13// The phoneme inventory is DATA in formant-Hz (record = [F1,F2,F3,voiced,dur]);
14// at read time each formant is warped to the speaker and converted to a 2-pole
15// resonator coefficient a1 = 2*r*cos(2*pi*F/fs) (r=0.95, matching nx_phoneme_synth).
16//
17// NOTE (tech debt): fx.nx has a CORDIC cos, but it imports syscalls.nx ->
18// nx_syscalls.nx, which collides with this arc's nx_syscalls_x86_64.nx (two
19// files, same symbols, no dedup). Until the arc's syscall module is unified,
20// an inline integer cos (Taylor + quadrant reduction, err ~1e-3, plenty for
21// formant tuning) is used here.
22//
23// license_tier: ORIGINAL
24// module: nishi-core.audio.voice_read
25// depends: nishi-core.audio.phoneme_synth
26// capability: AUDIO_SPEAKER_READER
27import "nx_syscalls_x86_64.nx"
28import "nx_phoneme_synth.nx"
29const VR_MAGIC_65536: i64 = 65536
30const VR_MAGIC_4000: i64 = 4000
31
32const VR_2R_Q15: i64 = 62259 // 2*0.95 in Q15
33const VR_PI_Q16: i64 = 205887 // pi in Q16.16
34const VR_HALFPI_Q16: i64 = 102944 // pi/2 in Q16.16
35const VR_FS: i64 = 8000
36
37// cos(theta), theta in Q16.16 over [0, pi]; returns Q16.16 in [-65536, 65536].
38func _vr_cosq16(th: i64) -> i64 {
39 var x: i64 = th
40 var sign: i64 = 1
41 if x > VR_HALFPI_Q16 { x = VR_PI_Q16 - x; sign = 0 - 1 } // [pi/2,pi] -> -cos(pi-x)
42 let x2: i64 = (x * x) >> 16
43 let x4: i64 = (x2 * x2) >> 16
44 let x6: i64 = (x4 * x2) >> 16
45 var c: i64 = VR_MAGIC_65536
46 c = c - (x2 / 2)
47 c = c + (x4 / 24)
48 c = c - (x6 / 720)
49 return sign * c
50}
51
52// resonator coefficient a1 = 2r*cos(2*pi*F/fs) in Q15, F in [0, fs/2=4000].
53// 2*pi*F/fs = pi*(F/4000) -> theta_q16 = PI_q16 * F / 4000.
54func _vr_a1(f_hz: i64) -> i64 {
55 var f: i64 = f_hz
56 if f < 0 { f = 0 }
57 if f > VR_MAGIC_4000 { f = VR_MAGIC_4000 }
58 let th: i64 = (VR_PI_Q16 * f) / VR_MAGIC_4000
59 let cq: i64 = _vr_cosq16(th)
60 return (VR_2R_Q15 * cq) >> 16
61}
62
63// nx_voice_read -- render hz_seq (formant-Hz phoneme records) in a speaker's
64// voice (formant warp num/den + F0). out_pcm i16 LE. Returns samples written.
65func nx_voice_read(hz_seq: *i64, n_phon: i64, f0_hz: i64,
66 warp_num: i64, warp_den: i64,
67 out_pcm: *u8, cap_samples: i64) -> i64 {
68 let a1_seq: *i64 = sys_mmap(n_phon * PH_REC * 8) as *i64
69 var p: i64 = 0
70 while p < n_phon {
71 let f1: i64 = hz_seq[p * PH_REC + 0] * warp_num / warp_den
72 let f2: i64 = hz_seq[p * PH_REC + 1] * warp_num / warp_den
73 let f3: i64 = hz_seq[p * PH_REC + 2] * warp_num / warp_den
74 a1_seq[p * PH_REC + 0] = _vr_a1(f1)
75 a1_seq[p * PH_REC + 1] = _vr_a1(f2)
76 a1_seq[p * PH_REC + 2] = _vr_a1(f3)
77 a1_seq[p * PH_REC + 3] = hz_seq[p * PH_REC + 3]
78 a1_seq[p * PH_REC + 4] = hz_seq[p * PH_REC + 4]
79 p = p + 1
80 }
81 return nx_phoneme_synth(a1_seq, n_phon, f0_hz, VR_FS, out_pcm, cap_samples)
82}