function KeyboardLatencyTest(triggerlevel, modality, submode, portString, audioDevice) % KeyboardLatencyTest([triggerlevel=0.01][,modality=0][,submode][,portString][,audioDevice]) % % Uses sound capture with high timing precision via % PsychPortAudio() for measuring latency, timing accuracy and variability % of various response input devices, e.g., keyboard, keypad, mouse, other % HID devices like Joysticks etc., various response button boxes, and some % exotic response devices. % % Whenever the script tells you, hit a key on the tested input device % hard and loud enough so the noise of hitting the button or key can be % recorded by the attached microphone. This noise will be timestamped by % the code as the "true" button press time. Timestamps acquired % by standard input device queries are compared against that reference % and the difference is computed as device latency. % % Sound is captured from the default recording device, waiting % until the amplitude exceeds some 'triggerlevel'. % % The 'modality' flag chooses between keyboard (==0 - the default), and % mouse (==1). 'portString' allows to select which keyboard to test on some % systems (OS/X and Linux). It also allows to select which mouse to test on % Linux, but not on other systems. % % A 'modality' of 2 queries the keyboard, a keypad, a mouse, or other HID % devices by use of the KbTriggerWait() function. 'submode' specifies the % KbName() keyCode of the key to test on a keyboard. By default the SPACE % key is used. For other devices you *must* specify a key or button number in % 'submode'. Specifiying numbers of non-existent keys or buttons will cause % an infinite hang of Matlab or Octave, as there is no way to interrupt the % function or press a non-existent key or button. The optional 'portString' % specifies the deviceIndex of the device to test. If omitted, the primary % keyboard is tested. % % A 'modality' of 3 will test the PsychRTBox() driver for the USTCRTBOX reaction % time button box, if such a box is attached. A setting 4 will also test % that box, but without opening a connection to it, ie., it is assumed that % the box is already open from a previous call of this function with % modality setting '3'. This allows to repeat the test many times without % recalibrating the box. The optional 'submode' flag selects different ways % of testing the box: A setting of zero will perform a 'ClockRatio' % calibration to provide exact live timestamps and to test drift % correction. A setting of 1 will skip this, so live timestamps will % exhibit clock-drift and only the post-hoc timestamps will be somewhat % exact. A setting of 2 will skip collection of post-hoc timestamps. % % A 'modality' of 5 will exercise the RB-x30 response pads from Cedrus. % % A 'modality' of 6 will exercise the PST serial response button box. % Setting 'submode' to 1 will optimize for use with FTDI serial-USB % converters. % % A 'modality' of 7 will exercise the CMU serial response button box. % Setting 'submode' to 1 will optimize for use with FTDI serial-USB % converters. % % A 'modality' of 8 will exercise the Bitwhacker emulated response button box. % Setting 'submode' to 1 will optimize for use with FTDI serial-USB % converters. % % A 'modality' of 9 will exercise the fORP serial response button box in % program mode 0. % Setting 'submode' to 1 will optimize for use with FTDI serial-USB % converters. % % A 'modality' of 10 will measure timing of touch input devices like % touchscreens or touchpads. 'portString' allows to select a specific % touch input device as enumerated via GetTouchDeviceIndices(). By default % the first detected touchscreen or touchpad is used. Note that on some % operating systems you must hit the touchscreen or touchpad inside the % displayed onscreen window, as those systems will not register touches % outside the onscreen window. % % % The optional 'portString' argument can be set to define the serial port % to connect to for response devices that are connected via serial port. % By default, the proper serial port is auto-detected, but you can override % a wrong detection this way. % % % The optional 'audioDevice' allows to specify the device index for the % sound card to use for audio capture, in case multiple soundcards are % installed in the machine. By default, a sound device is auto-selected, % which is capable of sound capture. % % % Obviously this method of measuring carries quite a bit of uncertainty % in exact timing, but with a high quality microphone, proper tuning and % good sound hardware, it shouldn't be off too much. At least you get a % rough feeling for the lags inherent to keyboards and mice. % % History: % 08/10/2008 Written (MK) % 02/15/2009 Updated for Cedrus and RTBox (MK). % 08/15/2009 Updated for CMU and PST serial response button boxes (MK). % 11/14/2021 Handle mono audio capture as well (MK). tdelay = []; global tdelay2; %#ok<*GVMIS> global tSecs; global toffset; if nargin < 1 || isempty(triggerlevel) triggerlevel = 0.01; fprintf('No "triggerlevel" argument in range 0.0 to 1.0 provided: Will use default of 0.01 ...\n\n'); end if nargin < 2 || isempty(modality) modality = 0; end if nargin < 3 submode = []; end if isempty(submode) submode = 0; end if nargin < 4 portString = []; end if nargin < 5 audioDevice = []; end if modality == 3 PsychRTBox('Open'); if submode == 0 PsychRTBox('SyncClocks'); PsychRTBox('ClockRatio', [], 30); end end if modality == 4 PsychRTBox('Start'); end if modality == 5 cedrusport = FindSerialPort('usbserial', 1); % Open box, at high baud-rate (aka lowBaudrate == 0), perform calibration / % clock sync (doCalibrate == 1): hcedrus=CedrusResponseBox('Open', cedrusport, 0, 1); bi = CedrusResponseBox('GetDeviceInfo', hcedrus); disp(bi); fprintf('Clearing all queues...'); CedrusResponseBox('ClearQueues', hcedrus); fprintf('... done.\n'); end % CMU or PST serial response button box? if ismember(modality, [6,7,8,9]) if submode > 0 foption = 'ftdi norelease'; else foption = 'norelease'; end if modality == 6 % Open PST box, calibrate, start response collection: hcmu = CMUBox('Open', 'pst', portString, foption); elseif modality == 7 % Open CMU box, calibrate, start response collection: hcmu = CMUBox('Open', 'cmu', portString, foption); elseif modality == 8 % Open Bitwhacker box, calibrate, start response collection: hcmu = CMUBox('Open', 'bitwhacker', portString, foption); elseif modality == 9 % Open forpserial-0 box, calibrate, start response collection: hcmu = CMUBox('Open', 'forpserial-0', portString, foption); end else % None of these... hcmu = []; end fprintf('Auditory keyboard / mouse / response box latency test:\n'); fprintf('After you see the instruction "Hit me baby one more time!", hit '); if modality == 1 fprintf('a mouse button\n'); else if modality == 0 fprintf('a keyboard button\n'); else if modality == 2 fprintf('the keyboard space bar\n'); else fprintf('a response button on your response box device\n'); end end end if IsWayland || modality == 10 % We need a onscreen window to collect key input on Wayland: w = Screen('OpenWindow', 0, [255 255 0], [0 0 500 500]); DrawFormattedText(w, 'Type in this window', 'center', 'center', [0 0 255]); Screen('Flip', w); end if modality == 10 dev = portString; % If no user-specified 'dev' was given, try to auto-select: if isempty(dev) % Get first touchscreen: dev = min(GetTouchDeviceIndices([], 1)); end if isempty(dev) % Get first touchpad: dev = min(GetTouchDeviceIndices([], 0)); end if isempty(dev) || ~ismember(dev, GetTouchDeviceIndices) fprintf('No touch input device found, or invalid dev given. Bye.\n'); sca; return; end TouchQueueCreate(w, dev); TouchQueueStart(dev); end fprintf('hard enough so the microphone can pick up the noise.\n'); fprintf('This measurement will be repeated 10 times and obvious wrong\n'); fprintf('measurements discarded. At the end the mean input device latency\n'); fprintf('and standard deviation will be printed.\n\n'); fprintf('Caution: Only works well with high-quality sound cards and proper\n'); fprintf('adjustment of the "triggerlevel" parameter in a silent room.\n'); fprintf('These numbers are only rough estimates, more meant to illustrate\n'); fprintf('input latencies than to provide hard dependable measurements!!!\n\n'); fprintf('Press a key to continue...\n'); KbStrokeWait; % Perform basic initialization of the sound driver, initialize for % low-latency, high timing precision mode: InitializePsychSound(1); % Open the audio device 'audioDevice', with mode 2 (== Only audio capture), % and a required latencyclass of two 2 == low-latency mode, as well as a % default frequency and default number of sound channels for capture. We % also set the required latency to a pretty high 20 msecs. Why? Because we % don't actually need low-latency here, we only need low-latency mode of % operation so we get maximum timing precision -- Therefore we request % low-latency mode, but loosen our requirement to 20 msecs. % % This returns a handle to the audio device: pahandle = PsychPortAudio('Open', audioDevice, 2, 2, [], [], [], 0.02); % Get what freq'uency we are actually using: s = PsychPortAudio('GetStatus', pahandle); freq = s.SampleRate; fprintf('\nPress a key to start the measurement...\n'); KbStrokeWait; clc; % Do ten trials: for trial = 1:10 % Preallocate an internal audio recording buffer with a capacity of 30 seconds: PsychPortAudio('GetAudioData', pahandle, 30); % Start audio capture immediately and wait for the capture to start. % We set the number of 'repetitions' to zero, i.e. record until recording % is manually stopped. PsychPortAudio('Start', pahandle, 0, 0, 1); % Tell user to shout: fprintf('Hit me baby one more time! '); switch (modality) case 0 % Wait for all keys to be released: KbReleaseWait(portString); % Wait for keypress in a tight loop: while 1 [pressed, tKeypress] = KbCheck(portString); if pressed fprintf('--> dM = %f msecs ', 1000 * (GetSecs - tKeypress)); break; end end case 1 [~, ~, b] = GetMouse([], portString); while any(b) [~, ~, b] = GetMouse([], portString); end % Wait for mousebutton press in a tight loop: while ~any(b) [~, ~, b] = GetMouse([], portString); end tKeypress = GetSecs; case 2 if submode == 0 tKeypress = KbTriggerWait(KbName('space'), portString); else tKeypress = KbTriggerWait(submode, portString); end case {3, 4} % Test RTbox: % Clear all buffers: PsychRTBox('Clear'); fprintf('From now on... '); tKeypress = []; % Wait for some event on box, return its 'GetSecs' time: while isempty(tKeypress) [tKeypress, ~, tBox] = PsychRTBox('GetSecs'); end tKeypress = tKeypress(1); tBox = tBox(1); % Store raw box timestamp as well: tdelay2(end+1, 1) = tBox; %#ok case 5 % Test Cedrus response box: CedrusResponseBox('FlushEvents', hcedrus); fprintf('From now on... '); evt = []; % Wait for some event on box, return its 'GetSecs' time: while isempty(evt) || evt.port~=0 || evt.action~=1 || evt.button~=2 evt = CedrusResponseBox('GetButtons', hcedrus); end disp(evt) tKeypress = evt.ptbtime; while ~isempty(evt) evt = CedrusResponseBox('GetButtons', hcedrus); end case {6, 7, 8, 9} % CMU or PST box, Bitwhacker, forpserial-0 handled by CMUBox driver: % Drain the event queue of the box to remove any stale events: while 1 WaitSecs(0.1); evt = CMUBox('GetEvent', hcmu); if isempty(evt) break; end end fprintf('From now on... '); % Wait for first event: evt = []; while isempty(evt) evt = CMUBox('GetEvent', hcmu); end % Decode: tKeypress = evt.time; case 10 % Flush touch event queue: TouchEventFlush(dev); fprintf('From now on... '); % Wait for touch for up to 10 seconds: evt = []; while isempty(evt) || evt.Type ~= 2 evt = TouchEventGet(dev, w, 10); end tKeypress = evt.Time; otherwise error('Unknown "modality" specified.'); end % Wait in a polling loop until some sound event of sufficient loudness % is captured: level = 0; offset = 0; while level < triggerlevel % Fetch current audiodata: [audiodata, offset, ~, tCaptureStart]= PsychPortAudio('GetAudioData', pahandle); % Compute maximum signal amplitude in this chunk of data: if ~isempty(audiodata) level = max(max(abs(audiodata))); else level = 0; end % Below trigger-threshold? if level < triggerlevel % Wait for five milliseconds before next scan: WaitSecs(0.005); end end % Ok, last fetched chunk was above threshold! % Find exact location of first above threshold sample. if isvector(audiodata) idx = min(find(abs(audiodata) >= triggerlevel)); %#ok else idx = min(find(max(abs(audiodata)) >= triggerlevel)); %#ok end % Compute absolute event time: tOnset = tCaptureStart + ((offset + idx - 1) / freq); % Stop sound capture: PsychPortAudio('Stop', pahandle); % Fetch and discard all remaining audio data from the buffer - Needs to be empty % before next trial: PsychPortAudio('GetAudioData', pahandle); if offset == 0 fprintf('--> Keypress registered in 1st audiobuffer '); end % Print RT: dt = (tKeypress - tOnset)*1000; fprintf('---> Input delay time is %f milliseconds.\n', dt); % Valid measurement? Must be between 0 and 300 msecs to be considered: if (dt > -100) && (dt < 300) tdelay = [tdelay dt]; %#ok end if modality == 3 || modality == 4 % Store raw box timestamp as well: tdelay2(end, 2) = tOnset; end % Next trial after 2 seconds: WaitSecs(2); end if modality == 10 TouchQueueStop(dev); TouchQueueRelease(dev); end % Close the audio device: PsychPortAudio('Close', pahandle); % Close Screen in case of Wayland: sca; % Done. fprintf('\nTest finished. Average delay across valid trials: %f msecs (stddev = %f msecs).\n\n', mean(tdelay), std(tdelay)); if ~isempty(tdelay2) && submode ~= 2 % Recompute RTBox timestamps via post-hoc remapping: [tSecs, sd] = PsychRTBox('BoxsecsToGetsecs', [], tdelay2(:, 1)); fprintf('Error margin on fit for box->host mapping is %f msecs.\n', sd.norm * 1000); % Recompute offset based on remapped timestamps: toffset = (tSecs - tdelay2(:, 2)) * 1000; fprintf('\n\n\n'); for i=1:length(toffset) fprintf('Remapped RTBox delay for trial %i == %f milliseconds. [delta = %f msecs]\n', i, toffset(i), sd.delta(i)*1000); end toffset = toffset((toffset > -100) & (toffset < 100)); fprintf('\nTest finished. RTBox 2nd Average delay across valid trials: %f msecs (stddev = %f msecs, range = %f msecs).\n\n', mean(toffset), std(toffset), psychrange(toffset)); end if modality == 5 CedrusResponseBox('Close', hcedrus); end if ~isempty(hcmu) fprintf('Final status of CMUBox is:\n'); status = CMUBox('Status', hcmu) %#ok CMUBox('Close', hcmu); end return;