Rebuild creaky voice detector for third tones, first working version
[sgc2.git] / ToneProt / SGC_ToneProt.praat
blob8fc686adb0a4fa91c7730a180e152a1172ec1c4b
1 #! praat
3 #     SpeakGoodChinese: SGC_ToneRecognizer.praat processes student utterances 
4 #     and generates a report on their tone production
5 #     
6 #     Copyright (C) 2007-2010  R.J.J.H. van Son
7 #     The SpeakGoodChinese team are:
8 #     Guangqin Chen, Zhonyan Chen, Stefan de Koning, Eveline van Hagen, 
9 #     Rob van Son, Dennis Vierkant, David Weenink
10
11 #     This program is free software; you can redistribute it and/or modify
12 #     it under the terms of the GNU General Public License as published by
13 #     the Free Software Foundation; either version 2 of the License, or
14 #     (at your option) any later version.
15
16 #     This program is distributed in the hope that it will be useful,
17 #     but WITHOUT ANY WARRANTY; without even the implied warranty of
18 #     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 #     GNU General Public License for more details.
20
21 #     You should have received a copy of the GNU General Public License
22 #     along with this program; if not, write to the Free Software
23 #     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
24
25 # Needs:
26 # include ToneRecognition.praat
27 # include ToneScript.praat
28 # procedure loadTable
30 procedure sgc_ToneProt sgc_ToneProt.currentSound$ sgc_ToneProt.pinyin$ sgc_ToneProt.register sgc_ToneProt.proficiency sgc_ToneProt.language$
31         # Remove if included in main program!
32         sgc_ToneProt.viewportMargin = 5
34         sgc_ToneProt.precision = 3
35         if sgc_ToneProt.proficiency
36                 sgc_ToneProt.precision = 1.5
37         endif
38         # Stick to the raw recognition results or not
39         sgc_ToneProt.ultraStrict = sgc_ToneProt.proficiency
40         
41         # Read and select the feedbacktext
42         call testLoadTable ToneFeedback_'sgc_ToneProt.language$'
43         if testLoadTable.table > 0
44                 call loadTable ToneFeedback_'sgc_ToneProt.language$'
45         else
46                 call loadTable ToneFeedback_EN
47         endif
48         Rename... ToneFeedback
49         numberOfFeedbackRows = Get number of rows
51         # Clean up input
52         if sgc_ToneProt.pinyin$ <> ""           
53         sgc_ToneProt.pinyin$ = replace_regex$(sgc_ToneProt.pinyin$, "^\s*(.+)\s*$", "\1", 1)
54         sgc_ToneProt.pinyin$ = replace_regex$(sgc_ToneProt.pinyin$, "5", "0", 0)
55                 # Missing neutral tones
56                 call add_missing_neutral_tones 'sgc_ToneProt.pinyin$'
57                 sgc_ToneProt.pinyin$ = add_missing_neutral_tones.pinyin$
58         endif
60         # Reduction (lower sgc_ToneProt.register and narrow range) means errors
61         # The oposite mostly not. Asymmetry alows more room upward
62         # than downward (asymmetry = 2 => highBoundaryFactor ^ 2)
63         asymmetry = 2
65         # Kill octave jumps: DANGEROUS
66         killOctaveJumps = 0
68         # Limit pitch range
69         sgc_ToneProt.minimumPitch = 50
70         sgc_ToneProt.maximumPitch = 500
71         if sgc_ToneProt.register > 400
72         sgc_ToneProt.minimumPitch = 60
73         sgc_ToneProt.maximumPitch = 600
74         elsif sgc_ToneProt.register > 250
75         sgc_ToneProt.minimumPitch = 50
76         sgc_ToneProt.maximumPitch = 500
77         else
78         sgc_ToneProt.minimumPitch = 40
79         sgc_ToneProt.maximumPitch = 400
80         endif
82         sgc_ToneProt.currentTestWord$ = sgc_ToneProt.pinyin$
83         spacing = 0.5
84         .interPrecision = sgc_ToneProt.precision;
85         
86         # Allow more "room" when there is a 3rd tone
87         if index(sgc_ToneProt.pinyin$, "3")
88                 .interPrecision *= 4/3 
89         endif
90         sgc_ToneProt.precisionFactor = 2^(.interPrecision/12)
91         highBoundaryFactor = sgc_ToneProt.precisionFactor ^ asymmetry
92         lowBoundaryFactor = 1/sgc_ToneProt.precisionFactor
94         # Generate reference example
95         # Start with a range of 1 octave and a speed factor of 1
96         toneRange = 1.0
97         speedFactor = 1.0
98         sgc_ToneProt.upperRegisterInput = sgc_ToneProt.register
99         call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 1 1 CorrectPitch
100         # Get range and top
101         select Pitch 'sgc_ToneProt.currentTestWord$'
102         sgc_ToneProt.durationModel = Get total duration
103         maximumModelFzero = Get quantile... 0 0 0.95 Hertz
104         minimumModelFzero = Get quantile... 0 0 0.05 Hertz
105         if maximumModelFzero = undefined
106                 maximumModelFzero = 0
107         endif
108         if minimumModelFzero = undefined
109                 minimumModelFzero = 0
110         endif
111         sgc_ToneProt.modelPitchRange = 2
112         if minimumModelFzero > 0
113         sgc_ToneProt.modelPitchRange = maximumModelFzero / minimumModelFzero
114     else
115                 sgc_ToneProt.modelPitchRange = 0
116         endif
118         # Get the sounds
119         if fileReadable(sgc_ToneProt.currentSound$)
120         Read from file... 'sgc_ToneProt.currentSound$'
121         Rename... Source
122         else
123         select Sound 'sgc_ToneProt.currentSound$'
124         Copy... Source
125         endif
127         # Calculate pitch
128         select Sound Source
129         durationSource = Get total duration
130         call convert2Pitch 'sgc_ToneProt.minimumPitch' 'sgc_ToneProt.maximumPitch'
131         te.recordedPitch = convert2Pitch.object
132         select te.recordedPitch
133         Rename... SourcePitch
134         
135         ################################################################
136         #
137         # If there is a third tone, replace creaky voice by low pitch
138         # 
139         ################################################################
140         toneProt.creackyThree = 0
141         if index(sgc_ToneProt.pinyin$, "3")
142                 # Lowest point for tone 3 is 1 octave and 3st below the top line
143                 # The cut-off is 0,05 quantile of model + 1/2 semitones
144                 
145                 .creakCO = 0.030
146                 # Calculate Shimmer&Jitter
147                 select Sound Source
148                 .sound = selected("Sound")
149                 call createJitterShimmerContour .sound
150                 .jitterShimmer = selected("Sound")
151                 
152                 # Determine the low F0 part of the 3rd tone
153                 # Generate word with voiceless low 3rd tone
154                 .newPinyin$ = replace_regex$(sgc_ToneProt.pinyin$, "3(?=[^0-9]+3)", "2", 0)
155                 .newPinyin$ = replace$(.newPinyin$, "3", "9", 0)
156                 call generateWord Pitch '.newPinyin$' 'sgc_ToneProt.register'
157                 select Pitch '.newPinyin$'
158                 .generatedWord9 = selected("Pitch")
159                 
160                 select Pitch 'sgc_ToneProt.pinyin$'
161                 .generatedWord3 = selected("Pitch")
162                 # Lower cutoff is 1/2 semitone above the 0.05 quantile
163                 .lowerCutOff = Get quantile: 0, 0, 0.05, "Hertz"
164                 .lowerCutOff /= sqrt(toneScript.oneSemit)
165                 
166                 # Create a tier with low part of the third tone as intervals
167                 select .generatedWord3
168                 .modelDuration = Get total duration
169                 .numFrames = Get number of frames
170                 .prevV = Get value in frame: 1, "Hertz"
171                 if .prevV = undefined or .prevV <= 0
172                         .prevV = sgc_ToneProt.register
173                 endif
174                 .prevT = 0
175                 .numIntervals = 1
176                 .pitchTier = Create TextGrid: 0, .modelDuration, "pitch", ""
177                 select .pitchTier
178                 if .prevV > .lowerCutOff
179                         Set interval text: 1, .numIntervals, "NotLow"
180                 elsif .prevV <= .lowerCutOff
181                         Set interval text: 1, .numIntervals, "3Low"
182                 endif
183                 .lowPresent = 0
184                 for .s to .numFrames
185                         select .generatedWord3
186                         .frameT = Get time from frame number: .s
187                         .value = Get value in frame: .s, "Hertz"
188                         if .value = undefined or .value <= 0
189                                 .value = sgc_ToneProt.register
190                         endif
191                         if .value > .lowerCutOff and .prevV <= .lowerCutOff
192                                 select .pitchTier
193                                 Insert boundary: 1, (.frameT+.prevT)/2
194                                 .numIntervals += 1
195                                 Set interval text: 1, .numIntervals, "NotLow"
196                         elsif .value <= .lowerCutOff and .prevV > .lowerCutOff
197                                 select .pitchTier
198                                 Insert boundary: 1, (.frameT+.prevT)/2
199                                 .numIntervals += 1
200                                 Set interval text: 1, .numIntervals, "3Low"
201                                 .lowPresent += 1
202                         endif
203                         .prevV = .value
204                         .prevT = .frameT
205                 endfor
206                 
207                 # DTW of .generatedWord9 with sourcePitch
208                 if .lowPresent > 0
209                         select te.recordedPitch
210                         plus .generatedWord3
211                         .dtw3 = noprogress To DTW... 24 10 yes yes no restriction
212                         Rename... DTW'.generatedWord9'
213                         .distance3 = Get distance (weighted)
214                         select te.recordedPitch
215                         plus .generatedWord9
216                         .dtw9 = noprogress To DTW... 24 10 yes yes no restriction
217                         Rename... DTW'.generatedWord9'
218                         .distance9 = Get distance (weighted)
219                         
220                         .dtw = .dtw3
221                         if .dtw3 > .dtw9
222                                 .dtw = .dtw9
223                         endif
224                         select .pitchTier
225                         .numPitchIntervals = Get number of intervals: 1
226                         for .i to .numPitchIntervals
227                                 select .pitchTier
228                                 .label$ = Get label of interval: 1, .i
229                                 if .label$ = "3Low"
230                                         .modelStart = Get starting point: 1, .i
231                                         .modelEnd = Get end point: 1, .i
232                                         select .dtw
233                                         .origStart = Get x time from y time: .modelStart
234                                         .origEnd = Get x time from y time: .modelEnd
235                                         select .jitterShimmer
236                                         .mean = Get mean: 0, .origStart, .origEnd
237                                         if .mean > .creakCO
238                                                 toneProt.creackyThree += 1;
239                                         endif
240                                 endif
241                         endfor
242                         select .dtw3
243                         plus .dtw9
244                         Remove
245                         
246                         # Weigh creacky voice by number of syllables
247                         toneProt.creackyThree /= toneScript.syllableCount
248                 endif
249                 
250                 # Clean up
251                 select .jitterShimmer
252                 plus .pitchTier
253                 plus .generatedWord9
254                 Remove
255         endif
257         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
258         select te.recordedPitch
259         upperCutOff = 1.7*sgc_ToneProt.upperRegisterInput
260         lowerCutOff = sgc_ToneProt.upperRegisterInput/4
261         Formula... if self > 'upperCutOff' then -1 else self endif
262         Formula... if self < 'lowerCutOff' then -1 else self endif
264         # Get range and top
265         select te.recordedPitch
266         maximumRecFzero = Get quantile... 0 0 0.95 Hertz
267         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
268         minimumRecFzero = Get quantile... 0 0 0.05 Hertz
269         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
270         if maximumRecFzero = undefined
271         # Determine what should be told to the student
272         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': ???"
273         for i from 1 to numberOfFeedbackRows
274                 select Table ToneFeedback
275                 .toneOne$ = Get value... 'i' T1
276                 .toneTwo$ = Get value... 'i' T2
277                 .toneText$ = Get value... 'i' Feedback
278                         .label$ = "Unknown"
280                 if .toneOne$ = "NoSound"
281                 .feedbackText$ = .toneText$
282                 endif
283         endfor
285         #exit Error, nothing recorded
286                 goto END
287         endif
288         recPitchRange = 2
289         if minimumRecFzero > 0
290            recPitchRange = maximumRecFzero / minimumRecFzero
291         endif
292         sgc_ToneProt.newUpperRegister = maximumRecFzero / maximumModelFzero * sgc_ToneProt.upperRegisterInput
293         sgc_ToneProt.newToneRange = recPitchRange / sgc_ToneProt.modelPitchRange
294         if sgc_ToneProt.newUpperRegister = undefined
295                 sgc_ToneProt.newUpperRegister = sgc_ToneProt.upperRegisterInput
296         endif
297         if sgc_ToneProt.newToneRange = undefined
298                 sgc_ToneProt.newToneRange = 1
299         endif
301         sgc_ToneProt.registerUsed$ = "OK"
302         rangeUsed$ = "OK"
303         # Advanced speakers must not speak too High, or too "Dramatic"
304         # Beginning speakers also not too Low or too Narrow ranges
305         if sgc_ToneProt.newUpperRegister > highBoundaryFactor * sgc_ToneProt.upperRegisterInput
306            sgc_ToneProt.newUpperRegister = highBoundaryFactor * sgc_ToneProt.upperRegisterInput
307            sgc_ToneProt.registerUsed$ = "High"
308         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newUpperRegister < lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
309            sgc_ToneProt.newUpperRegister = lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
310            sgc_ToneProt.registerUsed$ = "Low"
311         endif
312         
313         if sgc_ToneProt.newToneRange > highBoundaryFactor
314            sgc_ToneProt.newToneRange = highBoundaryFactor
315            rangeUsed$ = "Wide"
316         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newToneRange < lowBoundaryFactor and not sgc_ToneProt.proficiency
317                 # Don't do this for advanced speakers
318            sgc_ToneProt.newToneRange = lowBoundaryFactor
319            rangeUsed$ = "Narrow"
320         endif
322         # Duration 
323         if sgc_ToneProt.durationModel > spacing
324            speedFactor = (durationSource - spacing) / (sgc_ToneProt.durationModel - spacing)
325         endif
327         # Round values
328         sgc_ToneProt.newUpperRegister = round(sgc_ToneProt.newUpperRegister)
330         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
331         select te.recordedPitch
332         upperCutOff = 1.5*sgc_ToneProt.newUpperRegister
333         lowerCutOff = sgc_ToneProt.newUpperRegister/3
334         Formula... if self > 'upperCutOff' then -1 else self endif
335         Formula... if self < 'lowerCutOff' then -1 else self endif
337         # It is rather dangerous to kill Octave errors, so be careful
338         if killOctaveJumps > 0
339                 select te.recordedPitch
340         Rename... OldSourcePitch
341         Kill octave jumps
342         Rename... SourcePitch
343         te.recordedPitch = selected("Pitch")
344         select Pitch OldSourcePitch
345         Remove
346         endif
348         # It is good to have the lowest and highest pitch frequencies
349         select te.recordedPitch
350         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
351         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
353         # Clean up the old example pitch
354         select Pitch 'sgc_ToneProt.currentTestWord$'
355         Remove
357         # Do the tone recognition
358         .numSyllables = toneScript.syllableCount
359         sgc_ToneProt.choiceReference$ = sgc_ToneProt.currentTestWord$
360         .skipSyllables = 0
361         while sgc_ToneProt.choiceReference$ = sgc_ToneProt.currentTestWord$ and .skipSyllables < .numSyllables
362                 call FreeToneRecognition 'sgc_ToneProt.choiceReference$' "REUSEPITCH" "" 'sgc_ToneProt.newUpperRegister' 'sgc_ToneProt.newToneRange' 'speedFactor' '.skipSyllables'
363                 .skipSyllables += 1
364         endwhile
365         call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 'sgc_ToneProt.newToneRange' 'speedFactor' CorrectPitch
366         # Special cases
367         originalRecognizedWord$ = sgc_ToneProt.choiceReference$
368         if  sgc_ToneProt.ultraStrict = 0
369         # [23]3 is often misidentified as 23, 20 or 30
370         if rindex_regex(sgc_ToneProt.currentTestWord$, "[23][^0-9]+3") > 0
371                 if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+3") > 0
372                                 .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+3") - 1
373                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23][^0-9]+[023]") > 0
374                         sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23]([^0-9]+)[023]", "\13\23", 1)
375                         endif
376                 endif
377                 if rindex_regex(sgc_ToneProt.currentTestWord$, "2[^0-9]+3") > 0
378                                 .c = rindex_regex(sgc_ToneProt.currentTestWord$, "2[^0-9]+3") - 1
379                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23][^0-9]+[023]") > 0
380                         sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "([^0-9]+)[23]([^0-9]+)[023]", "\12\23", 1)
381                         endif
382                 endif
383             endif
384             
385         # First syllable: 2<->3 exchanges
386         if rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+2") > 0
387                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+3") > 0
388                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^([^0-9]+)[36]", "\12", 0)
389                 endif
390         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+3") > 0
391                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+2") > 0
392                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^([^0-9]+)2", "\13", 0)
393                 endif
394         # A single second tone is often misidentified as a neutral tone, 
395         # A real neutral tone would be too low or too narrow and be discarded
396         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+2$") > 0
397                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+0$") > 0 and timeMinimum < timeMaximum
398                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "2", 0)
399                 endif
400         # A single fourth tone is often misidentified as a neutral tone, 
401         # A real neutral tone would be too low or too narrow and be discarded
402         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+4$") > 0
403                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+0$") > 0 and timeMaximum < timeMinimum
404                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "4", 0)
405                 endif
406         endif
408         # 32 <-> 30 Sometimes this is recognized as 00
409         # A recognized 0/2 after a 3 can be a 2/0
410         if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+2") > 0
411                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+2") - 1
412                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+0") > 0
413                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)0", "\12", 0)
414                         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})0[^0-9]+0") > 0
415                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})0([^0-9]+)0", "\13\22", 0)
416                 endif
417         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+0") > 0
418                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+0") - 1
419                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+2") > 0
420                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)2", "\10", 0)
421                         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})0[^0-9]+0") > 0
422                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})0([^0-9]+)0", "\13\20", 0)
423                         endif
424         endif
425         
426         # 31 <-> 30 
427         # A recognized 0 after a 3 can be a 1
428         if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+1") > 0
429                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+1") - 1
430                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+0") > 0
431                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)0", "\11", 0)
432                 endif
433                 endif
434         
435         # 40 <-> 42
436         # A recognized 0 after a 4 can be a 2: 4-0 => 4-2
437         if rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+2") > 0
438                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+2") - 1
439                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})4[^0-9]+0") > 0
440                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}4[^0-9]+)0", "\12", 0)
441                 endif
442         endif
443         
444                 # 404 <-> 414
445         # A recognized 0 between two tones 4 can be a 1
446         if rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+1[^0-9]+4") > 0
447                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+1[^0-9]+4") - 1
448                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})4[^0-9]+0[^0-9]+4") > 0
449                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}4[^0-9]+)0([^0-9]+4)", "\11\2", 0)
450                 endif
451         endif
452         
453         endif
454         
455         # If wrong, then undo all changes
456         if sgc_ToneProt.currentTestWord$ != sgc_ToneProt.choiceReference$
457         sgc_ToneProt.choiceReference$ = originalRecognizedWord$
458         endif
460         sgc_ToneProt.toneChoiceReference$ = sgc_ToneProt.choiceReference$
462         ###############################################
463         #
464         # Report
465         #
466         ###############################################
467         result$ = "'tab$''sgc_ToneProt.currentTestWord$''tab$''sgc_ToneProt.choiceReference$''tab$''sgc_ToneProt.newUpperRegister''tab$''sgc_ToneProt.newToneRange''tab$''speedFactor''tab$''sgc_ToneProt.registerUsed$''tab$''rangeUsed$'"
468         if sgc_ToneProt.currentTestWord$ = sgc_ToneProt.toneChoiceReference$
469            result$ = "Correct:"+result$
470         else
471            result$ = "Wrong:"+result$
472         endif
474         # Initialize result texts
475         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': "
476         .choiceText$ = replace_regex$(sgc_ToneProt.choiceReference$, "6", "\?", 0)
477         .feedbackText$ = "----"
479         # Separate tone from pronunciation errors
480         currentToneWord$ = replace_regex$(sgc_ToneProt.currentTestWord$, "[a-z]+", "\*", 0)
481         choiceToneReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[a-z]+", "\*", 0)
483         # Determine what should be told to the student
484         if sgc_ToneProt.registerUsed$ = "Low"
485         .recognitionText$ = .recognitionText$ + "???"
486         for i from 1 to numberOfFeedbackRows
487                 select Table ToneFeedback
488                 .toneOne$ = Get value... 'i' T1
489                 .toneTwo$ = Get value... 'i' T2
490                 .toneText$ = Get value... 'i' Feedback
492                 if .toneOne$ = "Low"
493                 .feedbackText$ = .toneText$
494                                 .label$ = .toneOne$
495                 endif
496         endfor
497         elsif rangeUsed$ = "Narrow"
498         .recognitionText$ = .recognitionText$ + "???"
499         for i from 1 to numberOfFeedbackRows
500                 select Table ToneFeedback
501                 .toneOne$ = Get value... 'i' T1
502                 .toneTwo$ = Get value... 'i' T2
503                 .toneText$ = Get value... 'i' Feedback
505                 if .toneOne$ = "Narrow"
506                 .feedbackText$ = .toneText$
507                                 .label$ = .toneOne$
508                 endif
509         endfor
510         elsif sgc_ToneProt.registerUsed$ = "High"
511         .recognitionText$ = .recognitionText$ + .choiceText$
512         for i from 1 to numberOfFeedbackRows
513                 select Table ToneFeedback
514                 .toneOne$ = Get value... 'i' T1
515                 .toneTwo$ = Get value... 'i' T2
516                 .toneText$ = Get value... 'i' Feedback
518                 if .toneOne$ = "High"
519                 .feedbackText$ = .toneText$
520                                 .label$ = .toneOne$
521                 endif
522         endfor
523         elsif rangeUsed$ = "Wide"
524         .recognitionText$ = .recognitionText$ + .choiceText$
525         for i from 1 to numberOfFeedbackRows
526                 select Table ToneFeedback
527                 .toneOne$ = Get value... 'i' T1
528                 .toneTwo$ = Get value... 'i' T2
529                 .toneText$ = Get value... 'i' Feedback
531                 if .toneOne$ = "Wide"
532                 .feedbackText$ = .toneText$
533                                 .label$ = .toneOne$
534                 endif
535         endfor
536         # Bad tones, first handle first syllable
537         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+6") > 0
538         .recognitionText$ = .recognitionText$ + .choiceText$
539         # First syllable
540         for i from 1 to numberOfFeedbackRows
541                 select Table ToneFeedback
542                 .toneOne$ = Get value... 'i' T1
543                 .toneTwo$ = Get value... 'i' T2
544                 .toneText$ = Get value... 'i' Feedback
546                 # 
547                 .feedbackText$ = ""
548                 if .toneOne$ = "6"
549                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
550                                 .label$ = .toneOne$
551                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'") > 0 and .toneTwo$ = "-"
552                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
553                 endif
554         endfor
555         # Bad tones, then handle second syllable
556         elsif rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+6$") > 0
557         .recognitionText$ = .recognitionText$ + .choiceText$
558         # Last syllable
559         for i from 1 to numberOfFeedbackRows
560                 select Table ToneFeedback
561                 .toneOne$ = Get value... 'i' T1
562                 .toneTwo$ = Get value... 'i' T2
563                 .toneText$ = Get value... 'i' Feedback
565                 # 
566                 .feedbackText$ = ""
567                 if .toneOne$ = "6"
568                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
569                                 .label$ = .toneOne$
570                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
571                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
572                 endif
573         endfor
574         # Just plain wrong tones
575         elsif currentToneWord$ <> choiceToneReference$
576         .recognitionText$ = .recognitionText$ + .choiceText$
577         for i from 1 to numberOfFeedbackRows
578                 select Table ToneFeedback
579                 .toneOne$ = Get value... 'i' T1
580                 .toneTwo$ = Get value... 'i' T2
581                 .toneText$ = Get value... 'i' Feedback
583                 if rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
584                 .feedbackText$ = .toneText$
585                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'[a-zA-Z]+'.toneTwo$'$") > 0
586                 .feedbackText$ = .toneText$
587                 elsif .toneOne$ = "Wrong"
588                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
589                                 .label$ = .toneOne$
590                 endif
591         endfor
592         # Correct
593         else
594         .recognitionText$ = .recognitionText$ + .choiceText$
595         for i from 1 to numberOfFeedbackRows
596                 select Table ToneFeedback
597                 .toneOne$ = Get value... 'i' T1
598                 .toneTwo$ = Get value... 'i' T2
599                 .toneText$ = Get value... 'i' Feedback
601                 if .toneOne$ = "Correct"
602                 .feedbackText$ = .toneText$
603                                 .label$ = .toneOne$
604                 endif
605         endfor
606         endif
608         label END
610         # Write out result
611         Create Table with column names... Feedback 3 Text
612         Set string value... 1 Text '.recognitionText$'
613         Set string value... 2 Text '.feedbackText$'
614         Set string value... 3 Text '.label$'
616         # Clean up
617         select Table ToneFeedback
618         Remove
620         # Show pitch tracks
621     freqTop = 1.5 * sgc_ToneProt.upperRegisterInput
623         # Replace recorded sound with new sound
624         if not fileReadable(sgc_ToneProt.currentSound$)
625         select Sound 'sgc_ToneProt.currentSound$'
626                 Remove
627                 select Sound Source
628         Copy... 'sgc_ToneProt.currentSound$'
629         endif
632         # Clean up
633         select Sound Source
634         plus Pitch 'sgc_ToneProt.currentTestWord$'
635         Remove
636 endproc
639 # Use ~0.03 cutoff
640 procedure createJitterShimmerContour .sound
641         select .sound
642         .duration = Get total duration
643         .jitshimSound = Create Sound: "JitterShimmer", 0, .duration, 100, "0"
644         
645         # Create pointprocess
646         select .sound
647         .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
648         
649         .dT = 0.01
650         .w_half = 0.075/2
651         .t = 0.0
652         .sampleNum = 1
653         while .t < .duration - .dT
654                 select .pointProc
655                 .jitter = Get jitter (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3
656                 if .jitter = undefined
657                         .jitter = 0
658                 endif
659                 select .sound
660                 plus .pointProc
661                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
662                 if .shimmer = undefined
663                         .shimmer = 0
664                 endif
665                 select .jitshimSound
666                 Set value at sample number: 0, .sampleNum, 10 * .jitter * .shimmer
667         
668                 .t += .dT
669                 .sampleNum += 1
670         endwhile
671         
672         select .pointProc
673         Remove
674         
675         select .jitshimSound
676 endproc
678 procedure createShimmerCPPcontour .sound
679         call createCPPContour .sound
680         .cpp = selected("Sound")
681         
682         select .sound
683         .duration = Get total duration
684         .shimmerCPPSound = Create Sound: "ShimmerCPP", 0, .duration, 100, "0"
685         
686         # Create pointprocess
687         select .sound
688         .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
689         
690         .dT = 0.01
691         .w_half = 0.075/2
692         .t = 0.0
693         .sampleNum = 1
694         while .t < .duration - .dT
695                 select .sound
696                 plus .pointProc
697                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
698                 if .shimmer = undefined
699                         .shimmer = 0
700                 endif
701                 select .cpp
702                 .cppValue = Get value at time: 0, .t, "Sinc70"
703                 if .cppValue = undefined or .cppValue < 1
704                         .cppValue = 1
705                 endif
706                 select .shimmerCPPSound
707                 Set value at sample number: 0, .sampleNum, 10* .shimmer / .cppValue
708         
709                 .t += .dT
710                 .sampleNum += 1
711         endwhile
712         
713         select .pointProc
714         plus .cpp
715         Remove
716         
717         select .shimmerCPPSound
718         
719 endproc
721 # Use ~? cutoff
722 procedure createShimmerContour .sound
723         select .sound
724         .duration = Get total duration
725         .shimmerSound = Create Sound: "Shimmer", 0, .duration, 100, "0"
726         
727         # Create pointprocess
728         select .sound
729         .pointProc = To PointProcess (periodic, cc): 60, 350
730         
731         .dT = 0.01
732         .w_half = 0.075/2
733         .t = 0.0
734         .sampleNum = 1
735         while .t < .duration - .dT
736                 select .sound
737                 plus .pointProc
738                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
739                 if .shimmer = undefined
740                         .shimmer = 0
741                 endif
742                 select .shimmerSound
743                 Set value at sample number: 0, .sampleNum, .shimmer
744         
745                 .t += .dT
746                 .sampleNum += 1
747         endwhile
748         
749         select .pointProc
750         Remove
751         
752         select .shimmerSound
753 endproc
756 # Use ~? cutoff
757 procedure createCPPContour .sound
758         select .sound
759         .dT = 0.01
760         
761         # Create pointprocess
762         select .sound
763         .duration = Get total duration
764         .pcg = To PowerCepstrogram: 60, .dT, 5000, 50
765         .cppTable = To Table (peak prominence): 60, 330, 0.05, "Parabolic", 0.001, 0, "Exponential decay", "Robust"
766         .nRows = Get number of rows
767         .lastTime = Get value: .nRows, "time"
768         .lastTime += .dT
769         while .lastTime < .duration - .dT
770                 Append row
771                 .nRows += 1
772                 Set numeric value: .nRows, "cpp", 0
773                 .lastTime += .dT
774         endwhile
775         .firstTime = Get value: 1, "time"
776         .firstTime -= .dT
777         while .firstTime >= .dT/2
778                 Insert row: 1
779                 Set numeric value: 1, "cpp", 0
780                 .firstTime -= .dT
781         endwhile
782         Remove column: "quefrency"
783         Remove column: "f0"
784         Remove column: "rnr"
785         Remove column: "time"
786         .cppTable2 = Transpose
787         .cppMatrix = Down to Matrix
788         .cppSound = To Sound (slice): 1
789         Override sampling frequency: 1/.dT
790         Shift times to: "start time", 0
791         Rename: "CPP"
792         
793         select .pcg
794         plus .cppTable
795         plus .cppTable2
796         plus .cppMatrix
797         Remove
798         
799         select .cppSound
800 endproc