Module exp_collection
[hide private]
[frames] | no frames]

Source Code for Module exp_collection

  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  # import os 
 14  # import datetime 
 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  # import cairo 
 26  import pango 
 27   
28 -class ProgressBar(gtk.DrawingArea):
29 """This is a general-purpose progress bar for GTK that 30 extends horizontally. 31 """ 32 # __gsignals__ = {"expose-event": "override"} 33 34 RGB1 = (0.1, 0.2, 0.7) 35 RGB2 = (0.9, 0.4, 0.1) 36
37 - def __init__(self):
38 gtk.DrawingArea.__init__(self) 39 self.fraction = None 40 # self.connect("configure_event", self.drawme, None) 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
48 - def set_fraction(self, f):
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
57 - def do_expose_event(self, w, event, d):
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
80 -def drop_blanks(s):
81 return [ t for t in s if len(t)>0 ]
82 83 84
85 -class experiment_base(object):
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
93 - def __init__(self, stimlist, hdr, gui):
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
102 - def close(self):
103 pass
104 105
106 - def get_hdrs(self):
107 return self.hdr.copy()
108 109
110 - def get_current_stimulus(self):
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
133 - def next_stimulus(self):
134 self.i += 1 135 self.gui.progress(self.i, len(self.stimlist)) 136 return self.i < len(self.stimlist)
137 138
139 - def is_last_stimulus(self):
140 return self.i >= len(self.stimlist)-1
141 142
143 - def event(self, widget, ev, log):
144 handler = self.current_handler 145 # print 'Event handler=', handler, ev 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 # print "event yields", handler 155 self.gui.destroy(None, ev) 156 return handler 157 elif handler is True: 158 # print "event yields", handler 159 return handler 160 elif handler is None: 161 return True 162 # print "Changing handler to", handler 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
173 - def S_initial(self, ev, log):
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
186 -class GUI_base(object):
187 """Base class for the data-collection graphical user interface. 188 """ 189
190 - def delete_event(self, widget, event, data=None):
191 return False
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
200 - def connect_experiment(self, expcall, log):
201 """Connect the class that describes the experiment to the GUI. 202 """ 203 # keyname = gtk.gdk.keyval_name(event.keyval) 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
225 - def make_buttons(self, boxtop):
226 self._repeat = gtk.Button('<-') 227 self._repeat.show() 228 boxtop.pack_start(self._repeat, False, False) 229 self._next = gtk.Button('->') 230 boxtop.pack_start(self._next, False, False) 231 self._next.show() 232 return boxtop
233 234
235 - def make_boxtop(self, font_name):
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 # boxtop.pack_start(self._pbar, True, True) 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 # Defer the self.window.show() to allow tweaks 296 # of window properties, such as self.window.add_events() 297
298 - def main(self):
299 """Call this to start the GUI going.""" 300 self.window.show() 301 gtk.main()
302
303 - def progress(self, i, n):
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
314 - def status_push(self, context_id, message):
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
323 - def status_pop(self, context_id):
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
331 - def stim_win(self):
332 """Return a pointer to the stimulus area.""" 333 return self._stim
334
335 - def peak_win(self):
336 """Return a pointer to the area that displays the peak amplitude.""" 337 return self._peak
338
339 - def instr_win(self):
340 """Return a pointer to the instruction area.""" 341 return self._instr
342
343 - def set_peaks(self, peaks):
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
399 -def check_wav(fn, gui):
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 = {}
407 -def get_text(s):
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
426 -def set_defaults(d, **kv):
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