Simplified recognition by taking only a single 6-6 contour. Also extended recognition...
[sgc2.git] / ToneProt / ToneScript.praat
blob1cc84b2f56eebd3d7f854d7825544364f3231f2b
1 #! praat
3 #     SpeakGoodChinese: toneScript.praat generates synthetic tone contours
4 #     for Mandarin Chinese
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
9
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.
14
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.
19
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
23
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
30 #         option Pitch
31 #         option Sound
32 #         option CorrectPitch
33 #         option CorrectSound
34 # endform
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
46         endif
48         # Limit lowest tone
49         toneScript.absoluteMinimum = 80
51         toneScript.prevTone = -1
52         toneScript.nextTone = -1
54         toneScript.point = 0
55         toneScript.lastFrequency = 0
57         # Clean up input
58         if toneScript.inputWord$ <> ""
59         toneScript.inputWord$ = replace_regex$(toneScript.inputWord$, "^\s*(.+)\s*$", "\1", 1)
60         endif
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
66         endif
67         toneScript.segmentDuration = 0.150
68         toneScript.fixedDuration = 0.12
70         #
71         # Movements
72         # start * ?Semit is a fall
73         # start / ?Semit is a rise
74         # 1/(12 semitones)
75         toneScript.octave = 0.5
76         # 1/(9 semitones)
77         toneScript.nineSemit = 0.594603557501361
78         # 1/(6 semitones)
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
94         endif
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
105         for .i from 1 to 36
106                 select Table ToneList
107                 Set string value... '.i' Word ------EMPTY
108         endfor
109         endif
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
116         endif
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)
123                         # Write name in list
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
130                                 Append row
131                                 Set string value... 'toneScript.currLength' Word ------EMPTY
132                         endfor
133                         Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
134                         endif
136                         # Actually, generate something
137                                         call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
138                         endif
139                 endfor
140         endfor
141         
142         # 6,6
143         toneScript.first = 6
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)
148             # Write name in list
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
155                                         Append row
156                                         Set string value... 'toneScript.currLength' Word ------EMPTY
157                 endfor
158                 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
159             endif
161                         # Actually, generate something
162                 endif
163                 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
164         else
165         call generateWord 'toneScript.generate$' 'toneScript.inputWord$' 'toneScript.upperRegister'
166         endif
167 endproc
169 procedure extractTone .syllable$
170         toneScript.toneSyllable = -1
171         .toneScript.currentToneText$ = replace_regex$(.syllable$, "^[^\d]+([\d]+)(.*)$", "\1", 0)
172         toneScript.toneSyllable = extractNumber(.toneScript.currentToneText$, "")
173 endproc
175 procedure convertVoicing toneScript.voicingSyllable$
176         # Remove tones
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)
182         # Convert vowels
183         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "([aiuoe\XFC])", "V", 0)
184 endproc
186 procedure addToneMovement .syllable$ toneScript.topLine toneScript.prevTone toneScript.nextTone
187         # Get tone
188         toneScript.toneSyllable = -1
189         call extractTone '.syllable$'
190     if toneScript.toneSyllable = 3 and toneScript.nextTone = 3
191         toneScript.toneSyllable = 2
192     endif
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
201     call toneDuration
202         toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
204         # Unvoiced part
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
210         endif
211         # Voiced part
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
223     endif
225     call toneRules
226         
227     toneScript.lastFrequency = toneScript.endPoint
229 endproc
231 procedure wordToTones .wordInput$ toneScript.highPitch
232         .currentRest$ = .wordInput$;
233         toneScript.syllableCount = 0
234         .length = 2 * toneScript.margin
236     # Split syllables
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'$
242                 # Get the tone
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
256         call toneDuration
257             toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
259                 .length = .length + toneScript.toneFactor * (length(voicingSyllable'toneScript.syllableCount'$) * (toneScript.segmentDuration + toneScript.delta) + toneScript.fixedDuration)
261                 # Next round
262                 .currentRest$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\2", 1)
264                 # Safety valve
265                 if toneScript.syllableCount > 2000
266                         exit
267                 endif
268         endwhile
270         # Create tone pattern
271         Create PitchTier... '.wordInput$' 0 '.length'
273         # Add start toneScript.margin
274         toneScript.lastFrequency = 0
275     toneScript.point = 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
287             .j = .i+1
288             toneScript.followTone = toneScript.toneSyllable'.j'
289         endif
291                 call addToneMovement 'toneScript.currentSyllable$' 'toneScript.highPitch' 'toneScript.lastTone' 'toneScript.followTone'
293         toneScript.lastTone = toneScript.currentTone
294         endfor
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
301 endproc
303 procedure generateWord toneScript.whatToGenerate$ toneScript.theWord$ toneScript.upperRegister
305         # First generate model contour
306         call wordToTones 'toneScript.theWord$' 'toneScript.upperRegister'
307         # Generate pitch
308     select PitchTier 'toneScript.theWord$'
309     noprogress To Pitch... 0.0125 60.0 600.0
310         Rename... theOrigWord
311         Smooth... 10
312         Rename... 'toneScript.theWord$'
313         select Pitch theOrigWord
314         Remove
316         # Then look if "real" model exists, and use that
317         if config.strict 
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")
325                 Remove
326                 if fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
327                         Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch
328                 else
329                         .modelSound = Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav
330                 select .modelSound
331                 # Third tones get really low
332                 if index(toneScript.theWord$, "3") > 0
333                                 call convert2Pitch 15 600
334                         else
335                                 call convert2Pitch 60 600
336                         endif
337                         .modelPitch = convert2Pitch.object
339                         select .modelSound
340                         Remove
341                         select .modelPitch
342                 endif
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
349         endif
350         
351     # Generate sound if wanted
352     select Pitch 'toneScript.theWord$'
353     if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
354             noprogress To Sound (hum)
355     endif
357     # Clean up
358     select PitchTier 'toneScript.theWord$'
359     if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
360         plus Pitch 'toneScript.theWord$'
361     endif
362     Remove
363 endproc
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'
368 endproc
370 # Not everyone add all the zeros for the neutral tones. Here we try to guess where
371 # they would belong.
372 procedure add_missing_neutral_tones .pinyin$
373         # Missing neutral tones
374         # Missing last tone
375         .pinyin$ = replace_regex$(.pinyin$, "([^0-9])$", "\10", 0)
376         # Easy cases V [^n]
377         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)([^0-9neuioa])", "\10\2", 0)
378         # Vng cases
379         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+ng)([^0-9])", "\10\2", 0)
380         # VnC cases C != g
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)
384 endproc