3 # SpeakGoodChinese: toneScript.praat generates synthetic tone contours
5 # Copyright (C) 2007 R.J.J.H. van Son
6 # The SpeakGoodChinese team are:
7 # Guangqin Chen, Zhonyan Chen, Stefan de Konink, Eveline van Hagen,
8 # Rob van Son, Dennis Vierkant, David Weenink
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 # form Enter pinyin and tone 1 frequency
25 # word toneScript.inputWord ba1ba1
26 # positive toneScript.upperRegister_(Hz) 300
27 # real toneScript.range_Factor 1
28 # real toneScript.durationScale 1
29 # optionmenu toneScript.generate 1
36 # Get the rules of the tones
37 # include ToneRules.praat
39 #call toneScript 'toneScript.inputWord$' 'toneScript.upperRegister' 'toneScript.range_Factor' 'toneScript.durationScale' 'toneScript.generate$'
41 procedure toneScript toneScript.inputWord$ toneScript.upperRegister toneScript.range_Factor toneScript.durationScale toneScript.generate$
42 # To supress the ToneList, change to 0
43 toneScript.createToneList = 1
44 if rindex_regex(toneScript.generate$, "Correct") > 0
45 toneScript.createToneList = 0
51 if index_regex(toneScript.generate$, "_[\d]+$") > 0
52 .pos = index_regex(toneScript.generate$, "_[\d]+$")
53 .left$ = left$(toneScript.generate$, .pos)
54 .startSyll = extractNumber(toneScript.generate$, .left$) - 1
55 if .startSyll <> undefined and .startSyll > 0
56 .skipSyll$ = "([^\d]+[\d]+){'.startSyll'}"
63 toneScript.absoluteMinimum = 80
65 toneScript.prevTone = -1
66 toneScript.nextTone = -1
69 toneScript.lastFrequency = 0
72 if toneScript.inputWord$ <> ""
73 toneScript.inputWord$ = replace_regex$(toneScript.inputWord$, "^\s*(.+)\s*$", "\1", 1)
74 toneScript.inputWord$ = replace_regex$(toneScript.inputWord$, "5", "0", 0)
75 # Missing neutral tones
76 call add_missing_neutral_tones 'toneScript.inputWord$'
77 toneScript.inputWord$ = add_missing_neutral_tones.pinyin$
80 # Add a tone movement. The current time toneScript.point is 'toneScript.point'
81 toneScript.delta = 0.0000001
82 if toneScript.durationScale <= 0
83 toneScript.durationScale = 1.0
85 toneScript.segmentDuration = 0.150
86 toneScript.fixedDuration = 0.12
90 # start * ?Semit is a fall
91 # start / ?Semit is a rise
93 toneScript.octave = 0.5
95 toneScript.nineSemit = 0.594603557501361
97 toneScript.sixSemit = 0.707106781186547
98 # 1/(3 semitones) down
99 toneScript.threeSemit = 0.840896415253715
100 # 1/(2 semitones) down
101 toneScript.twoSemit = 0.890898718140339
102 # 1/(1 semitones) down
103 toneScript.oneSemit = 0.943874313
104 # 1/(4 semitones) down
105 toneScript.fourSemit = toneScript.twoSemit * toneScript.twoSemit
106 # 1/(5 semitones) down
107 toneScript.fiveSemit = toneScript.threeSemit * toneScript.twoSemit
109 toneScript.frequency_Range = toneScript.octave
110 if toneScript.range_Factor > 0
111 toneScript.frequency_Range = toneScript.frequency_Range * toneScript.range_Factor
114 # Previous end frequency
115 toneScript.lastFrequency = 0
116 # Split input into syllables
117 toneScript.margin = 0.25
119 # Get a list of items
120 if toneScript.createToneList = 1
121 Create Table with column names... ToneList 36 Word
124 select Table ToneList
125 Set string value... '.i' Word ------EMPTY
129 toneScript.syllableCount = length(replace_regex$(toneScript.inputWord$, "[^\d]+([\d]+)", "1", 0))
130 if toneScript.syllableCount < .startSyll + 1
133 toneScript.wordNumber = 0
134 toneScript.lowerBound = 0
135 if rindex(toneScript.generate$, "Correct") <= 0
136 for toneScript.first from toneScript.lowerBound to 4
137 toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^('.skipSyll$')([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5", 1)
138 for toneScript.second from 0 to 4
139 if (toneScript.first <> 5 and toneScript.second <> 5) and (toneScript.syllableCount > 1 or toneScript.second == 1)
140 toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^('.skipSyll$')([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5'toneScript.second'\7", 1)
142 toneScript.wordNumber = toneScript.wordNumber+1
143 if toneScript.createToneList = 1
144 select Table ToneList
145 toneScript.listLength = Get number of rows
146 toneScript.listLength = toneScript.listLength + 1
147 for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
149 Set string value... 'toneScript.currLength' Word ------EMPTY
151 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
153 # Actually, generate something
154 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
160 #toneScript.first = 6
161 #toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^('.skipSyll$')([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5", 1)
162 #toneScript.second = 6
163 #if toneScript.syllableCount > 1
164 # toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^('.skipSyll$')([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5'toneScript.second'\7", 1)
165 # # Write name in list
166 # toneScript.wordNumber = toneScript.wordNumber+1
167 # if toneScript.createToneList = 1
168 # select Table ToneList
169 # toneScript.listLength = Get number of rows
170 # toneScript.listLength = toneScript.listLength + 1
171 # for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
173 # Set string value... 'toneScript.currLength' Word ------EMPTY
175 # Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
179 # "broken" third tones
180 if index_regex(toneScript.inputWord$, "3") > 0
181 toneScript.currentWord$ = replace$(toneScript.inputWord$, "3", "9", 0)
183 toneScript.wordNumber = toneScript.wordNumber+1
184 if toneScript.createToneList = 1
185 select Table ToneList
186 toneScript.listLength = Get number of rows
187 toneScript.listLength = toneScript.listLength + 1
188 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
190 # Actually, generate something
191 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
195 if toneScript.syllableCount > 1
196 toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "[\d+]", "6", 0)
198 toneScript.wordNumber = toneScript.wordNumber+1
199 if toneScript.createToneList = 1
200 select Table ToneList
201 toneScript.listLength = Get number of rows
202 toneScript.listLength = toneScript.listLength + 1
203 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
205 # Actually, generate something
206 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
209 call generateWord 'toneScript.generate$' 'toneScript.inputWord$' 'toneScript.upperRegister'
213 procedure extractTone .syllable$
214 toneScript.toneSyllable = -1
215 .toneScript.currentToneText$ = replace_regex$(.syllable$, "^[^\d]+([\d]+)(.*)$", "\1", 0)
216 toneScript.toneSyllable = extractNumber(.toneScript.currentToneText$, "")
219 procedure convertVoicing toneScript.voicingSyllable$
221 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "^([^\d]+)[\d]+", "\1", 0)
222 # Convert voiced consonants
223 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(ng|[wrlmny])", "C", 0)
224 # Convert unvoiced consonants
225 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(sh|ch|zh|[fsxhktpgqdbzcj])", "U", 0)
227 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "([aiuoe\XFC])", "V", 0)
230 procedure addToneMovement .syllable$ toneScript.topLine toneScript.prevTone toneScript.nextTone
232 toneScript.toneSyllable = -1
233 call extractTone '.syllable$'
234 if toneScript.toneSyllable = 3 and toneScript.nextTone = 3
235 toneScript.toneSyllable = 2
237 if toneScript.toneSyllable = 9 and toneScript.nextTone = 9
238 toneScript.toneSyllable = 2
241 # Get voicing pattern
242 toneScript.voicingSyllable$ = ""
243 call convertVoicing '.syllable$'
245 # Account for tones in duration
246 toneScript.toneFactor = 1
247 # Scale the duration of the current syllable
249 toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
252 toneScript.unvoicedDuration = 0
253 if rindex_regex(toneScript.voicingSyllable$, "U") = 1
254 toneScript.point = toneScript.point + toneScript.delta
255 Add point... 'toneScript.point' 0
256 toneScript.point = toneScript.point + toneScript.segmentDuration * toneScript.toneFactor
257 Add point... 'toneScript.point' 0
258 toneScript.unvoicedDuration = toneScript.segmentDuration * toneScript.toneFactor
261 toneScript.voiceLength$ = replace_regex$(toneScript.voicingSyllable$, "U*([CV]+)U*", "\1", 0)
262 toneScript.voicedLength = length(toneScript.voiceLength$)
263 toneScript.voicedDuration = toneScript.toneFactor * (toneScript.segmentDuration*toneScript.voicedLength + toneScript.fixedDuration)
264 toneScript.point = toneScript.point + toneScript.delta
266 # Write contour of each tone
267 # Note that tones are influenced by the previous (tone 0) and next (tone 3)
268 # tones. Tone 6 is the Dutch intonation
269 # sqrt(toneScript.frequency_Range) is the mid toneScript.point
270 if toneScript.topLine * toneScript.frequency_Range < toneScript.absoluteMinimum
271 toneScript.frequency_Range = toneScript.absoluteMinimum / toneScript.topLine
276 toneScript.lastFrequency = toneScript.endPoint
280 procedure wordToTones .wordInput$ toneScript.highPitch
281 .currentRest$ = .wordInput$;
282 toneScript.syllableCount = 0
283 .length = 2 * toneScript.margin
286 toneScript.prevTone = -1
287 while rindex_regex(.currentRest$, "^[^\d]+[\d]+") > 0
288 toneScript.syllableCount += 1
289 syllable'toneScript.syllableCount'$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\1", 1)
290 toneScript.currentSyllable$ = syllable'toneScript.syllableCount'$
293 .currentRest$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\2", 1)
295 .nextSyllable$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\1", 1)
296 call extractTone '.nextSyllable$'
297 toneScript.nextTone = toneScript.toneSyllable
299 # Get the current tone
300 call extractTone 'toneScript.currentSyllable$'
301 toneScript.toneSyllable'toneScript.syllableCount' = toneScript.toneSyllable
302 toneScript.currentTone = toneScript.toneSyllable'toneScript.syllableCount'
304 # Get the Voicing pattern
305 call convertVoicing 'toneScript.currentSyllable$'
306 voicingSyllable'toneScript.syllableCount'$ = toneScript.voicingSyllable$
307 currentVoicing$ = voicingSyllable'toneScript.syllableCount'$
309 # Calculate new .length
310 # Account for tones in duration
311 toneScript.toneFactor = 1
312 # Scale the duration of the current syllable
314 toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
316 .length = .length + toneScript.toneFactor * (length(voicingSyllable'toneScript.syllableCount'$) * (toneScript.segmentDuration + toneScript.delta) + toneScript.fixedDuration)
319 if toneScript.syllableCount > 2000
322 toneScript.prevTone = toneScript.currentTone
325 # Create tone pattern
326 toneScript.pitchTier = Create PitchTier: .wordInput$, 0, .length
327 toneScript.pitchTierWord$ = .wordInput$
328 # Create TextGrid with syllables
329 toneScript.textGrid = Create TextGrid: 0, .length, "Tones", ""
333 # Add start toneScript.margin
334 select toneScript.pitchTier
335 toneScript.lastFrequency = 0
337 Add point... 'toneScript.point' 0
338 toneScript.point = toneScript.margin
339 Add point... 'toneScript.point' 0
341 # Add interval to TextGrid
342 select toneScript.textGrid
343 Insert boundary: 1, toneScript.point
345 Set interval text: 1, .int, ""
347 toneScript.lastTone = -1
348 toneScript.followTone = -1
349 for .i from 1 to toneScript.syllableCount
350 select toneScript.pitchTier
351 toneScript.currentSyllable$ = syllable'.i'$
352 toneScript.currentTone = toneScript.toneSyllable'.i'
353 toneScript.followTone = -1
354 if .i < toneScript.syllableCount
356 toneScript.followTone = toneScript.toneSyllable'.j'
358 .lastPoint = toneScript.point
359 call addToneMovement 'toneScript.currentSyllable$' 'toneScript.highPitch' 'toneScript.lastTone' 'toneScript.followTone'
361 toneScript.lastTone = toneScript.currentTone
363 # Add interval to TextGrid
364 .writeTone = toneScript.currentTone
366 if (toneScript.currentTone = 3 or toneScript.currentTone = 9) and toneScript.currentTone = toneScript.followTone
369 select toneScript.textGrid
370 if toneScript.unvoicedDuration > 0
371 Insert boundary: 1, .lastPoint + toneScript.unvoicedDuration
373 Set interval text: 1, .int, "U"
375 Insert boundary: 1, toneScript.point
377 Set interval text: 1, .int, "'.writeTone'"
380 # Add end toneScript.margin
381 select toneScript.pitchTier
382 toneScript.point = toneScript.point + toneScript.delta
383 Add point... 'toneScript.point' 0
384 toneScript.point = toneScript.point + toneScript.margin
385 Add point... 'toneScript.point' 0
388 procedure generateWord toneScript.whatToGenerate$ toneScript.theWord$ toneScript.upperRegister
390 # First generate model contour
391 call wordToTones 'toneScript.theWord$' 'toneScript.upperRegister'
393 if index(toneScript.whatToGenerate$, "TextGrid") <= 0
394 if variableExists("toneScript.textGrid") and toneScript.textGrid > 0
395 select toneScript.textGrid
401 if toneScript.pitchTierWord$ = toneScript.theWord$
402 select toneScript.pitchTier
404 select PitchTier 'toneScript.theWord$'
406 toneScript.pitch = noprogress To Pitch... 0.0125 60.0 600.0
407 Rename... theOrigWord
409 Rename... 'toneScript.theWord$'
410 select Pitch theOrigWord
413 # Then look if "real" model exists, and use that
414 if index_regex(config.strict$, "[^0-9]") > 0
417 .userLevel = 'config.strict$'
418 if 0 and .userLevel >= sgc.highestLevel
419 ... and (fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
420 ... or fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav"))
421 # Get mean of generated contour
422 select toneScript.pitch
423 Rename... GeneratedContour
424 toneScript.generatedMean = do ("Get mean...", 0, 0, "Hertz")
425 toneScript.generatedMaximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
427 if fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
428 Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch
430 .modelSound = Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav
432 # Third tones get really low
433 if index_regex(toneScript.theWord$, "3") > 0
434 call convert2Pitch 20 600
435 .modelPitch = Kill octave jumps
436 select convert2Pitch.object
439 call convert2Pitch 60 600
440 .modelPitch = convert2Pitch.object
449 Rename... 'toneScript.theWord$'
450 toneScript.mean = do ("Get mean...", 0, 0, "Hertz")
451 toneScript.maximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
452 toneScript.shiftFreq = toneScript.generatedMean - toneScript.mean
453 # toneScript.shiftFreq = toneScript.generatedMaximum - toneScript.maximum
454 Formula... self + toneScript.shiftFreq
457 # Generate sound if wanted
458 select Pitch 'toneScript.theWord$'
459 if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
460 noprogress To Sound (hum)
464 select PitchTier 'toneScript.theWord$'
465 if rindex_regex(toneScript.whatToGenerate$, "(Sound|TextGrid)") > 0
466 plus Pitch 'toneScript.theWord$'
471 procedure convert2Pitch .minimumPitch .maximumPitch
472 #.object = noprogress To Pitch (ac)... 0.01 '.minimumPitch' 25 yes 0.05 0.3 0.01 0.6 0.14 '.maximumPitch'
473 .object = noprogress To Pitch (cc)... 0.005 '.minimumPitch' 25 yes 0.03 0.50 0.045 0.35 0.14 '.maximumPitch'
476 # Not everyone add all the zeros for the neutral tones. Here we try to guess where
478 procedure add_missing_neutral_tones .pinyin$
479 # Missing neutral tones
481 .pinyin$ = replace_regex$(.pinyin$, "([^0-9])$", "\10", 0)
483 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)([^0-9neuioar])", "\10\2", 0)
485 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)(r[euioa]+)", "\10\2", 0)
487 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+r)([^0-9euioa]+)", "\10\2", 0)
489 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+ng)([^0-9])", "\10\2", 0)
491 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+n)([^0-9geuioa])", "\10\2", 0)
492 # VnV cases -> Maximal onset
493 .pinyin$ = replace_regex$(.pinyin$, "([euioa])(n[euioa])", "\10\2", 0)