New attempt to creacky voice detection in third tones
[sgc2.git] / ToneProt / SGC_ToneProt.praat
blob4baac9637384f1268fac34f6470d4b6a0fe15202
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                 .creakCO = 0.025
143                 .delta = 0.0000001
144                 
145                 # Lowest point for tone 3 is 1 octave and 3st below the top line
146                 # The cut-off is 0,05 quantile of model + 1/2 semitones
147                 select te.recordedPitch
148                 .recordedPitchTier = Down to PitchTier
149                 
150                 # Calculate Shimmer&Jitter
151                 select Sound Source
152                 .sound = selected("Sound")
153                 call createJitterShimmerContour .sound
154                 .jitterShimmer = selected("Sound")
155                 
156                 # Determine the low F0 part of the 3rd tone
157                 # Generate word with voiceless low 3rd tone
158                 .newPinyin$ = replace$(sgc_ToneProt.pinyin$, "3", "9", 0)
159                 call generateWord Pitch '.newPinyin$' 'sgc_ToneProt.register'
160                 select Pitch '.newPinyin$'
161                 .generatedWord9 = selected("Pitch")
162                 
163                 select Pitch 'sgc_ToneProt.pinyin$'
164                 .generatedWord3 = selected("Pitch")
165                 
166                 # Create a tier with low part of the third tone as intervals
167                 call generateWord TextGrid 'sgc_ToneProt.pinyin$' 'sgc_ToneProt.register'
168                 select TextGrid 'sgc_ToneProt.pinyin$'
169                 .pitchTextGrid = selected("TextGrid")
170                 .lowPresent = Count intervals where: 1, "is equal to", "3"
171                                 
172                 # DTW of .generatedWord9 with sourcePitch
173                 if .lowPresent > 0
174                         select te.recordedPitch
175                         plus .generatedWord3
176                         .dtw3 = noprogress To DTW... 24 10 yes yes no restriction
177                         Rename... DTW'sgc_ToneProt.pinyin$'
178                         .distance3 = Get distance (weighted)
179                         select te.recordedPitch
180                         plus .generatedWord9
181                         .dtw9 = To DTW... 24 10 yes yes no restriction
182                         Rename... DTW'.newPinyin$'
183                         .distance9 = Get distance (weighted)
184                         
185                         select .pitchTextGrid
186                         if .distance3 <= .distance9
187                                 plus .dtw3
188                         else
189                                 plus .dtw9
190                         endif
191                         .origTextGrid = To TextGrid (warp times)
192                         .numPitchIntervals = Get number of intervals: 1
193                         for .i to .numPitchIntervals
194                                 select .origTextGrid
195                                 .label$ = Get label of interval: 1, .i
196                                 if .label$ = "3"
197                                         select .origTextGrid
198                                         .threeStart = Get starting point: 1, .i
199                                         .threeEnd = Get end point: 1, .i
200                                         .lowStart = .threeStart + (.threeEnd - .threeStart)/3
201                                         .lowEnd = .threeStart + 5*(.threeEnd - .threeStart)/6
202                                         select .jitterShimmer
203                                         .mean = Get mean: 0, .lowStart, .lowEnd
204                                         if .mean > .creakCO
205                                                 toneProt.creackyThree += 1;
206                                 
207                                                 # Replace creacky voice with low pitch in PitchTier
208                                                 # Use minimumModelFzero
209                                                 select .recordedPitchTier
210                                                 # Store begin and endvalues of pitch
211                                                 .startValue = Get value at time: .lowStart
212                                                 .endValue = Get value at time: .lowEnd
213                                                 
214                                                 # Remove all pitch points between start and end
215                                                 Remove points between: .lowStart, .lowEnd
216                                                 
217                                                 # Add begin and en values at start and end times
218                                                 Add point: .lowStart, .startValue
219                                                 Add point: .lowEnd, .endValue
220                                                 
221                                                 # Interpolate or add low pitch values in between start and end times if the interval is long
222                                                 Add point: .lowStart + .delta, minimumModelFzero
223                                                 Add point: .lowEnd - .delta, minimumModelFzero
224                                         endif
225                                 endif
226                         endfor
227                         select .dtw3
228                         plus .dtw9
229                         Remove
230                         
231                         # Weigh creacky voice by number of syllables
232                         toneProt.creackyThree /= toneScript.syllableCount
233                 endif
234                 
235                 # If creacky voice was detected, create new Pitch from adapted PitchTier
236                 if toneProt.creackyThree
237                         # Convert new Pitchtier to Pitch object (based on old Pitch object)
238                         select te.recordedPitch
239                         plus .recordedPitchTier
240                         .newPitch = To Pitch
241                         # Move new Pitch object to old Pitch object ID
242                         select te.recordedPitch
243                         .pitchName$ = selected$("Pitch")
244                         Remove
245                         te.recordedPitch = .newPitch
246                         select te.recordedPitch
247                         Rename: .pitchName$
248                         .newPitch = 0
249                 endif
250 #pause
251                 # Clean up
252                 select .jitterShimmer
253                 plus .pitchTextGrid
254                 plus .recordedPitchTier
255                 plus .generatedWord9
256                 Remove
257         endif
259         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
260         select te.recordedPitch
261         upperCutOff = 1.7*sgc_ToneProt.upperRegisterInput
262         lowerCutOff = sgc_ToneProt.upperRegisterInput/4
263         Formula... if self > 'upperCutOff' then -1 else self endif
264         Formula... if self < 'lowerCutOff' then -1 else self endif
266         # Get range and top
267         select te.recordedPitch
268         maximumRecFzero = Get quantile... 0 0 0.95 Hertz
269         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
270         minimumRecFzero = Get quantile... 0 0 0.05 Hertz
271         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
272         if maximumRecFzero = undefined
273         # Determine what should be told to the student
274         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': ???"
275         for i from 1 to numberOfFeedbackRows
276                 select Table ToneFeedback
277                 .toneOne$ = Get value... 'i' T1
278                 .toneTwo$ = Get value... 'i' T2
279                 .toneText$ = Get value... 'i' Feedback
280                         .label$ = "Unknown"
282                 if .toneOne$ = "NoSound"
283                 .feedbackText$ = .toneText$
284                 endif
285         endfor
287         #exit Error, nothing recorded
288                 goto END
289         endif
290         recPitchRange = 2
291         if minimumRecFzero > 0
292            recPitchRange = maximumRecFzero / minimumRecFzero
293         endif
294         sgc_ToneProt.newUpperRegister = maximumRecFzero / maximumModelFzero * sgc_ToneProt.upperRegisterInput
295         sgc_ToneProt.newToneRange = recPitchRange / sgc_ToneProt.modelPitchRange
296         if sgc_ToneProt.newUpperRegister = undefined
297                 sgc_ToneProt.newUpperRegister = sgc_ToneProt.upperRegisterInput
298         endif
299         if sgc_ToneProt.newToneRange = undefined
300                 sgc_ToneProt.newToneRange = 1
301         endif
303         sgc_ToneProt.registerUsed$ = "OK"
304         rangeUsed$ = "OK"
305         # Advanced speakers must not speak too High, or too "Dramatic"
306         # Beginning speakers also not too Low or too Narrow ranges
307         if sgc_ToneProt.newUpperRegister > highBoundaryFactor * sgc_ToneProt.upperRegisterInput
308            sgc_ToneProt.newUpperRegister = highBoundaryFactor * sgc_ToneProt.upperRegisterInput
309            sgc_ToneProt.registerUsed$ = "High"
310         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newUpperRegister < lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
311            sgc_ToneProt.newUpperRegister = lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
312            sgc_ToneProt.registerUsed$ = "Low"
313         endif
314         
315         if sgc_ToneProt.newToneRange > highBoundaryFactor
316            sgc_ToneProt.newToneRange = highBoundaryFactor
317            rangeUsed$ = "Wide"
318         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newToneRange < lowBoundaryFactor and not sgc_ToneProt.proficiency
319                 # Don't do this for advanced speakers
320            sgc_ToneProt.newToneRange = lowBoundaryFactor
321            rangeUsed$ = "Narrow"
322         endif
324         # Duration 
325         if sgc_ToneProt.durationModel > spacing
326            speedFactor = (durationSource - spacing) / (sgc_ToneProt.durationModel - spacing)
327         endif
329         # Round values
330         sgc_ToneProt.newUpperRegister = round(sgc_ToneProt.newUpperRegister)
332         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
333         select te.recordedPitch
334         upperCutOff = 1.5*sgc_ToneProt.newUpperRegister
335         lowerCutOff = sgc_ToneProt.newUpperRegister/3
336         Formula... if self > 'upperCutOff' then -1 else self endif
337         Formula... if self < 'lowerCutOff' then -1 else self endif
339         # It is rather dangerous to kill Octave errors, so be careful
340         if killOctaveJumps > 0
341                 select te.recordedPitch
342         Rename... OldSourcePitch
343         Kill octave jumps
344         Rename... SourcePitch
345         te.recordedPitch = selected("Pitch")
346         select Pitch OldSourcePitch
347         Remove
348         endif
350         # It is good to have the lowest and highest pitch frequencies
351         select te.recordedPitch
352         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
353         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
355         # Clean up the old example pitch
356         select Pitch 'sgc_ToneProt.currentTestWord$'
357         Remove
359         # Do the tone recognition
360         .numSyllables = toneScript.syllableCount
361         sgc_ToneProt.choiceReference$ = sgc_ToneProt.currentTestWord$
362         .skipSyllables = 0
363         while sgc_ToneProt.choiceReference$ = sgc_ToneProt.currentTestWord$ and .skipSyllables < .numSyllables
364                 call FreeToneRecognition 'sgc_ToneProt.choiceReference$' "REUSEPITCH" "" 'sgc_ToneProt.newUpperRegister' 'sgc_ToneProt.newToneRange' 'speedFactor' '.skipSyllables'
365                 .skipSyllables += 1
366         endwhile
367         call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 'sgc_ToneProt.newToneRange' 'speedFactor' CorrectPitch
368         # Special cases
369         originalRecognizedWord$ = sgc_ToneProt.choiceReference$
370         if  sgc_ToneProt.ultraStrict = 0
371         # [23]3 is often misidentified as 23, 20 or 30
372         if rindex_regex(sgc_ToneProt.currentTestWord$, "[23][^0-9]+3") > 0
373                 if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+3") > 0
374                                 .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+3") - 1
375                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23][^0-9]+[023]") > 0
376                         sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23]([^0-9]+)[023]", "\13\23", 1)
377                         endif
378                 endif
379                 if rindex_regex(sgc_ToneProt.currentTestWord$, "2[^0-9]+3") > 0
380                                 .c = rindex_regex(sgc_ToneProt.currentTestWord$, "2[^0-9]+3") - 1
381                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})[23][^0-9]+[023]") > 0
382                         sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "([^0-9]+)[23]([^0-9]+)[023]", "\12\23", 1)
383                         endif
384                 endif
385             endif
386             
387         # First syllable: 2<->3 exchanges
388         if rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+2") > 0
389                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+3") > 0
390                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^([^0-9]+)[36]", "\12", 0)
391                 endif
392         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+3") > 0
393                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+2") > 0
394                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^([^0-9]+)2", "\13", 0)
395                 endif
396         # A single second tone is often misidentified as a neutral tone, 
397         # A real neutral tone would be too low or too narrow and be discarded
398         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+2$") > 0
399                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+0$") > 0 and timeMinimum < timeMaximum
400                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "2", 0)
401                 endif
402         # A single fourth tone is often misidentified as a neutral tone, 
403         # A real neutral tone would be too low or too narrow and be discarded
404         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[^0-9]+4$") > 0
405                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[^0-9]+0$") > 0 and timeMaximum < timeMinimum
406                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "4", 0)
407                 endif
408         endif
410         # 32 <-> 30 Sometimes this is recognized as 00
411         # A recognized 0/2 after a 3 can be a 2/0
412         if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+2") > 0
413                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+2") - 1
414                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+0") > 0
415                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)0", "\12", 0)
416                         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})0[^0-9]+0") > 0
417                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})0([^0-9]+)0", "\13\22", 0)
418                 endif
419         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+0") > 0
420                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+0") - 1
421                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+2") > 0
422                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)2", "\10", 0)
423                         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})0[^0-9]+0") > 0
424                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'})0([^0-9]+)0", "\13\20", 0)
425                         endif
426         endif
427         
428         # 31 <-> 30 
429         # A recognized 0 after a 3 can be a 1
430         if rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+1") > 0
431                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "3[^0-9]+1") - 1
432                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})3[^0-9]+0") > 0
433                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}3[^0-9]+)0", "\11", 0)
434                 endif
435                 endif
436         
437         # 40 <-> 42
438         # A recognized 0 after a 4 can be a 2: 4-0 => 4-2
439         if rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+2") > 0
440                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+2") - 1
441                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})4[^0-9]+0") > 0
442                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}4[^0-9]+)0", "\12", 0)
443                 endif
444         endif
445         
446                 # 404 <-> 414
447         # A recognized 0 between two tones 4 can be a 1
448         if rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+1[^0-9]+4") > 0
449                         .c = rindex_regex(sgc_ToneProt.currentTestWord$, "4[^0-9]+1[^0-9]+4") - 1
450                         if rindex_regex(sgc_ToneProt.choiceReference$, "^(.{'.c'})4[^0-9]+0[^0-9]+4") > 0
451                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "^(.{'.c'}4[^0-9]+)0([^0-9]+4)", "\11\2", 0)
452                 endif
453         endif
454         
455         endif
456         
457         # If wrong, then undo all changes
458         if sgc_ToneProt.currentTestWord$ != sgc_ToneProt.choiceReference$
459         sgc_ToneProt.choiceReference$ = originalRecognizedWord$
460         endif
462         sgc_ToneProt.toneChoiceReference$ = sgc_ToneProt.choiceReference$
464         ###############################################
465         #
466         # Report
467         #
468         ###############################################
469         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$'"
470         if sgc_ToneProt.currentTestWord$ = sgc_ToneProt.toneChoiceReference$
471            result$ = "Correct:"+result$
472         else
473            result$ = "Wrong:"+result$
474         endif
476         # Initialize result texts
477         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': "
478         .choiceText$ = replace_regex$(sgc_ToneProt.choiceReference$, "6", "\?", 0)
479         .feedbackText$ = "----"
481         # Separate tone from pronunciation errors
482         currentToneWord$ = replace_regex$(sgc_ToneProt.currentTestWord$, "[a-z]+", "\*", 0)
483         choiceToneReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[a-z]+", "\*", 0)
485         # Determine what should be told to the student
486         if sgc_ToneProt.registerUsed$ = "Low"
487         .recognitionText$ = .recognitionText$ + "???"
488         for i from 1 to numberOfFeedbackRows
489                 select Table ToneFeedback
490                 .toneOne$ = Get value... 'i' T1
491                 .toneTwo$ = Get value... 'i' T2
492                 .toneText$ = Get value... 'i' Feedback
494                 if .toneOne$ = "Low"
495                 .feedbackText$ = .toneText$
496                                 .label$ = .toneOne$
497                 endif
498         endfor
499         elsif rangeUsed$ = "Narrow"
500         .recognitionText$ = .recognitionText$ + "???"
501         for i from 1 to numberOfFeedbackRows
502                 select Table ToneFeedback
503                 .toneOne$ = Get value... 'i' T1
504                 .toneTwo$ = Get value... 'i' T2
505                 .toneText$ = Get value... 'i' Feedback
507                 if .toneOne$ = "Narrow"
508                 .feedbackText$ = .toneText$
509                                 .label$ = .toneOne$
510                 endif
511         endfor
512         elsif sgc_ToneProt.registerUsed$ = "High"
513         .recognitionText$ = .recognitionText$ + .choiceText$
514         for i from 1 to numberOfFeedbackRows
515                 select Table ToneFeedback
516                 .toneOne$ = Get value... 'i' T1
517                 .toneTwo$ = Get value... 'i' T2
518                 .toneText$ = Get value... 'i' Feedback
520                 if .toneOne$ = "High"
521                 .feedbackText$ = .toneText$
522                                 .label$ = .toneOne$
523                 endif
524         endfor
525         elsif rangeUsed$ = "Wide"
526         .recognitionText$ = .recognitionText$ + .choiceText$
527         for i from 1 to numberOfFeedbackRows
528                 select Table ToneFeedback
529                 .toneOne$ = Get value... 'i' T1
530                 .toneTwo$ = Get value... 'i' T2
531                 .toneText$ = Get value... 'i' Feedback
533                 if .toneOne$ = "Wide"
534                 .feedbackText$ = .toneText$
535                                 .label$ = .toneOne$
536                 endif
537         endfor
538         # Bad tones, first handle first syllable
539         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+6") > 0
540         .recognitionText$ = .recognitionText$ + .choiceText$
541         # First syllable
542         for i from 1 to numberOfFeedbackRows
543                 select Table ToneFeedback
544                 .toneOne$ = Get value... 'i' T1
545                 .toneTwo$ = Get value... 'i' T2
546                 .toneText$ = Get value... 'i' Feedback
548                 # 
549                 .feedbackText$ = ""
550                 if .toneOne$ = "6"
551                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
552                                 .label$ = .toneOne$
553                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'") > 0 and .toneTwo$ = "-"
554                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
555                 endif
556         endfor
557         # Bad tones, then handle second syllable
558         elsif rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+6$") > 0
559         .recognitionText$ = .recognitionText$ + .choiceText$
560         # Last syllable
561         for i from 1 to numberOfFeedbackRows
562                 select Table ToneFeedback
563                 .toneOne$ = Get value... 'i' T1
564                 .toneTwo$ = Get value... 'i' T2
565                 .toneText$ = Get value... 'i' Feedback
567                 # 
568                 .feedbackText$ = ""
569                 if .toneOne$ = "6"
570                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
571                                 .label$ = .toneOne$
572                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
573                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
574                 endif
575         endfor
576         # Just plain wrong tones
577         elsif currentToneWord$ <> choiceToneReference$
578         .recognitionText$ = .recognitionText$ + .choiceText$
579         for i from 1 to numberOfFeedbackRows
580                 select Table ToneFeedback
581                 .toneOne$ = Get value... 'i' T1
582                 .toneTwo$ = Get value... 'i' T2
583                 .toneText$ = Get value... 'i' Feedback
585                 if rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
586                 .feedbackText$ = .toneText$
587                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'[a-zA-Z]+'.toneTwo$'$") > 0
588                 .feedbackText$ = .toneText$
589                 elsif .toneOne$ = "Wrong"
590                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
591                                 .label$ = .toneOne$
592                 endif
593         endfor
594         # Correct
595         else
596         .recognitionText$ = .recognitionText$ + .choiceText$
597         for i from 1 to numberOfFeedbackRows
598                 select Table ToneFeedback
599                 .toneOne$ = Get value... 'i' T1
600                 .toneTwo$ = Get value... 'i' T2
601                 .toneText$ = Get value... 'i' Feedback
603                 if .toneOne$ = "Correct"
604                 .feedbackText$ = .toneText$
605                                 .label$ = .toneOne$
606                 endif
607         endfor
608         endif
610         label END
612         # Write out result
613         Create Table with column names... Feedback 3 Text
614         Set string value... 1 Text '.recognitionText$'
615         Set string value... 2 Text '.feedbackText$'
616         Set string value... 3 Text '.label$'
618         # Clean up
619         select Table ToneFeedback
620         Remove
622         # Show pitch tracks
623     freqTop = 1.5 * sgc_ToneProt.upperRegisterInput
625         # Replace recorded sound with new sound
626         if not fileReadable(sgc_ToneProt.currentSound$)
627         select Sound 'sgc_ToneProt.currentSound$'
628                 Remove
629                 select Sound Source
630         Copy... 'sgc_ToneProt.currentSound$'
631         endif
634         # Clean up
635         select Sound Source
636         plus Pitch 'sgc_ToneProt.currentTestWord$'
637         Remove
638 endproc
641 # Use ~0.03 cutoff
642 procedure createJitterShimmerContour .sound
643         select .sound
644         .duration = Get total duration
645         .jitshimSound = Create Sound: "JitterShimmer", 0, .duration, 100, "0"
646         
647         # Create pointprocess
648         select .sound
649         .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
650         
651         .dT = 0.01
652         .w_half = 0.075/2
653         .t = 0.0
654         .sampleNum = 1
655         while .t < .duration - .dT
656                 select .pointProc
657                 .jitter = Get jitter (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3
658                 if .jitter = undefined
659                         .jitter = 0
660                 endif
661                 select .sound
662                 plus .pointProc
663                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
664                 if .shimmer = undefined
665                         .shimmer = 0
666                 endif
667                 select .jitshimSound
668                 Set value at sample number: 0, .sampleNum, 10 * .jitter * .shimmer
669         
670                 .t += .dT
671                 .sampleNum += 1
672         endwhile
673         
674         select .pointProc
675         Remove
676         
677         select .jitshimSound
678 endproc
680 procedure createShimmerCPPcontour .sound
681         call createCPPContour .sound
682         .cpp = selected("Sound")
683         
684         select .sound
685         .duration = Get total duration
686         .shimmerCPPSound = Create Sound: "ShimmerCPP", 0, .duration, 100, "0"
687         
688         # Create pointprocess
689         select .sound
690         .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
691         
692         .dT = 0.01
693         .w_half = 0.075/2
694         .t = 0.0
695         .sampleNum = 1
696         while .t < .duration - .dT
697                 select .sound
698                 plus .pointProc
699                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
700                 if .shimmer = undefined
701                         .shimmer = 0
702                 endif
703                 select .cpp
704                 .cppValue = Get value at time: 0, .t, "Sinc70"
705                 if .cppValue = undefined or .cppValue < 1
706                         .cppValue = 1
707                 endif
708                 select .shimmerCPPSound
709                 Set value at sample number: 0, .sampleNum, 10* .shimmer / .cppValue
710         
711                 .t += .dT
712                 .sampleNum += 1
713         endwhile
714         
715         select .pointProc
716         plus .cpp
717         Remove
718         
719         select .shimmerCPPSound
720         
721 endproc
723 # Use ~? cutoff
724 procedure createShimmerContour .sound
725         select .sound
726         .duration = Get total duration
727         .shimmerSound = Create Sound: "Shimmer", 0, .duration, 100, "0"
728         
729         # Create pointprocess
730         select .sound
731         .pointProc = To PointProcess (periodic, cc): 60, 350
732         
733         .dT = 0.01
734         .w_half = 0.075/2
735         .t = 0.0
736         .sampleNum = 1
737         while .t < .duration - .dT
738                 select .sound
739                 plus .pointProc
740                 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
741                 if .shimmer = undefined
742                         .shimmer = 0
743                 endif
744                 select .shimmerSound
745                 Set value at sample number: 0, .sampleNum, .shimmer
746         
747                 .t += .dT
748                 .sampleNum += 1
749         endwhile
750         
751         select .pointProc
752         Remove
753         
754         select .shimmerSound
755 endproc
758 # Use ~? cutoff
759 procedure createCPPContour .sound
760         select .sound
761         .dT = 0.01
762         
763         # Create pointprocess
764         select .sound
765         .duration = Get total duration
766         .pcg = To PowerCepstrogram: 60, .dT, 5000, 50
767         .cppTable = To Table (peak prominence): 60, 330, 0.05, "Parabolic", 0.001, 0, "Exponential decay", "Robust"
768         .nRows = Get number of rows
769         .lastTime = Get value: .nRows, "time"
770         .lastTime += .dT
771         while .lastTime < .duration - .dT
772                 Append row
773                 .nRows += 1
774                 Set numeric value: .nRows, "cpp", 0
775                 .lastTime += .dT
776         endwhile
777         .firstTime = Get value: 1, "time"
778         .firstTime -= .dT
779         while .firstTime >= .dT/2
780                 Insert row: 1
781                 Set numeric value: 1, "cpp", 0
782                 .firstTime -= .dT
783         endwhile
784         Remove column: "quefrency"
785         Remove column: "f0"
786         Remove column: "rnr"
787         Remove column: "time"
788         .cppTable2 = Transpose
789         .cppMatrix = Down to Matrix
790         .cppSound = To Sound (slice): 1
791         Override sampling frequency: 1/.dT
792         Shift times to: "start time", 0
793         Rename: "CPP"
794         
795         select .pcg
796         plus .cppTable
797         plus .cppTable2
798         plus .cppMatrix
799         Remove
800         
801         select .cppSound
802 endproc