aligned Patch file with new Praat version
[sgc2.git] / ToneProt / SGC_ToneProt.praat
blob127fb9e6beabfe14610586754f632cc5b92c1330
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
41         
42         # Read and select the feedbacktext
43         call loadTable ToneFeedback_'sgc_ToneProt.language$'
44         Rename... ToneFeedback
45         numberOfFeedbackRows = Get number of rows
47         # Clean up input
48         if sgc_ToneProt.pinyin$ <> ""
49         sgc_ToneProt.pinyin$ = replace_regex$(sgc_ToneProt.pinyin$, "^\s*(.+)\s*$", "\1", 1)
50         sgc_ToneProt.pinyin$ = replace_regex$(sgc_ToneProt.pinyin$, "5", "0", 0)
51         endif
53         # Reduction (lower sgc_ToneProt.register and narrow range) means errors
54         # The oposite mostly not. Asymmetry alows more room upward
55         # than downward (asymmetry = 2 => highBoundaryFactor ^ 2)
56         asymmetry = 2
58         # Kill octave jumps: DANGEROUS
59         killOctaveJumps = 0
61         # Limit pitch range
62         sgc_ToneProt.minimumPitch = 60
63         sgc_ToneProt.maximumPitch = 500
64         if sgc_ToneProt.register > 400
65         sgc_ToneProt.minimumPitch = 100
66         sgc_ToneProt.maximumPitch = 600
67         elsif sgc_ToneProt.register > 250
68         sgc_ToneProt.minimumPitch = 80
69         sgc_ToneProt.maximumPitch = 500
70         else
71         sgc_ToneProt.minimumPitch = 60
72         sgc_ToneProt.maximumPitch = 400
73         endif
75         sgc_ToneProt.currentTestWord$ = sgc_ToneProt.pinyin$
76         spacing = 0.5
77         sgc_ToneProt.precisionFactor = 2^(sgc_ToneProt.precision/12)
78         highBoundaryFactor = sgc_ToneProt.precisionFactor ^ asymmetry
79         lowBoundaryFactor = 1/sgc_ToneProt.precisionFactor
81         # Generate reference example
82         # Start with a range of 1 octave and a speed factor of 1
83         toneRange = 1.0
84         speedFactor = 1.0
85         sgc_ToneProt.upperRegisterInput = sgc_ToneProt.register
86         call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 1 1 CorrectPitch
88         # Get range and top
89         select Pitch 'sgc_ToneProt.currentTestWord$'
90         sgc_ToneProt.durationModel = Get total duration
91         maximumModelFzero = Get quantile... 0 0 0.95 Hertz
92         minimumModelFzero = Get quantile... 0 0 0.05 Hertz
93         sgc_ToneProt.modelPitchRange = 2
94         if minimumModelFzero > 0
95         sgc_ToneProt.modelPitchRange = maximumModelFzero / minimumModelFzero
96         endif
98         # Get the sounds
99         if fileReadable(sgc_ToneProt.currentSound$)
100         Read from file... 'sgc_ToneProt.currentSound$'
101         Rename... Source
102         else
103         select Sound 'sgc_ToneProt.currentSound$'
104         Copy... Source
105         endif
107         # Calculate pitch
108         select Sound Source
109         durationSource = Get total duration
110         # noprogress To Pitch (ac)... 0 'sgc_ToneProt.minimumPitch' 15 yes 0.2 0.6 0.02 0.5 0.3 'sgc_ToneProt.maximumPitch'
111         noprogress To Pitch... 0.0 'sgc_ToneProt.minimumPitch' 'sgc_ToneProt.maximumPitch'
112         Rename... SourcePitch
114         # It is rather dangerous to kill Octave errors, so be careful
115         if killOctaveJumps > 0
116         Rename... OldSource
117         Kill octave jumps
118         Rename... SourcePitch
119         select Pitch OldSource
120         Remove
121         endif
123         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
124         select Pitch SourcePitch
125         upperCutOff = 1.7*sgc_ToneProt.upperRegisterInput
126         lowerCutOff = sgc_ToneProt.upperRegisterInput/4
127         Formula... if self > 'upperCutOff' then -1 else self endif
128         Formula... if self < 'lowerCutOff' then -1 else self endif
130         # Get range and top
131         select Pitch SourcePitch
132         maximumRecFzero = Get quantile... 0 0 0.95 Hertz
133         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
134         minimumRecFzero = Get quantile... 0 0 0.05 Hertz
135         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
136         if maximumRecFzero = undefined
137         # Determine what should be told to the student
138         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': ???"
139         for i from 1 to numberOfFeedbackRows
140                 select Table ToneFeedback
141                 .toneOne$ = Get value... 'i' T1
142                 .toneTwo$ = Get value... 'i' T2
143                 .toneText$ = Get value... 'i' Feedback
144                         .label$ = "Unknown"
146                 if .toneOne$ = "NoSound"
147                 .feedbackText$ = .toneText$
148                 endif
149         endfor
151         #exit Error, nothing recorded
152                 goto END
153         endif
154         recPitchRange = 2
155         if minimumRecFzero > 0
156            recPitchRange = maximumRecFzero / minimumRecFzero
157         endif
158         sgc_ToneProt.newUpperRegister = maximumRecFzero / maximumModelFzero * sgc_ToneProt.upperRegisterInput
159         sgc_ToneProt.newToneRange = recPitchRange / sgc_ToneProt.modelPitchRange
161         sgc_ToneProt.registerUsed$ = "OK"
162         rangeUsed$ = "OK"
163         # Advanced speakers must not speak too High, or too "Dramatic"
164         # Beginning speakers also not too Low or too Narrow ranges
165         if sgc_ToneProt.newUpperRegister > highBoundaryFactor * sgc_ToneProt.upperRegisterInput
166            sgc_ToneProt.newUpperRegister = highBoundaryFactor * sgc_ToneProt.upperRegisterInput
167            sgc_ToneProt.registerUsed$ = "High"
168         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newUpperRegister < lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
169            sgc_ToneProt.newUpperRegister = lowBoundaryFactor * sgc_ToneProt.upperRegisterInput
170            sgc_ToneProt.registerUsed$ = "Low"
171         endif
172         
173         if sgc_ToneProt.newToneRange > highBoundaryFactor
174            sgc_ToneProt.newToneRange = highBoundaryFactor
175            rangeUsed$ = "Wide"
176         elsif not sgc_ToneProt.proficiency and sgc_ToneProt.newToneRange < lowBoundaryFactor and not sgc_ToneProt.proficiency
177                 # Don't do this for advanced speakers
178            sgc_ToneProt.newToneRange = lowBoundaryFactor
179            rangeUsed$ = "Narrow"
180         endif
182         # Duration 
183         if sgc_ToneProt.durationModel > spacing
184            speedFactor = (durationSource - spacing) / (sgc_ToneProt.durationModel - spacing)
185         endif
187         # Round values
188         sgc_ToneProt.newUpperRegister = round(sgc_ToneProt.newUpperRegister)
190         # Remove all pitch points outside a band around the upper sgc_ToneProt.register
191         select Pitch SourcePitch
192         upperCutOff = 1.5*sgc_ToneProt.newUpperRegister
193         lowerCutOff = sgc_ToneProt.newUpperRegister/3
194         Formula... if self > 'upperCutOff' then -1 else self endif
195         Formula... if self < 'lowerCutOff' then -1 else self endif
197         if killOctaveJumps > 0
198         Rename... OldSourcePitch
199         Kill octave jumps
200         Rename... SourcePitch
201         select Pitch OldSourcePitch
202         Remove
203         endif
205         # It is good to have the lowest and highest pitch frequencies
206         select Pitch SourcePitch
207         timeMaximum = Get time of maximum... 0 0 Hertz Parabolic
208         timeMinimum = Get time of minimum... 0 0 Hertz Parabolic
210         # Clean up the old example pitch
211         select Pitch 'sgc_ToneProt.currentTestWord$'
212         Remove
214         # Do the tone recognition
215         call FreeToneRecognition 'sgc_ToneProt.currentTestWord$' "REUSEPITCH" "" 'sgc_ToneProt.newUpperRegister' 'sgc_ToneProt.newToneRange' 'speedFactor'
216         call toneScript 'sgc_ToneProt.currentTestWord$' 'sgc_ToneProt.upperRegisterInput' 'sgc_ToneProt.newToneRange' 'speedFactor' CorrectPitch
218         # Special cases
219         originalRecognizedWord$ = sgc_ToneProt.choiceReference$
220         if  sgc_ToneProt.ultraStrict = 0
221         # First syllable: 2<->3 (6) exchanges (incl 6)
222         if rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+2[a-zA-Z]+[0-4]$") > 0
223                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+[36][a-zA-Z]+[0-4]$") > 0
224                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[36]([a-zA-Z]+[0-4])$", "2\1", 0)
225                 endif
226         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+3[a-zA-Z]+[0-4]$") > 0
227                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+[26][a-zA-Z]+[0-4]$") > 0
228                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[26]([a-zA-Z]+[0-4])$", "3\1", 0)
229                 endif
230         # A single second tone is often misidentified as a neutral tone, 
231         # A real neutral tone would be too low or too narrow and be discarded
232         # Leaves us with erroneous tone 4
233         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+2$") > 0
234                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+0$") > 0 and timeMinimum < timeMaximum
235                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "2", 0)
236                 endif
237         # A single fourth tone is often misidentified as a neutral tone, 
238         # A real neutral tone would be too low or too narrow and be discarded
239         # Leaves us with erroneous tones 2 and 3
240         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+4$") > 0
241                 if rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+0$") > 0 and timeMaximum < timeMinimum
242                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "0", "4", 0)
243                 endif
244         endif
246         # Second (last) syllable, 0<->6 exchanges and 2<->3
247         # A recognized 0 after a 4 can be a 2: 4-0 => 4-2
248         if rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[4][a-zA-Z]+2$") > 0
249                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[4][a-zA-Z]+[0]$") > 0
250                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[0]$", "2", 0)
251                 endif
252         endif
253         # A final 6 after a valid tone is often a recognition error
254         # A final 6 can be a 0
255         if rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[0-9][a-zA-Z]+0$") > 0
256                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[0-4][a-zA-Z]+6$") > 0
257                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "6$", "0", 0)
258                 endif
259         # Second (last) syllable, 2<->3 exchanges after [23] tones
260         # A recognized 6 (or 3) after a valid tone [1-4] is mostly wrong, can be a 2
261         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[1-4][a-zA-Z]+2$") > 0
262                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[1-4][a-zA-Z]+[36]$") > 0
263                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[36]$", "2", 0)
264                 endif
265         # A recognized 6 after a [23] is mostly wrong, can be a 3
266         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[23][a-zA-Z]+3$") > 0
267                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[23][a-zA-Z]+[26]$") > 0
268                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[26]$", "3", 0)
269                 endif
270         # A recognized 6 after a [3] is mostly wrong, can be a 1
271         elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[3][a-zA-Z]+1$") > 0
272                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[3][a-zA-Z]+[6]$") > 0
273                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[6]$", "1", 0)
274                 endif
275         endif
277         # Clean up odd things constructed with special cases
278         # Target is 3-3, but recognized is 2-3, which is CORRECT. Change it into 3-3
279         if rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+[3][a-zA-Z]+[3]$") > 0
280                 if rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+[2][a-zA-Z]+[3]$") > 0
281                 sgc_ToneProt.choiceReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[2]([a-zA-Z]+[3])$", "3\1", 0)
282                 endif
283         endif
284         endif
286         # If wrong, then undo all changes
287         if sgc_ToneProt.currentTestWord$ != sgc_ToneProt.choiceReference$
288         sgc_ToneProt.choiceReference$ = originalRecognizedWord$
289         endif
291         sgc_ToneProt.toneChoiceReference$ = sgc_ToneProt.choiceReference$
293         ###############################################
294         #
295         # Report
296         #
297         ###############################################
298         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$'"
299         if sgc_ToneProt.currentTestWord$ = sgc_ToneProt.toneChoiceReference$
300            result$ = "Correct:"+result$
301         else
302            result$ = "Wrong:"+result$
303         endif
305         # Initialize result texts
306         .recognitionText$ =  "'sgc_ToneProt.currentTestWord$': "
307         .choiceText$ = replace_regex$(sgc_ToneProt.choiceReference$, "6", "\?", 0)
308         .feedbackText$ = "----"
310         # Separate tone from pronunciation errors
311         currentToneWord$ = replace_regex$(sgc_ToneProt.currentTestWord$, "[a-z]+", "\*", 0)
312         choiceToneReference$ = replace_regex$(sgc_ToneProt.choiceReference$, "[a-z]+", "\*", 0)
314         # Determine what should be told to the student
315         if sgc_ToneProt.registerUsed$ = "Low"
316         .recognitionText$ = .recognitionText$ + "???"
317         for i from 1 to numberOfFeedbackRows
318                 select Table ToneFeedback
319                 .toneOne$ = Get value... 'i' T1
320                 .toneTwo$ = Get value... 'i' T2
321                 .toneText$ = Get value... 'i' Feedback
323                 if .toneOne$ = "Low"
324                 .feedbackText$ = .toneText$
325                                 .label$ = .toneOne$
326                 endif
327         endfor
328         elsif rangeUsed$ = "Narrow"
329         .recognitionText$ = .recognitionText$ + "???"
330         for i from 1 to numberOfFeedbackRows
331                 select Table ToneFeedback
332                 .toneOne$ = Get value... 'i' T1
333                 .toneTwo$ = Get value... 'i' T2
334                 .toneText$ = Get value... 'i' Feedback
336                 if .toneOne$ = "Narrow"
337                 .feedbackText$ = .toneText$
338                                 .label$ = .toneOne$
339                 endif
340         endfor
341         elsif sgc_ToneProt.registerUsed$ = "High"
342         .recognitionText$ = .recognitionText$ + .choiceText$
343         for i from 1 to numberOfFeedbackRows
344                 select Table ToneFeedback
345                 .toneOne$ = Get value... 'i' T1
346                 .toneTwo$ = Get value... 'i' T2
347                 .toneText$ = Get value... 'i' Feedback
349                 if .toneOne$ = "High"
350                 .feedbackText$ = .toneText$
351                                 .label$ = .toneOne$
352                 endif
353         endfor
354         elsif rangeUsed$ = "Wide"
355         .recognitionText$ = .recognitionText$ + .choiceText$
356         for i from 1 to numberOfFeedbackRows
357                 select Table ToneFeedback
358                 .toneOne$ = Get value... 'i' T1
359                 .toneTwo$ = Get value... 'i' T2
360                 .toneText$ = Get value... 'i' Feedback
362                 if .toneOne$ = "Wide"
363                 .feedbackText$ = .toneText$
364                                 .label$ = .toneOne$
365                 endif
366         endfor
367         # Bad tones, first handle first syllable
368         elsif rindex_regex(sgc_ToneProt.choiceReference$, "^[a-zA-Z]+6") > 0
369         .recognitionText$ = .recognitionText$ + .choiceText$
370         # First syllable
371         for i from 1 to numberOfFeedbackRows
372                 select Table ToneFeedback
373                 .toneOne$ = Get value... 'i' T1
374                 .toneTwo$ = Get value... 'i' T2
375                 .toneText$ = Get value... 'i' Feedback
377                 # 
378                 .feedbackText$ = ""
379                 if .toneOne$ = "6"
380                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
381                                 .label$ = .toneOne$
382                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'") > 0 and .toneTwo$ = "-"
383                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
384                 endif
385         endfor
386         # Bad tones, then handle second syllable
387         elsif rindex_regex(sgc_ToneProt.choiceReference$, "[a-zA-Z]+6$") > 0
388         .recognitionText$ = .recognitionText$ + .choiceText$
389         # Last syllable
390         for i from 1 to numberOfFeedbackRows
391                 select Table ToneFeedback
392                 .toneOne$ = Get value... 'i' T1
393                 .toneTwo$ = Get value... 'i' T2
394                 .toneText$ = Get value... 'i' Feedback
396                 # 
397                 .feedbackText$ = ""
398                 if .toneOne$ = "6"
399                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
400                                 .label$ = .toneOne$
401                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
402                 .feedbackText$ = .feedbackText$ + .toneText$ + " "
403                 endif
404         endfor
405         # Just plain wrong tones
406         elsif currentToneWord$ <> choiceToneReference$
407         .recognitionText$ = .recognitionText$ + .choiceText$
408         for i from 1 to numberOfFeedbackRows
409                 select Table ToneFeedback
410                 .toneOne$ = Get value... 'i' T1
411                 .toneTwo$ = Get value... 'i' T2
412                 .toneText$ = Get value... 'i' Feedback
414                 if rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'$") > 0 and .toneTwo$ = "-"
415                 .feedbackText$ = .toneText$
416                 elsif rindex_regex(sgc_ToneProt.currentTestWord$, "^[a-zA-Z]+'.toneOne$'[a-zA-Z]+'.toneTwo$'$") > 0
417                 .feedbackText$ = .toneText$
418                 elsif .toneOne$ = "Wrong"
419                 .recognitionText$ = .recognitionText$ + " ('.toneText$')"
420                                 .label$ = .toneOne$
421                 endif
422         endfor
423         # Correct
424         else
425         .recognitionText$ = .recognitionText$ + .choiceText$
426         for i from 1 to numberOfFeedbackRows
427                 select Table ToneFeedback
428                 .toneOne$ = Get value... 'i' T1
429                 .toneTwo$ = Get value... 'i' T2
430                 .toneText$ = Get value... 'i' Feedback
432                 if .toneOne$ = "Correct"
433                 .feedbackText$ = .toneText$
434                                 .label$ = .toneOne$
435                 endif
436         endfor
437         endif
439         label END
441         # Write out result
442         Create Table with column names... Feedback 3 Text
443         Set string value... 1 Text '.recognitionText$'
444         Set string value... 2 Text '.feedbackText$'
445         Set string value... 3 Text '.label$'
447         # Clean up
448         select Table ToneFeedback
449         Remove
451         # Show pitch tracks
452     freqTop = 1.5 * sgc_ToneProt.upperRegisterInput
453         call reset_viewport
454     demo Paint rectangle... White 15 85 40 105
455         demo Select inner viewport... 20 80 40 100
456         demo Axes... 0 100 0 100
458     select Pitch SourcePitch
459     demo Red
460     demo Line width... 3
461     demo Draw... 0 0 0 'freqTop' 0
463     select Pitch 'sgc_ToneProt.currentTestWord$'
464     demo Green
465     demo Draw... 0 0 0 'freqTop' 0
467     demo Line width... 1
468     demo 12
470         call reset_viewport
472         # Replace recorded sound with new sound
473         if not fileReadable(sgc_ToneProt.currentSound$)
474         select Sound 'sgc_ToneProt.currentSound$'
475                 Remove
476                 select Sound Source
477         Copy... 'sgc_ToneProt.currentSound$'
478         endif
481         # Clean up
482         select Sound Source
483         plus Pitch SourcePitch
484         plus Pitch 'sgc_ToneProt.currentTestWord$'
485         Remove
486 endproc