I timestamp MEG stimuli with a photodiode, a microphone, and an NI card at 10 kHz, and I only kept the setups whose trial-to-trial jitter stayed under 1 ms. Absolute delay depends on whether the trigger is written before or after the flip, so I am not quoting those means here. Jitter and light–sound alignment are what matter for averaging. Hardware, the measurement recipe, and two example scripts are in the note:
Jitter, after the rules below. Auditory jitter delay went from ±20 ms to ±0.1 ms.
Light versus sound. On a single trial the two rising edges sit on top of each other. Across about 260 trials, sound minus light (negative means the sound is slightly early) lies on the 0.1 ms sample grid, mostly between −0.6 and −0.3 ms, with the whole range about 0.5 ms. The tightest run in the notes was −0.02 ± 0.08 ms.
The rig that produced this was a desktop, an AMD GPU from before 2020, Ubuntu 20.04 with XOrg and Wayland off, linux-lowlatency, PTB 3.0.21, and MATLAB. SkipSyncTests = 0 had to pass cleanly. Nvidia proprietary drivers, laptop hybrid graphics, and Windows did not hold the jitter.
Seven rules. Breaking one of them put the jitter back to ±5 ms, sometimes ±50 ms. The long version, with code, is in the repository.
- Desktop, discrete AMD GPU, Ubuntu, MATLAB or Octave. Not a laptop iGPU, not a proprietary Nvidia driver, not Windows for a real session.
- Own the sound card, and keep the buffer at the device minimum.
PsychPortAudio('Open',[],1,3,sf,2,1). Only latency class 3 stayed inside 1 ms. - One screen owns the GPU, and every screen uses the same resolution and refresh. In single-screen mode, offset drawing in X by half a screen.
- Name the time for audio, and name the frame for the picture. Fill the audio buffer, then start at a future anchor with
waitForStart = 0. For a picture, take the anchor fromPredictVisualOnsetForTimeand flip attonset+(n+0.5)*ifi, so the image lands on frame n+1 instead of slipping one more. Audio and picture share that anchor. Write the trigger after the flip, or a missed frame shows up as jitter. Do not block inStart(waitForStart = 1). It looks precise and it made the rest of the paradigm fall apart. - Warm up. Run the script once so the mex files are loaded, then do not change folders.
- Buttons are a coarser clock.
KbWaitis about 5 ms. LoopKbCheckand readdeltaSecs, or record the button on the same NI or MEG clock. - Preload video.
PlayMovieat the start of the trial, then a blockingGetMovieImage. Draw the first frame into the back buffer before scheduling it. Later frames useonset+ifi*(holdFrames-0.5).

