class SimpleVisionEgg: keyboard_controller = None trigger_controller = None screen = None presentation = None keys = None presses = None releases = None def __init__(self): """We break up initialization a bit as we need to go back and forth with some information. In this case, we need screen size before specifying the stimuli""" # pasted in from where it used to be at the beginning of the script # used to be outside of any methods... VisionEgg.start_default_logging() VisionEgg.watch_exceptions() # get screen size for setting fullscreen resolution # comment this block out if you don't want to use full-screen. screen = pygame.display.set_mode((0,0)) WIDTH, HEIGHT = screen.get_size() pygame.quit() VisionEgg.config.VISIONEGG_SCREEN_W = WIDTH VisionEgg.config.VISIONEGG_SCREEN_H = HEIGHT self.screen = get_default_screen() self.keys = [] self.presses = [] self.releases = [] def set_stimuli(self, stimuli, trigger=None, kb_controller=False): """Now that we have our stimuli, we initialize everything we can""" viewport = Viewport(screen=self.screen, size=self.screen.size, stimuli=stimuli) # We disable "check_events" so that we don't lose "instantaneous" key # presses and can check these in our Response classes self.presentation = Presentation(viewports=[viewport], check_events=False) if trigger: trigger_controller = KeyboardTriggerInController(trigger) self.presentation.add_controller(self.presentation, 'trigger_go_if_armed', trigger_controller) self.presentation.set(trigger_go_if_armed=0) if kb_controller: self.keyboard_controller = KeyboardResponseController() self.presentation.add_controller(None, None, self.keyboard_controller) def set_functions(self, update=None, pause_update=None): """Interface for cognac.StimulusController or similar""" self.presentation.add_controller(None, None, FunctionController(during_go_func=update, between_go_func=pause_update, return_type=NoneType) ) def go(self, go_duration=('forever',)): self.presentation.parameters.go_duration = go_duration self.presentation.go() def pause(self): self.presentation.parameters.go_duration = (0, 'frames') def get_new_response(self, t, min_interval=2.0 / 60, releases=False): """(key, press) = get_new_response(self, t, min_interval=2.0 / 60) DEPRECATED! Use this function to get responses from the keyboard controller in real time. Returns (None, None) if no new response is available. Maintains three instance variables - keys, presses and releases, which you can also access directly (but they won't be updated during loops where you don't call this function) This function makes a number of assumptions and is a little brittle right now. By not hard-coding the min_interval and maybe using key presses and release events directly, we'd have a much better function. But I don't really care right now. DJC """ raise DeprecationWarning("please use pygame directly, as in" + "StimController.Response") # Note - this is deprecated anyway, but it'd probably make more sense to # use the keyboard_controller.get_responses() to simply get the keys # that are down _right_now_ press_time = self.keyboard_controller.get_time_last_response_since_go() key = self.keyboard_controller.get_last_response_since_go() # Our first response! if len(self.keys) == 0: if key: self.keys.append(key) self.presses.append(press_time) self.releases.append(None) if releases: return (key, None) else: return (key, press_time) else: return (None, None) # We haven't seen a key press for min_interval if t >= press_time + min_interval and not self.releases[-1]: # This is only approximate! self.releases[-1] = t if releases: return (self.keys[-1], t) else: return (None, None) # We've seen a release, or we see a new key if (self.releases[-1] and press_time > self.releases[-1]) or \ key != self.keys[-1]: if not self.releases[-1]: self.releases[-1] = press_time self.keys.append(key) self.presses.append(press_time) self.releases.append(None) if releases: return (key, None) else: return (key, press_time) return (None, None) def get_responses(self, timeToSubtract=0, min_interval=2.0/60): """ Use this function to post-process the results of a KeyboardController VisionEgg's keyboard libraries records a keypress and timestamp every time something is down. So if a key is held down for 100 ms, there will be an entry in the keylist for every sample during that 100ms. This is a bit much; I'd rather just save onsets and offsets for every key. This function evaluates that. """ ''' If we're using the FORP, this isn't necessary, as events have no duration; they are represented as instantaneous keypresses. -- John ''' response = self.keyboard_controller.get_responses_since_go() responseTime = self.keyboard_controller.get_time_responses_since_go() # If I've only got one item in my response list, then it's silly to worry about onset/offset. Just keep it. if len(response) < 2: return (response,responseTime) # Save the first response, as by definition that's the first onset: goodResp = [response[0]] goodRespTime = [responseTime[0]-timeToSubtract] # Now step through every other item in the response list to check for unique-ness. for i in range(1,len(responseTime)): if (not(response[i] == response[i-1]) or \ (responseTime[i] - responseTime[i-1] > \ min_interval)): # ie, if something changed, or we have a long gap: offsetResp = [] # we might want to save an offset for item in response[i-1]: # loop through last item's data if (responseTime[i] - responseTime[i-1] < \ min_interval) and \ not(item in response[i]): # Bit clunky. Basically, holding down a key while pressing another creates # a unique response. So if you only let up one of those keys, code the # offset just for that key. offsetResp.append(item+'_Off') else: # it's been long enough that everything that was on should be called off. offsetResp.append(item+'_Off') if len(offsetResp) > 0: # If there's offset stuff to worry about, save it. goodResp.append(offsetResp) goodRespTime.append(responseTime[i-1]-timeToSubtract) # Save the new (onset) response. goodResp.append(response[i]) goodRespTime.append(responseTime[i]-timeToSubtract) # The final event should be an offset for whatever was down. offsetResp = [] for item in response[-1]: offsetResp.append(item+'_Off') goodResp.append(offsetResp) #goodResp.append(response[-1]+'_Off') goodRespTime.append(responseTime[-1]-timeToSubtract) return (goodResp, goodRespTime)
class TRStimController: """This is a relatively simple controller that simply updates what's on the screen every TR (which is the next occurrence of keyboard input of '5' after the TR length is exceeded. Currently it sets one stimulus on, and all others off, though we may want to change that to turn a list of stimuli on eventually""" # 3T laptop forp sends TTL pulses as "5"; buttons as "1","2","3","4" # John used to do things this way: # trigger_in_controller = KeyboardTriggerInController(pygame.locals.K_5) # Expected length of 1 TR TR = 2.0 # Time interval after which we assume we missed the trigger eps = 0.1 t = 0 trial_times = None missed_trigs = None stim_list = [] stim_dict = {} stim_seq = [] keyboard_controller = None presentation = None screen = None def __init__(self, TR=None, eps=None): # Get the initial setup if TR: self.TR = TR if eps: self.eps = eps self.trial_times = [] self.missed_trigs = [] self.state = self.state_generator() self.screen = get_default_screen() # background black (RGBA) self.screen.parameters.bgcolor = (0.0,0.0,0.0,0.0) self.keyboard_controller = KeyboardResponseController() self.firstTTL_trigger = KeyboardTriggerInController(K_5) def set_stims(self, stim_list, stim_dict, stim_seq_file): self.stim_list = stim_list self.stim_dict = stim_dict self.stim_seq = yaml.load(stim_seq_file) viewport = Viewport(screen=self.screen, size=self.screen.size, stimuli=self.stim_list) # Need to at least wait for the first trigger if this is going to work. go_duration = (self.TR * len(self.stim_seq), 'seconds') self.presentation = Presentation(go_duration=go_duration, trigger_go_if_armed=0, viewports=[viewport]) self.presentation.add_controller(None, None, FunctionController(during_go_func=self.update) ) self.presentation.add_controller(None, None, self.keyboard_controller) # Adding this so that we can start the stimuli ahead of time self.presentation.add_controller(self.presentation,'trigger_go_if_armed', self.firstTTL_trigger) def run(self): self.presentation.go() self.screen.close() def update(self, t): self.t = t try: self.state.next() except StopIteration: # shouldn't really happen, what with epsilon and all... self.blank_all_stims() def blank_all_stims(self): for stim in self.stim_list: stim.parameters.on=False def state_generator(self): for stim_info in self.stim_seq: self.trial_times.append(self.t) self.blank_all_stims() try: for stim_name, params in stim_info.items(): stim = self.stim_dict[stim_name] stim.parameters.on = True try: for name, value in params.items(): setattr(stim.parameters, name, value) except AttributeError: # params was None or something else we don't deal with pass except AttributeError: # We assume a no-colon single token / key self.stim_dict[stim_info].parameters.on = True # Don't even bother 'til we're close to the expected TR time while self.t - self.trial_times[-1] < self.TR - 2*self.eps: yield while self.t - self.trial_times[-1] < self.TR + self.eps: # Handle the rare case when a key might register between # function calls - THIS WOULD NEVER HAPPEN WITH VisionEgg as # written! while True: keys = self.keyboard_controller.get_responses_since_go() times = \ self.keyboard_controller.get_time_responses_since_go() if len(keys) == len(times): break i = None try: # Find the last value of '5' without inline reversal of keys/times # VisionEgg returns "responses" as a list of lists of chars, not just a list of chars... i = len(keys)-1-list(reversed(keys)).index(['5']) except ValueError: pass # If anybody presses the escape key, quit entirely. try: needToQuit = keys.index(['escape']) #self.presentation = None #exit() except ValueError: pass if i and times[i] > self.trial_times[-1]: break else: yield if self.t - self.trial_times[-1] >= self.TR + self.eps: # We missed a TR (we think) self.missed_trigs.append(self.t) self.t = self.trial_times[-1] + self.TR def get_responses(self, timeToSubtract=0, min_interval=2.0/60): """ This function isn't especially elegant, but it's functional. VisionEgg's keyboard libraries records a keypress and timestamp every time something is down. So if a key is held down for 100 ms, there will be an entry in the keylist for every sample during that 100ms. This is a bit much; I'd rather just save onsets and offsets for every key. This function evaluates that. """ ''' If we're using the FORP, this isn't necessary, as events have no duration; they are represented as instantaneous keypresses. -- John ''' response = self.keyboard_controller.get_responses_since_go() responseTime = self.keyboard_controller.get_time_responses_since_go() # If I've only got one item in my response list, then it's silly to worry about onset/offset. Just keep it. if len(response) < 2: return (response,responseTime) # Save the first response, as by definition that's the first onset: goodResp = [response[0]] goodRespTime = [responseTime[0]-timeToSubtract] # Now step through every other item in the response list to check for unique-ness. for i in range(1,len(responseTime)): if (not(response[i] == response[i-1]) or \ (responseTime[i] - responseTime[i-1] > \ min_interval)): # ie, if something changed, or we have a long gap: offsetResp = [] # we might want to save an offset for item in response[i-1]: # loop through last item's data if (responseTime[i] - responseTime[i-1] < \ min_interval) and \ not(item in response[i]): # Bit clunky. Basically, holding down a key while pressing another creates # a unique response. So if you only let up one of those keys, code the # offset just for that key. offsetResp.append(item+'_Off') else: # it's been long enough that everything that was on should be called off. offsetResp.append(item+'_Off') if len(offsetResp) > 0: # If there's offset stuff to worry about, save it. goodResp.append(offsetResp) goodRespTime.append(responseTime[i-1]-timeToSubtract) # Save the new (onset) response. goodResp.append(response[i]) goodRespTime.append(responseTime[i]-timeToSubtract) # The final event should be an offset for whatever was down. offsetResp = [] for item in response[-1]: offsetResp.append(item+'_Off') goodResp.append(offsetResp) #goodResp.append(response[-1]+'_Off') goodRespTime.append(responseTime[-1]-timeToSubtract) return (goodResp, goodRespTime)