psychopy.org | Reference | Downloads | Github

Measuring key lift time relative to stimulus onset

Hi There,

We want to measure the time a key is lifted relative to the start of a stimulus occurring (i.e. the start but not the end of the key duration measurement should be synchronised with the refresh of the screen). We think we have a solution but I am reaching out to check if others agree with this solution (and also to make a record for future users, because I couldn’t find anywhere this was discussed!)

Using the Keyboard class, we can measure RTs in multiple ways the two main ways being:

key.rt: time from clock reset to key press event
key.duration: time from key press event to key lift event

The first method can be locked to the refresh of the screen through resetting the clock when the window flips (i.e. win.callOnFlip(kb.clock.reset)). The second would result in a measurement where both the start and end of the measurement is asynchronous to screen refresh. To get a measurement of duration that it locked to the start of a stimulus/screen refresh we could think we can use method 4 or 5 below (i.e. using the key press time, clock reset time and key lift time, to measure the time from the start of the stimulus to the key lift).

I guess this isn’t so much a question as a point of discussion to see if others are in agreement with this approach/notice any possible issues.

Thanks all in advance,
Becca


from psychopy.hardware import keyboard #allows us to watch for key lifts 
from psychopy import event, core, visual # for when the key is first pressed
import numpy as np

# A window to show a message in
win = visual.Window(
    fullscr=False,
    monitor='testMonitor', color=[-1,-1,-1], colorSpace='rgb',
    blendMode='avg', mouseVisible = False, allowGUI=False)

# Some text for the message
waiting_message=visual.TextStim(win, pos=[0, 0], height=1, color= [1,1,1],
    text="Ready for key press")

# Set the Keyboard
kb = keyboard.Keyboard(bufferSize=10, waitForStart=True)

# 5 "trials"
for trial in range(5):
    
    waiting_message.draw()
    win.flip()
    
    k = event.waitKeys()
    
    win.callOnFlip(kb.clock.reset)
    count = 0
    while count==0:
        win.flip()
        remainingKeys = kb.getKeys(keyList=['space', 'escape'], waitRelease=False, clear=False)
        if remainingKeys:
            for key in remainingKeys:
                if key.duration:#if the key has been lifted
                    
                    # Show 5 examples of different RTs that can be measured using the Keyboard class
                    
                    method_1 = kb.clock.getTime() # this gets the current time on the kb clock
                    method_2 = key.duration # this gets the time from the key pressed event to the key lift event (will include the 2 seconds of core.wait)
                    method_3 = key.rt # this gets the time the key is pressed versus the time the kb clock was reset (we reset the clock after button press so it is negative)
                    method_4 =key.duration-np.abs((key.tDown-kb.clock.getLastResetTime())) # this gets the time the kb clock was reset to the key lift event
                    method_5 = key.duration + key.rt #this also gets the time the kb clock was reset to the key lift event but if we 
                    
                    print('method 1 (clock reset to clock now):', method_1, 'method 2 (duration):', method_2, 'method 3 (rt):', method_3, 'method 4 (clock reset to key lift):', method_4, 'method 5 (duration + rt):', method_5)
                    
                    kb.clearEvents() #clear the key events
                    
                    count = count + 1
1 Like

Hi @Becca ,

I am having some trouble trying to record key press durations using #builder and was wondering if you’d be able to help?

In my experiment, participants need to type a word they see on screen, and I want to record the key press duration of each key they press.

I have tried adding the following into a code component in my routine:

Begin Routine

duration=[]
kb = keyboard.Keyboard()

Each Frame

keys2 = kb.getKeys()
for key in keys2: 
    if key.duration:
        duration.append(key.duration)

End Routine

trials.addData("Hold Times", duration)

When I run the experiment, I get the column in the .xlsx file called ‘Hold Times’ but in the first row I just get ‘[]’ and nothing inside the list. It seems to me that nothing is actually getting appended to the list ‘duration’ but I can’t figure out why. I have also tried using ‘thisKey’ instead of ‘key’ in the ‘Each Frame’ code to no avail.

As FYI, I am using PsychoPy v2020.2.1 Standalone in Windows 10 and will need to push the experiment to Pavlovia in the future.

Here is my experiment if that helps: StudentExperiment_counterbalanced.psyexp (96.3 KB)

Any help would be hugely appreciated!

Hi There!

Getting key durations times is a bit tricker online and needs some custom javascript. Here is a demo from myself and Thomas Pronk to help you on your way
run link https://run.pavlovia.org/lpxrh6/online-key-lifts
code: Rebecca Hirst / online-key-lifts · GitLab
File in case: online-key-lifts.psyexp (18.0 KB)

Essentially we are monitoring for key events on a list of defined keys ( in this case ‘a’, ‘d’ and ‘l’) when the key is lifted you can see the duration time and that is saved to file - so this should be what you can plug in to your experiment.

Hope this helps,
Becca

Hi @Becca ,

This custom code was really helpful - thanks! However, I’m still having trouble coding key durations in my experiment in local PsychoPy, let alone in Pavlovia.

Where my needs differ from the code you sent is that my participants will type a whole word as opposed to just a letter. I want the key duration for each key pressed in that word (6 keys/letters) to be saved to a list which then appears in my .xlsx file as a list of 6 key durations for that word stimulus.

I am so stuck with this and would be incredibly grateful for any assistance! Here is my code:

Begin Routine

# a keyboard object (must differ from object used online)
mykb = keyboard.Keyboard()
#numPresses = 10

# which keys are we watching? 
keysWatched=['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p',
'q','r','s','t','u','v','w','x','y','z','backspace','return','space',',','.','/',
'`',';','#','[',']','1','2','3','4','5','6','7','8','9','0','-','=','rshift',
'lshift']

# what are the assumed key statuses at the start of the routine
status =['up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up',
'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 
'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 
'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up', 'up']

# how many keyPresses have been counted so far
keyCount = 0 
statusList = []

pressTime = 0
keyPressTime = []
liftTime = 0 
keyPressDuration = []

Each Frame

# poll the keyboard
keys = mykb.getKeys(keysWatched, waitRelease = False, clear = False)

if len(keys):# if a key has been pressed
    for i, key in enumerate(keysWatched):
        if keys[-1].name == key:
            if keys[-1].duration:
                status[i] = 'up'
                statusList.append('up')
            else:
                status[i] = 'down'
                statusList.append('down')
                
#get times:
if len(statusList)>1:
    if statusList [-1] != statusList[-2]:# the last 2 key events were different
        if statusList[-1] =='down':# this was a press event
            pressTime = taskClock.getTime()
            keyPressTime.append(pressTime)
        elif statusList[-1] =='up':
            liftTime = taskClock.getTime() - pressTime #key press duration, (i.e. 
            # difference between press and release)
            keyPressDuration.append(liftTime)
            #continueRoutine = False

As you can see this is incredibly similar to the code you and Thomas provided, with the main addition of the .append code in the ‘Each Frame’ section I have added.

End Routine

thisExp.addData('keyPressTime', keyPressTime)
thisExp.addData('keyPressDuration', keyPressDuration)
thisExp.addData('statusList', statusList)

The issue is that in the .xlsx file I get this:
image

Why are keyPressTime and keyPressDuration empty?

Many thanks for any ideas!

As a follow up, I have now found a solution to this. In my routine I also had a keyboard component which was conflicting in some way with the keyboard set up in my code. Once I deleted my keyboard component, the above code worked!

However, if key presses remotely overlap, they do not get a duration: