3 # SpeakGoodChinese: SGC_ToneRecognizer.praat processes student utterances
4 # and generates a report on their tone production
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
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.
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.
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
26 # include ToneRecognition.praat
27 # include ToneScript.praat
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
38 # Stick to the raw recognition results or not
39 sgc_ToneProt.ultraStrict = sgc_ToneProt.proficiency
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$'
46 call loadTable ToneFeedback_EN
48 Rename... ToneFeedback
49 numberOfFeedbackRows = Get number of rows
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$
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)
65 # Kill octave jumps: DANGEROUS
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
78 sgc_ToneProt.minimumPitch = 40
79 sgc_ToneProt.maximumPitch = 400
82 sgc_ToneProt.currentTestWord$ = sgc_ToneProt.pinyin$
84 .interPrecision = sgc_ToneProt.precision;
86 # Allow more "room" when there is a 3rd tone
87 if index(sgc_ToneProt.pinyin$, "3")
88 .interPrecision *= 4/3
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
98 sgc_ToneProt.upperRegisterInput = sgc_ToneProt.register
99 call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 1 1 CorrectPitch
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
108 if minimumModelFzero = undefined
109 minimumModelFzero = 0
111 sgc_ToneProt.modelPitchRange = 2
112 if minimumModelFzero > 0
113 sgc_ToneProt.modelPitchRange = maximumModelFzero / minimumModelFzero
115 sgc_ToneProt.modelPitchRange = 0
119 if fileReadable(sgc_ToneProt.currentSound$)
120 Read from file... 'sgc_ToneProt.currentSound$'
123 select Sound 'sgc_ToneProt.currentSound$'
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
135 ################################################################
137 # If there is a third tone, replace creaky voice by low pitch
139 ################################################################
140 toneProt.creackyThree = 0
141 if index(sgc_ToneProt.pinyin$, "3")
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
150 # Calculate Shimmer&Jitter
152 .sound = selected("Sound")
153 call createJitterShimmerContour .sound
154 .jitterShimmer = selected("Sound")
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")
163 select Pitch 'sgc_ToneProt.pinyin$'
164 .generatedWord3 = selected("Pitch")
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"
172 # DTW of .generatedWord9 with sourcePitch
174 select te.recordedPitch
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
181 .dtw9 = To DTW... 24 10 yes yes no restriction
182 Rename... DTW'.newPinyin$'
183 .distance9 = Get distance (weighted)
185 select .pitchTextGrid
186 if .distance3 <= .distance9
191 .origTextGrid = To TextGrid (warp times)
192 .numPitchIntervals = Get number of intervals: 1
193 for .i to .numPitchIntervals
195 .label$ = Get label of interval: 1, .i
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
205 toneProt.creackyThree += 1;
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
214 # Remove all pitch points between start and end
215 Remove points between: .lowStart, .lowEnd
217 # Add begin and en values at start and end times
218 Add point: .lowStart, .startValue
219 Add point: .lowEnd, .endValue
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
231 # Weigh creacky voice by number of syllables
232 toneProt.creackyThree /= toneScript.syllableCount
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
241 # Move new Pitch object to old Pitch object ID
242 select te.recordedPitch
243 .pitchName$ = selected$("Pitch")
245 te.recordedPitch = .newPitch
246 select te.recordedPitch
252 select .jitterShimmer
254 plus .recordedPitchTier
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
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
282 if .toneOne$ = "NoSound"
283 .feedbackText$ = .toneText$
287 #exit Error, nothing recorded
291 if minimumRecFzero > 0
292 recPitchRange = maximumRecFzero / minimumRecFzero
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
299 if sgc_ToneProt.newToneRange = undefined
300 sgc_ToneProt.newToneRange = 1
303 sgc_ToneProt.registerUsed$ = "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"
315 if sgc_ToneProt.newToneRange > highBoundaryFactor
316 sgc_ToneProt.newToneRange = highBoundaryFactor
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"
325 if sgc_ToneProt.durationModel > spacing
326 speedFactor = (durationSource - spacing) / (sgc_ToneProt.durationModel - spacing)
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
344 Rename... SourcePitch
345 te.recordedPitch = selected("Pitch")
346 select Pitch OldSourcePitch
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$'
359 # Do the tone recognition
360 .numSyllables = toneScript.syllableCount
361 sgc_ToneProt.choiceReference$ = sgc_ToneProt.currentTestWord$
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'
367 call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 'sgc_ToneProt.newToneRange' 'speedFactor' CorrectPitch
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
457 # If wrong, then undo all changes
458 if sgc_ToneProt.currentTestWord$ != sgc_ToneProt.choiceReference$
459 sgc_ToneProt.choiceReference$ = originalRecognizedWord$
462 sgc_ToneProt.toneChoiceReference$ = sgc_ToneProt.choiceReference$
464 ###############################################
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$
473 result$ = "Wrong:"+result$
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
495 .feedbackText$ = .toneText$
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$
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$
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$
538 # Bad tones, first handle first syllable
539 elsif rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+6") > 0
540 .recognitionText$ = .recognitionText$ + .choiceText$
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
551 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
553 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'") > 0 and .toneTwo$ = "-"
554 .feedbackText$ = .feedbackText$ + .toneText$ + " "
557 # Bad tones, then handle second syllable
558 elsif rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+6$") > 0
559 .recognitionText$ = .recognitionText$ + .choiceText$
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
570 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
572 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
573 .feedbackText$ = .feedbackText$ + .toneText$ + " "
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$')"
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$
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$'
619 select Table ToneFeedback
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$'
630 Copy... 'sgc_ToneProt.currentSound$'
636 plus Pitch 'sgc_ToneProt.currentTestWord$'
642 procedure createJitterShimmerContour .sound
644 .duration = Get total duration
645 .jitshimSound = Create Sound: "JitterShimmer", 0, .duration, 100, "0"
647 # Create pointprocess
649 .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
655 while .t < .duration - .dT
657 .jitter = Get jitter (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3
658 if .jitter = undefined
663 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
664 if .shimmer = undefined
668 Set value at sample number: 0, .sampleNum, 10 * .jitter * .shimmer
680 procedure createShimmerCPPcontour .sound
681 call createCPPContour .sound
682 .cpp = selected("Sound")
685 .duration = Get total duration
686 .shimmerCPPSound = Create Sound: "ShimmerCPP", 0, .duration, 100, "0"
688 # Create pointprocess
690 .pointProc = noprogress To PointProcess (periodic, cc): 60, 350
696 while .t < .duration - .dT
699 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
700 if .shimmer = undefined
704 .cppValue = Get value at time: 0, .t, "Sinc70"
705 if .cppValue = undefined or .cppValue < 1
708 select .shimmerCPPSound
709 Set value at sample number: 0, .sampleNum, 10* .shimmer / .cppValue
719 select .shimmerCPPSound
724 procedure createShimmerContour .sound
726 .duration = Get total duration
727 .shimmerSound = Create Sound: "Shimmer", 0, .duration, 100, "0"
729 # Create pointprocess
731 .pointProc = To PointProcess (periodic, cc): 60, 350
737 while .t < .duration - .dT
740 .shimmer = Get shimmer (local): .t-.w_half, .t+.w_half, 0.0001, 0.02, 1.3, 1.6
741 if .shimmer = undefined
745 Set value at sample number: 0, .sampleNum, .shimmer
759 procedure createCPPContour .sound
763 # Create pointprocess
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"
771 while .lastTime < .duration - .dT
774 Set numeric value: .nRows, "cpp", 0
777 .firstTime = Get value: 1, "time"
779 while .firstTime >= .dT/2
781 Set numeric value: 1, "cpp", 0
784 Remove column: "quefrency"
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