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)
Esempio n. 2
0
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)