Added levels to recognition precission
[sgc2.git] / ToneProt / ToneScript.praat
blob3c3276bc553a94271cc92e7fe663519426fc09ed
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
47         
48         # First tone to check
49         .skipSyll$ = "()"
50         .startSyll = 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'}"
57                 else
58                         .startSyll = 0
59                 endif
60         endif
62         # Limit lowest tone
63         toneScript.absoluteMinimum = 80
65         toneScript.prevTone = -1
66         toneScript.nextTone = -1
68         toneScript.point = 0
69         toneScript.lastFrequency = 0
71         # Clean up input
72         if toneScript.inputWord$ <> ""
73         toneScript.inputWord$ = replace_regex$(toneScript.inputWord$, "^\s*(.+)\s*$", "\1", 1)
74         endif
76         # Add a tone movement. The current time toneScript.point is 'toneScript.point'
77         toneScript.delta = 0.0000001
78         if toneScript.durationScale <= 0
79         toneScript.durationScale = 1.0
80         endif
81         toneScript.segmentDuration = 0.150
82         toneScript.fixedDuration = 0.12
84         #
85         # Movements
86         # start * ?Semit is a fall
87         # start / ?Semit is a rise
88         # 1/(12 semitones)
89         toneScript.octave = 0.5
90         # 1/(9 semitones)
91         toneScript.nineSemit = 0.594603557501361
92         # 1/(6 semitones)
93         toneScript.sixSemit = 0.707106781186547
94         # 1/(3 semitones) down
95         toneScript.threeSemit = 0.840896415253715
96         # 1/(2 semitones) down
97         toneScript.twoSemit = 0.890898718140339
98         # 1/(1 semitones) down
99         toneScript.oneSemit = 0.943874313
100         # 1/(4 semitones) down
101         toneScript.fourSemit = toneScript.twoSemit * toneScript.twoSemit
102         # 1/(5 semitones) down
103         toneScript.fiveSemit = toneScript.threeSemit * toneScript.twoSemit
105         toneScript.frequency_Range = toneScript.octave
106         if toneScript.range_Factor > 0
107         toneScript.frequency_Range =  toneScript.frequency_Range * toneScript.range_Factor
108         endif
110         # Previous end frequency
111         toneScript.lastFrequency = 0
112         # Split input into syllables
113         toneScript.margin = 0.25
115         # Get a list of items
116         if toneScript.createToneList = 1
117         Create Table with column names... ToneList 36 Word
119         for .i from 1 to 36
120                 select Table ToneList
121                 Set string value... '.i' Word ------EMPTY
122         endfor
123         endif
125         toneScript.syllableCount = length(replace_regex$(toneScript.inputWord$, "[^\d]+([\d]+)", "1", 0))
126         if toneScript.syllableCount < .startSyll + 1
127                 .skipSyll$ = "()"
128         endif
129         toneScript.wordNumber = 0
130         toneScript.lowerBound = 0
131         if rindex(toneScript.generate$, "Correct") <= 0
132         for toneScript.first from toneScript.lowerBound to 4
133                 toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^('.skipSyll$')([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5", 1)
134                 for toneScript.second from 0 to 4
135                         if (toneScript.first <> 5 and toneScript.second <> 5) and (toneScript.syllableCount > 1 or toneScript.second == 1)
136                                 toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^('.skipSyll$')([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5'toneScript.second'\7", 1)
137                 # Write name in list
138                         toneScript.wordNumber = toneScript.wordNumber+1
139                         if toneScript.createToneList = 1
140                                 select Table ToneList
141                         toneScript.listLength = Get number of rows
142                         toneScript.listLength = toneScript.listLength + 1
143                         for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
144                                 Append row
145                                 Set string value... 'toneScript.currLength' Word ------EMPTY
146                         endfor
147                         Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
148                         endif
149                         # Actually, generate something
150                                         call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
151                         endif
152                 endfor
153         endfor
154         
155         # 6,6
156         toneScript.first = 6
157             toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "^('.skipSyll$')([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5", 1)
158             toneScript.second = 6
159                 if toneScript.syllableCount > 1
160                         toneScript.currentWord$ = replace_regex$(toneScript.currentWord$, "^('.skipSyll$')([^\d]+)([\d]+)([^\d]+)([\d]+)(.*)$", "\1\3'toneScript.first'\5'toneScript.second'\7", 1)
161             # Write name in list
162             toneScript.wordNumber = toneScript.wordNumber+1
163             if toneScript.createToneList = 1
164                                 select Table ToneList
165                 toneScript.listLength = Get number of rows
166                 toneScript.listLength = toneScript.listLength + 1
167                 for toneScript.currLength from toneScript.listLength to toneScript.wordNumber
168                                         Append row
169                                         Set string value... 'toneScript.currLength' Word ------EMPTY
170                 endfor
171                 Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
172             endif
174                         # Actually, generate something
175                 endif
177                 call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
178         else
179         call generateWord 'toneScript.generate$' 'toneScript.inputWord$' 'toneScript.upperRegister'
180         endif
181 endproc
183 procedure extractTone .syllable$
184         toneScript.toneSyllable = -1
185         .toneScript.currentToneText$ = replace_regex$(.syllable$, "^[^\d]+([\d]+)(.*)$", "\1", 0)
186         toneScript.toneSyllable = extractNumber(.toneScript.currentToneText$, "")
187 endproc
189 procedure convertVoicing toneScript.voicingSyllable$
190         # Remove tones
191         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "^([^\d]+)[\d]+", "\1", 0)
192         # Convert voiced consonants
193         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(ng|[wrlmny])", "C", 0)
194         # Convert unvoiced consonants
195         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "(sh|ch|zh|[fsxhktpgqdbzcj])", "U", 0)
196         # Convert vowels
197         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "([aiuoe\XFC])", "V", 0)
198 endproc
200 procedure addToneMovement .syllable$ toneScript.topLine toneScript.prevTone toneScript.nextTone
201         # Get tone
202         toneScript.toneSyllable = -1
203         call extractTone '.syllable$'
204     if toneScript.toneSyllable = 3 and toneScript.nextTone = 3
205         toneScript.toneSyllable = 2
206     endif
208         # Get voicing pattern
209         toneScript.voicingSyllable$ = ""
210         call convertVoicing '.syllable$'
212         # Account for tones in duration
213         toneScript.toneFactor = 1
214     # Scale the duration of the current syllable
215     call toneDuration
216         toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
218         # Unvoiced part
219         if rindex_regex(toneScript.voicingSyllable$, "U") = 1
220                 toneScript.point = toneScript.point + toneScript.delta
221         Add point... 'toneScript.point' 0
222                 toneScript.point = toneScript.point + toneScript.segmentDuration * toneScript.toneFactor
223         Add point... 'toneScript.point' 0
224         endif
225         # Voiced part
226         toneScript.voiceLength$ = replace_regex$(toneScript.voicingSyllable$, "U*([CV]+)U*", "\1", 0)
227         toneScript.voicedLength = length(toneScript.voiceLength$)
228         toneScript.voicedDuration = toneScript.toneFactor * (toneScript.segmentDuration*toneScript.voicedLength + toneScript.fixedDuration)
229         toneScript.point = toneScript.point + toneScript.delta
231     # Write contour of each tone
232     # Note that tones are influenced by the previous (tone 0) and next (tone 3)
233     # tones. Tone 6 is the Dutch intonation
234     # sqrt(toneScript.frequency_Range) is the mid toneScript.point
235     if toneScript.topLine * toneScript.frequency_Range < toneScript.absoluteMinimum
236         toneScript.frequency_Range = toneScript.absoluteMinimum / toneScript.topLine
237     endif
239     call toneRules
240         
241     toneScript.lastFrequency = toneScript.endPoint
243 endproc
245 procedure wordToTones .wordInput$ toneScript.highPitch
246         .currentRest$ = .wordInput$;
247         toneScript.syllableCount = 0
248         .length = 2 * toneScript.margin
250     # Split syllables
251         while rindex_regex(.currentRest$, "^[^\d]+[\d]+") > 0
252         toneScript.syllableCount += 1
253         syllable'toneScript.syllableCount'$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\1", 1)
254                 toneScript.currentSyllable$ = syllable'toneScript.syllableCount'$
256                 # Get the tone
257                 call extractTone 'toneScript.currentSyllable$'
258                 toneScript.toneSyllable'toneScript.syllableCount' = toneScript.toneSyllable
259                 toneScript.currentTone = toneScript.toneSyllable'toneScript.syllableCount'
261                 # Get the Voicing pattern
262                 call convertVoicing 'toneScript.currentSyllable$'
263                 voicingSyllable'toneScript.syllableCount'$ = toneScript.voicingSyllable$
264                 currentVoicing$ = voicingSyllable'toneScript.syllableCount'$
266                 # Calculate new .length
267             # Account for tones in duration
268             toneScript.toneFactor = 1
269         # Scale the duration of the current syllable
270         call toneDuration
271             toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
273                 .length = .length + toneScript.toneFactor * (length(voicingSyllable'toneScript.syllableCount'$) * (toneScript.segmentDuration + toneScript.delta) + toneScript.fixedDuration)
275                 # Next round
276                 .currentRest$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\2", 1)
278                 # Safety valve
279                 if toneScript.syllableCount > 2000
280                         exit
281                 endif
282         endwhile
284         # Create tone pattern
285         Create PitchTier... '.wordInput$' 0 '.length'
287         # Add start toneScript.margin
288         toneScript.lastFrequency = 0
289     toneScript.point = 0
290         Add point... 'toneScript.point' 0
291         toneScript.point = toneScript.margin
292         Add point... 'toneScript.point' 0
294     toneScript.lastTone = -1
295     toneScript.followTone = -1
296         for .i from 1 to toneScript.syllableCount
297                 toneScript.currentSyllable$ = syllable'.i'$
298         toneScript.currentTone = toneScript.toneSyllable'.i'
299         toneScript.followTone = -1
300         if .i < toneScript.syllableCount
301             .j = .i+1
302             toneScript.followTone = toneScript.toneSyllable'.j'
303         endif
305                 call addToneMovement 'toneScript.currentSyllable$' 'toneScript.highPitch' 'toneScript.lastTone' 'toneScript.followTone'
307         toneScript.lastTone = toneScript.currentTone
308         endfor
310         # Add end toneScript.margin
311         toneScript.point = toneScript.point + toneScript.delta
312         Add point... 'toneScript.point' 0
313         toneScript.point = toneScript.point + toneScript.margin
314         Add point... 'toneScript.point' 0
315 endproc
317 procedure generateWord toneScript.whatToGenerate$ toneScript.theWord$ toneScript.upperRegister
319         # First generate model contour
320         call wordToTones 'toneScript.theWord$' 'toneScript.upperRegister'
321         # Generate pitch
322     select PitchTier 'toneScript.theWord$'
323     noprogress To Pitch... 0.0125 60.0 600.0
324         Rename... theOrigWord
325         Smooth... 10
326         Rename... 'toneScript.theWord$'
327         select Pitch theOrigWord
328         Remove
330         # Then look if "real" model exists, and use that
331         if index_regex(config.strict$, "[^0-9]") > 0
332                 config.strict$ = "1"    
333         endif
334         .userLevel = 'config.strict$'
335         if 0 and .userLevel >= sgc.highestLevel
336 ...    and (fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
337 ...             or fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav"))
338                 # Get mean of generated contour
339                 select Pitch 'toneScript.theWord$'
340                 Rename... GeneratedContour
341                 toneScript.generatedMean = do ("Get mean...", 0, 0, "Hertz")
342                 toneScript.generatedMaximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
343                 Remove
344                 if fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
345                         Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch
346                 else
347                         .modelSound = Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav
348                 select .modelSound
349                 # Third tones get really low
350                 if index(toneScript.theWord$, "3") > 0
351                                 call convert2Pitch 15 600
352                         else
353                                 call convert2Pitch 60 600
354                         endif
355                         .modelPitch = convert2Pitch.object
357                         select .modelSound
358                         Remove
359                         select .modelPitch
360                 endif
361                 Rename... 'toneScript.theWord$'
362                 toneScript.mean = do ("Get mean...", 0, 0, "Hertz")
363                 toneScript.maximum = do ("Get maximum...", 0, 0, "Hertz", "Parabolic")
364                 toneScript.shiftFreq = toneScript.generatedMean - toneScript.mean
365                 # toneScript.shiftFreq = toneScript.generatedMaximum - toneScript.maximum
366                 Formula... self + toneScript.shiftFreq
367         endif
368         
369     # Generate sound if wanted
370     select Pitch 'toneScript.theWord$'
371     if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
372             noprogress To Sound (hum)
373     endif
375     # Clean up
376     select PitchTier 'toneScript.theWord$'
377     if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
378         plus Pitch 'toneScript.theWord$'
379     endif
380     Remove
381 endproc
383 procedure convert2Pitch .minimumPitch .maximumPitch
384         #.object = noprogress To Pitch (ac)... 0 '.minimumPitch' 25 yes 0.05 0.3 0.01 0.6 0.14 '.maximumPitch'
385         .object = noprogress To Pitch (cc)... 0 '.minimumPitch' 15 yes 0.03 0.50 0.045 0.35 0.14 '.maximumPitch'
386 endproc
388 # Not everyone add all the zeros for the neutral tones. Here we try to guess where
389 # they would belong.
390 procedure add_missing_neutral_tones .pinyin$
391         # Missing neutral tones
392         # Missing last tone
393         .pinyin$ = replace_regex$(.pinyin$, "([^0-9])$", "\10", 0)
394         # Easy cases V [^n]
395         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)([^0-9neuioa])", "\10\2", 0)
396         # Vng cases
397         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+ng)([^0-9])", "\10\2", 0)
398         # VnC cases C != g
399         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+n)([^0-9geuioa])", "\10\2", 0)
400         # VnV cases -> Maximal onset
401         .pinyin$ = replace_regex$(.pinyin$, "([euioa])(n[euioa])", "\10\2", 0)
402 endproc