New attempt to creacky voice detection in third tones
[sgc2.git] / ToneProt / ToneScript.praat
blob44065abe4f17a158bf2c02bbc048e086a8c0ff13
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         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$
78         endif
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
84         endif
85         toneScript.segmentDuration = 0.150
86         toneScript.fixedDuration = 0.12
88         #
89         # Movements
90         # start * ?Semit is a fall
91         # start / ?Semit is a rise
92         # 1/(12 semitones)
93         toneScript.octave = 0.5
94         # 1/(9 semitones)
95         toneScript.nineSemit = 0.594603557501361
96         # 1/(6 semitones)
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
112         endif
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
123         for .i from 1 to 36
124                 select Table ToneList
125                 Set string value... '.i' Word ------EMPTY
126         endfor
127         endif
129         toneScript.syllableCount = length(replace_regex$(toneScript.inputWord$, "[^\d]+([\d]+)", "1", 0))
130         if toneScript.syllableCount < .startSyll + 1
131                 .skipSyll$ = "()"
132         endif
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)
141                 # Write name in list
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
148                                 Append row
149                                 Set string value... 'toneScript.currLength' Word ------EMPTY
150                         endfor
151                         Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
152                         endif
153                         # Actually, generate something
154                                         call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
155                         endif
156                 endfor
157         endfor
158         
159         # 6,6
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
172                 #                       Append row
173                 #                       Set string value... 'toneScript.currLength' Word ------EMPTY
174         #        endfor
175         #       Set string value... 'toneScript.wordNumber' Word 'toneScript.currentWord$'
176         #    endif
177                 #endif
178                 
179         # "broken" third tones
180         if index_regex(toneScript.inputWord$, "3") > 0
181                     toneScript.currentWord$ = replace$(toneScript.inputWord$, "3", "9", 0)
182                 # Write name in list
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$'
189             endif
190                         # Actually, generate something
191                         call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
192                 endif
193                 
194         # 6 tones
195         if toneScript.syllableCount > 1
196                     toneScript.currentWord$ = replace_regex$(toneScript.inputWord$, "[\d+]", "6", 0)
197                 # Write name in list
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$'
204             endif
205                         # Actually, generate something
206                         call generateWord 'toneScript.generate$' 'toneScript.currentWord$' 'toneScript.upperRegister'
207                 endif
208         else
209         call generateWord 'toneScript.generate$' 'toneScript.inputWord$' 'toneScript.upperRegister'
210         endif
211 endproc
213 procedure extractTone .syllable$
214         toneScript.toneSyllable = -1
215         .toneScript.currentToneText$ = replace_regex$(.syllable$, "^[^\d]+([\d]+)(.*)$", "\1", 0)
216         toneScript.toneSyllable = extractNumber(.toneScript.currentToneText$, "")
217 endproc
219 procedure convertVoicing toneScript.voicingSyllable$
220         # Remove tones
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)
226         # Convert vowels
227         toneScript.voicingSyllable$ = replace_regex$(toneScript.voicingSyllable$, "([aiuoe\XFC])", "V", 0)
228 endproc
230 procedure addToneMovement .syllable$ toneScript.topLine toneScript.prevTone toneScript.nextTone
231         # Get tone
232         toneScript.toneSyllable = -1
233         call extractTone '.syllable$'
234     if toneScript.toneSyllable = 3 and toneScript.nextTone = 3
235         toneScript.toneSyllable = 2
236     endif
237     if toneScript.toneSyllable = 9 and toneScript.nextTone = 9
238         toneScript.toneSyllable = 2
239     endif
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
248     call toneDuration
249         toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
251         # Unvoiced part
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
259         endif
260         # Voiced part
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
272     endif
274     call toneRules
275         
276     toneScript.lastFrequency = toneScript.endPoint
278 endproc
280 procedure wordToTones .wordInput$ toneScript.highPitch
281         .currentRest$ = .wordInput$;
282         toneScript.syllableCount = 0
283         .length = 2 * toneScript.margin
285     # Split syllables
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'$
292                 # For next round
293                 .currentRest$ = replace_regex$(.currentRest$, "^([^\d]+[\d]+)(.*)$", "\2", 1)
294                 # Get next syllable
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
313         call toneDuration
314             toneScript.toneFactor = toneScript.toneFactor * toneScript.durationScale
316                 .length = .length + toneScript.toneFactor * (length(voicingSyllable'toneScript.syllableCount'$) * (toneScript.segmentDuration + toneScript.delta) + toneScript.fixedDuration)
318                 # Safety valve
319                 if toneScript.syllableCount > 2000
320                         exit
321                 endif
322                 toneScript.prevTone = toneScript.currentTone
323         endwhile
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", ""
330         Rename: .wordInput$
331         .int = 0;
333         # Add start toneScript.margin
334         select toneScript.pitchTier
335         toneScript.lastFrequency = 0
336     toneScript.point = 0
337         Add point... 'toneScript.point' 0
338         toneScript.point = toneScript.margin
339         Add point... 'toneScript.point' 0
340         
341     # Add interval to TextGrid
342         select toneScript.textGrid
343         Insert boundary: 1, toneScript.point
344         .int += 1
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
355             .j = .i+1
356             toneScript.followTone = toneScript.toneSyllable'.j'
357         endif
358                 .lastPoint = toneScript.point
359                 call addToneMovement 'toneScript.currentSyllable$' 'toneScript.highPitch' 'toneScript.lastTone' 'toneScript.followTone'
361         toneScript.lastTone = toneScript.currentTone
362         
363         # Add interval to TextGrid
364         .writeTone = toneScript.currentTone
365         # 
366         if (toneScript.currentTone = 3 or toneScript.currentTone = 9) and toneScript.currentTone = toneScript.followTone
367                         .writeTone = 2
368         endif
369                 select toneScript.textGrid
370                 if toneScript.unvoicedDuration > 0
371                         Insert boundary: 1, .lastPoint + toneScript.unvoicedDuration
372                         .int += 1
373                         Set interval text: 1, .int, "U"
374                 endif
375                 Insert boundary: 1, toneScript.point
376                 .int += 1
377                 Set interval text: 1, .int, "'.writeTone'"
378         endfor
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
386 endproc
388 procedure generateWord toneScript.whatToGenerate$ toneScript.theWord$ toneScript.upperRegister
390         # First generate model contour
391         call wordToTones 'toneScript.theWord$' 'toneScript.upperRegister'
392         
393         if index(toneScript.whatToGenerate$, "TextGrid") <= 0
394                 if variableExists("toneScript.textGrid") and toneScript.textGrid > 0
395                         select toneScript.textGrid
396                         Remove
397                 endif
398         endif
399         
400         # Generate pitch
401         if toneScript.pitchTierWord$ = toneScript.theWord$
402                 select toneScript.pitchTier
403         else
404                 select PitchTier 'toneScript.theWord$'
405         endif
406     toneScript.pitch = noprogress To Pitch... 0.0125 60.0 600.0
407         Rename... theOrigWord
408         Smooth... 10
409         Rename... 'toneScript.theWord$'
410         select Pitch theOrigWord
411         Remove
413         # Then look if "real" model exists, and use that
414         if index_regex(config.strict$, "[^0-9]") > 0
415                 config.strict$ = "1"    
416         endif
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")
426                 Remove
427                 if fileReadable("'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch")
428                         Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.Pitch
429                 else
430                         .modelSound = Read from file... 'preferencesAppDir$'/pitchmodels/'toneScript.theWord$'.wav
431                 select .modelSound
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
437                                 Remove
438                         else
439                                 call convert2Pitch 60 600
440                                 .modelPitch = convert2Pitch.object
441                         endif
442                         select .modelPitch
443                         Rename... ModelPitch
445                         select .modelSound
446                         Remove
447                         select .modelPitch
448                 endif
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
455         endif
456         
457     # Generate sound if wanted
458     select Pitch 'toneScript.theWord$'
459     if rindex_regex(toneScript.whatToGenerate$, "Sound") > 0
460             noprogress To Sound (hum)
461     endif
463     # Clean up
464     select PitchTier 'toneScript.theWord$'
465     if rindex_regex(toneScript.whatToGenerate$, "(Sound|TextGrid)") > 0
466         plus Pitch 'toneScript.theWord$'
467     endif
468     Remove
469 endproc
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'
474 endproc
476 # Not everyone add all the zeros for the neutral tones. Here we try to guess where
477 # they would belong.
478 procedure add_missing_neutral_tones .pinyin$
479         # Missing neutral tones
480         # Missing last tone
481         .pinyin$ = replace_regex$(.pinyin$, "([^0-9])$", "\10", 0)
482         # Easy cases V [^n]
483         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)([^0-9neuioar])", "\10\2", 0)
484         # Complex case V r V
485         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+)(r[euioa]+)", "\10\2", 0)
486         # Complex case V r C
487         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+r)([^0-9euioa]+)", "\10\2", 0)
488         # Vng cases
489         .pinyin$ = replace_regex$(.pinyin$, "([euioa]+ng)([^0-9])", "\10\2", 0)
490         # VnC cases C != g
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)
494 endproc