Repository: psychopy/psychopy

Issue:
kb.getKeys() misses button presses on Windows
Hi,
kb.getKeys() sometimes does not register a keypress in my setup.
- this happens only on Windows 10/11 (PsychoPy Version 2021.2.3), not on my MBP running Big Sur
- this happens only when getKeys is called as a function of the keyboard class, event.getKeys() works fine
- the problem was replicated using a Millikey button box, a standard Dell keyboard, and a standard Logitech keyboard on two different PCs
- Psychtoolbox's functions work fine from within matlab on these PCs (tested using KbCheck() as well as  PsychHID('KbQueueCheck')) 
- the percentage of missed keypresses ranged from 2% to 10% 

This is the script I used to test the problem and keep track of the number of missed button presses via RT monitoring (sorry, I don't know how to post it as python code)

```python
from psychopy.core import Clock, quit, wait
from psychopy.visual import Window, GratingStim, TextStim
from psychopy.hardware.keyboard import Keyboard
from psychopy import event
import pandas as pd

# intialize timer
trialClock = Clock()
# initialize keyboard
kb = Keyboard()
# create a window
mywin = Window([800,600],monitor="testMonitor", units="deg")
# create visual stimuli
grating = GratingStim(win=mywin, mask='circle', size=3, pos=[0,0], sf=3)
fixation = GratingStim(win=mywin, size=0.2, pos=[0,0], sf=0, rgb=-1)
text = TextStim(win=mywin,
    text='Press the space bar to start the block');
    
# loop through the blocks
for i in range(1, 2 + 1):
    
    # initialize conditions 
    conditions = pd.DataFrame()
    # add column for response key
    conditions["responseKey"] = 'NA'
    # add column for RTs
    conditions["RT"] = 'NA'

    # show text
    text.draw()
    mywin.flip()
    # wait for button press to start the experiment
    print("\nPress the space bar to start the block\n")
    while True:
        keys = kb.getKeys()
        if "space" in keys:
            mywin.flip()
            break

    # print info 
    print("\nStarting block " + str(i) + "\n")
    # wait shortly
    wait(2)

    # iterate over trials
    for idx in range(1,51):
        
        # show fixation
        fixation.draw()
        mywin.flip()
 
        # ITI
        wait(0.5)

        # show stimulus
        grating.draw()
        mywin.flip()
        
        # clear events
        # event.clearEvents()
        kb.clearEvents()
        
        # reset clocks
        # trialClock.reset()
        kb.clock.reset()


        # start endless/large response window
        while True: #trialClock.getTime() < 25:        
            # read out key presses
            # keys = event.getKeys(["left", "right"])
            keys = kb.getKeys(["left", "right"])
            # if there was a response
            if len(keys) > 0:  
                # key at the last keypress
                # conditions.loc[idx, "responseKey"] = keys[-1]
                conditions.loc[idx, "responseKey"] = keys[-1].name
                # RT at the last keypress
                # conditions.loc[idx, "RT"] = trialClock.getTime()
                conditions.loc[idx, "RT"] = keys[-1].rt
                # end response window
                break
                
        # write trial info to results file
        conditions.loc[idx:idx].to_csv('testData.csv', mode = 'a', index = False, header = False)
        
# quit experiment
quit()  
```


Discussion:
I think this is caused by checking the keyboard in too "tight" a loop - you're calling getKeys so rapidly that the OS isn't able to do anything else (including some of the housekeeping required to do the keyboard polling behind the scenes).

I think either of the following options will fix that:
- add something like `time.sleep(0.001)` to your `while True` loop to give a millisecond to the CPU for housekeeping. Possibly just a microsecond would suffice even
- switch to ioHub for keyboard instead of the PTB keyboard. IoHub seems better at getting events even in a tight loop and has nearly the same performance (certainly polling in a separate process and time-stamping there). If you simply create an iohub client at the top of your script (before creating the keyboard object) then this will be used in preference to PTB for the keyboard

Maybe you could confirm that the above solutions work for you and then we can close this issue.

In most studies, including all those from Builder, this isn't an issue because the screen refresh interval gives time for all the events to be processed.

Resolving patch:
diff --git a/psychopy/hardware/keyboard.py b/psychopy/hardware/keyboard.py
--- a/psychopy/hardware/keyboard.py
+++ b/psychopy/hardware/keyboard.py
@@ -82,8 +82,11 @@
     havePTB = False
 
 defaultBufferSize = 10000
+# default ptb flush_type, used by macOS & linux
+_ptb_flush_type = 1
 
-# monkey-patch bug in PTB keyboard where winHandle=0 is documented but crashes
+# monkey-patch bug in PTB keyboard where winHandle=0 is documented but crashes.
+# Also set ptb _ptb_flush_type to 0 for win32.
 if havePTB and sys.platform == 'win32':
     from psychtoolbox import PsychHID
     # make a new function where we set default win_handle to be None instead of 0
@@ -93,6 +96,9 @@ def _replacement_create_queue(self, num_slots=10000, flags=0, win_handle=None):
     # replace the broken function with ours
     hid.Keyboard._create_queue = _replacement_create_queue
 
+    # On win32, flush_type must be 0 or events can get flushed before being processed
+    _ptb_flush_type = 0
+
 
 def getKeyboards():
     """Get info about the available keyboards.
@@ -453,11 +459,7 @@ def flush(self):
         self._processEvts()
 
     def _flushEvts(self):
-        # SS: sleep is only needed on Windows, but test further before
-        # committing to this.
-        #if sys.platform == 'win32':
-        ptb.WaitSecs('YieldSecs', 0.00001)
-        while self.dev.flush():
+        while self.dev.flush(flush_type=_ptb_flush_type):
             evt, remaining = self.dev.queue_get_event()
             key = {}
             key['keycode'] = int(evt['Keycode'])


Test patch: