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
49 toneScript.absoluteMinimum = 80
51 toneScript.prevTone = -1
52 toneScript.nextTone = -1
55 toneScript.lastFrequency = 0
58 if toneScript.inputWord$ <> ""
59 toneScript.inputWord$ = replace_regex$(toneScript.inputWord$, "^\s*(.+)\s*$", "\1", 1)
62 # Add a tone movement. The current time toneScript.point is 'toneScript.point'
63 toneScript.delta = 0.0000001
64 if toneScript.durationScale <= 0
65 toneScript.durationScale = 1.0
67 toneScript.segmentDuration = 0.150
68 toneScript.fixedDuration = 0.12
72 # start * ?Semit is a fall
73 # start / ?Semit is a rise
75 toneScript.octave = 0.5
77 toneScript.nineSemit = 0.594603557501361
79 toneScript.sixSemit = 0.707106781186547
80 # 1/(3 semitones) down
81 toneScript.threeSemit = 0.840896415253715
82 # 1/(2 semitones) down
83 toneScript.twoSemit = 0.890898718140339
84 # 1/(1 semitones) down
85 toneScript.oneSemit = 0.943874313
86 # 1/(4 semitones) down
87 toneScript.fourSemit = toneScript.twoSemit * toneScript.twoSemit
88 # 1/(5 semitones) down
89 toneScript.fiveSemit = toneScript.threeSemit * toneScript.twoSemit
91 toneScript.frequency_Range = toneScript.octave
92 if toneScript.range_Factor > 0
93 toneScript.frequency_Range = toneScript.frequency_Range * toneScript.range_Factor
96 # Previous end frequency
97 toneScript.lastFrequency = 0
98 # Split input into syllables
99 toneScript.margin = 0.25
101 # Get a list of items
102 if toneScript.createToneList = 1
103 Create Table with column names... ToneList 36 Word
106 select Table ToneList
107 Set string value... '.i' Word ------EMPTY
111 toneScript.syllableCount = length(replace_regex$(toneScript.inputWord$, "[^\d]+([\d]+)", "1", 0))
112 toneScript.wordNumber = 0
113 toneScript.lowerBound = 1
114 if toneScript.syllableCount = 1
115 toneScript.lowerBound = 0
117 if rindex(toneScript.generate$, "Correct") <= 0
118 for toneScript.first from toneScript.lowerBound to 4
119 toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^([^\d]+)([\d]+)(.*)$", "\1'toneScript.first'\3", 1)
120 for toneScript.second from 0 to 4
121 if (toneScript.first <> 5 and toneScript.second <> 5) and (toneScript.syllableCount > 1 or toneScript.second == 1)
122 toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1'toneScript.first'\3'toneScript.second'\5", 1)
124 toneScript.wordNumber = toneScript.wordNumber+1
125 if toneScript.createToneList = 1
126 select Table ToneList
127 toneScript.listLength = Get number of rows
128 toneScript.listLength = toneScript.listLength + 1
129 for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
131 Set string value... 'toneScript.currLength' Word ------EMPTY
133 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
136 # Actually, generate something
137 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
144 toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^([^\d]+)([\d]+)(.*)$", "\1'toneScript.first'\3", 1)
145 toneScript.second = 6
146 if toneScript.syllableCount > 1
147 toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1'toneScript.first'\3'toneScript.second'\5", 1)
149 toneScript.wordNumber = toneScript.wordNumber+1
150 if toneScript.createToneList = 1
151 select Table ToneList
152 toneScript.listLength = Get number of rows
153 toneScript.listLength = toneScript.listLength + 1
154 for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
156 Set string value... 'toneScript.currLength' Word ------EMPTY
158 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
161 # Actually, generate something
163 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
165 call generateWord 'toneScript.generate$' 'toneScript.inputWord$' 'toneScript.upperRegister'
169 procedure extractTone .syllable$
170 toneScript.toneSyllable = -1
171 .toneScript.currentToneText$ = replace_regex$(.syllable$, "^[^\d]+([\d]+)(.*)$", "\1", 0)
172 toneScript.toneSyllable = extractNumber(.toneScript.currentToneText$, "")
175 procedure convertVoicing toneScript.voicingSyllable$
177 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "^([^\d]+)[\d]+", "\1", 0)
178 # Convert voiced consonants
179 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(ng|[wrlmny])", "C", 0)
180 # Convert unvoiced consonants
181 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(sh|ch|zh|[fsxhktpgqdbzcj])", "U", 0)
183 toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "([aiuoe\XFC])", "V", 0)
186 procedure addToneMovement .syllable$ toneScript.topLine toneScript.prevTone toneScript.nextTone
188 toneScript.toneSyllable = -1
189 call extractTone '.syllable$'
190 if toneScript.toneSyllable = 3 and toneScript.nextTone = 3
191 toneScript.toneSyllable = 2
194 # Get voicing pattern
195 toneScript.voicingSyllable$ = ""
196 call convertVoicing '.syllable$'
198 # Account for tones in duration
199 toneScript.toneFactor = 1
200 # Scale the duration of the current syllable
202 toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
205 if rindex_regex(toneScript.voicingSyllable$, "U") = 1
206 toneScript.point = toneScript.point + toneScript.delta
207 Add point... 'toneScript.point' 0
208 toneScript.point = toneScript.point + toneScript.segmentDuration * toneScript.toneFactor
209 Add point... 'toneScript.point' 0
212 toneScript.voiceLength$ = replace_regex$(toneScript.voicingSyllable$, "U*([CV]+)U*", "\1", 0)
213 toneScript.voicedLength = length(toneScript.voiceLength$)
214 toneScript.voicedDuration = toneScript.toneFactor * (toneScript.segmentDuration*toneScript.voicedLength + toneScript.fixedDuration)
215 toneScript.point = toneScript.point + toneScript.delta
217 # Write contour of each tone
218 # Note that tones are influenced by the previous (tone 0) and next (tone 3)
219 # tones. Tone 6 is the Dutch intonation
220 # sqrt(toneScript.frequency_Range) is the mid toneScript.point
221 if toneScript.topLine * toneScript.frequency_Range < toneScript.absoluteMinimum
222 toneScript.frequency_Range = toneScript.absoluteMinimum / toneScript.topLine
227 toneScript.lastFrequency = toneScript.endPoint
231 procedure wordToTones .wordInput$ toneScript.highPitch
232 .currentRest$ = .wordInput$;
233 toneScript.syllableCount = 0
234 .length = 2 * toneScript.margin
237 while rindex_regex(.currentRest$, "^[^\d]+[\d]+") > 0
238 toneScript.syllableCount += 1
239 syllable'toneScript.syllableCount'$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\1", 1)
240 toneScript.currentSyllable$ = syllable'toneScript.syllableCount'$
243 call extractTone 'toneScript.currentSyllable$'
244 toneScript.toneSyllable'toneScript.syllableCount' = toneScript.toneSyllable
245 toneScript.currentTone = toneScript.toneSyllable'toneScript.syllableCount'
247 # Get the Voicing pattern
248 call convertVoicing 'toneScript.currentSyllable$'
249 voicingSyllable'toneScript.syllableCount'$ = toneScript.voicingSyllable$
250 currentVoicing$ = voicingSyllable'toneScript.syllableCount'$
252 # Calculate new .length
253 # Account for tones in duration
254 toneScript.toneFactor = 1
255 # Scale the duration of the current syllable
257 toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
259 .length = .length + toneScript.toneFactor * (length(voicingSyllable'toneScript.syllableCount'$) * (toneScript.segmentDuration + toneScript.delta) + toneScript.fixedDuration)
262 .currentRest$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\2", 1)
265 if toneScript.syllableCount > 2000
270 # Create tone pattern
271 Create PitchTier... '.wordInput$' 0 '.length'
273 # Add start toneScript.margin
274 toneScript.lastFrequency = 0
276 Add point... 'toneScript.point' 0
277 toneScript.point = toneScript.margin
278 Add point... 'toneScript.point' 0
280 toneScript.lastTone = -1
281 toneScript.followTone = -1
282 for .i from 1 to toneScript.syllableCount
283 toneScript.currentSyllable$ = syllable'.i'$
284 toneScript.currentTone = toneScript.toneSyllable'.i'
285 toneScript.followTone = -1
286 if .i < toneScript.syllableCount
288 toneScript.followTone = toneScript.toneSyllable'.j'
291 call addToneMovement 'toneScript.currentSyllable$' 'toneScript.highPitch' 'toneScript.lastTone' 'toneScript.followTone'
293 toneScript.lastTone = toneScript.currentTone
296 # Add end toneScript.margin
297 toneScript.point = toneScript.point + toneScript.delta
298 Add point... 'toneScript.point' 0
299 toneScript.point = toneScript.point + toneScript.margin
300 Add point... 'toneScript.point' 0
303 procedure generateWord toneScript.whatToGenerate$ toneScript.theWord$ toneScript.upperRegister
305 # First generate model contour
306 call wordToTones 'toneScript.theWord$' 'toneScript.upperRegister'
308 select PitchTier 'toneScript.theWord$'
309 noprogress To Pitch... 0.0125 60.0 600.0
310 Rename... theOrigWord
312 Rename... 'toneScript.theWord$'
313 select Pitch theOrigWord
316 # Then look if "real" model exists, and use that
318 ... and (fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
319 ... or fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav"))
320 # Get mean of generated contour
321 select Pitch 'toneScript.theWord$'
322 Rename... GeneratedContour
323 toneScript.generatedMean = do ("Get mean...", 0, 0, "Hertz")
324 toneScript.generatedMaximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
326 if fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
327 Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch
329 .modelSound = Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav
331 # Third tones get really low
332 if index(toneScript.theWord$, "3") > 0
333 call convert2Pitch 15 600
335 call convert2Pitch 60 600
337 .modelPitch = convert2Pitch.object
343 Rename... 'toneScript.theWord$'
344 toneScript.mean = do ("Get mean...", 0, 0, "Hertz")
345 toneScript.maximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
346 toneScript.shiftFreq = toneScript.generatedMean - toneScript.mean
347 # toneScript.shiftFreq = toneScript.generatedMaximum - toneScript.maximum
348 Formula... self + toneScript.shiftFreq
351 # Generate sound if wanted
352 select Pitch 'toneScript.theWord$'
353 if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
354 noprogress To Sound (hum)
358 select PitchTier 'toneScript.theWord$'
359 if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
360 plus Pitch 'toneScript.theWord$'
365 procedure convert2Pitch .minimumPitch .maximumPitch
366 #.object = noprogress To Pitch (ac)... 0 '.minimumPitch' 25 yes 0.05 0.3 0.01 0.6 0.14 '.maximumPitch'
367 .object = noprogress To Pitch (cc)... 0 '.minimumPitch' 15 yes 0.03 0.50 0.045 0.35 0.14 '.maximumPitch'
370 # Not everyone add all the zeros for the neutral tones. Here we try to guess where
372 procedure add_missing_neutral_tones .pinyin$
373 # Missing neutral tones
375 .pinyin$ = replace_regex$(.pinyin$, "([^0-9])$", "\10", 0)
377 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)([^0-9neuioa])", "\10\2", 0)
379 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+ng)([^0-9])", "\10\2", 0)
381 .pinyin$ = replace_regex$(.pinyin$, "([euioa]+n)([^0-9geuioa])", "\10\2", 0)
382 # VnV cases -> Maximal onset
383 .pinyin$ = replace_regex$(.pinyin$, "([euioa])(n[euioa])", "\10\2", 0)