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 online-key-lifts [PsychoPy]
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:

Just to follow up this post - this is the current solution I am using for measuring key durations both locally and online in builder (2021.2.3) Rebecca Hirst / type_dynamics_demo · GitLab

1 Like

@Becca @milsandhills I am having a similar issue where my csv file does not provide values for key_press, key_duration, and key name are appearing blank on my csv (only displaying ). Does this also happen for you in your experiment output files?

I’m afraid I can’t be much help with this. Unfortunately I never got this code to work properly online and I ended up scrapping the key duration measure.

thank you for your response. Did it work locally on your device?

Hi @Becca I tried using your code you shared for recording key_durations; however, it resulted in each of the variables (key name, key press time, and key duration being blank in the csv code). I played around with the code (got rid of the second line in your original code) and now have it as shown in the image… this code prints the key names and key press time in the csv but for the duration column, it prints [None]. I also tried adding a line of code to set waitRelease=true to fix the duration issue but this did not seem to do the trick. Any suggestions/ have you experienced similar issues with your code? Many thanks!
image

Hi, maybe the missing duration column was related to this Possible unexpected and/or unintended behaviour of different keyboard backends for continuous keypresses with getKeys(waitRelease=False, clear=False) (ioHub, PsychToolBox, Pyglet) (e.g. missing key.duration in ioHub) · Issue #4845 · psychopy/psychopy · GitHub
You may want to change the Keyboard Backend to PTB (instead of iohub) if you want to stay on your current PsychoPy Version or update to 2022.2.1.
The missing duration for iohub was one element of the issue.

1 Like

Thank you for your response. I am still using PyschToolbox but I updated to the newest version and this helped! The only issue is that I am only getting values for the last key pressed due to the [-1}. Additionally, if only one key is pressed for the entire duration of the trial (ex, number 1 is held down the whole time), the csv does not record a value.

I’m having exactly this problem: key.duration remains None with pyglet even after the key is released. Unfortunately I cannot switch to PTB backend which does not seem to work with my MRI-compatible button box (cf. Buttonbox delay - Programming Help - Psychtoolbox).
Is there another way of checking whether a key has been released that would work even with pyglet?

Hi There,

Apologies for my delay - sorry but I just checked the demo and the type dynamics are saving to the columns as expected (i.e. not blank). Is it possible you are running a different version? the demo is 2021.2.3

Hi There - if this is a button box rather than a standard keyboard it is possible this is a different issue. If this is the case, please may you start a new post and share details on the button box that you are using?

Thanks!

This indeed turned out to be a button box issue: For Current Design button boxes, the standard setting “HID KEY 12345” appears to send the “key released” signal immediately after the button is pressed. The setting “HID NAR 12345” (non-auto release) sends the “key release” signal when the button is actually released, and resolves the issue. Thank you for your help!

2 Likes