Reference

Synchronization

What the timestamps mean, what they leave out, and how to line up records from different modules.

How it works

Every module runs on the same computer and reads the same two clocks. When a piece of data reaches a module, the module reads both clocks at once and writes the two values into the row as record_time_unix and record_time_mono. That is the whole mechanism. There is no clock distribution to the devices and no correction for how long each device took to deliver its data.

ClockColumnProperties
Wall clockrecord_time_unixSeconds since 1970-01-01 UTC. Comparable with other computers whose clocks are set. Can jump if the operating system adjusts the clock during a session
Monotonic clockrecord_time_monoPython’s perf_counter. Never jumps. The zero point is arbitrary, so only differences mean anything, and only within one boot of one computer

Use record_time_mono to compare rows from different modules recorded on the same computer. Use record_time_unix when you need the time of day or need to relate the session to something recorded elsewhere.

What each module’s timestamp means

record_time_* is when the data reached RSLogger, not when the event happened. The gap between the two depends on the device and its connection, and RSLogger does not measure it. Where a device has its own clock, the module also records that.

Modulerecord_time_* is takenDevice clock
Cameras, USBWhen the frame is read from the camera driverNone
Cameras, CSIWhen the frame arrives from the camera stacksensor_timestamp_ns, the sensor’s exposure timestamp
AudioIn the driver callback that delivers each chunkdevice_time_unix, the driver’s ADC time, when it provides one
Eye trackerWhen each sample arrives over the networkdevice_time_unix and device_time_ns, the Neon’s own timestamp
DRTWhen the device’s result message for a stimulus arrivesdevice_time_offset, stimulus onset in ms on the device’s clock. The reaction time itself is measured on the device and does not depend on any host timestamp
GogglesWhen the trial’s result arrives from the controllerTSOT and TSCT are accumulated on the controller and do not depend on any host timestamp
GPSWhen the NMEA sentence is parseddevice_time_unix, the UTC time inside the sentence. RSLogger reads it from the serial text and does not use a pulse-per-second signal
NotesWhen you post the noteNone
Control fileWhen you press a button in the main windowNone

The DRT and goggle results are the clearest case. A reaction time of 342 ms is 342 ms on the device’s clock whatever the host was doing. The host timestamp on that row tells you roughly where in the session the stimulus fell, to within the time the result took to travel over USB or the radio. The wired unit sends the result when the participant responds, or at the end of the stimulus interval on a miss. The wireless unit sends it at the end of the interval. Neither sends it at onset.

Check alignment in your own setup

RSLogger does not measure device latency, so the only way to know how well two streams line up on your equipment is to test it. Record an event that several sensors see at once, such as a clap in view of the camera and the microphone, or a lens closing in view of the eye tracker’s scene camera, and compare where it lands in each file.

Finding the same moment in different files

The examples use Python and pandas. Column names are from the data output reference.

Trial boundaries

The control file records every press of Record and Pause on the shared clock, so it is the place to start.

PYTHON PY
import pandas as pd

control = pd.read_csv("20260318_143022_CONTROL.csv")
starts = control[control["event_type"] == "trial_start"]
stops = control[control["event_type"] == "trial_stop"]
t0 = starts.iloc[0]["record_time_mono"]   # start of trial 1
t1 = stops.iloc[0]["record_time_mono"]

The video frame at a given time

PYTHON PY
timing = pd.read_csv("20260318_143022_CAM_trial001_hdprowebcamc920_timing.csv")
target = 373235.004822011                      # a record_time_mono from any module

i = (timing["record_time_mono"] - target).abs().idxmin()
frame = timing.loc[i, "frame_index"]          # frames are numbered from 1

Seek to frame in the MP4. frame_index counts frames as written, so it is a safer index than time × frame rate when a frame was dropped.

The audio sample at a given time

Each row of the audio timing file is one chunk, and the WAV holds the chunks end to end. The timing file does not record how many samples each chunk held, and RSLogger lets the driver choose the chunk size, so an exact sample position cannot be rebuilt from the timing file alone. Two usable estimates:

PYTHON PY
import soundfile as sf

audio, rate = sf.read("20260318_143022_AUD_trial001_MIC0_usbmicrophone.wav")
timing = pd.read_csv("20260318_143022_AUD_trial001_MIC0_usbmicrophone_timing.csv")

# 1. By elapsed time from the first chunk. Assumes the driver dropped nothing.
sample = int((target - timing.loc[0, "record_time_mono"]) * rate)

# 2. By chunk. Assumes the driver kept the chunk size constant, which most do.
chunk_len = len(audio) / len(timing)
before = timing[timing["record_time_mono"] <= target]
chunk = before.iloc[-1]
sample = int((chunk["chunk_number"] - 1) * chunk_len
           + (target - chunk["record_time_mono"]) * rate)

If the two estimates disagree by more than a chunk, the driver dropped audio during the trial. The module logs a warning when that happens.

Gaze samples during a video frame

PYTHON PY
gaze = pd.read_csv("20260318_143022_ET_trial001_GAZE.csv")
frame_time = timing.loc[100, "record_time_mono"]     # frame 101 of the camera file
half_frame = 0.5 / 30                                  # for a 30 fps camera

during = gaze[(gaze["record_time_mono"] >= frame_time - half_frame) &
            (gaze["record_time_mono"] <  frame_time + half_frame)]

The DRT stimulus nearest a video frame

PYTHON PY
drt = pd.read_csv("20260318_143022_DRT_dev_ttyacm0.csv")
frame_time = timing.loc[100, "record_time_mono"]

i = (drt["record_time_mono"] - frame_time).abs().idxmin()
row = drt.loc[i]        # row["reaction_time_ms"], row["responses"], row["trial"]

The DRT row’s record_time_mono is when the result arrived, not the onset. On a hit from the wired unit that is about the reaction time after onset. On a miss, or from the wireless unit, it is at the end of the stimulus interval. Differences in device_time_offset between rows give the spacing of onsets on the device’s own clock.

A note and the frame it was typed during

PYTHON PY
notes = pd.read_csv("20260318_143022_NTS_trial001_notes.csv")
note_time = notes.loc[0, "record_time_mono"]

i = (timing["record_time_mono"] - note_time).abs().idxmin()
print(notes.loc[0, "content"], "at frame", timing.loc[i, "frame_index"])

Sample rates

Modules record at different rates, so match rows by nearest neighbour or interpolate as suits the analysis.

  • Video: the frame rate set in the camera module, 30 fps by default
  • Audio: the sample rate set in the audio module, 48 kHz by default, delivered in chunks
  • Gaze: up to 200 Hz from the Neon
  • DRT: one row per stimulus, every 3 to 5 s by default
  • Goggles: one row per trial
  • GPS: one row per NMEA sentence, at the receiver’s update rate