-
Notifications
You must be signed in to change notification settings - Fork 1
/
FVR.py
executable file
·3252 lines (2916 loc) · 120 KB
/
FVR.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
import wx
import re
import subprocess
import os
import sys
import wave
import glob
import time
import platform
from os.path import isdir, isfile, join, basename, dirname
import numpy as np
if platform.system() == 'Darwin':
SENDPRAAT = join('bin','sendpraat_carbon')
OPEN = 'open'
elif platform.system() == 'Windows':
SENDPRAAT = join('bin','sendpraat_carbon')
OPEN = None
elif platform.system() == 'Linux':
if '32' in platform.architecture()[0]:
SENDPRAAT = join('bin','sendpraat_gtk32')
elif '64' in platform.architecture()[0]:
SENDPRAAT = join('bin','sendpraat_gtk64')
else:
raise Exception('Unable to determine system architecture (32 or 64 bit?)')
OPEN = 'xdg-open'
else:
raise Exception('Unable to determine system (Darwin, Windows, or Linux?)')
## list of cmu vowels sorted into columns (as displayed on the window)
cmu = [('IY', 'IH', 'EY', 'EH', 'AE'), ('', '','AH', '', ''), ('UW', 'UH', 'OW', 'AO', 'AA'), ('AY','OY', 'AW', 'IW', 'ER')]
## dictionary mapping cmu labels to colours
colourDict = {'IY':(238,155,50),
'IH':(139,69,26),
'EY':(141,51,139),
'EH':(235,95,67),
'AE':(216,191,216),
'AH':(127,242,5),
'UW':(120,120,120),
'UH':(233,101,178),
'OW':(79,192,251),
'AO':(233,63,51),
'AA':(122,247,212),
'AY':(90,180,113),
'OY':(65,105,225),
'AW':(107,142,35),
'IW':(140,34,27),
'ER':(127,242,5)}
def UpdateFAVE(filePath, outPath):
## converts layout of formant.txt files output from FAVE-extract
## so they can be read by FVR
delim = '\t'
headingsRow = 2
## read in files
files = glob.glob(join(filePath,'*formant.txt'))
## iterate over files
for fPath in files:
lines = []
with open(fPath) as f:
for i,line in enumerate(f):
if i < headingsRow: ## if the row is before the heading row
lines.append(line)
elif i == headingsRow: ## get location of relevant columns from the heading row
line = line.split(delim)
newLine = []
percInts = []
poleInt = None
for j,name in enumerate(line):
if 'F1@' in name:
name = 'F'+name[2:]
percInts.append(j)
elif 'F2@' in name:
continue
elif 'poles' in name:
name = delim.join(['F@Max3', 'F@Max4', 'F@Max5', 'F@Max6'])
poleInt = j
newLine.append(name)
lines.append(delim.join(newLine))
else: ## convert rows to new format
line = line.split(delim)
newLine = []
skip = False
for j,value in enumerate(line):
if skip:
skip = False
continue
if j in percInts:
value = ', '.join(line[j:j+2])
skip = True
elif poleInt != None and j == poleInt:
value = delim.join([', '.join(v.split(', ')[:2]) for v in value.strip('[').strip(']').split('],[')])
newLine.append(value)
lines.append(delim.join(newLine))
## write new file
with open(join(outPath, basename(fPath)), 'w') as out:
for l in lines:
out.write(l)
class VowelButton():
## class defining vowel instances on plot panel
## for speed reasons, this class does not inheret from any wx object (ie. staticbitmap)
## click events are handled in the plotpanel
def __init__(self,parent, lineInFile, cmuType, formants, word, stress,
timeRange, timePoint, maxFormant,
index, wav, infoFile, pronunciation = None, durationAlternates = [], maxFormantAlternates = [],
otherType = '', pitch = None, alternate = False, original = None):
self.parent = parent
self.line = lineInFile ## row in the file this vowel appears in (used to save changes back to the file later)
self.position = None
self.show = False
## define vowel values
self.cmuType = cmuType
self.f1 = formants[0]
self.f2 = formants[1]
self.word = word
self.stress = stress
self.min = timeRange[0]
self.max = timeRange[1]
self.duration = (self.max-self.min)
self.timePoint = timePoint
self.timePercentage = (self.timePoint-self.min)/self.duration
self.pronunciation = pronunciation
self.index = index
self.wav = wav
self.infoFile = infoFile
self.environment = (pronunciation[index-1] if index != 0 else '#') + ' v ' + (pronunciation[index+1] if index != len(pronunciation)-1 else '#') if pronunciation and index else ''
self.maxFormant = maxFormant
self.pitch = pitch
self.otherType = otherType.decode('utf8') if otherType else ''
self.durationAlternateValues = durationAlternates
self.maxFormantAlternateValues = maxFormantAlternates
## Set bitmaps
self.circleBitmap = self.parent.GetPreloadedBitmap(self.cmuType)
self.currentBitmap = self.circleBitmap
## remeasurement settings
self.alternates = []
self.original = original
## add values to appropriate dicts and sets
self.parent.AddVowelValues(self, self.f1, self.f2, self.word, self.duration, self.cmuType, self.otherType)
def __str__(self):
## used for displaying all relevant vowel info
otherLabel = self.parent.GetTopLevelParent().GetOtherLabel()
return 'CMU\t'+ self.cmuType +'\n'+\
(otherLabel+'\t' if otherLabel else 'OTHER\t')+ self.otherType +'\n'+\
'F1\t' + str(self.f1) +'\n'+\
'F2\t' + str(self.f2) +'\n'+\
'STRESS\t' + str(self.stress)+'\n'+\
'DURATION\t'+ str(int(self.duration*1000))+'\n'+\
'WORD\t'+ self.word+'\n'+\
'TIME\t'+ str(self.timePoint)+'\n'+\
'ENVIRONMENT\t'+ self.environment+'\n'+\
'MAX FORMANTS\t'+ str(self.maxFormant)
def Play(self):
## plays corresponding wav file in self.timerange
## read chunk from wav file corresponding to the vowel
wav = wave.open(self.wav)
frameRate = wav.getframerate()
nChannels = wav.getnchannels()
sampWidth = wav.getsampwidth()
start = int(self.min*frameRate)
end = int(self.max*frameRate)
wav.setpos(start)
wavChunk = wav.readframes(end-start)
wav.close()
## create temporary wav file containing the vowel sound
tempWav = wave.open('temp.wav', 'w')
tempWav.setnchannels(nChannels)
tempWav.setsampwidth(sampWidth)
tempWav.setframerate(frameRate)
tempWav.writeframes(wavChunk)
tempWav.close()
## play the new wav file then remove it
sound = wx.Sound('temp.wav')
sound.Play()
try: os.remove('temp.wav')
except: pass
## This does the same thing as above but requires SoX to run
# subprocess.call(['play',self.wav,'trim', str(self.min), '='+str(self.max)])
def MakeAlternate(self, altValues, altType):
## creates alternate vowel buttons when remeasuring
## usually then sent to self.alternates
## altValues = (timePercentage OR maxFormant OR timePoint, (alternateF1, alternateF2))
## altType = ('d' OR 'm') ... duration or maxformant
alternates = []
for a in altValues:
alt = VowelButton( parent = self.parent,
lineInFile = self.line,
cmuType = self.cmuType,
formants = a[1],
word = self.word,
stress = self.stress,
timeRange = (self.min, self.max),
timePoint = a[0]*self.duration+self.min if altType == 'd' else self.timePoint if altType == 'm' else a[0],
pronunciation = self.pronunciation,
maxFormant = a[0] if altType == 'm' else self.maxFormant,
index = self.index,
wav = self.wav,
infoFile = self.infoFile,
durationAlternates = [d for d in self.durationAlternateValues + [(self.timePercentage, (self.f1,self.f2))] \
if a != d] if altType == 'd' else self.durationAlternateValues,
maxFormantAlternates = [m for m in self.maxFormantAlternateValues + [(self.maxFormant, (self.f1,self.f2))] if a != m] \
if altType == 'm' else self.maxFormantAlternateValues,
otherType = self.otherType,
pitch = self.pitch,
alternate = True,
original = self)
alternates.append(alt)
return alternates
def GetAdjustedBitmapPosition(self):
## Adjusts the bitmap so it is displayed with self.position at the centre
## used to draw it to the panel in the right place
x,y = self.position
return (x-5,y-5)
def Hide(self):
## set show setting to false
try:
self.parent.visibleVowels.remove(self)
except:
pass
def Show(self):
## set show setting
self.parent.visibleVowels.add(self)
def SetBitmap(self, bm):
## set current bitmap value
self.currentBitmap = bm
def PlaceBitmap(self):
## places the vowels on the plot or hides them if they are out of range
f1Min, f1Max, f2Min, f2Max = self.parent.maxmins
## if in range of maxmin formants or if the vowel is an alternate (has a self.original value)
## to be displayed properly all vowels must have a position (alternate values receive position here after they are created)
if self.original or f1Max >= self.f1 >= f1Min and f2Max >= self.f2 >= f2Min:
plotWidth, plotHeight = self.parent.GetAdjustedSize()
x = plotWidth - int(plotWidth * (float(self.f2-f2Min)/(f2Max-f2Min))) + 10
y = int(plotHeight * (float(self.f1-f1Min)/(f1Max-f1Min))) + 10
## set position
self.position = (x,y)
## add position to positions dict and positionKeys set
try:
self.parent.positions[self.position].append(self)
except:
self.parent.positionKeys.add(self.position)
self.parent.positions[self.position] = []
self.parent.positions[self.position].append(self)
## now hide alternate vowels that are off the plot (now that they have received a position value)
if self.original and (f1Max >= self.f1 >= f1Min or f2Max >= self.f2 >= f2Min):
self.Hide()
else:
self.Hide()
def OnRemeasure(self):
## when clicking a vowel point (when not removing vowels)
if self.parent.remeasureOptions: # if selecting a remeasurement for a vowel
self.TheChosenOne()
else: # if selecting a vowel to remeasure
if self.parent.GetRemeasurePermissions():
self.parent.GetTopLevelParent().toolBarPanel.cancelButton.button.Enable()
remeasureMode = self.parent.GetTopLevelParent().toolBarPanel.reMeasureButton.GetMode()
# create remeasurements or open praat and wait
self.SetBitmap(self.parent.GetPreloadedBitmap('org'))
if remeasureMode == 'F':
self.alternates = self.MakeAlternate(self.maxFormantAlternateValues, 'm')
elif remeasureMode == 'D':
self.alternates = self.MakeAlternate(self.durationAlternateValues, 'd')
else:
self.parent.vowelInFocus = self
self.parent.SetRemeasurePermissions(False)
self.MakePraatAlternates()
return None
self.parent.remeasureOptions.append(self)
# change bitmaps of all relevant vowels
self.parent.CalculateFormantMaxMins()
rePlaceCheck = False
for a in self.alternates:
a.SetBitmap(self.parent.GetPreloadedBitmap('alt'))
self.parent.remeasureOptions.append(a)
## check if the plot needs to be remeasured
if a.f1 in self.parent.maxmins[:2] or a.f2 in self.parent.maxmins[2:]:
rePlaceCheck = True
self.parent.SetRemeasurePermissions(False)
self.parent.vowelInFocus = self
## update the plotpanel
if rePlaceCheck:
self.parent.PlaceVowels()
else:
self.parent.PlaceVowels(altsOnly = True)
def ReadPraatAlternates(self):
## Reads the new vowel values remeasured in Praat
## called in order to display the alternate vowels from Praat
## (this currently gets the pitch as well but currently it isn't really used
## so it does nothing with the pitch value)
with open('praatLog', 'r+') as pLog:
alts = []
for line in pLog:
info = line.split('\t')
alts.append( (float(info[0]), (int(info[1]), int(info[2])) ) )
pLog.truncate(0)
return alts
def MakePraatAlternates(self):
## opens praat to the vowel position
## if the path to praat is bad it prompts the user to reset it
try: os.remove('praatLog')
except: pass
try:
## opens dialog with button to remeasure in praat
## this needs to be shown before opening praat or it will not remain on top of everything
self.parent.logDialog = PraatLogDialog(self.parent, self.wav)
## open praat and editor to the correct location
if OPEN:
subprocess.check_output([OPEN, self.parent.GetTopLevelParent().Praat])
else:
subprocess.check_output('"'+self.parent.GetTopLevelParent().Praat+'"')
subprocess.check_output([SENDPRAAT, '0', 'praat',
'execute \"'+join(os.getcwd(),'zoomIn.praat')+'\" \"' + \
self.wav + '\" \"'+join(os.getcwd(),'praatLog')+ '\" ' + \
str(self.timePoint) + ' 1 '+str(self.maxFormant)+'"'])
self.parent.remeasureOptions.append(self)
self.parent.Refresh()
except:
if wx.MessageDialog(self.parent, 'Woah, that wasn\'t Praat...\nFind the real Praat?').ShowModal() == wx.ID_OK:
self.parent.GetTopLevelParent().OnFindPraat(None)
self.parent.GetTopLevelParent().toolBarPanel.cancelButton.OnClick(None)
def TheChosenOne(self):
## called when an alternate vowel is selected
## redraws the vowel at new location, logs the change, etc
self.SetBitmap(self.circleBitmap)
originalVowel = self.parent.remeasureOptions[0] #first vowel in the list is the original vowel
if self is not originalVowel:
# update relevant lists
self.parent.allVowels.remove(originalVowel)
self.parent.allVowels.add(self)
#log the change and update undo button
self.LogChange()
self.parent.GetTopLevelParent().past.append(('remeasure', originalVowel, self))
self.parent.GetTopLevelParent().future = [] ## clear future list
self.parent.GetTopLevelParent().toolBarPanel.undoRedoButtons.CheckState()
# change bitmap back
originalVowel.SetBitmap(originalVowel.circleBitmap)
# remove original value so the vowel is not treated like an alternate
self.original = None
# hide remeasure options
rePlaceCheck = False
for rb in self.parent.remeasureOptions:
if rb is not self:
self.parent.RemoveStoredVowelValues(rb, rb.f1, rb.f2, rb.word, rb.duration, rb.cmuType, rb.otherType, rb.position)
rb.Hide()
## check if the plot needs to be remeasured
if rb.f1 in self.parent.maxmins[:2] or rb.f2 in self.parent.maxmins[2:]:
rePlaceCheck = True
else:
rb.Show()
## reset to normal mode
self.parent.remeasureOptions = []
self.parent.vowelInFocus = None
self.parent.SetRemeasurePermissions(True)
self.parent.GetTopLevelParent().toolBarPanel.cancelButton.button.Disable()
self.parent.CalculateFormantMaxMins()
if rePlaceCheck:
self.parent.PlaceVowels()
else:
self.parent.Refresh()
def GetFormants(self):
## returns formant values for the vowel
return (self.f1, self.f2)
def RemoveVowel(self, click = False):
## Hides the vowel on the plot and removes it from the relevant lists
good, note = self.parent.GetTopLevelParent().toolBarPanel.removeButton.dialog.GetRemoveInfo()
self.LogChange(good, note) # log the removal as a change
self.parent.allVowels.remove(self)
self.parent.RemoveStoredVowelValues(self, self.f1, self.f2, self.word, self.duration, self.cmuType, self.otherType, self.position)
if click: # if called by clicking the vowel point
self.parent.CalculateFormantMaxMins()
if self.f1 in self.parent.maxmins[:2] or self.f2 in self.parent.maxmins[2:]:
self.parent.PlaceVowels()
else:
self.parent.Refresh()
self.parent.GetTopLevelParent().past.append(('remove', [self], None))
self.parent.GetTopLevelParent().future = [] ## makes sure you can't redo something you've already modified
self.parent.GetTopLevelParent().toolBarPanel.undoRedoButtons.CheckState()
self.Hide()
def LogChange(self, state = 'changed', note = ''):
## Logs changes to a dict in plotpanel to save later
## if state == True OR False, the vowel is marked as removed, not changed
if state is not 'changed':
state = 'removed_good' if state else 'removed_bad'
change = [self.line] + [str(wr) for wr in [self.timePoint, self.maxFormant ,self.f1, self.f2, state, note]]
try:
self.parent.changes[self.infoFile] += [change]
except:
self.parent.changes[self.infoFile] = [change]
class RemoveFromPlotOptions(wx.Frame):
## popup window that gives options when removing vowels from the plot
def __init__(self, parent):
wx.Frame.__init__(self, parent, style = wx.FRAME_FLOAT_ON_PARENT)
## define sizers and controls
sizer = wx.BoxSizer(wx.HORIZONTAL)
vSizer = wx.BoxSizer(wx.VERTICAL)
rmvText = wx.StaticText(self, label = 'Mark all removed vowels as...')
self.goodButton = wx.RadioButton(self, label = 'good')
self.goodButton.SetValue(True)
self.badButton = wx.RadioButton(self, label = 'bad')
noteText = wx.StaticText(self, label = 'Add note')
self.noteOption = wx.TextCtrl(self, size = (200, wx.DefaultSize[1]))
## arrange controls in the sizers
sizer.AddSpacer(10)
sizer.Add(vSizer)
sizer.AddSpacer(10)
vSizer.Add(rmvText)
vSizer.AddSpacer(2)
vSizer.Add(self.goodButton)
vSizer.AddSpacer(2)
vSizer.Add(self.badButton)
vSizer.AddSpacer(2)
vSizer.Add(noteText)
vSizer.AddSpacer(2)
vSizer.Add(self.noteOption)
vSizer.AddSpacer(10)
## set sizer for dialog box
self.SetSizerAndFit(sizer)
##Bind events
self.Bind(wx.EVT_ACTIVATE, self.OnClose)
def OnClose(self, e):
# hides the frame if it loses focus
if not e.GetActive():
self.Hide()
def GetRemoveInfo(self):
## returns info from dialog (used like GetValue())
return (self.goodButton.GetValue(), self.noteOption.GetValue())
class FilterOptions(wx.Frame):
## popup window that gives options when removing vowels from the plot
def __init__(self, parent):
wx.Frame.__init__(self, parent, style = wx.FRAME_FLOAT_ON_PARENT)
self.parent = parent
## define sizers
sizer = wx.BoxSizer(wx.VERTICAL)
wordSizer = wx.BoxSizer(wx.VERTICAL)
horSizer = wx.BoxSizer(wx.HORIZONTAL)
rightSizer = wx.BoxSizer(wx.VERTICAL)
stressSizer = wx.BoxSizer(wx.VERTICAL)
minMaxSizer = wx.BoxSizer(wx.VERTICAL)
minSizer = wx.BoxSizer(wx.HORIZONTAL)
maxSizer = wx.BoxSizer(wx.HORIZONTAL)
## define title and main button
self.filterBit = wx.Bitmap(join('icons','control_buttons','filter.png'))
self.cancelBit = wx.Bitmap(join('icons','control_buttons','filter_on.png'))
#self.button = wx.BitmapButton(self, bitmap = self.filterBit, size = (55,55))
#titleText = wx.StaticText(self, label = "FILTER", style = wx.ALIGN_CENTER)
## define list of words in the plot
self.words = []
self.checked_words = []
wordText = wx.StaticText(self, label = "By word...", style = wx.ALIGN_LEFT)
#self.wordBox = wx.ComboBox(self, value = ' ', choices = self.words, style = wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER, size = (200, wx.DefaultSize[1]))
self.wordBox = wx.CheckListBox(self, choices = self.words, size = (200,200))
#self.wordBox.SetValue('') ## hack to make sure default value is empty but curser starts in left-most position
## define min and max duration input boxes
durText = wx.StaticText(self, label = "By duration...", style = wx.ALIGN_LEFT)
minText = wx.StaticText(self, label = "min: ", style = wx.ALIGN_LEFT)
maxText = wx.StaticText(self, label = "max:", style = wx.ALIGN_LEFT)
self.minDurBox = wx.TextCtrl(self, value = '', style = wx.TE_PROCESS_ENTER|wx.TE_RICH, size = (40, wx.DefaultSize[1]))
self.maxDurBox = wx.TextCtrl(self, value = '', style = wx.TE_PROCESS_ENTER|wx.TE_RICH, size = (40, wx.DefaultSize[1]))
## define stress check boxes
stressText = wx.StaticText(self, label = "By stress...", style = wx.ALIGN_LEFT)
self.stress1 = wx.CheckBox(self, label = 'primary')
self.stress2 = wx.CheckBox(self, label = 'secondary')
self.stress0 = wx.CheckBox(self, label = 'unstressed')
self.stress1.SetValue(True)
self.stress2.SetValue(True)
self.stress0.SetValue(True)
## arrange controls in sizer
sizer.AddSpacer(20)
# sizer.Add(titleText, flag = wx.ALIGN_CENTER)
# sizer.Add(self.button, flag = wx.ALIGN_CENTER)
sizer.Add(horSizer)
sizer.AddSpacer(20)
horSizer.AddSpacer(20)
horSizer.Add(wordSizer)
horSizer.AddSpacer(10)
horSizer.Add(rightSizer)
horSizer.AddSpacer(20)
wordSizer.Add(wordText)
wordSizer.Add(self.wordBox)
rightSizer.Add(minMaxSizer)
rightSizer.AddSpacer(10)
rightSizer.Add(stressSizer)
minMaxSizer.Add(durText)
minMaxSizer.Add(minSizer)
minMaxSizer.Add(maxSizer)
minSizer.Add(minText)
# minSizer.AddSpacer(3)
minSizer.Add(self.minDurBox)
minSizer.Add(wx.StaticText(self, label = 'ms'))
maxSizer.Add(maxText)
maxSizer.Add(self.maxDurBox)
maxSizer.Add(wx.StaticText(self, label = 'ms'))
stressSizer.Add(stressText)
stressSizer.Add(self.stress1)
stressSizer.Add(self.stress2)
stressSizer.Add(self.stress0)
## display the panel
self.SetSizerAndFit(sizer)
self.Show(True)
## bind events to class functions
#self.button.Bind(wx.EVT_BUTTON, self.OnPress)
#self.wordBox.Bind(wx.EVT_TEXT_ENTER, self.OnEnter)
self.wordBox.Bind(wx.EVT_CHECKLISTBOX, self.OnWordSelect)
self.minDurBox.Bind(wx.EVT_KILL_FOCUS, self.OnMin)
self.maxDurBox.Bind(wx.EVT_KILL_FOCUS, self.OnMax)
self.minDurBox.Bind(wx.EVT_TEXT_ENTER, self.OnMin)
self.maxDurBox.Bind(wx.EVT_TEXT_ENTER, self.OnMax)
self.stress1.Bind(wx.EVT_CHECKBOX, self.OnCheck)
self.stress2.Bind(wx.EVT_CHECKBOX, self.OnCheck)
self.stress0.Bind(wx.EVT_CHECKBOX, self.OnCheck)
self.Bind(wx.EVT_ACTIVATE, self.OnClose)
def OnCheck(self, e):
if self.parent.button.GetBitmapLabel() == self.cancelBit:
self.parent.OnFilter(enter = True)
def showVowelStats(self):
## define list of words in the plot
self.words = self.FindWords()
self.wordBox.SetItems(self.words)
## define min and max duration input boxes
self.minDurBox.SetValue(self.GetTotalMinDur())
self.maxDurBox.SetValue(self.GetTotalMaxDur())
self.Refresh()
# def OnPress(self, e = None):
# ## call plotpanel.filterVowels() when button or return key is pressed
# plotpanel = self.parent.GetTopLevelParent().plotPanel
# if self.parent.button.GetBitmapLabel() == self.filterBit:
# ## get the word selection
# words = self.wordBox.GetCheckedStrings()
# ## get which stress buttons are on
# stress = [['unstressed','primary','secondary'].index(s.GetLabel()) for s in [self.stress1, self.stress2, self.stress0] if s.GetValue()]
# ## filter the vowels on the plot
# minDur = int(self.minDurBox.GetValue())
# maxDur = int(self.maxDurBox.GetValue())
# if minDur >= maxDur: ## only filters by word if the duration limits are weird
# self.minDurBox.SetBackgroundColour('red')
# self.maxDurBox.SetBackgroundColour('red')
# ## reset mindur/maxdur to the plot min/max
# minDur = self.GetTotalMinDur()
# maxDur = self.GetTotalMaxDur()
# self.parent.button.SetBitmapLabel(self.cancelBit)
# else: ## undo filtering
# minDur = self.GetTotalMinDur()
# maxDur = self.GetTotalMaxDur()
# stress = [0,1,2]
# words = '.*'
# self.parent.button.SetBitmapLabel(self.filterBit)
# plotpanel.filterVowels(words = words, minDur = minDur, maxDur = maxDur, stress = stress)
# self.Layout()
# def OnEnter(self, e):
# ## returns word value to default if the typed word isn't found in the plot
# if self.wordBox.GetValue().upper() not in self.words:
# self.wordBox.SetValue('')
# else:
# self.OnPress(enter = True)
def OnWordSelect(self, e):
## calls OnPress if a word in the popup list is selected
self.checked_words = self.wordBox.GetCheckedStrings()
self.parent.OnFilter(enter=True)
def OnMin(self, e):
## only integers allowed in minBox, also processes return key
self.minDurBox.SetBackgroundColour('white') # resets background in case previous values were weird
self.maxDurBox.SetBackgroundColour('white')
# if mindur is empty or less than 1: reset it to the minimum duration of all vowels on the plot
try:
if int(self.minDurBox.GetValue()) < 1:
self.minDurBox.SetValue(self.GetTotalMinDur())
except:
self.minDurBox.SetValue(self.GetTotalMinDur())
return
# if enter is pressed: filter vowels
if isinstance(e, wx.CommandEvent):
self.parent.OnFilter(enter = True)
def OnMax(self, e):
## only integers allowed in maxBox, also processes return key
self.minDurBox.SetBackgroundColour('white') # resets background in case previous values were weird
self.maxDurBox.SetBackgroundColour('white')
# if maxdur is empty or less than 1: reset it to the maximum duration of all vowels on the plot
try:
if int(self.maxDurBox.GetValue()) < 1:
self.maxDurBox.SetValue(self.GetTotalMaxDur())
except:
self.maxDurBox.SetValue(self.GetTotalMaxDur())
return
# if enter is pressed: filter vowels
if isinstance(e, wx.CommandEvent):
self.parent.OnFilter(enter = True)
def OnClose(self, e):
# hides the frame if it loses focus
# and reset checked words when gains focus
if not e.GetActive():
self.Hide()
else:
self.wordBox.SetCheckedStrings(self.checked_words)
def GetTotalMinDur(self):
## get min duration from all vowels on the plot
try:
return str(int(min(self.parent.GetTopLevelParent().plotPanel.durations)*1000))
except:
return None
def GetTotalMaxDur(self):
## get max duration from all vowels on the plot
try:
return str(int(max(self.parent.GetTopLevelParent().plotPanel.durations)*1000))
except:
return None
def FindWords(self):
## get list of all words from vowels on the plot
return sorted(list(self.parent.GetTopLevelParent().plotPanel.words))
class PhonButton(wx.Button):
## button subclass representing all cmu vowels
def __init__(self, parent, label, other = False):
# init button
wx.Button.__init__(self, parent, label = label, size = (37,25))
self.label = label
self.other = other
self.value = False
self.colour = self.GetColour()
# bind events
self.Bind(wx.EVT_BUTTON, self.OnPress)
def OnPress(self, e):
## update list of vowels shown on plotpanel when pressed
plotpanel = self.GetTopLevelParent().plotPanel
if self.value:
# self.SetBitmapLabel(self.offBitmap)
self.SetBackgroundColour(None)
self.value = False
if self.other:
plotpanel.RemoveOther(self.label)
else:
plotpanel.RemoveCmu(self.label)
else:
#self.SetBitmapLabel(self.onBitmap)
self.SetBackgroundColour(self.colour)
self.value = True
if self.other:
plotpanel.AddOther(self.label)
else:
plotpanel.AddCmu(self.label)
plotpanel.Refresh()
self.Refresh()
def GetColour(self):
if self.other:
return (79,192,251)
else:
return colourDict[self.label]
def SetValue(self, value):
## allows the master button to toggle the button
self.value = not value
self.OnPress(None)
def GetValue(self):
## returns current button value
return self.value
class UButton(wx.BitmapButton):
## pseudo togglebutton that displays either the union or intersect of
## the selected cmu and other vowels
def __init__(self, parent):
## load bitmaps
self.unionBit = wx.Bitmap(join('icons','control_buttons','union.png'))
self.intersectBit = wx.Bitmap(join('icons','control_buttons','intersect.png'))
# init button
wx.BitmapButton.__init__(self, parent = parent, bitmap = self.unionBit)
self.value = True
self.parent = parent
# bind events
self.Bind(wx.EVT_BUTTON, self.OnPress)
def OnPress(self, e):
# toggles the bitmap and sets the value when pressed
if self.GetBitmapLabel() == self.unionBit:
self.SetBitmapLabel(self.intersectBit)
self.value = False
else:
self.SetBitmapLabel(self.unionBit)
self.value = True
# updates plotpanel and button appearance
self.parent.Layout()
self.GetTopLevelParent().plotPanel.OnUnionButtonPress()
self.GetTopLevelParent().plotPanel.Refresh()
def GetValue(self):
## returns current value
return self.value
class masterButton(wx.ToggleButton):
## togglebutton subclass that controls the toggle state of other toggle buttons (its "minions")
def __init__(self, parent, label):
wx.ToggleButton.__init__(self, parent = parent, label = label, size = (92, 30))
self.minions = []
# bind events
self.Bind(wx.EVT_TOGGLEBUTTON, self.OnMasterToggle)
def OnMasterToggle(self, e):
## toggles all buttons below it (minion buttons)
for m in self.minions:
m.SetValue(self.GetValue())
def AddMinion(self, minionButton):
## add a minion (togglebuttons accepted only)
self.minions.append(minionButton)
class VowelInfo(wx.Frame):
## popup window that displays vowel info on right click
def __init__(self, parent):
wx.Frame.__init__(self, parent, style = wx.FRAME_FLOAT_ON_PARENT)
self.SetBackgroundColour('wheat')
self.sizer = wx.BoxSizer(wx.VERTICAL)
horSizer = wx.BoxSizer(wx.HORIZONTAL)
horSizer.AddSpacer(10)
horSizer.Add(self.sizer)
horSizer.AddSpacer(10)
self.SetSizer(horSizer)
self.Hide()
self.Bind(wx.EVT_ACTIVATE, self.OnClose)
def OnClose(self, e):
# hides the frame if it loses focus
if not e.GetActive():
self.Hide()
def UpdateMessage(self, message):
self.sizer.Clear(True)
self.sizer.AddSpacer(10)
for m in message.split('\n'):
self.sizer.Add(wx.StaticText(self, label = m))
self.sizer.AddSpacer(10)
self.Fit()
self.Show()
class PraatLogDialog(wx.Dialog):
## when praat opens up. Displays buttons that that call Query > Log 1 to measure the
## formants at the cursor. Also a button to close and prompts a return to the FVR frame
def __init__(self, parent, wavFile):
wx.Dialog.__init__(self, parent, style = wx.DEFAULT_DIALOG_STYLE|wx.STAY_ON_TOP)
## init sizers and controls
self.parent = parent
sizer = wx.BoxSizer(wx.HORIZONTAL)
buttonSizer = wx.BoxSizer(wx.VERTICAL)
measureButton = wx.BitmapButton(self, bitmap = wx.Bitmap(join('icons','control_buttons','donut.png')), size = (55,55))
closeButton = wx.Button(self, label = 'Done', size = (55, wx.DefaultSize[1]))
measureButton.SetDefault() ## doesn't do much at the moment because the dialog loses focus when clicking in Praat
measureButton.SetToolTip(wx.ToolTip('Measure formants at the cursor position in Praat\nThis is identical to Query > Log 1 in the Praat window'))
closeButton.SetToolTip(wx.ToolTip('Close Praat Window'))
## layout panel
sizer.AddSpacer(10)
sizer.Add(buttonSizer)
sizer.AddSpacer(10)
buttonSizer.AddSpacer(10)
buttonSizer.Add(measureButton)
buttonSizer.Add(closeButton)
buttonSizer.AddSpacer(10)
## bind events
measureButton.Bind(wx.EVT_BUTTON, self.OnMeasure)
closeButton.Bind(wx.EVT_BUTTON, self.OnClose)
self.Bind(wx.EVT_CLOSE, self.OnClose)
# get wav name as it is displayed in praat
self.wavName = basename(wavFile)[:-4].replace('\s','_')
self.SetSizerAndFit(sizer)
self.Show()
def OnMeasure(self, e):
## calls log 1 in praat and shows the new vowel on the plot
try:
subprocess.check_output([SENDPRAAT, 'praat', 'editor: "LongSound '+self.wavName+'" ', 'Log 1'])
except:
self.parent.logDialog = None
self.Destroy()
self.parent.ShowPraatMeasurements()
def OnClose(self, e):
## closes the panel and the praat editor and prompts the user to return to the plot panel
try:
subprocess.check_output([SENDPRAAT, 'praat', 'removeObject: "LongSound '+self.wavName+'"'])
except:
pass
self.parent.CheckRemainingPraatMeasurements()
self.parent.GetTopLevelParent().RequestUserAttention()
self.parent.logDialog = None
self.Destroy()
class DisambigDialog(wx.Dialog):
## miniature version of the plotpanel only showing overlapping vowels
def __init__(self, parent, vowels, position):
wx.Dialog.__init__(self, parent, style = 0, size = (300, 200))
self.parent = parent
pos = parent.ClientToScreen(position)
self.SetPosition((pos[0]-150, pos[1]-100))
self.vowels = vowels
self.vowelPosition = {}
self.maxmins = ()
self.Place()
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftClick)
self.Bind(wx.EVT_RIGHT_UP, self.OnRightClick)
self.Bind(wx.EVT_ACTIVATE, self.OnClose)
def OnClose(self, e):
# hides the frame if it loses focus
if not e.GetActive() and not self.parent.vowelInfoPanel.IsShown():
self.Hide()
def OnPaint(self, e):
## draw to the miniplot
dc = wx.PaintDC(self)
dc.Clear()
dc.SetBrush(wx.Brush('grey', wx.TRANSPARENT))
dc.SetPen(wx.Pen('grey'))
dc.DrawRectanglePointSize((0,0), self.GetSize())
width, height = self.GetSize()
if self.maxmins:
dc.DrawText(str(self.maxmins[0]), width-30, 15)
dc.DrawText(str(self.maxmins[1]), width-30, height-15)
dc.DrawText(str(self.maxmins[2]), width-40, 2)
dc.DrawText(str(self.maxmins[3]), 2,2)
dc.DrawText('F1', width-20, height/2)
dc.DrawText('F2', width/2, 2)
for p,v in self.vowelPosition.items():
dc.DrawBitmapPoint(v.currentBitmap, (p[0]-5, p[1]-5))
def OnLeftClick(self, e):
## process vowel when left clicking
pos = e.GetPosition()
vowels = self.GetVowelsInClickRange(pos)
selected = self.vowelPosition[vowels[0]]
if self.parent.removing:
selected.RemoveVowel(True)
self.Hide()
elif self.parent.GetTopLevelParent().toolBarPanel.playButton.GetPlayState():
selected.Play()
else:
selected.OnRemeasure()
self.Hide()
def OnRightClick(self, e):
## show info when right clicking a vowel
pos = e.GetPosition()
self.parent.vowelInfoPanel.SetPosition(self.ClientToScreen(pos))
vowels = self.GetVowelsInClickRange(pos)
selected = self.vowelPosition[vowels[0]]
self.parent.vowelInfoPanel.UpdateMessage(str(selected))
def GetVowelsInClickRange(self, p):
## simplified version of get vowels in click range from plot panel
xyGrid = {(x,y) for x in range(p[0]-5,p[0]+6) for y in range(p[1]-5,p[1]+6)}
return [ i for i in xyGrid&set(self.vowelPosition.keys())]
def GetMaxMins(self):
## returns and sets self.maxmins (probably shouldn't have Get in the name <-- confusing)
f1s = []
f2s = []
for v in self.vowels:
f1s.append(v.f1)
f2s.append(v.f2)
self.maxmins = (min(f1s), max(f1s), min(f2s), max(f2s))
return self.maxmins
def Place(self):
## places all vowels on the miniplot
f1Min, f1Max, f2Min, f2Max = self.GetMaxMins()
plotWidth, plotHeight = self.GetSize()
for v in self.vowels:
x = plotWidth - int(plotWidth * (float(v.f2-f2Min)/(f2Max-f2Min))) + 30
y = int(plotHeight * (float(v.f1-f1Min)/(f1Max-f1Min))) + 30
self.vowelPosition[(x,y)] = v
size = self.GetSize()
## increase the size of the panel so vowels won't be right on the edge
## the increase should be double the constant at the end of the x,y calculations above
self.SetSize((size[0]+60, size[1]+60))
class ScrolledMessageDialog(wx.Dialog):
def __init__(self, parent, message):
wx.Dialog.__init__(self, parent)
text = wx.TextCtrl(self, -1, message, size=(320,240), style=wx.TE_MULTILINE | wx.TE_READONLY)
sizer = wx.BoxSizer(wx.VERTICAL )
btnsizer = wx.BoxSizer()
btn = wx.Button(self, wx.ID_OK)
btnsizer.Add(btn, 0, wx.ALL, 5)
btnsizer.Add((5,-1), 0, wx.ALL, 5)
btn = wx.Button(self, wx.ID_CANCEL)
btnsizer.Add(btn, 0, wx.ALL, 5)
sizer.Add(text, 0, wx.EXPAND|wx.ALL, 5)
sizer.Add(btnsizer, 0, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
self.SetSizerAndFit(sizer)
class PlotPanel(wx.Panel):
## panel containing all plotted vowels
def __init__(self, parent):
## init panel and values
wx.Panel.__init__(self, parent = parent, style=wx.SUNKEN_BORDER)
# f1/f2 max mins
self.f1s = {} ## dictionary of {f1 : int} where int == the number of vowels on the plot with that f1 value
self.f2s = {} ## dictionary of {f2 : int} where int == the number of vowels on the plot with that f2 value
self.maxmins = () # (minF1, maxF1, minF2, maxF2) of all vowels on the plot
self.words = {} ## dictionary of {word : int} where int == the number of vowels on the plot in that word
self.durations = {} ## dictionary of {dur : int} where int == the number of vowels on the plot with that duration value
self.cmus = {} ## dictionary of {cmuLabel: [vowels]} where vowels are all vowels with that cmuType
self.others = {} ## dictionary of {otherLabel: [vowels]} where vowels are all vowels with that otherType
self.positions = {} ## dictionary of {(x,y):[vowels]} where (x,y) are the coordinates on the plot and vowels are all vowels with those coordinates
self.positionKeys = set() ## keeps track of all positions of vowels in allVowels for quick retrieval when more than 121 vowels are visible
self.visibleVowels = set() ## set of all visible vowels on the plot
self.allVowels = set() # all vowels
self.cmuLabels = [] # vowels with a cmu value in this list will be shown
self.otherLabels = [] # vowels with an other value in this list will be shown
self.remeasureOptions = [] # contains vowel instances of remeasured vowels
self.allowRemeasurements = True
self.vowelInFocus = None # vowel currently being remeasured
self.zoomCoords = [] # coordinates of beginning of zoombox/remove vowel box
self.zooming = False # if true: panel is waiting to zoom
self.removing = False # if true: waiting to remove vowels
self.drawing = False # if true: currently drawing a box on the overlay
self.changes = {} # stores all changes to vowels (saving reads from here)
self.overlay = wx.Overlay() # draws zoombox to this overlay
self.ignoreclick = False # when set to True, the next mouse click will be ignored (used when reactivating plot panel)
self.stdDev = 0 ## stores current number of std devs displayed by the confidence ellipse (0 = no ellipse)
self.filtering = {} ## contains options for filtering vowels on the plot
## create vowel bitmaps from files to be used for vowel points on the plot
self.BuildVowelBitmaps()
## draw labels
width, height = self.GetAdjustedSize()
self.f1Label = wx.StaticText(self, label = 'F1', pos = (width/2 , 0) )
self.f1Label.SetForegroundColour('GREY')
self.f2Label = wx.StaticText(self, label = 'F2', pos = (width, height/2) )
self.f2Label.SetForegroundColour('GREY')
self.f1MinLabel = wx.StaticText(self)