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}