The EyeLink EDF files contain many types of data, including eye movement events, messages, button presses, and samples. The EDF file format is a highly compressed binary format, intended for use with SR Research EyeLink Data Viewer (https://www.sr-support.com/forum-7.html). Data Viewer can interpret a range of messages written to the EDF file that allow the software to automate some of the viewer configuration. Please check out Chapter 7 "Protocol for EyeLink Data to Viewer Integration" of the Data Viewer User Manual (https://www.sr-support.com/thread-135.html).
Other data analysis options are available besides the EyeLink Data Viewer software. One approach is to work with EDF file directly (see the "EDF_Access_API" example in the "C:\\Program Files (x86)\\SR Research\\EyeLink" folder, or the EDFMEX tool when using MATLAB, see https://www.sr-support.com/thread-54.html). The other data access method is to make use of the ASC file and write experiment-specific analyzers. The ASC file is created by the EDF2ASC translator program (https://www.sr-support.com/thread-23.html). This program converts selected events and samples into text, and sorts and formats the data into a form that is easier to work with. The EDF2ASC translator, EDF data types, and the ASC file format are covered in the Chapter 4 of the EyeLink User Manual (https://www.sr-support.com/forum-34.html).
The current section describes how to write your data analysis application using the C language. However, you can apply the same idea to other programming languages you prefer to use.
If you have EyeLink Data Viewer installed on your computer, you may run the GUI version of the EDF2ASC (edfconverterW.exe) from "C:\\Program Files\\SR Research\\edfconverter". On Windows, macOS, and Linux, if you have the EyeLink Developers Kit installed, you can run the converter directly from the command prompt or terminal.
When the translation is completed, a file with the extension of "ASC" will be available for processing.
The ASC files is plain text file which may be viewed with any text editor. Creating and viewing an ASC file should be the first step in creating an analyzer program, to see which messages need to be handled.
Viewing the ASC file is also important in validating an experimental application, to see if the messages, time, etc. match those expected. The EyeLink Data Viewer program can also be used to view messages in combination with eye-movement data.
Programs that process an ASC file must read the ASC file line by line, determine the type of data from the line from the first word in the line, and read words or numbers from the rest of the line as appropriate. A set of tools for reading ASC files is included in the asc_proc folder. This is the C source file read_asc.c, and its header file read_asc.h.
A sample analyzer using this toolkit has also been included. This is the source file sac_proc.c which processes the sample data file data.asc. These can be used as a template for your own analyzers.
As each line is of the ASC file is read, your analyzer program must determine which part of the experiment it is in (if in a trial, which trial, whether the display is visible, what response has been made, etc.) and compile data on each trial and the entire experimental session. This requires support functions to parse each line of the ASC file, reading keywords, numbers, and text.
The ASC-file processing support functions in read_asc.c perform these operations:
When creating an ASC file, EDF2ASC adds the extension ".asc" to the end of the filename. A matching extension can be added to any file name using add_extension()
, which will not add the extension if force
is 0 and an extension is already present in the file name.
// copies file name from <in> to <out>// if no extension given, adds <ext>// <force> is nonzero, ALWAYS replaces extensionvoid add_extension(char *in, char *out, char *ext, int force);
An ASC file is opened by calling asc_open_file()
, which returns 0 if it was successful. The file can be closed by asc_close_file()
.
// opens ASC file (adds .ASC extension if none given)// returns 0 if OK, -1 if errorint asc_open_file(char *fname);// closes ASC file if openvoid asc_close_file(void);
Each line in the ASC file is first read by asc_read_line()
, which scans the file until it finds the first non-blank line. It separates out and returns a pointer to the first word in the line, which can be used to determine the data in the line.
// Starts new ASC file line, returns first word// skips blank lines and comments// returns "" if end of file// NOTE: word string is volatile!char *asc_read_line(void);
Sometimes a line being processed must be re-read by asc_read_line()
. The file position can be restored by asc_rewind_line()
so the current line can be read again.
// rewinds to start of line:// asc_read_line() can again be used to read first word.void asc_rewind_line(void);
The first word of a line is returned by asc_read_line()
. This word must be compared to expected line-type identifiers such as "ESACC", in order to determine how to process the line. The comparison is best done with the match()
and matchpart()
macros. These compare the target string argument to the variable token
, where the return value of asc_read_line()
should be stored. These macros perform a comparison without considering uppercase or lowercase characters. The matchpart()
macro will only check the first characters in token
, stopping when it reaches the end of the target string. For example, the target string "ES" would match "ESS" and "ES ET" but not "ET".
// this will check for full-word match#define match(a) (!_cmpnocase(token,a))// this will check the first characters of the word#define matchpart(a) (!_cmpnocasepart(token,a))
Numbers or words can be read from the current line using asc_long()
, asc_float()
, and asc_string()
. If the number was a missing value ("." in the ASC file) the value MISSING_VALUE
is returned. Each number or word is read from the line from left to right. The entire line from the current read point can be fetched with asc_rest_of_line()
.
// NOTE: when reading a float or long,// if missing data ('.') or non-numerical values// are encountered, MISSING_DATA is returned.#define MISSING_DATA -32768// reads integer or long value// returns MISSING_VALUE if '.' or non-numericlong asc_long(void);// reads floating-point value// returns MISSING_VALUE if '.' or non-numericdouble asc_float(void);// returns pointer to next token (VOLATILE)// returns "" if end of linechar *asc_string(void);// returns pointer to rest of line text (VOLATILE)// returns "" if end of linechar *asc_rest_of_line(void);
For lines of unknown length, the function asc_at_eol()
can be called to test if there are any more words or numbers to read. When the line has been read, asc_errors()
will return 0 if no errors were found, or the code of the last error found.
// returns 0 if more tokens available, 1 if at end of lineint asc_at_eol(void);// returns 0 if no errors so far in line, else error codeint asc_errors(void);#define NUMBER_EXPECTED -32000 // read string when number expected#define LINE_TOO_SHORT -32001 // missing token#define SYNTAX_ERROR -32002 // unexpected word
The "START" line and several following lines in an ASC file contain information on the data that is available. These lines can be read with asc_start_block()
, which should be called to finish reading any line with "START" as its first word.
// Before calling, the token "START" must have been read by asc_read_line()// Scans the file for all block-start data// Sets data flags, selects eye to use for event processing// Returns: 0 if OK, else error encounteredint asc_start_block(void);
The asc_start_block()
function processes the ASC file, setting these boolean variables to indicate what data is present in the trial:
// This reads all data associated with block starts// It sets flags, and selects the eye to processextern int block_has_left; // nonzero if left eye data presentextern int block_has_right; // nonzero if right eye data presentextern int block_has_samples; // nonzero if samples presentextern int block_has_events; // nonzero if events presentextern int samples_have_velocity; // nonzero if samples have velocity dataextern int samples_have_resolution; // nonzero if samples have resolution dataextern int events_have_resolution; // nonzero if events have resolution dataextern int pupil_size_is_diameter; // 0 if pupil units is area, 1 if diameter
For binocular recordings, one eye's data may need to be selected to be processed. If the variable preferred_eye
is set to LEFT_EYE
or RIGHT_EYE
, then asc_start_block()
will determine if that eye's data is available. If not, the other eye's data will be selected. The selected eye is stored in the variable selected_eye
. You generally won't have to worry about eye selection, as the eye event-reading functions can do monocular eye data filtering.
// After opening the file, set preferred_eye// to the code for the eye you wish to process// After starting a data block, selected_eye// will contain the code for the eye that can be used// (depends on preferred_eye and which eye(s) data is available)#define LEFT_EYE 0 // codes for eyes (also index into sample data)#define RIGHT_EYE 1extern int preferred_eye; // which eye's data to use if presentextern int selected_eye; // eye to select events from
If a line starts with a number instead of a word, it contains a sample. If your analyzer expects to read both samples and events, it should call asc_read_sample()
to read each line. If this returns 1, it should then call asc_read_line()
and process an event as usual.
The data from the sample is placed in the structure a_sample
, of type ASC_SAMPLE
defined below.
// Reads a file line, processes if sample// Places data in the a_sample structure// If not sample, rewinds line for event processing// returns -1 if error, 0 if sample read,// else 1 (not sample: use asc_read_line() as usual).// x, y, p read to index of proper eye in a_sample// For example, if right-eye data only,// data is placed in a_sample.x[RIGHT_EYE],// but not in a_sample.x[LEFT_EYE].// Both are filled if binocular data.int asc_read_sample(void);typedef struct {UINT32 t; // time of samplefloat x[2]; // X position (left and right eyes)float y[2]; // Y position (left and right eyes)float p[2]; // pupil size (left and right eyes)float resx; // resolution (if samples_have_resolution==1)float resy;float velx[2]; // velocity (if samples_have_velocity==1)float vely[2]; // (left and right eyes)} ASC_SAMPLE;extern ASC_SAMPLE a_sample; // asc_read_sample() places data here
Data from ASC events may be read by special functions. Which function to call is determined by the first word read from the ASC file line with asc_read_line()
. All event routines return 0 if no error occurred, or -1 if an error occurred. Eye data event readers can optionally filter out events from the non-processed eye in binocular recordings, and return 1 if the event is to be discarded.
A "BUTTON" line contains a button press or release event. The data from this line is stored in the variable a_button
. This includes the time of the button press, the button number, and the state (0 if released and 1 if pressed).
// must have read "BUTTON" with asc_read_line() before calling// returns -1 if error, 0 if read OKint asc_read_button(void);// "BUTTON" event data structure, filled by asc_read_button()typedef struct {UINT32 t; // time of button pressint b; // button number (1-8)int s; // button change (1=pressed, 0=released)} ASC_BUTTON;extern ASC_BUTTON a_button; // asc_read_button() places data here
Eye events are of two types: start events which contain only the time of a saccade, blink, or fixation; and end events which contain both start and end time (actually the time of the last sample fully within the saccade, blink or fixation), plus summary data. All eye event reading functions can filter the data by eye: If their argument is 1 then wrong-eye events will be discarded and 1 returned by the function.
All eye start events ("SSACC", "SFIX", and "SBLINK") are read by asc_start_event()
, which fills in the variable a_start
.
// must have read "SBLINK", "SSACC", or "SFIX"// with asc_read_line() before calling// returns -1 if error, 0 if skipped (wrong eye), 1 if read// if <select_eye>==1, will skip unselected eye in binocular dataint asc_read_start(int select_eye);// "SBLINK", "SSACC", "SFIX" events, read by asc_read_start()typedef struct {int eye; // eyeUINT32 st; // start time} ASC_START;extern ASC_START a_start; // asc_read_start() places data here
Fixation end events ("EFIX") are read by asc_read_efix()
which fills the variable a_efix
with the start and end times, and average gaze position, pupil size, and angular resolution for the fixation.
int asc_read_efix(int select_eye);// "EFIX" event data structure, filled by asc_read_efix()typedef struct {int eye; // eyeUINT32 st; // start timeUINT32 et; // end timeUINT32 d; // durationfloat x; // X positionfloat y; // Y positionfloat p; // pupilfloat resx; // resolution (if events_have_resolution==1)float resy;} ASC_EFIX;// Global event data, filled by asc_read functionsextern ASC_EFIX a_efix; // asc_read_efix() places data here
Saccade end events ("ESACC") are read by asc_read_esacc()
which fills the variable a_esacc
with the start and end times, start and end gaze position, duration, amplitude, and peak velocity. Angular resolution may also be available.
// must have read "ESACC" with asc_read_line() before calling// returns -1 if error, 0 if skipped (wrong eye), 1 if read// if <select_eye>==1, will skip unselected eye in binocular dataint asc_read_esacc(int select_eye);// "ESACC" event data structure, filled by asc_read_esacc()typedef struct {int eye; // eyeUINT32 st; // start timeUINT32 et; // end timeUINT32 d; // durationfloat sx; // start X positionfloat sy; // start Y positionfloat ex; // end X positionfloat ey; // end Y positionfloat ampl; // amplitude in degreesfloat pvel; // peak velocity, degr/secfloat resx; // resolution (if events_have_resolution==1)float resy;} ASC_ESACC;extern ASC_ESACC a_esacc; // asc_read_esacc() places data here
Blink end events ("EBLINK") mark the reappearance of the eye pupil. These are read by asc_read_eblink()
which fills the variable a_eblink
with the start and end times, and duration. Blink events may be used to label the next "ESACC" event as being part of a blink and not a true saccade.
// must have read "EBLINK" with asc_read_line() before calling// returns -1 if error, 0 if skipped (wrong eye), 1 if read// if <select_eye>==1, will skip unselected eye in binocular dataint asc_read_eblink(int select_eye);// "EBLINK" event data structure, filled by asc_read_eblink()typedef struct {int eye; // eyeUINT32 st; // start timeUINT32 et; // end timeUINT32 d; // duration} ASC_EBLINK;extern ASC_EBLINK a_eblink; // asc_read_eblink() places data here
It is common in experimental analysis to process a trial or an entire file more than once: for example, to determine statistical measures to reject outliers in the data. Several functions are supplied to allow rewinding of the ASC file processing to an earlier position.
The simplest is to rewind to the start of a trial (the "START" line, not the "TRIALID" message), which requires no setup.
int asc_rewind_trial(void);
You can also declare a variable of type BOOKMARK, then use it to record the current file position with asc_set_bookmark()
. When asc_goto_bookmark()
is called later, reading of the file will resume with the line where the bookmark was set. Bookmarks may be set anywhere inside or outside of a trial.
typedef struct {long fpos;long blkpos;int seleye; // which eye's data to use if presentint has_left;int has_right;int has_samples;int has_events;int vel;int sam_res;int evt_res;int pupil_dia;} BOOKMARK;int asc_set_bookmark(BOOKMARK *bm);int asc_goto_bookmark(BOOKMARK *bm);
The sac_proc.c source file implements a complete analyzer for an express saccade gap/overlap express saccade experiment. The experiment was run for one participant, then converted to data.asc using EDF2ASC.
The first step in writing an analyzer is to know how data is to be analyzed, what data is present, what measures are required, and under what circumstances the trial should be discarded.
In this experiment a target was displayed at the center of the display, and served as the drift correction target as well as part of the trial. After a short delay, a new target was drawn to the left or right of center. The center target was erased either before, at the same time as, or after the new target appeared. The participant ended the trial by pressing the left button (#2) or right button (#3).
For this very rudimentary analysis, the number of saccades made during the entire trial were counted, and the button response was scored as correct or incorrect. Trials were flagged if a blink had occurred at any time during recording. The most important measure is the time from the new target appearing to the start of the next saccade.
This is the ASC file content for a typical trial:
MSG 2436129 TRIALID T1Rg200 0 0 220 200PRESCALER 1VPRESCALER 1SFIX L 2436164SFIX R 2436164MSG 2436678 SYNCTIMEMSG 2436678 DRAWN NEW TARGETEFIX L 2436164 2436832 672 321.7 246.8 1422EFIX R 2436164 2436832 672 321.7 242.1 1683SSACC L 2436836SSACC R 2436836ESACC R 2436836 2436872 40 323.6 247.4 496.5 250.2 6.75 276.4SFIX R 2436876ESACC L 2436836 2436876 44 324.3 251.6 500.5 247.4 6.93 273.3MSG 2436878 ERASED OLD TARGETSFIX L 2436880EFIX R 2436876 2437000 128 492.7 249.2 1682SSACC R 2437004EFIX L 2436880 2437004 128 499.8 245.0 1323SSACC L 2437008ESACC L 2437008 2437028 24 506.6 242.2 565.4 251.1 2.35 151.4ESACC R 2437004 2437028 28 493.9 248.5 551.7 258.4 2.29 147.2SFIX L 2437032SFIX R 2437032EFIX L 2437032 2437500 472 556.2 248.2 1281EFIX R 2437032 2437500 472 546.2 250.2 1653BUTTON 2437512 2 1MSG 2437521 ENDBUTTON 2END 2437523 EVENTS RES 25.70 24.98MSG 2437628 TRIAL_RESULT 2MSG 2437628 TRIAL OK
These messages and events are placed in every trial:
The program begins by requesting the file to be analyzed. You can also supply it on the command line from the command prompt.
The function process_file()
analyzes a singe ASC file. If the analyzer were designed to process multiple files, this function would be called once per file.
// call with file name to process// returns error flag if can't open fileint process_file(char *fname){char ascname[120];char *token;long etime;add_extension(fname, ascname, "asc", 0); // make file nameif(asc_open_file(ascname)) return -1; // can't open file!!!while(1){token = asc_read_line(); // get first word on lineif(token[0]==0) break;else if (match("MSG")) // message{etime = asc_long(); // message timetoken = asc_string(); // first word of messageif(match("TRIALID")){process_trial(); // process trial data}} // IGNORE EVERYTHING ELSE}asc_close_file();return 0;}
The extension ".asc" is added to the file name (unless an extension is already specified). The file is then opened with asc_open_file()
.
The program now loops until it finds a message line. It does this by calling asc_read_line()
, and checking if the first word in the line is "MSG". The time filed is read with asc_long()
, and the first word of the message read with asc_string()
. Your messages should all be designed so the first word identifies the message uniquely. In this case, we are looking for the "TRIALID" message that identifies a trial and marks its start. We could also look for other messages that were written at the start of the experiment or between trials by using match()
for each case in this loop.
Once we have located a trial by finding the "TRIALID" message, we call process_trial()
to read and analyze the trial.
The first thing process_trial()
does is to continue reading the trial information from the TRIALID line. After reading the numbers, a call to asc_errors()
tests to see if any problems were encountered in reading the trial information.
asc_string(); // skip ID stringblock_number = asc_long(); // read datatrial_number = asc_long();target_posn = asc_long();target_delay = asc_long();is_left = (target_posn < 320); // left or right?if(asc_errors()) return -1; // any errors?
In this example, analysis of the trial is handled by a loop that reads a line from the file, determines its type, and processes it. We have a number of data variables that are preset to a value that will not occur in the experiment, such as zero for the time of an event. These allow us to track the point in the trial we are in. In this analysis, the important variables are:
long target_on_time = 0; // set if new target has appearedlong first_sacc_time = 0; // set if a saccade occurred to the targetint response_button = 0; // set if a button was pressedlong button_time = 0; // when the button was pressedint num_saccades = 0; // conts the number of saccadesint have_blink = 0; // set if a blink occurredint is_left; // set if left target offsetint correct_response = 0; // set if correct button was pressed// THESE WERE READ FROM TRIALID MESSAGEint target_posn; // offset of new targetint target_delay; // delay from new target drawn to old erasedint trial_number; // trial and block numberint block_number;char *token; // variables for reading line elementslong etime;
The "START" line marks the point where recording began. We process this line and any other recording data line by calling asc_start_block()
. This will determine whether samples and events are available, and which eye(s) were recorded from.
Each "MSG" line contains a message. The time of the message and first word are read with asc_long()
and asc_string()
, and the first word checked with match()
to determine how to process it. If the first word is "DRAWN", it marks the offset target being drawn and the time is recorded. The message "TRIAL_RESULT" records the response: a button press or timeout if zero.
If the first word in the message was "TRIAL", the second word determines the trial status and is read with asc_string()
. If this is "OK" the trial was recorded successfully and we can process the trial data: in this case, we simply print it out. Otherwise, the trial was aborted and we return without further processing.
Each button press or release event line starts with the word "BUTTON", followed by the time, button number, and state. All this can be read by calling asc_read_button()
, with the result placed in the ASC_BUTTON
structure a_button
. The button number is used to determine if the participant responded correctly.
Lines with "ESACC" as the first word contain end-of-saccade event data, including summary data on the saccade. Call asc_read_esacc()
to read the event: this will return 0 checks if this saccade was produced by the correct eye (determined by the call to the asc_read_start()
function), and reads the line's contents to the ASC_ESACC
structure a_esacc
. This will contain the start and end times and positions of the saccade, the amplitude, and the peak velocity. Saccades with amplitudes of less than 1 degree are ignored in this analysis. The time of the first large saccade that occurs after the offset target appeared is recorded in the variable first_saccade
.
Finally, the "EBLINK" lines mark the end of a blink. In this example, we simply use this to flag that a blink occurred. In a more complex analysis, we would use this event to mark the next "ESACC" and belonging to a blink, not a saccade.
This is the entire analysis loop:
while(1){token = asc_read_line(); // get first word on lineif(token[0]==0) return -1; // end of fileelse if (match("START")) // START: select eye to process{asc_start_block();}else if (match("MSG")) // message{etime = asc_long(); // message timetoken = asc_string(); // first word of messageif(match("DRAWN")) // new target drawn{target_on_time = etime;}else if(match("TRIAL_RESULT")) // trial result{response_button = asc_long();}else if(match("TRIAL")) // trial is OK?{token = asc_string();if(match("OK") && response_button!=0) // report data, only if OK{// A VERY SIMPLE DATA REPORT: FORMAT AS REQUIRED.printf("trial:%d delay:%d sac_rt:%ld but_rt:%ld corr:%d nsac:%d blink:%d\n",trial_number, target_delay, first_sacc_time-target_on_time,button_time-target_on_time,correct_response,num_saccades,have_blink);}return 0; // done trial!}}else if (match("BUTTON")) // button{asc_read_button();if(a_button.s==1 && (a_button.b==3 || a_button.b==2)){button_time = a_button.t;response_button = a_button.b;correct_response = ((is_left==1 && a_button.b==2) || (is_left==0 && a_button.b==3));}}else if (match("ESACC")) // end saccade{if(asc_read_esacc(1)) continue; // skip if wrong eye// ignore if smaller than 1 degree// or if we haven't displayed target yetif(a_esacc.ampl>1.0 && target_on_time>0){num_saccades++;if(num_saccades==1) first_sacc_time = a_esacc.st;}}else if (match("EBLINK")) // blink{have_blink++;}// IGNORE EVERYTHING ELSE}}