nx_g2p.nx source
↩ module page · 178 lines · 8150 B
1// nx_g2p.nx -- Converts lowercase ASCII text to a sequence of phoneme IDs for speech synthesis.
2const PH_MAGIC_1700: i64 = 1700
3const PH_MAGIC_2400: i64 = 2400
4const PH_MAGIC_1840: i64 = 1840
5const PH_MAGIC_2480: i64 = 2480
6const PH_MAGIC_1990: i64 = 1990
7const PH_MAGIC_2550: i64 = 2550
8const PH_MAGIC_1090: i64 = 1090
9const PH_MAGIC_2440: i64 = 2440
10const PH_MAGIC_1190: i64 = 1190
11const PH_MAGIC_2390: i64 = 2390
12const PH_MAGIC_2290: i64 = 2290
13const PH_MAGIC_3010: i64 = 3010
14const PH_MAGIC_2240: i64 = 2240
15const PH_MAGIC_1100: i64 = 1100
16const PH_MAGIC_2200: i64 = 2200
17const PH_MAGIC_1500: i64 = 1500
18const PH_MAGIC_2500: i64 = 2500
19const PH_MAGIC_1300: i64 = 1300
20const PH_MAGIC_2600: i64 = 2600
21const PH_MAGIC_1350: i64 = 1350
22const PH_MAGIC_2000: i64 = 2000
23const PH_MAGIC_1900: i64 = 1900
24const PH_MAGIC_1800: i64 = 1800
25const PH_MAGIC_3400: i64 = 3400
26const PH_MAGIC_3600: i64 = 3600
27const PH_MAGIC_3000: i64 = 3000
28const PH_MAGIC_1400: i64 = 1400
29const PH_MAGIC_1200: i64 = 1200
30const PH_MAGIC_3500: i64 = 3500
31// nx_g2p.nx -- AUDIO arc: TEXT -> PHONEME front-end (grapheme-to-phoneme).
32// The bridge that lets you TYPE words and get a phoneme stream the synthesizer
33// can read aloud (nx_phoneme_synth / nx_voice_read). Sovereign rule-based
34// letter-to-sound: longest-match digraphs (sh/th/ee/oo/ck) then per-letter
35// rules, over a small DATA phoneme inventory (formant-Hz per phoneme).
36//
37// HONEST SCOPE: rule-based English LTS with a compact phoneme set -- deterministic
38// and synthesizable, NOT a full pronunciation dictionary (true English needs an
39// exception lexicon for "though"/"colonel"/etc). A later rung adds a lexicon.
40//
41// Pure integer, NO floats, NO imports (caller owns buffers).
42// nx_g2p(text, len, out_ids, max) -> count (lowercase ASCII; spaces skipped)
43// nx_g2p_phoneme(id, rec) rec=[F1,F2,F3,voiced,dur]
44//
45// license_tier: ORIGINAL
46// module: nishi-core.audio.g2p
47// depends: nishi-core.audio.phoneme_synth (record format)
48// capability: AUDIO_TEXT_TO_PHONEME
49
50// ---- phoneme ids ----
51const PH_AE: i64 = 0
52const PH_EH: i64 = 1
53const PH_IH: i64 = 2
54const PH_AA: i64 = 3
55const PH_AH: i64 = 4
56const PH_IY: i64 = 5
57const PH_UW: i64 = 6
58const PH_M: i64 = 7
59const PH_N: i64 = 8
60const PH_L: i64 = 9
61const PH_R: i64 = 10
62const PH_W: i64 = 11
63const PH_V: i64 = 12
64const PH_Z: i64 = 13
65const PH_B: i64 = 14
66const PH_D: i64 = 15
67const PH_G: i64 = 16
68const PH_S: i64 = 17
69const PH_SH: i64 = 18
70const PH_TH: i64 = 19
71const PH_F: i64 = 20
72const PH_HH: i64 = 21
73const PH_K: i64 = 22
74const PH_T: i64 = 23
75const PH_P: i64 = 24
76const PH_CH: i64 = 25
77const PH_JH: i64 = 26
78
79const G2P_VDUR: i64 = 1200 // vowel duration (samples)
80const G2P_CDUR: i64 = 500 // consonant duration
81
82// single lowercase letter -> phoneme id, or -1 to skip
83func _g2p_single(c: i64) -> i64 {
84 if c == 97 { return PH_AE } // a
85 if c == 98 { return PH_B } // b
86 if c == 99 { return PH_K } // c
87 if c == 100 { return PH_D } // d
88 if c == 101 { return PH_EH } // e
89 if c == 102 { return PH_F } // f
90 if c == 103 { return PH_G } // g
91 if c == 104 { return PH_HH } // h
92 if c == 105 { return PH_IH } // i
93 if c == 106 { return PH_JH } // j
94 if c == 107 { return PH_K } // k
95 if c == 108 { return PH_L } // l
96 if c == 109 { return PH_M } // m
97 if c == 110 { return PH_N } // n
98 if c == 111 { return PH_AA } // o
99 if c == 112 { return PH_P } // p
100 if c == 113 { return PH_K } // q
101 if c == 114 { return PH_R } // r
102 if c == 115 { return PH_S } // s
103 if c == 116 { return PH_T } // t
104 if c == 117 { return PH_AH } // u
105 if c == 118 { return PH_V } // v
106 if c == 119 { return PH_W } // w
107 if c == 120 { return PH_K } // x
108 if c == 121 { return PH_IY } // y
109 if c == 122 { return PH_Z } // z
110 return 0 - 1
111}
112
113func nx_g2p(text: *u8, len: i64, out_ids: *i64, max: i64) -> i64 {
114 var i: i64 = 0
115 var cnt: i64 = 0
116 while i < len {
117 if cnt >= max { i = len }
118 if i < len {
119 let c0: i64 = text[i]
120 var c1: i64 = 0
121 if i + 1 < len { c1 = text[i + 1] }
122 var id: i64 = 0 - 1
123 var adv: i64 = 1
124 var matched: i64 = 0
125 if c0 == 115 { if c1 == 104 { id = PH_SH; adv = 2; matched = 1 } } // sh
126 if matched == 0 { if c0 == 116 { if c1 == 104 { id = PH_TH; adv = 2; matched = 1 } } } // th
127 if matched == 0 { if c0 == 99 { if c1 == 104 { id = PH_CH; adv = 2; matched = 1 } } } // ch
128 if matched == 0 { if c0 == 101 { if c1 == 101 { id = PH_IY; adv = 2; matched = 1 } } } // ee
129 if matched == 0 { if c0 == 111 { if c1 == 111 { id = PH_UW; adv = 2; matched = 1 } } } // oo
130 if matched == 0 { if c0 == 99 { if c1 == 107 { id = PH_K; adv = 2; matched = 1 } } } // ck
131 if matched == 0 { id = _g2p_single(c0) }
132 if id >= 0 { if cnt < max { out_ids[cnt] = id; cnt = cnt + 1 } }
133 i = i + adv
134 }
135 }
136 return cnt
137}
138
139// phoneme -> formant record [F1,F2,F3,voiced,dur] (approximate English).
140func nx_g2p_phoneme(id: i64, rec: *i64) -> i64 {
141 rec[3] = 1 // default voiced
142 rec[4] = G2P_VDUR
143 // vowels
144 if id == PH_AE { rec[0] = 660; rec[1] = PH_MAGIC_1700; rec[2] = PH_MAGIC_2400; return 0 }
145 if id == PH_EH { rec[0] = 530; rec[1] = PH_MAGIC_1840; rec[2] = PH_MAGIC_2480; return 0 }
146 if id == PH_IH { rec[0] = 390; rec[1] = PH_MAGIC_1990; rec[2] = PH_MAGIC_2550; return 0 }
147 if id == PH_AA { rec[0] = 730; rec[1] = PH_MAGIC_1090; rec[2] = PH_MAGIC_2440; return 0 }
148 if id == PH_AH { rec[0] = 640; rec[1] = PH_MAGIC_1190; rec[2] = PH_MAGIC_2390; return 0 }
149 if id == PH_IY { rec[0] = 270; rec[1] = PH_MAGIC_2290; rec[2] = PH_MAGIC_3010; return 0 }
150 if id == PH_UW { rec[0] = 300; rec[1] = 870; rec[2] = PH_MAGIC_2240; return 0 }
151 // voiced consonants (shorter)
152 rec[4] = G2P_CDUR
153 if id == PH_M { rec[0] = 250; rec[1] = PH_MAGIC_1100; rec[2] = PH_MAGIC_2200; return 0 }
154 if id == PH_N { rec[0] = 250; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 }
155 if id == PH_L { rec[0] = 360; rec[1] = PH_MAGIC_1300; rec[2] = PH_MAGIC_2600; return 0 }
156 if id == PH_R { rec[0] = 490; rec[1] = PH_MAGIC_1350; rec[2] = PH_MAGIC_1700; return 0 }
157 if id == PH_W { rec[0] = 300; rec[1] = 610; rec[2] = PH_MAGIC_2200; return 0 }
158 if id == PH_V { rec[0] = 400; rec[1] = PH_MAGIC_1100; rec[2] = PH_MAGIC_2400; return 0 }
159 if id == PH_Z { rec[0] = 300; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 }
160 if id == PH_B { rec[0] = 250; rec[1] = 800; rec[2] = PH_MAGIC_2000; return 0 }
161 if id == PH_D { rec[0] = 300; rec[1] = PH_MAGIC_1700; rec[2] = PH_MAGIC_2600; return 0 }
162 if id == PH_G { rec[0] = 250; rec[1] = PH_MAGIC_1900; rec[2] = PH_MAGIC_2500; return 0 }
163 if id == PH_JH { rec[0] = 250; rec[1] = PH_MAGIC_1800; rec[2] = PH_MAGIC_2600; return 0 }
164 // unvoiced consonants (noise source)
165 rec[3] = 0
166 if id == PH_S { rec[0] = PH_MAGIC_2200; rec[1] = PH_MAGIC_3400; rec[2] = PH_MAGIC_3600; return 0 }
167 if id == PH_SH { rec[0] = PH_MAGIC_1800; rec[1] = PH_MAGIC_2400; rec[2] = PH_MAGIC_3000; return 0 }
168 if id == PH_TH { rec[0] = PH_MAGIC_1400; rec[1] = PH_MAGIC_2600; rec[2] = PH_MAGIC_3400; return 0 }
169 if id == PH_F { rec[0] = PH_MAGIC_1200; rec[1] = PH_MAGIC_2200; rec[2] = PH_MAGIC_3000; return 0 }
170 if id == PH_HH { rec[0] = 1000; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 }
171 if id == PH_K { rec[0] = PH_MAGIC_1800; rec[1] = PH_MAGIC_2000; rec[2] = PH_MAGIC_2400; return 0 }
172 if id == PH_T { rec[0] = PH_MAGIC_2000; rec[1] = PH_MAGIC_3000; rec[2] = PH_MAGIC_3500; return 0 }
173 if id == PH_P { rec[0] = 800; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2200; return 0 }
174 if id == PH_CH { rec[0] = PH_MAGIC_1700; rec[1] = PH_MAGIC_2400; rec[2] = PH_MAGIC_3000; return 0 }
175 // unknown -> neutral schwa-ish, voiced
176 rec[0] = 500; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; rec[3] = 1; rec[4] = G2P_CDUR
177 return 0
178}