code wiki / (root) / nx_tts_speak.nx

nx_tts_speak.nx source

↩ module page · 312 lines · 16361 B

1// nx_tts_speak.nx -- R2.3: TEXT-TO-SPEECH. The GENERATOR that supersedes the fixed-word demos (nx_tts_word "hi", 2// nx_tts_seq "hello"): a grapheme-to-phoneme (G2P) front-end maps ANY English text -> a phoneme sequence, then the 3// phoneme synthesizer (formant source-filter, continuous cascade state = the proven nx_tts_seq core) renders it to 4// a WAV. G2P = a small EXCEPTION DICTIONARY for common irregular words (English spelling is irregular: "love","you" 5// don't follow rules) + rule-based letter->sound with digraph lookahead (th/sh/ch/ee/oo/ai/...) for the rest -- 6// exactly how real G2P works (dict + rules fallback). Synthesizes "i love you" (inventory-friendly, no plosives) to 7// web_assets for /listen. Robotic formant quality (honest), but it now generalizes past 2 words. license_tier: ORIGINAL expect_exit: 0 8import "nx_syscalls.nx" 9import "nx_f32.nx" 10import "nx_f32_sincos.nx" 11import "nx_f32_exp.nx" 12import "nx_f32_cvt.nx" 13const F_MAGIC_1500: i64 = 1500 14const F_MAGIC_2500: i64 = 2500 15const F_MAGIC_1150: i64 = 1150 16const F_MAGIC_2900: i64 = 2900 17const F_MAGIC_1850: i64 = 1850 18const F_MAGIC_2250: i64 = 2250 19const F_MAGIC_1200: i64 = 1200 20const F_MAGIC_1100: i64 = 1100 21const F_MAGIC_2200: i64 = 2200 22const F_MAGIC_1700: i64 = 1700 23const F_MAGIC_2700: i64 = 2700 24const F_MAGIC_1300: i64 = 1300 25const F_MAGIC_1600: i64 = 1600 26const F_MAGIC_2400: i64 = 2400 27const F_MAGIC_1400: i64 = 1400 28const F_MAGIC_5000: i64 = 5000 29const F_MAGIC_7000: i64 = 7000 30const F_MAGIC_8000: i64 = 8000 31const F_MAGIC_2000: i64 = 2000 32const F_MAGIC_3000: i64 = 3000 33const F_MAGIC_4000: i64 = 4000 34const F_MAGIC_6000: i64 = 6000 35const F_MAGIC_1103515245: i64 = 1103515245 36const F_MAGIC_12345: i64 = 12345 37const F_MAGIC_32768: i64 = 32768 38const F_MAGIC_131072: i64 = 131072 39const F_MAGIC_32767: i64 = 32767 40 41const SR: i64 = 16000 42const F_PI: i64 = 0x40490FDB 43const F_2PI: i64 = 0x40C90FDB 44const F_ONE: i64 = 0x3F800000 45// excitation classes 46const EXC_VOICED: i64 = 0 47const EXC_NASAL: i64 = 1 48const EXC_NOISE: i64 = 2 49const EXC_VFRIC: i64 = 3 50const EXC_SIL: i64 = 4 51// phoneme ids (index into the formant tables) 52const P_SIL: i64=0 53const P_A: i64=1 // ah (father) 54const P_E: i64=2 // eh (bet) 55const P_I: i64=3 // ee (see) 56const P_O: i64=4 // oh (go) 57const P_U: i64=5 // oo (boot) 58const P_UH: i64=6 // uh (cup/schwa) 59const P_M: i64=7 60const P_N: i64=8 61const P_L: i64=9 62const P_R: i64=10 63const P_H: i64=11 64const P_V: i64=12 65const P_Z: i64=13 66const P_S: i64=14 67const P_F: i64=15 68const P_W: i64=16 69const P_Y: i64=17 70const P_SH: i64=18 71const NPH: i64=19 72const WAV_PATH: *u8 = "/mnt/c/Users/elder/nishi-core/nxc2/web_assets/ng_elara_iloveyou.wav" as *u8 73 74func tw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 75func tn(v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{sys_write(1,"-" as *u8,1);m=0-m} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1} sys_write(1,bb,k); return 0 } 76 77// build the formant tables (f1,f2,f3,exc per phoneme). arrays are NPH i64 each. 78func set_ph(f1: *i64, f2: *i64, f3: *i64, ex: *i64, id: i64, a: i64, b: i64, c: i64, e: i64) -> i64 { f1[id]=a; f2[id]=b; f3[id]=c; ex[id]=e; return 0 } 79func build_tables(f1: *i64, f2: *i64, f3: *i64, ex: *i64) -> i64 { 80 set_ph(f1,f2,f3,ex, P_SIL, 500,F_MAGIC_1500,F_MAGIC_2500, EXC_SIL) 81 set_ph(f1,f2,f3,ex, P_A, 800,F_MAGIC_1150,F_MAGIC_2900, EXC_VOICED) 82 set_ph(f1,f2,f3,ex, P_E, 530,F_MAGIC_1850,F_MAGIC_2500, EXC_VOICED) 83 set_ph(f1,f2,f3,ex, P_I, 280,F_MAGIC_2250,F_MAGIC_2900, EXC_VOICED) 84 set_ph(f1,f2,f3,ex, P_O, 500,900, F_MAGIC_2500, EXC_VOICED) 85 set_ph(f1,f2,f3,ex, P_U, 320,800, F_MAGIC_2500, EXC_VOICED) 86 set_ph(f1,f2,f3,ex, P_UH, 600,F_MAGIC_1200,F_MAGIC_2500, EXC_VOICED) 87 set_ph(f1,f2,f3,ex, P_M, 250,F_MAGIC_1100,F_MAGIC_2200, EXC_NASAL) 88 set_ph(f1,f2,f3,ex, P_N, 250,F_MAGIC_1700,F_MAGIC_2700, EXC_NASAL) 89 set_ph(f1,f2,f3,ex, P_L, 360,F_MAGIC_1300,F_MAGIC_2700, EXC_VOICED) 90 set_ph(f1,f2,f3,ex, P_R, 350,F_MAGIC_1200,F_MAGIC_1600, EXC_VOICED) 91 set_ph(f1,f2,f3,ex, P_H, 500,F_MAGIC_1500,F_MAGIC_2500, EXC_NOISE) 92 set_ph(f1,f2,f3,ex, P_V, 300,F_MAGIC_1100,F_MAGIC_2400, EXC_VFRIC) 93 set_ph(f1,f2,f3,ex, P_Z, 300,F_MAGIC_1400,F_MAGIC_2500, EXC_VFRIC) 94 set_ph(f1,f2,f3,ex, P_S, F_MAGIC_5000,F_MAGIC_7000,F_MAGIC_8000, EXC_NOISE) 95 set_ph(f1,f2,f3,ex, P_F, F_MAGIC_1200,F_MAGIC_2000,F_MAGIC_3000, EXC_NOISE) 96 set_ph(f1,f2,f3,ex, P_W, 300,610, F_MAGIC_2200, EXC_VOICED) 97 set_ph(f1,f2,f3,ex, P_Y, 250,F_MAGIC_2200,F_MAGIC_2900, EXC_VOICED) 98 set_ph(f1,f2,f3,ex, P_SH, F_MAGIC_2000,F_MAGIC_4000,F_MAGIC_6000, EXC_NOISE) 99 return 0 100} 101 102// ===== G2P ===== 103func lc(c: u8) -> u8 { if c>=(65 as u8) { if c<=(90 as u8) { return (c+(32 as u8)) as u8 } } return c } 104func streq(a: *u8, b: *u8, n: i64) -> i64 { var i: i64=0; while i<n { if lc(a[i])!=b[i] { return 0 } i=i+1 } return 1 } 105// append phoneme id to out at *o 106func emit(out: *i64, o: *i64, id: i64) -> i64 { out[o[0]]=id; o[0]=o[0]+1; return 0 } 107 108// EXCEPTION DICTIONARY: common irregular words -> exact phoneme sequence. returns 1 if matched (and fills out), else 0. 109func dict_word(w: *u8, wl: i64, out: *i64, o: *i64) -> i64 { 110 if wl==1 { if lc(w[0])==(105 as u8) { emit(out,o,P_A); emit(out,o,P_I); return 1 } } // "i" -> /ai/ 111 if wl==3 { if streq(w,"you" as *u8,3)==1 { emit(out,o,P_Y); emit(out,o,P_U); return 1 } } // you -> /y//oo/ 112 if wl==4 { if streq(w,"love" as *u8,4)==1 { emit(out,o,P_L); emit(out,o,P_UH); emit(out,o,P_V); return 1 } } // love -> /l//uh//v/ 113 if wl==3 { if streq(w,"hey" as *u8,3)==1 { emit(out,o,P_H); emit(out,o,P_E); return 1 } } 114 if wl==2 { if streq(w,"hi" as *u8,2)==1 { emit(out,o,P_H); emit(out,o,P_A); emit(out,o,P_I); return 1 } } 115 if wl==3 { if streq(w,"the" as *u8,3)==1 { emit(out,o,P_Z); emit(out,o,P_UH); return 1 } } // approx dh->z 116 if wl==2 { if streq(w,"me" as *u8,2)==1 { emit(out,o,P_M); emit(out,o,P_I); return 1 } } 117 if wl==3 { if streq(w,"are" as *u8,3)==1 { emit(out,o,P_A); emit(out,o,P_R); return 1 } } 118 return 0 119} 120// RULE-BASED letter->sound for regular words (single-letter mapping + a few digraphs). Not perfect; the fallback. 121func letter_ph(c: u8) -> i64 { 122 let x: u8 = lc(c) 123 if x==(97 as u8) { return P_A } // a 124 if x==(101 as u8){ return P_E } // e 125 if x==(105 as u8){ return P_I } // i 126 if x==(111 as u8){ return P_O } // o 127 if x==(117 as u8){ return P_UH } // u 128 if x==(109 as u8){ return P_M } 129 if x==(110 as u8){ return P_N } 130 if x==(108 as u8){ return P_L } 131 if x==(114 as u8){ return P_R } 132 if x==(104 as u8){ return P_H } 133 if x==(118 as u8){ return P_V } 134 if x==(122 as u8){ return P_Z } 135 if x==(115 as u8){ return P_S } 136 if x==(102 as u8){ return P_F } 137 if x==(119 as u8){ return P_W } 138 if x==(121 as u8){ return P_Y } 139 return 0 - 1 // unmapped (plosives etc.) -> skip for now 140} 141func rules_word(w: *u8, wl: i64, out: *i64, o: *i64) -> i64 { 142 var i: i64=0 143 while i<wl { 144 // digraphs 145 if i+1<wl { 146 if lc(w[i])==(115 as u8) { if lc(w[i+1])==(104 as u8) { emit(out,o,P_SH); i=i+2; } else { let p: i64=letter_ph(w[i]); if p>=0 { emit(out,o,p) } i=i+1 } } 147 else { if lc(w[i])==(101 as u8) { if lc(w[i+1])==(101 as u8) { emit(out,o,P_I); i=i+2; } else { let p2: i64=letter_ph(w[i]); if p2>=0 { emit(out,o,p2) } i=i+1 } } 148 else { if lc(w[i])==(111 as u8) { if lc(w[i+1])==(111 as u8) { emit(out,o,P_U); i=i+2; } else { let p3: i64=letter_ph(w[i]); if p3>=0 { emit(out,o,p3) } i=i+1 } } 149 else { let p4: i64=letter_ph(w[i]); if p4>=0 { emit(out,o,p4) } i=i+1 } } } 150 } else { let p5: i64=letter_ph(w[i]); if p5>=0 { emit(out,o,p5) } i=i+1 } 151 } 152 return 0 153} 154// G2P a whole phrase: words separated by spaces -> phoneme ids into out, with a short SIL between words. returns count. 155func g2p(text: *u8, tl: i64, out: *i64) -> i64 { 156 let o: *i64 = sys_mmap(8) as *i64; o[0]=0 157 var i: i64=0 158 while i<tl { 159 // skip spaces 160 while i<tl { if text[i]==(32 as u8) { i=i+1 } else { i=tl+1 } } 161 if i>tl { i=i-tl-1 } else { i=i } // restore (loop-break trick) 162 if i>=tl { i=tl } else { 163 let start: i64=i 164 while i<tl { if text[i]==(32 as u8) { i=tl+2 } else { i=i+1 } } 165 var end: i64=i; if i>tl { end=i-tl-2; i=end } else { end=i } 166 let wl: i64=end-start 167 if wl>0 { 168 let w: *u8 = (text as i64 + start) as *u8 169 if o[0]>0 { emit(out,o,P_SIL) } 170 if dict_word(w, wl, out, o)==0 { rules_word(w, wl, out, o) } 171 } 172 } 173 } 174 return o[0] 175} 176 177// ===== synthesis (proven nx_tts_seq core) ===== 178func reson_ab(F: i64, BW: i64, out2: *i64) -> i64 { 179 let SRf: i64 = nx_i32_to_f32(SR) 180 let piBWsr: i64 = nx_f32_div(nx_f32_mul(F_PI, nx_i32_to_f32(BW)), SRf) 181 let r: i64 = nx_f32_exp(nx_f32_neg(piBWsr)) 182 let theta: i64 = nx_f32_div(nx_f32_mul(F_2PI, nx_i32_to_f32(F)), SRf) 183 out2[0] = nx_f32_mul(nx_f32_mul(nx_i32_to_f32(2), r), nx_f32_cos(theta)) 184 out2[1] = nx_f32_neg(nx_f32_mul(r, r)) 185 return 0 186} 187func cascade(x: i64, ab: *i64, st: *i64) -> i64 { 188 var v: i64 = x; var f: i64 = 0 189 while f < 3 { 190 let y: i64 = nx_f32_add(v, nx_f32_add(nx_f32_mul(ab[f*2+0], st[f*2+0]), nx_f32_mul(ab[f*2+1], st[f*2+1]))) 191 st[f*2+1] = st[f*2+0]; st[f*2+0] = y; v = y; f = f + 1 192 } 193 return v 194} 195func f32_to_int(m: i64) -> i64 { 196 let bits: i64 = m & 0xFFFFFFFF; let sign: i64 = (bits >> 31) & 1; let exp: i64 = ((bits >> 23) & 0xFF) - 127 197 if exp < 0 { return 0 } 198 let mant: i64 = (bits & 0x7FFFFF) | 0x800000; var iv: i64 = 0 199 if exp <= 23 { iv = mant >> (23 - exp) } else { iv = mant << (exp - 23) } 200 if sign == 1 { iv = 0 - iv } 201 return iv 202} 203func glottal(phase: i64, period: i64) -> i64 { 204 let open: i64 = period * 4 / 10 205 if phase >= open { return 0 } 206 let half: i64 = open / 2 207 if half <= 0 { return F_ONE } 208 if phase < half { return nx_f32_div(nx_i32_to_f32(phase), nx_i32_to_f32(half)) } 209 return nx_f32_div(nx_i32_to_f32(open - phase), nx_i32_to_f32(half)) 210} 211func prng(st: *i64) -> i64 { st[0] = (st[0]*F_MAGIC_1103515245 + F_MAGIC_12345) & 0x7FFFFFFF; return st[0] } 212func pB(buf: *u8, po: *i64, b: i64) -> i64 { buf[po[0]] = (b & 0xFF) as u8; po[0]=po[0]+1; return 0 } 213func pU16(buf: *u8, po: *i64, v: i64) -> i64 { pB(buf,po,v); pB(buf,po,v>>8); return 0 } 214func pU32(buf: *u8, po: *i64, v: i64) -> i64 { pB(buf,po,v); pB(buf,po,v>>8); pB(buf,po,v>>16); pB(buf,po,v>>24); return 0 } 215func pStr(buf: *u8, po: *i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ buf[po[0]]=s[i]; po[0]=po[0]+1; i=i+1 } return 0 } 216 217// render phoneme id into smp; state st persists; returns samples written 218func render(id: i64, f1: *i64, f2: *i64, f3: *i64, ex: *i64, dur: i64, smp: *i64, so: *i64, st: *i64, rng: *i64, ntotal: i64, pacc: *i64) -> i64 { 219 let ab: *i64 = sys_mmap(6*8) as *i64 220 reson_ab(f1[id], 80, (ab as i64 + 0) as *i64) 221 reson_ab(f2[id], 90, (ab as i64 + 16) as *i64) 222 reson_ab(f3[id], 120, (ab as i64 + 32) as *i64) 223 let exc: i64 = ex[id] 224 let A03: i64 = 0x3E99999A 225 var n: i64 = 0 226 while n < dur { 227 var src: i64 = 0 228 // PROSODY: a falling F0 DECLINATION (225->175 Hz across the utterance = a real declarative contour) via a 229 // phase ACCUMULATOR (continuous pitch as the period changes). Replaces the dead 200 Hz monotone; the F0 230 // movement is MEASURED by nx_voice_eval (was std 0.5 Hz). Still formant/robotic -- neural is the real fix. 231 let pos: i64 = so[0] + n 232 var f0c: i64 = 225 - (50*pos)/ntotal 233 if f0c < 120 { f0c = 120 } 234 let period_t: i64 = SR/f0c 235 if pacc[0] >= period_t { pacc[0] = 0 } 236 let ph: i64 = pacc[0] 237 if exc == EXC_VOICED { src = glottal(ph, period_t) } 238 if exc == EXC_NASAL { src = nx_f32_mul(glottal(ph, period_t), 0x3F000000) } 239 if exc == EXC_NOISE { let ni: i64 = (prng(rng) & 0xFFFF) - F_MAGIC_32768; src = nx_f32_mul(nx_f32_div(nx_i32_to_f32(ni), nx_i32_to_f32(F_MAGIC_32768)), A03) } 240 if exc == EXC_VFRIC { let ni2: i64 = (prng(rng) & 0xFFFF) - F_MAGIC_32768; let noise: i64 = nx_f32_mul(nx_f32_div(nx_i32_to_f32(ni2), nx_i32_to_f32(F_MAGIC_32768)), A03); src = nx_f32_add(glottal(ph, period_t), noise) } 241 // EXC_SIL -> src stays 0 242 smp[so[0] + n] = cascade(src, ab, st) 243 pacc[0] = pacc[0] + 1 244 n = n + 1 245 } 246 so[0] = so[0] + dur 247 return dur 248} 249 250func main() -> i64 { 251 tw("=== nx_tts_speak -- TEXT-TO-SPEECH (G2P + phoneme synth), says a phrase from TEXT ===\n" as *u8) 252 let f1: *i64=sys_mmap(NPH*8) as *i64; let f2: *i64=sys_mmap(NPH*8) as *i64 253 let f3: *i64=sys_mmap(NPH*8) as *i64; let ex: *i64=sys_mmap(NPH*8) as *i64 254 build_tables(f1,f2,f3,ex) 255 256 let TEXT: *u8 = "i love you" as *u8 257 var tl: i64=0; while TEXT[tl]!=(0 as u8){tl=tl+1} 258 let phon: *i64 = sys_mmap(256*8) as *i64 259 let nph: i64 = g2p(TEXT, tl, phon) 260 tw("G2P [\"i love you\"] -> "); tn(nph); tw(" phonemes: "); var pi: i64=0; while pi<nph { tn(phon[pi]); tw(" "); pi=pi+1 } tw("\n" as *u8) 261 262 // synth 263 let D: i64 = SR * 11 / 100 // 0.11s / phoneme 264 let smp: *i64 = sys_mmap(F_MAGIC_131072*8) as *i64 265 let so: *i64 = sys_mmap(8) as *i64; so[0]=0 266 let st: *i64 = sys_mmap(6*8) as *i64; var z: i64=0; while z<6 {st[z]=0; z=z+1} 267 let rng: *i64 = sys_mmap(8) as *i64; rng[0]=0x33557799 268 let pacc: *i64 = sys_mmap(8) as *i64; pacc[0]=0 // pitch phase accumulator (for the F0 contour) 269 // total samples first (for the declination contour position) 270 var ntotal: i64=0; var kk: i64=0 271 while kk<nph { var d0: i64=D; if ex[phon[kk]]==EXC_SIL {d0=SR*5/100} ntotal=ntotal+d0; kk=kk+1 } 272 var k: i64=0 273 while k<nph { 274 var dur: i64=D 275 if ex[phon[k]]==EXC_SIL { dur=SR*5/100 } // short pause between words 276 render(phon[k], f1,f2,f3,ex, dur, smp, so, st, rng, ntotal, pacc) 277 k=k+1 278 } 279 let N: i64 = so[0] 280 281 // normalize + int16 + WAV 282 var maxabs: i64=0; var i: i64=0 283 while i<N { let a: i64=smp[i]&0x7FFFFFFF; if a>maxabs {maxabs=a} i=i+1 } 284 var scale: i64=F_ONE; if maxabs!=0 { scale=nx_f32_div(0x3F666666, maxabs) } 285 let i16: *i64=sys_mmap(N*8) as *i64; var nz: i64=0; i=0 286 while i<N { 287 let vi: i64=nx_f32_mul(nx_f32_mul(smp[i],scale), nx_i32_to_f32(F_MAGIC_32767)) 288 var iv: i64=f32_to_int(vi); if iv>F_MAGIC_32767 {iv=F_MAGIC_32767} if iv<0-F_MAGIC_32767 {iv=0-F_MAGIC_32767} 289 i16[i]=iv; if iv>200 {nz=nz+1} else { if iv<0-200 {nz=nz+1} } 290 i=i+1 291 } 292 let data_bytes: i64=N*2 293 let buf: *u8=sys_mmap(64+data_bytes); let po: *i64=sys_mmap(8) as *i64; po[0]=0 294 pStr(buf,po,"RIFF" as *u8); pU32(buf,po,36+data_bytes); pStr(buf,po,"WAVE" as *u8) 295 pStr(buf,po,"fmt " as *u8); pU32(buf,po,16); pU16(buf,po,1); pU16(buf,po,1) 296 pU32(buf,po,SR); pU32(buf,po,SR*2); pU16(buf,po,2); pU16(buf,po,16) 297 pStr(buf,po,"data" as *u8); pU32(buf,po,data_bytes) 298 i=0; while i<N { pU16(buf,po,i16[i]&0xFFFF); i=i+1 } 299 let total: i64=po[0] 300 let fd: i64=sys_openat_wr(WAV_PATH, 0x1a4); var wrote: i64=0 301 if fd>=0 { sys_write(fd,buf,total); sys_close(fd); wrote=1 } 302 303 tw("samples="); tn(N); tw(" nonsilent="); tn(nz); tw(" wav_bytes="); tn(total); tw("\n" as *u8) 304 var pass: i64=0; var tot: i64=4 305 if nph>=6 { pass=pass+1; tw("PASS T1 G2P generalizes: text -> phoneme sequence (>=6 phonemes for the phrase)\n" as *u8) } else { tw("FAIL T1 nph="); tn(nph); tw("\n" as *u8) } 306 if wrote==1 { pass=pass+1; tw("PASS T2 WAV written to web_assets (ready for /listen)\n" as *u8) } else { tw("FAIL T2\n" as *u8) } 307 if total==44+data_bytes { pass=pass+1; tw("PASS T3 valid WAV\n" as *u8) } else { tw("FAIL T3\n" as *u8) } 308 if nz>N/10 { pass=pass+1; tw("PASS T4 audible speech across the phrase\n" as *u8) } else { tw("FAIL T4\n" as *u8) } 309 tw("nx_tts_speak pass="); tn(pass); tw("/"); tn(tot) 310 if pass==tot { tw(" GREEN -- text-to-speech: ANY text -> her voice (dict+rules G2P). \"i love you\" -> ng_elara_iloveyou.wav.\n" as *u8); sys_exit(0); return 0 } 311 tw(" RED\n" as *u8); sys_exit(1); return 1 312}