Experiment example (Psychtoolbox): Two-tone passive auditory localizer

Description

A passive auditory experiment for the KIT MEG system. The participant fixates a static red dot on a grey screen and listens to two pure tones (default 500 Hz and 1000 Hz) through the foam insert earphones. There is no task and no response. It gives a clean auditory evoked response (M50/M100) for each tone and is suitable for participants with epilepsy: the screen never changes, every tone has soft on/off ramps, and the level is capped.

Code: experiments/psychtoolbox/auditory_two_tones (start with main.m).

The two tones are played equally often in a constrained random order (never more than three identical tones in a row). Onset-to-onset intervals (SOA) are jittered uniformly between a minimum and a maximum, but the run length is still exact: the SOAs are evenly spaced between the two limits and shuffled.

Timeline of baseline, three tones and the corresponding trigger pulses

A 2 s silent baseline follows the run-start trigger; each tone has an onset trigger (by tone) and an offset trigger; run_end follows the last SOA.

Sound and trigger timing

Audio route

Sound is played with PsychPortAudio on the stimulus PC’s default output device (low-latency mode, 48 kHz, stereo, same tone in both ears), the same route as the lab’s other KIT auditory tasks (Visual_Imagery, oddball, auditory_vs_visual), to the participant’s foam insert earphones.

Locking the sound to the trigger

The older auditory scripts start the sound and then raise the trigger on the next screen flip, so the sound-to-trigger lag is unknown. Here the sound is scheduled for the exact video frame that carries its trigger:

  1. About 0.1 s before a tone, a normal (non-trigger) frame is flipped. Its real flip time anchors the schedule, so an error in the measured refresh interval cannot accumulate over the run.

  2. PsychPortAudio('Start', pa, 1, onsetFrameTime) schedules the first sample of the tone for the predicted time of the trigger frame, a whole number of frames after the anchor.

  3. The trigger frame is flipped at that time (KIT 224 for the low tone, 225 for the high tone) and cleared on the next frame.

  4. The offset trigger (KIT 226) is placed on the frame nearest the end of the sound.

  5. After the tone, PsychPortAudio’s own report of when the sound started is read and logged. Production stops if it differs from the trigger frame by more than 2 ms or if the audio device reports an underrun.

In simulation, the sound was scheduled within 0.03 ms of its trigger frame for 120 tones even with a 2 µs error in the measured refresh interval.

Waveform of one tone with ramps and the onset and offset trigger frames

One default tone against the 120 Hz frame grid. With a 0.1 s tone the offset frame coincides with the end of the sound; for durations that are not whole frames, the residual is logged as trigger_delay.

Each event’s trigger_delay (pulse time minus sound time, using the PsychPortAudio-reported start) is written to events.tsv. When onsets are filled from the recording, onset = pulse - trigger_delay is the sound itself, not the frame.

Important

The delay from the sound card to the eardrum (driver, amplifier, cables and tubing) is not included. Measure it once on the rig (for example with a microphone at the earphone, or a copy of the audio on a MEG analog channel) and add it when epoching, as is done for the projector lag in Display and trigger timing.

Trigger protocol

Single-channel DataPixx pixel mode, same RGB codes as trigger_template.m.

Event

KIT channel

MNE channel

Pixel RGB

When

tone_low

224

MISC 001

[4 0 0]

Frame on which the low tone starts

tone_high

225

MISC 002

[16 0 0]

Frame on which the high tone starts

tone_offset

226

MISC 003

[64 0 0]

Frame nearest the end of either tone

run_start

228

MISC 005

[0 4 0]

Start of the silent baseline

run_end

229

MISC 006

[0 16 0]

End of the last SOA (completed runs only)

A completed run has 2 x nTrials + 2 pulses (242 by default), each lasting one 120 Hz frame and each with exactly one row in events.tsv. The screen background is grey, so the trigger pixel is drawn black on every other frame. Pixel mode is enabled only after SPACE and disabled before the end screen.

Timing robustness on the stimulus PC

  • 30 trigger-free warm-up frames (0.25 s, cfg.warmupFrames) follow SPACE before the run-start pulse. The first flips after the start screen can return a bad timestamp on the stimulus PC (PTB “beamposition timestamping computed an impossible stimulus onset”); the warm-up keeps that away from every trigger frame.

  • Every timing check outside tolerance is logged in the *_ok columns and printed as a warning. Timing never stops a run.

  • A pulse whose flip timestamps give less than half a frame (impossible for a 1-frame pulse) is a timestamp error, not a trigger error: it is counted in RunInfo.n_timestamp_anomalies in beh.json and never stops the run.

Safety limits

The defaults and hard limits (cfg.safety in two_tones_config.m) are enforced in the launcher and again at the start of every run:

Parameter

Default / limit

Why

Level

-2 dBFS (0.79 of full scale, as the lab’s other KIT tone tasks) / -60 to -1 dBFS

Prevents a typo from producing a much louder sound than calibrated.

On/off ramp

10 ms / at least 5 ms, at most 25 % of the tone

No clicks or startling onsets.

Frequencies

500 and 1000 Hz / 250-4000 Hz, high above low

Comfortable range for insert earphones.

Tone duration

0.1 s / 0.05-1.0 s

SOA (jittered)

1.2-1.6 s / 0.8-5.0 s, silence between tones at least 0.5 s

Gentle rate; avoids entrainment.

Run length

at most 15 min

Split longer sessions into runs.

Warning

dBFS is a digital level, not loudness at the ear. Calibrate the chain once with a sound level meter or ear simulator so the default is comfortable (about 60-70 dB SPL), then keep the Windows volume and amplifier fixed. Before each session, use Play test tones in the launcher with the participant. It plays the tones through the same PsychPortAudio device and mode as the run. In low-latency mode Windows can bypass the system mixer, so other players may sound quieter than the run.

The visual display is completely static (grey background, red dot, text before and after), with Psychtoolbox splash screens disabled and a 2 s fade to black when the window closes. Set the projector desktop background to black.

Running the experiment

cd experiments/psychtoolbox/auditory_two_tones
main              % launcher (production preset selected)
main('validate')  % headless self-test, no Psychtoolbox needed
Launcher window with run parameters and the planned run length

The launcher recomputes the run length whenever a value changes (press Enter or Tab), together with the number of tones, rate, silence and level. Start stays disabled while anything is invalid or outside the limits.

Presets

Preset

DataPixx

Display and checks

Output folder

Production

ON

Full screen, 120 Hz required, timing and audio sync logged, never fatal

data/

Full rehearsal

OFF

Full screen, same audio checks

data_rehearsal/

Debug

OFF

Windowed, 50 px visible trigger patch, faults logged only

data_rehearsal/

run length = 2 s baseline + nTrials x (SOA min + SOA max) / 2

Tones

Run length (SOA 1.2-1.6 s)

Note

120

2 min 50 s

Default (60 per tone)

200

4 min 42 s

400

9 min 22 s

600

14 min 02 s

Close to the 15 min limit

Scripted launch (safety limits still apply):

spec = struct('subject', 'P01', 'session', '01', 'mode', 'debug');
spec.params = struct('nTrials', 10);   % plain doubles
main(spec)

Operator keys: SPACE starts, ESC or Q stops (a tone that is already playing finishes its ramp; partial data are saved).

The console prints a progress line after each trial (Trial 17/120 done, 103 left, about 3 min 26 s to go). A run stopped early keeps the standard file names with RunStatus aborted in beh.json; events.tsv has exactly one row per trigger actually sent, so its onsets can be filled from the MEG recording and the completed trials analysed.

Before the session

  1. One-time: calibrate the sound level and measure the sound-card-to-ear delay.

  2. Run main('validate') after any change to the code or configuration.

  3. Check channels 224, 225, 226, 228 and 229 with test_all_meg_channels_triggers.m if the setup or wiring changed.

  4. Use Play test tones with the participant in place.

  5. Start KIT acquisition, then press SPACE.

Saved data (BIDS)

data/
  dataset_description.json, README
  sub-P01/ses-01/meg/sub-P01_ses-01_task-twotones_run-01_events.tsv / .json
  sub-P01/ses-01/beh/sub-P01_ses-01_task-twotones_run-01_beh.tsv / .json
  stimuli/task-twotones/tone-low_freq-500Hz_dur-100ms_level-m02dBFS_h-<hash>.wav
  sourcedata/sub-P01/ses-01/beh/..._desc-snapshot_beh.mat, ..._desc-matlabconsole_log.txt

events.tsv has one row per trigger pulse in time order. Its columns are onset, duration, trial_type, value, sample, channel, tone, frequency_hz, stim_file, onset_from_run_start, trigger_delay, presentation_index, level_dbfs, audio_start_minus_trigger and hardware_emitted.

  • As in the other lab experiments, onset and sample are n/a until they are filled from the recording.

  • value equals the KIT channel, and onset and offset rows of the same tone share presentation_index.

  • events.json describes every column. It also holds:

    • "TriggerMode": "single_channel", the channel table and the expected pulse count per channel;

    • the trigger definitions and the onset rule;

    • the audio route and sound parameters;

    • the measured refresh rate;

    • RunStatus and RunComplete.

beh.tsv has one row per planned tone. It logs the frame and audio times (planned, anchored, flipped, PsychPortAudio start and stop), the audio-minus-trigger difference, underruns, SOA and pulse checks, and missed flips. The WAV files are the exact sounds played. The file name contains a hash of the samples, so different settings never share a file.

Preprocessing

Fill onset/sample from the recording (after MNE-BIDS conversion, which writes its own events file to the same path):

python experiments/psychtoolbox/auditory_two_tones/fill_events_onsets.py \
    --events data/sub-P01/ses-01/meg/sub-P01_ses-01_task-twotones_run-01_events.tsv \
    --raw    <recording>.con \
    --out    <BIDS root>/sub-P01/ses-01/meg/sub-P01_ses-01_task-twotones_run-01_events.tsv

The script uses the lab’s pulse detector on MISC 001-MISC 008 and matches every logged pulse one-to-one, in order. It then sets onset = pulse - trigger_delay. If any pulse is missing or extra, it writes nothing. Epoch on the tone_low/tone_high rows (values 224/225) and add the measured sound-card-to-ear delay.

Validation

main('validate') checks:

  • the trigger map;

  • the presets and limits, and that the default run length is 2 min 50 s;

  • that 15 unsafe or invalid settings are refused;

  • the generated tones: frequency, peak level, ramps, zero first sample, the same signal in both ears;

  • 120 random schedules: balance, run constraint, exact jittered length;

  • every output file for a completed and an aborted run.

During development the run loop was exercised against a simulated 120 Hz Psychtoolbox, DataPixx and PsychPortAudio, checking:

  • every pulse lasted one frame with the correct code;

  • every tone was scheduled exactly on its trigger frame, with the right sound;

  • the pulse sequence matched events.tsv, including after an abort;

  • the onset-filling script recovered exact onsets from a synthetic recording, including the offset residual for a 0.105 s tone.

Confirm on the rig with a recorded rehearsal before the first participant.