1 """This module contains code for collecting experimental data.
2 It uses the pygame module to give us a reasonably nice GUI.
3
4 This software is copyright Greg Kochanski (2010) and is
5 available under the Lesser Gnu Public License, version 3 or higher.
6 It was funded by the UK's Economic and Social Research
7 Council under project RES-062-23-1323. This is available from
8 http://sourceforge.org/projects/speechresearch,
9 http://kochanski.org/gpk/papers/2010/aesop_data_collect, and
10 http://www.phon.ox.ac.uk/files/releases/2008aesopus2_data_collect.tar
11 """
12
13
14
15 import math
16 import wave
17 import numpy
18 from gmisclib import die
19 from gmisclib import gpkmisc
20 from gmisclib import Numeric_gpk as NG
21
22 import pygtk
23 pygtk.require('2.0')
24 import gtk
25
26 import pango
27
29 """This is a general-purpose progress bar for GTK that
30 extends horizontally.
31 """
32
33
34 RGB1 = (0.1, 0.2, 0.7)
35 RGB2 = (0.9, 0.4, 0.1)
36
38 gtk.DrawingArea.__init__(self)
39 self.fraction = None
40
41 self.connect("expose-event", self.do_expose_event, None)
42 self.connect("expose_event", self.do_expose_event, None)
43 self.gc1 = None
44 self.gc2 = None
45 self.f = 0.0
46
47
49 if f is not None:
50 self.f = float(f)
51 assert 0.0 <= self.f
52 if self.window is not None:
53 cr = self.window.cairo_create()
54 self.draw(cr, *self.window.get_size())
55
56
58 cr = self.window.cairo_create()
59 cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
60 cr.clip()
61 self.draw(cr, *self.window.get_size())
62
63
64 - def draw(self, cr, width, height):
65 cr.set_source_rgb(0.5, 0.5, 0.5)
66 cr.rectangle(0, 0, width, height)
67 cr.fill()
68
69 cr.set_source_rgb(*self.RGB1)
70 iw = int(round(width*self.f))
71 cr.rectangle(0, 0, iw, height)
72 cr.fill()
73
74 cr.set_source_rgb(*self.RGB2)
75 cr.rectangle(iw, 0, width, height)
76 cr.fill()
77 return False
78
79
81 return [ t for t in s if len(t)>0 ]
82
83
84
86 """This class is intended to be the basis of a finite-state machine
87 description of the experiment.
88 It helps you iterate through a list of stimuli with L{get_current_stimulus}()
89 and L{next_stimulus}(), it manages state transitions with L{event}(),
90 and displays prompts on the screen.
91 """
92
94 self.stimlist = drop_blanks(stimlist)
95 self.current_handler = self.S_initial
96 self.last_handler = None
97 self.i = -1
98 self.hdr = hdr
99 self.gui = gui
100
101
104
105
107 return self.hdr.copy()
108
109
111 if self.i >= len(self.stimlist):
112 return None
113 tmp = self.hdr.copy()
114 tmp.update(self.stimlist[self.i])
115 return tmp
116
117
118 - def get(self, *kd):
119 key = kd[0]
120 try:
121 return self.stimlist[self.i][key]
122 except KeyError:
123 if len(kd) == 1:
124 try:
125 return self.hdr[key]
126 except KeyError:
127 die.warn('Please define "%s" in the header or as a column in the input file.' % key)
128 raise
129 else:
130 return self.hdr.get(key, kd[1])
131
132
134 self.i += 1
135 self.gui.progress(self.i, len(self.stimlist))
136 return self.i < len(self.stimlist)
137
138
140 return self.i >= len(self.stimlist)-1
141
142
143 - def event(self, widget, ev, log):
144 handler = self.current_handler
145
146 if isinstance(ev, gtk.gdk.Event) and ev.type == gtk.gdk.KEY_PRESS:
147 e = ev.string
148 else:
149 e = widget
150 try:
151 while True:
152 handler = handler(e, log)
153 if handler is False:
154
155 self.gui.destroy(None, ev)
156 return handler
157 elif handler is True:
158
159 return handler
160 elif handler is None:
161 return True
162
163 self._needs_show = True
164 self.current_handler = handler
165 e = None
166 except:
167 die.catch("ERROR")
168 self.gui.destroy(None)
169 die.die("ERROR")
170 return True
171
172
174 raise RuntimeError, "Virtual Function"
175
176
177 - def first_entry(self):
178 if self.last_handler is self.current_handler:
179 return False
180 self.last_handler = self.current_handler
181 return True
182
183
184
185
187 """Base class for the data-collection graphical user interface.
188 """
189
192
193 - def destroy(self, widget, data=None):
194 """Shut down the GUI."""
195 try:
196 gtk.main_quit()
197 except RuntimeError, x:
198 die.die('RuntimeError "%s" while shutting down.' % str(x))
199
201 """Connect the class that describes the experiment to the GUI.
202 """
203
204 self.window.connect("key_press_event", expcall, log)
205 self.window.connect("key-press-event", expcall, log)
206 expcall(None, None, log)
207
208
209 - def make_TextView(self, extra_line_space, extra_para_space, font_name):
210 box = gtk.TextView()
211 box.set_editable(False)
212 box.set_cursor_visible(False)
213 box.set_wrap_mode(gtk.WRAP_WORD)
214 box.set_justification(gtk.JUSTIFY_LEFT)
215 box.set_pixels_inside_wrap(extra_line_space)
216 box.set_pixels_below_lines(extra_para_space)
217 box.set_pixels_above_lines(extra_para_space)
218 box.set_indent(10)
219 print 'Set font to [%s]' % font_name
220 pangoFont = pango.FontDescription(font_name)
221 box.modify_font(pangoFont)
222 return box
223
224
233
234
236 boxtop = gtk.HBox(False, 10)
237
238 self._sbar = gtk.Statusbar()
239 self._sbar.show()
240 boxtop.pack_start(self._sbar, True, True)
241
242 self._pbar = ProgressBar()
243 self._pbar.set_size_request(300, 50)
244 self.progress(0, 1)
245
246 boxtop.pack_start(self._pbar, False, False)
247 self._pbar.show()
248
249 self._peak = self.make_TextView(0, 0, font_name)
250 self.set_peaks((-0.1, -0.1))
251 self._peak.show()
252 boxtop.pack_start(self._peak, False, False)
253
254 self.make_buttons(boxtop)
255 boxtop.show()
256 return boxtop
257
258
259
260
261 - def __init__(self, extra_line_space=5, extra_para_space=5,
262 stim_font=None, top_font=None):
263 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
264 self.window.set_title('Experiment Aesop 1')
265 self.window.connect("delete_event", self.delete_event)
266 self.window.connect("delete-event", self.delete_event)
267 self.window.connect("destroy", self.destroy)
268 self.window.set_border_width(10)
269 self.window.maximize()
270 self.window.set_focus_on_map(True)
271
272 box1 = gtk.VBox(False, 10)
273 self.window.add(box1)
274
275 if stim_font is None:
276 stim_font = "Serif 24"
277 if top_font is None:
278 top_font = stim_font
279
280 box1.pack_start(self.make_boxtop(top_font), False, False, 10)
281
282 self._instr = self.make_TextView(extra_line_space, extra_para_space,
283 stim_font)
284 self._instr.get_buffer().set_text("Instructions")
285 self._instr.show()
286 box1.pack_start(self._instr, False, True, 5)
287
288 self._stim = self.make_TextView(extra_line_space, extra_para_space,
289 stim_font)
290 self._stim.get_buffer().set_text(".")
291 self._stim.show()
292 box1.pack_start(self._stim, True, True, 5)
293 box1.show()
294 self.status_push(1, "-")
295
296
297
299 """Call this to start the GUI going."""
300 self.window.show()
301 gtk.main()
302
304 """Set the position of the progress bar.
305 The idea is that i out of n items have been completed.
306 @param i: How much has been done.
307 @type i: int
308 @param n: What's the total amount of work.
309 @type n: int
310 """
311 if n > 0:
312 self._pbar.set_fraction(min(i+0.5, n)/float(n))
313
315 """Push some text onto the status bar.
316 @param context_id: What category of the thing?
317 @type context_id: int
318 @param message: The thing to display in the "status" area.
319 @type message: str
320 """
321 self._sbar.push(context_id, message)
322
324 """Pop some text off the status bar. The most recent text of
325 the specified category is removed.
326 @param context_id: What category of the thing?
327 @type context_id: int
328 """
329 self._sbar.pop(context_id)
330
332 """Return a pointer to the stimulus area."""
333 return self._stim
334
336 """Return a pointer to the area that displays the peak amplitude."""
337 return self._peak
338
340 """Return a pointer to the instruction area."""
341 return self._instr
342
344 """Set the displayed peak amplitude."""
345 text = ', '.join(['%.1f' % p for p in peaks])
346 self.peak_win().get_buffer().set_text(text)
347
348
349 _sizes = {
350 2: (numpy.int16, 16, 32767),
351 4: (numpy.int32, 32, 2**31-1)
352 }
353
354
355 -def wav_peak(fn, tstart=None, t_end=None):
356 """This function checks that a WAV file is legal, and
357 returns the peak amplitude for each column,
358 relative to the largest possible amplitude.
359 @param fn: filename
360 @type fn: str
361 @param tstart: where to start in the file
362 @param t_end: where to start in the file
363 @type tstart: float
364 @type t_end: float
365 @rtype: list
366 @return: list of peak amplitudes, one for each column.
367 """
368 print 'Checking file', fn
369 w = wave.open(fn, 'r')
370 if not 1000 < w.getframerate() < 100000:
371 raise ValueError, "Silly frame rate in WAV file: %d" % w.getframerate()
372 nc = w.getnchannels()
373 if not 1 <= nc < 10:
374 raise ValueError, "Silly number of channels in WAV file: %d" % nc
375 if not 1 < w.getsampwidth() <= 16:
376 raise ValueError, "Silly sample width in WAV file: %d" % w.getsampwidth()
377 nf = w.getnframes()
378 if 100 >= nf:
379 raise ValueError, "Absurdly short WAV file: %d frames" % nf
380 if tstart is not None:
381 istart = int(round(tstart*w.getframerate()))
382 w.setpos(istart)
383 if t_end is not None:
384 nf = int(round(t_end*w.getframerate())) - istart
385 if nf <= 0:
386 return (0.0, 0.0)
387 numtype, bitpix, scale = _sizes[w.getsampwidth()]
388 data = numpy.fromstring(w.readframes(nf), numtype)
389 w.close()
390 data = numpy.reshape(data, (data.shape[0]//nc, nc))
391 assert len(data.shape)==2 and data.shape[1]==nc
392 if data.shape[0] <= 1:
393 raise ValueError, "Time range yield (nearly?) no data: %s to %s" % (tstart, t_end)
394 return [ max(NG.N_maximum(data[:,i]), -NG.N_minimum(data[:,i]))/float(scale)
395 for i in range(nc)
396 ]
397
398
400 """This checks a wav file and displays its peak amplitude.
401 """
402 gui.set_peaks( [ 10.0*math.log10(pk) for pk in wav_peak(fn) ] )
403
404
405
406 _get_text_cache = {}
408 """Pull text from a named file.
409 """
410 global _get_text_cache
411 try:
412 tmp = _get_text_cache[s]
413 except KeyError:
414 o = []
415 try:
416 for tmp in open(s, 'r'):
417 o.append(tmp.strip())
418 except IOError, x:
419 die.warn("Read failed on file %s inside get_text() cause: %s" % (s, str(x)))
420 raise
421 tmp = '\n'.join(o)
422 _get_text_cache[s] = tmp
423 return tmp
424
425
427 """Set a value into dictionary d only if there is nothing there already.
428 @param d: dictionary to be updated
429 @type d: dict
430 @param kv: dictionary of default values
431 @type kv: dict
432 """
433 for (k, v) in kv.items():
434 if k not in d:
435 d[k] = v
436