code wiki / (root) / nx_voice_read.nx

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}