code wiki / (root) / nx_phoneme_synth.nx

nx_phoneme_synth.nx source

↩ module page · 113 lines · 4183 B

1// nx_phoneme_synth.nx -- VOICE-CLONE-001 rung 5a: render a PHONEME SEQUENCE into 2// time-varying speech. This is the mechanism that turns a string of sounds into 3// a spoken WORD -- the difference between a sustained vowel and reading a book. 4// 5// Source-filter, integer-only. Each phoneme is a DATA record (so the phoneme 6// inventory lives in a table, not in code -- "data not code"): 7// record = [ a1_F1, a1_F2, a1_F3, voiced(1/0), dur_samples ] (PH_REC words) 8// a1_Fk = the 2-pole resonator coefficient 2*r*cos(2*pi*Fk/fs) in Q15 for 9// formant k (precomputed per phoneme); shared pole radius r=0.95. 10// voiced = 1 -> glottal impulse train at f0 ; 0 -> noise (fricatives etc.) 11// 12// The three formant resonators run in CASCADE and their state is CARRIED ACROSS 13// phoneme boundaries -- so when the formant targets change, the filter rings 14// smoothly into the new shape. That is coarticulation-lite: natural glides 15// between sounds instead of clicks, which is what makes a sequence sound spoken. 16// 17// license_tier: ORIGINAL 18// module: nishi-core.audio.phoneme_synth 19// depends: (none -- pure integer DSP; mmap scratch only) 20// capability: AUDIO_ARTICULATION 21import "nx_syscalls_x86_64.nx" 22const PH_MAGIC_1103515245: i64 = 1103515245 23const PH_MAGIC_12345: i64 = 12345 24const PH_MAGIC_22695477: i64 = 22695477 25const PH_MAGIC_20000: i64 = 20000 26const PH_MAGIC_32000: i64 = 32000 27 28const PH_A2Q: i64 = 0 - 29573 // -r^2 in Q15, r=0.95 29const PH_AMP: i64 = 800 // glottal impulse amplitude 30const PH_REC: i64 = 5 // i64 words per phoneme record 31 32func _ph_lcg(seed: i64) -> i64 { return (seed * PH_MAGIC_1103515245 + PH_MAGIC_12345) & 0x7fffffff } 33 34// nx_phoneme_synth -- render seq[0..n_phon) into out_pcm (i16 LE), normalized to 35// +-20000. Returns the number of samples written. 36func nx_phoneme_synth(seq: *i64, n_phon: i64, f0_hz: i64, fs: i64, 37 out_pcm: *u8, cap_samples: i64) -> i64 { 38 var total: i64 = 0 39 var pp: i64 = 0 40 while pp < n_phon { total = total + seq[pp * PH_REC + 4]; pp = pp + 1 } 41 if total > cap_samples { total = cap_samples } 42 if total < 1 { return 0 } 43 let buf: *i64 = sys_mmap(total * 8) as *i64 44 45 var y1a: i64 = 0 46 var y2a: i64 = 0 47 var y1b: i64 = 0 48 var y2b: i64 = 0 49 var y1c: i64 = 0 50 var y2c: i64 = 0 51 var period: i64 = fs / f0_hz 52 if period < 1 { period = 1 } 53 var nsrc: i64 = 0 54 var seed: i64 = PH_MAGIC_22695477 55 var w: i64 = 0 56 57 var p: i64 = 0 58 while p < n_phon { 59 let a1a: i64 = seq[p * PH_REC + 0] 60 let a1b: i64 = seq[p * PH_REC + 1] 61 let a1c: i64 = seq[p * PH_REC + 2] 62 let voiced: i64 = seq[p * PH_REC + 3] 63 let dur: i64 = seq[p * PH_REC + 4] 64 var d: i64 = 0 65 while d < dur { 66 if w < total { 67 var x: i64 = 0 68 if voiced == 1 { 69 if nsrc % period == 0 { x = PH_AMP } 70 } 71 if voiced == 0 { 72 seed = _ph_lcg(seed) 73 x = (seed & 0x3ff) - 512 74 } 75 let ya: i64 = x + ((a1a * y1a + PH_A2Q * y2a) >> 15) 76 y2a = y1a 77 y1a = ya 78 let yb: i64 = ya + ((a1b * y1b + PH_A2Q * y2b) >> 15) 79 y2b = y1b 80 y1b = yb 81 let yc: i64 = yb + ((a1c * y1c + PH_A2Q * y2c) >> 15) 82 y2c = y1c 83 y1c = yc 84 buf[w] = yc 85 w = w + 1 86 nsrc = nsrc + 1 87 } 88 d = d + 1 89 } 90 p = p + 1 91 } 92 93 var mx: i64 = 1 94 var i: i64 = 0 95 while i < total { 96 var v: i64 = buf[i] 97 if v < 0 { v = 0 - v } 98 if v > mx { mx = v } 99 i = i + 1 100 } 101 i = 0 102 while i < total { 103 var s: i64 = buf[i] * PH_MAGIC_20000 / mx 104 if s > PH_MAGIC_32000 { s = PH_MAGIC_32000 } 105 if s < -PH_MAGIC_32000 { s = -PH_MAGIC_32000 } 106 var x2: i64 = s 107 if x2 < 0 { x2 = x2 + 0x10000 } 108 out_pcm[i * 2] = (x2 & 0xff) as u8 109 out_pcm[i * 2 + 1] = ((x2 >> 8) & 0xff) as u8 110 i = i + 1 111 } 112 return total 113}