Authored by Olivier David

Bug fix for read edf (update of Brainstorm) and maybe ValidateStimNames for VAL (but to be checked)

1 -function [sFile, ChannelMat] = in_fopen_edf(DataFile, ImportOptions) 1 +function [sFile, ChannelMat, ImportOptions] = in_fopen_edf(DataFile, ImportOptions)
2 % IN_FOPEN_EDF: Open a BDF/EDF file (continuous recordings) 2 % IN_FOPEN_EDF: Open a BDF/EDF file (continuous recordings)
3 % 3 %
4 -% USAGE: [sFile, ChannelMat] = in_fopen_edf(DataFile, ImportOptions) 4 +% USAGE: [sFile, ChannelMat, ImportOptions] = in_fopen_edf(DataFile, ImportOptions)
5 5
6 % @============================================================================= 6 % @=============================================================================
7 % This function is part of the Brainstorm software: 7 % This function is part of the Brainstorm software:
8 % https://neuroimage.usc.edu/brainstorm 8 % https://neuroimage.usc.edu/brainstorm
9 % 9 %
10 -% Copyright (c)2000-2019 University of Southern California & McGill University 10 +% Copyright (c)2000-2020 University of Southern California & McGill University
11 % This software is distributed under the terms of the GNU General Public License 11 % This software is distributed under the terms of the GNU General Public License
12 % as published by the Free Software Foundation. Further details on the GPLv3 12 % as published by the Free Software Foundation. Further details on the GPLv3
13 % license can be found at http://www.gnu.org/copyleft/gpl.html. 13 % license can be found at http://www.gnu.org/copyleft/gpl.html.
@@ -21,7 +21,7 @@ function [sFile, ChannelMat] = in_fopen_edf(DataFile, ImportOptions) @@ -21,7 +21,7 @@ function [sFile, ChannelMat] = in_fopen_edf(DataFile, ImportOptions)
21 % For more information type "brainstorm license" at command prompt. 21 % For more information type "brainstorm license" at command prompt.
22 % =============================================================================@ 22 % =============================================================================@
23 % 23 %
24 -% Authors: Francois Tadel, 2012-2018 24 +% Authors: Francois Tadel, 2012-2019
25 25
26 26
27 % Parse inputs 27 % Parse inputs
@@ -184,8 +184,8 @@ if hdr.interrupted @@ -184,8 +184,8 @@ if hdr.interrupted
184 'Brainstorm will read this file as a continuous file ("EDF+C"), the timing of the samples after the first discontinuity will be wrong.' 10 ... 184 'Brainstorm will read this file as a continuous file ("EDF+C"), the timing of the samples after the first discontinuity will be wrong.' 10 ...
185 'This may not cause any major problem unless there are time markers in the file, they might be inaccurate in all the segments >= 2.']); 185 'This may not cause any major problem unless there are time markers in the file, they might be inaccurate in all the segments >= 2.']);
186 end 186 end
187 -else%OD  
188 - hdr.fixinterrupted=1; 187 +else
  188 + hdr.fixinterrupted = 0;
189 end 189 end
190 190
191 191
@@ -365,80 +365,86 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore') @@ -365,80 +365,86 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore')
365 % Sample indices for the current epoch (=record) 365 % Sample indices for the current epoch (=record)
366 SampleBounds = [irec-1,irec] * sFile.header.signal(iEvtChans(ichan)).nsamples - [0,1]; 366 SampleBounds = [irec-1,irec] * sFile.header.signal(iEvtChans(ichan)).nsamples - [0,1];
367 % Read record 367 % Read record
368 - F = char(in_fread(sFile, ChannelMat, 1, SampleBounds, iEvtChans(ichan), ImportOptions));  
369 - % Split after removing the 0 values  
370 - Fsplit = str_split(F(F~=0), 20);  
371 - if isempty(Fsplit)  
372 - continue;  
373 - end  
374 - if ichan == 1  
375 - % Get record time stamp  
376 - t0_rec = str2double(char(Fsplit{1}));  
377 - if (irec == 1)  
378 - t0_file = t0_rec;  
379 - % Find discontinuities  
380 - elseif abs(t0_rec - prev_rec - hdr.reclen) > 1e-8  
381 - % Brainstorm fills partial/interrupted records with zeros  
382 - bstTime = prev_rec + hdr.reclen;  
383 - timeDiff = bstTime - t0_rec;  
384 - % If we want to fix timing, apply skip to initial timestamp  
385 - if hdr.fixinterrupted  
386 - t0_file = t0_file - timeDiff; 368 + F = in_fread(sFile, ChannelMat, 1, SampleBounds, iEvtChans(ichan), ImportOptions);
  369 + % Find all the TALs (Time-stamped Annotations Lists): separated with [20][0]
  370 + % Possible configurations:
  371 + % Onset[21]Duration[20]Annot1[20]Annot2[20]..AnnotN[20][0]
  372 + % Onset[20]Annot1[20]Annot2[20]..AnnotN[20][0]
  373 + % Onset[20]Annot1[20][0]
  374 + iSeparator = [-1, find((F(1:end-1) == 20) & (F(2:end) == 0))];
  375 + % Loop on all the TALs
  376 + for iAnnot = 1:length(iSeparator)-1
  377 + % Get annotation
  378 + strAnnot = char(F(iSeparator(iAnnot)+2:iSeparator(iAnnot+1)-1));
  379 + % Split in blocks with [20]
  380 + splitAnnot = str_split(strAnnot, 20);
  381 + % The first TAL in a record should be indicating the timing of the first sample of the record
  382 + if (iAnnot == 1) && (length(splitAnnot) == 1) && ~any(splitAnnot{1} == 21)
  383 + % Ignore if this is not the first channel
  384 + if (ichan > 1)
  385 + continue;
387 end 386 end
388 - % Warn user of discontinuity  
389 - if timeDiff > 0  
390 - expectMsg = 'blank data';  
391 - else  
392 - expectMsg = 'skipped data'; 387 + % Get record time stamp
  388 + t0_rec = str2double(splitAnnot{1});
  389 + if isempty(t0_rec) || isnan(t0_rec)
  390 + continue;
393 end 391 end
394 - startTime = min(t0_rec - t0_file - [0, timeDiff]); % before and after t0_file adjustment  
395 - endTime = max(t0_rec - t0_file - [0, timeDiff]);  
396 - fprintf('WARNING: Found discontinuity between %.3fs and %.3fs, expect %s in between.\n', startTime, endTime, expectMsg);  
397 - % Create event for users information  
398 - if timeDiff < 0  
399 - endTime = startTime; % no extent in this case, there is skipped time. 392 + if (irec == 1)
  393 + t0_file = t0_rec;
  394 + % Find discontinuities larger than 1 sample
  395 + elseif abs(t0_rec - prev_rec - (irec - prev_irec) * hdr.reclen) > (1 / sFile.prop.sfreq)
  396 + % Brainstorm fills partial/interrupted records with zeros
  397 + bstTime = prev_rec + hdr.reclen;
  398 + timeDiff = bstTime - t0_rec;
  399 + % If we want to fix timing, apply skip to initial timestamp
  400 + if hdr.fixinterrupted
  401 + t0_file = t0_file - timeDiff;
  402 + end
  403 + % Warn user of discontinuity
  404 + if timeDiff > 0
  405 + expectMsg = 'blank data';
  406 + else
  407 + expectMsg = 'skipped data';
  408 + end
  409 + startTime = min(t0_rec - t0_file - [0, timeDiff]); % before and after t0_file adjustment
  410 + endTime = max(t0_rec - t0_file - [0, timeDiff]);
  411 + fprintf('WARNING: Found discontinuity between %.3fs and %.3fs, expect %s in between.\n', startTime, endTime, expectMsg);
  412 + % Create event for users information
  413 + if timeDiff < 0
  414 + endTime = startTime; % no extent in this case, there is skipped time.
  415 + end
  416 + evtList(end+1,:) = {'EDF+D Discontinuity', [startTime; endTime]};
400 end 417 end
401 - evtList(end+1,:) = {'EDF+D Discontinuity', [startTime; endTime]};  
402 - end  
403 - prev_rec = t0_rec;  
404 - end  
405 -  
406 - %% FIXME: There can be multiple text annotations (separated by 20) for a single onset/duration.  
407 - %% The zero characters should not be removed above as they delimit the TALs (Time-stamped Annotations Lists)  
408 - % If there is an initial time: 3 values (ex: "+44.00000+44.47200Event1Event2)  
409 - if (mod(length(Fsplit),2) == 1) && (length(Fsplit) >= 3)  
410 - iStart = 2;  
411 - % If there is no initial time: 2 values (ex: "+44.00000Epoch1)  
412 - elseif (mod(length(Fsplit),2) == 0)  
413 - iStart = 1;  
414 - else  
415 - continue;  
416 - end  
417 - % If there is information on this channel  
418 - for iAnnot = iStart:2:length(Fsplit)  
419 - % If there are no 2 values, skip  
420 - if (iAnnot == length(Fsplit))  
421 - break;  
422 - end  
423 - % Split time in onset/duration  
424 - t_dur = str_split(Fsplit{iAnnot}, 21);  
425 - % Get time and label  
426 - t = str2double(t_dur{1});  
427 - label = Fsplit{iAnnot+1};  
428 - if (length(t_dur) > 1)  
429 - duration = str2double(t_dur{2});  
430 - % Exclude 1-sample long events  
431 - if (round(duration .* sFile.prop.sfreq) <= 1) 418 + prev_rec = t0_rec;
  419 + prev_irec = irec;
  420 + % Regular TAL: indicating a marker
  421 + else
  422 + % Split time in onset/duration
  423 + splitTime = str_split(splitAnnot{1}, 21);
  424 + % Get time
  425 + t = str2double(splitTime{1});
  426 + if isempty(t) || isnan(t)
  427 + continue;
  428 + end
  429 + % Get duration
  430 + if (length(splitTime) > 1)
  431 + duration = str2double(splitTime{2});
  432 + % Exclude 1-sample long events
  433 + if isempty(duration) || isnan(duration) || (round(duration .* sFile.prop.sfreq) <= 1)
  434 + duration = 0;
  435 + end
  436 + else
432 duration = 0; 437 duration = 0;
433 end 438 end
434 - else  
435 - duration = 0;  
436 - end  
437 - if isempty(t) || isnan(t) || isempty(label) || (~isempty(duration) && isnan(duration))  
438 - continue; 439 + % Unnamed event
  440 + if (length(splitAnnot) == 1) || isempty(splitAnnot{2})
  441 + splitAnnot{2} = 'Unnamed';
  442 + end
  443 + % Create one event for each label in the TAL
  444 + for iLabel = 2:length(splitAnnot)
  445 + evtList(end+1,:) = {splitAnnot{iLabel}, (t-t0_file) + [0;duration]};
  446 + end
439 end 447 end
440 - % Add to list of read events  
441 - evtList(end+1,:) = {label, (t-t0_file) + [0;duration]};  
442 end 448 end
443 end 449 end
444 end 450 end
@@ -471,9 +477,9 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore') @@ -471,9 +477,9 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore')
471 end 477 end
472 478
473 % BDF Status line 479 % BDF Status line
474 - elseif strcmpi(sFile.format, 'EEG-BDF') 480 + elseif strcmpi(sFile.format, 'EEG-BDF') && ~strcmpi(ImportOptions.EventsTrackMode, 'ignore')
475 % Ask how to read the events 481 % Ask how to read the events
476 - events = process_evt_read('Compute', sFile, ChannelMat, ChannelMat.Channel(iEvtChans).Name, ImportOptions.EventsTrackMode); 482 + [events, ImportOptions.EventsTrackMode] = process_evt_read('Compute', sFile, ChannelMat, ChannelMat.Channel(iEvtChans).Name, ImportOptions.EventsTrackMode);
477 if isequal(events, -1) 483 if isequal(events, -1)
478 sFile = []; 484 sFile = [];
479 ChannelMat = []; 485 ChannelMat = [];
@@ -490,6 +496,4 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore') @@ -490,6 +496,4 @@ if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore')
490 end 496 end
491 end 497 end
492 498
493 -  
494 -  
495 - 499 +
1 -function F = in_fread_edf(sFile, sfid, SamplesBounds, ChannelsRange)  
2 -% IN_FREAD_EDF: Read a block of recordings from a EDF/BDF file 1 +function [sFile, ChannelMat, ImportOptions] = in_fopen_edf(DataFile, ImportOptions)
  2 +% IN_FOPEN_EDF: Open a BDF/EDF file (continuous recordings)
3 % 3 %
4 -% USAGE: F = in_fread_edf(sFile, sfid, SamplesBounds, ChannelsRange)  
5 -% F = in_fread_edf(sFile, sfid, SamplesBounds) : Read all channels  
6 -% F = in_fread_edf(sFile, sfid) : Read all channels, all the times 4 +% USAGE: [sFile, ChannelMat, ImportOptions] = in_fopen_edf(DataFile, ImportOptions)
7 5
8 % @============================================================================= 6 % @=============================================================================
9 % This function is part of the Brainstorm software: 7 % This function is part of the Brainstorm software:
10 % https://neuroimage.usc.edu/brainstorm 8 % https://neuroimage.usc.edu/brainstorm
11 % 9 %
12 -% Copyright (c)2000-2019 University of Southern California & McGill University 10 +% Copyright (c)2000-2020 University of Southern California & McGill University
13 % This software is distributed under the terms of the GNU General Public License 11 % This software is distributed under the terms of the GNU General Public License
14 % as published by the Free Software Foundation. Further details on the GPLv3 12 % as published by the Free Software Foundation. Further details on the GPLv3
15 % license can be found at http://www.gnu.org/copyleft/gpl.html. 13 % license can be found at http://www.gnu.org/copyleft/gpl.html.
@@ -23,203 +21,477 @@ function F = in_fread_edf(sFile, sfid, SamplesBounds, ChannelsRange) @@ -23,203 +21,477 @@ function F = in_fread_edf(sFile, sfid, SamplesBounds, ChannelsRange)
23 % For more information type "brainstorm license" at command prompt. 21 % For more information type "brainstorm license" at command prompt.
24 % =============================================================================@ 22 % =============================================================================@
25 % 23 %
26 -% Authors: Francois Tadel, 2012-2017  
27 -  
28 -%% ===== PARSE INPUTS =====  
29 -nChannels = sFile.header.nsignal;  
30 -iChanAnnot = find(strcmpi({sFile.header.signal.label}, 'EDF Annotations'));  
31 -iBadChan = find(sFile.channelflag == -1);  
32 -iChanSignal = setdiff(1:nChannels, iChanAnnot);  
33 -iChanWrongRate = find([sFile.header.signal(iChanSignal).sfreq] ~= max([sFile.header.signal(setdiff(iChanSignal,iBadChan)).sfreq]));  
34 -iChanSkip = union(iChanAnnot, iChanWrongRate);  
35 -if (nargin < 4) || isempty(ChannelsRange)  
36 - ChannelsRange = [1, nChannels];  
37 -end  
38 -if (nargin < 3) || isempty(SamplesBounds)  
39 - SamplesBounds = [0, sFile.header.nrec * sFile.header.signal(ChannelsRange(1)).nsamples - 1];  
40 -end  
41 -nTimes = sFile.header.reclen * sFile.header.signal(ChannelsRange(1)).sfreq;  
42 -iTimes = SamplesBounds(1):SamplesBounds(2);  
43 -% Block of times/channels to extract  
44 -nReadChannels = double(ChannelsRange(2) - ChannelsRange(1) + 1);  
45 -% Read annotations instead of real data ?  
46 -isAnnotOnly = ~isempty(iChanAnnot) && (ChannelsRange(1) == ChannelsRange(2)) && ismember(ChannelsRange(1), iChanAnnot);  
47 -isBdfStatus = strcmpi(sFile.format, 'EEG-BDF') && (ChannelsRange(1) == ChannelsRange(2)) && strcmpi(sFile.header.signal(ChannelsRange(1)).label, 'Status');  
48 -% Data channels to read  
49 -if isAnnotOnly  
50 - % Read only on annotation channel  
51 - iChanF = 1;  
52 -else  
53 - % Remove all the annotation channels from the list of channels to read  
54 - iChanF = setdiff(ChannelsRange(1):ChannelsRange(2), iChanSkip) - ChannelsRange(1) + 1;  
55 -% if any(diff(iChanF) ~= 1)  
56 -% error('All the data channels to read from the file must be contiguous (EDF Annotation channels must be at the end of the list).');  
57 -% end  
58 - if any(diff(iChanF) ~= 1)  
59 - iChanLast = find(diff(iChanF) ~= 1, 1);  
60 - iChanF = iChanF(1:iChanLast);  
61 - else  
62 - iChanLast = length(iChanF);  
63 - end  
64 - ChannelsRange = [iChanF(1), iChanF(iChanLast)] + ChannelsRange(1) - 1;  
65 -end  
66 -% Cannot read channels with different sampling rates at the same time  
67 -if (ChannelsRange(1) ~= ChannelsRange(2))  
68 - allFreq = [sFile.header.signal(ChannelsRange(1):ChannelsRange(2)).nsamples];  
69 - if any(allFreq ~= allFreq(1))  
70 - error(['Cannot read channels with mixed sampling rates at the same time.' 10 ...  
71 - 'Mark as bad channels with different sampling rates than EEG.' 10 ...  
72 - '(right-click on data file > Good/bad channels > Edit good/bad channels)']);  
73 - end 24 +% Authors: Francois Tadel, 2012-2019
  25 +
  26 +
  27 +% Parse inputs
  28 +if (nargin < 2) || isempty(ImportOptions)
  29 + ImportOptions = db_template('ImportOptions');
74 end 30 end
75 31
76 32
77 -%% ===== READ ALL NEEDED EPOCHS =====  
78 -% Detect which epochs are necessary for the range of data selected  
79 -epochRange = floor(SamplesBounds ./ nTimes);  
80 -epochsToRead = epochRange(1) : epochRange(2);  
81 -% Initialize block of data to read  
82 -if isAnnotOnly  
83 - F = zeros(nReadChannels, 2 * length(iTimes)); 33 +%% ===== READ HEADER =====
  34 +% Open file
  35 +fid = fopen(DataFile, 'r', 'ieee-le');
  36 +if (fid == -1)
  37 + error('Could not open file');
  38 +end
  39 +% Read all fields
  40 +hdr.version = fread(fid, [1 8], 'uint8=>char'); % Version of this data format ('0 ' for EDF, [255 'BIOSEMI'] for BDF)
  41 +hdr.patient_id = fread(fid, [1 80], '*char'); % Local patient identification
  42 +hdr.rec_id = fread(fid, [1 80], '*char'); % Local recording identification
  43 +hdr.startdate = fread(fid, [1 8], '*char'); % Startdate of recording (dd.mm.yy)
  44 +hdr.starttime = fread(fid, [1 8], '*char'); % Starttime of recording (hh.mm.ss)
  45 +hdr.hdrlen = str2double(fread(fid, [1 8], '*char')); % Number of bytes in header record
  46 +hdr.unknown1 = fread(fid, [1 44], '*char'); % Reserved ('24BIT' for BDF)
  47 +hdr.nrec = str2double(fread(fid, [1 8], '*char')); % Number of data records (-1 if unknown)
  48 +hdr.reclen = str2double(fread(fid, [1 8], '*char')); % Duration of a data record, in seconds
  49 +hdr.nsignal = str2double(fread(fid, [1 4], '*char')); % Number of signals in data record
  50 +% Check file integrity
  51 +if isnan(hdr.nsignal) || isempty(hdr.nsignal) || (hdr.nsignal ~= round(hdr.nsignal)) || (hdr.nsignal < 0)
  52 + error('File header is corrupted.');
  53 +end
  54 +% Read values for each nsignal
  55 +for i = 1:hdr.nsignal
  56 + hdr.signal(i).label = strtrim(fread(fid, [1 16], '*char'));
  57 +end
  58 +for i = 1:hdr.nsignal
  59 + hdr.signal(i).type = strtrim(fread(fid, [1 80], '*char'));
  60 +end
  61 +for i = 1:hdr.nsignal
  62 + hdr.signal(i).unit = strtrim(fread(fid, [1 8], '*char'));
  63 +end
  64 +for i = 1:hdr.nsignal
  65 + hdr.signal(i).physical_min = str2double(fread(fid, [1 8], '*char'));
  66 +end
  67 +for i = 1:hdr.nsignal
  68 + hdr.signal(i).physical_max = str2double(fread(fid, [1 8], '*char'));
  69 +end
  70 +for i = 1:hdr.nsignal
  71 + hdr.signal(i).digital_min = str2double(fread(fid, [1 8], '*char'));
  72 +end
  73 +for i = 1:hdr.nsignal
  74 + hdr.signal(i).digital_max = str2double(fread(fid, [1 8], '*char'));
  75 +end
  76 +for i = 1:hdr.nsignal
  77 + hdr.signal(i).filters = strtrim(fread(fid, [1 80], '*char'));
  78 +end
  79 +for i = 1:hdr.nsignal
  80 + hdr.signal(i).nsamples = str2num(fread(fid, [1 8], '*char'));
  81 +end
  82 +for i = 1:hdr.nsignal
  83 + hdr.signal(i).unknown2 = fread(fid, [1 32], '*char');
  84 +end
  85 +% Unknown record size, determine correct nrec
  86 +if (hdr.nrec == -1)
  87 + datapos = ftell(fid);
  88 + fseek(fid, 0, 'eof');
  89 + endpos = ftell(fid);
  90 + hdr.nrec = floor((endpos - datapos) / (sum([hdr.signal.nsamples]) * 2));
  91 +end
  92 +% Close file
  93 +fclose(fid);
  94 +
  95 +
  96 +%% ===== RECONSTRUCT INFO =====
  97 +% Individual signal gain
  98 +for i = 1:hdr.nsignal
  99 + % Interpet units
  100 + switch (hdr.signal(i).unit)
  101 + case 'mV', unit_gain = 1e3;
  102 + case {'uV', char([166 204 86])}, unit_gain = 1e6;
  103 + otherwise, unit_gain = 1;
  104 + end
  105 + % Check min/max values
  106 + if isempty(hdr.signal(i).digital_min) || isnan(hdr.signal(i).digital_min)
  107 + disp(['EDF> Warning: The digitial minimum is not set for channel "' hdr.signal(i).label '".']);
  108 + hdr.signal(i).digital_min = -2^15;
  109 + end
  110 + if isempty(hdr.signal(i).digital_max) || isnan(hdr.signal(i).digital_max)
  111 + disp(['EDF> Warning: The digitial maximum is not set for channel "' hdr.signal(i).label '".']);
  112 + hdr.signal(i).digital_max = -2^15;
  113 + end
  114 + if isempty(hdr.signal(i).physical_min) || isnan(hdr.signal(i).physical_min)
  115 + disp(['EDF> Warning: The physical minimum is not set for channel "' hdr.signal(i).label '".']);
  116 + hdr.signal(i).physical_min = hdr.signal(i).digital_min;
  117 + end
  118 + if isempty(hdr.signal(i).physical_max) || isnan(hdr.signal(i).physical_max)
  119 + disp(['EDF> Warning: The physical maximum is not set for channel "' hdr.signal(i).label '".']);
  120 + hdr.signal(i).physical_max = hdr.signal(i).digital_max;
  121 + end
  122 + if (hdr.signal(i).physical_min >= hdr.signal(i).physical_max)
  123 + disp(['EDF> Warning: Physical maximum larger than minimum for channel "' hdr.signal(i).label '".']);
  124 + hdr.signal(i).physical_min = hdr.signal(i).digital_min;
  125 + hdr.signal(i).physical_max = hdr.signal(i).digital_max;
  126 + end
  127 + % Calculate and save channel gain
  128 + hdr.signal(i).gain = unit_gain ./ (hdr.signal(i).physical_max - hdr.signal(i).physical_min) .* (hdr.signal(i).digital_max - hdr.signal(i).digital_min);
  129 + hdr.signal(i).offset = hdr.signal(i).physical_min ./ unit_gain - hdr.signal(i).digital_min ./ hdr.signal(i).gain;
  130 + % Error: The number of samples is not specified
  131 + if isempty(hdr.signal(i).nsamples)
  132 + % If it is not the first electrode: try to use the previous one
  133 + if (i > 1)
  134 + disp(['EDF> Warning: The number of samples is not specified for channel "' hdr.signal(i).label '".']);
  135 + hdr.signal(i).nsamples = hdr.signal(i-1).nsamples;
  136 + else
  137 + error(['The number of samples is not specified for channel "' hdr.signal(i).label '".']);
  138 + end
  139 + end
  140 + hdr.signal(i).sfreq = hdr.signal(i).nsamples ./ hdr.reclen;
  141 +end
  142 +% Find annotations channel
  143 +iAnnotChans = find(strcmpi({hdr.signal.label}, 'EDF Annotations')); % Mutliple "EDF Annotation" channels allowed in EDF+
  144 +iStatusChan = find(strcmpi({hdr.signal.label}, 'Status'), 1); % Only one "Status" channel allowed in BDF
  145 +iOtherChan = setdiff(1:hdr.nsignal, [iAnnotChans iStatusChan]);
  146 +% % Remove channels with lower sampling rates
  147 +% iIgnoreChan = find([hdr.signal(iOtherChan).sfreq] < max([hdr.signal(iOtherChan).sfreq])); % Ignore all the channels with lower sampling rate
  148 +% if ~isempty(iIgnoreChan)
  149 +% iOtherChan = setdiff(iOtherChan, iIgnoreChan);
  150 +% end
  151 +% Get all the other channels
  152 +if isempty(iOtherChan)
  153 + error('This file does not contain any data channel.');
  154 +end
  155 +% Read events preferencially from the EDF Annotations track
  156 +if ~isempty(iAnnotChans)
  157 + iEvtChans = iAnnotChans;
  158 +elseif ~isempty(iStatusChan)
  159 + iEvtChans = iStatusChan;
84 else 160 else
85 - F = zeros(nReadChannels, length(iTimes));  
86 -end  
87 -% Marker that we increment when we add data to F  
88 -iF = 1;  
89 -% Read all the needed epochs  
90 -for i = 1:length(epochsToRead)  
91 - % Find the samples to read from this epoch  
92 - BoundsEpoch = nTimes * epochsToRead(i) + [0, nTimes-1];  
93 - BoundsRead = [max(BoundsEpoch(1), iTimes(1)), ...  
94 - min(BoundsEpoch(2), iTimes(end))];  
95 - iTimeRead = BoundsRead(1):BoundsRead(2);  
96 - % Convert this samples into indices in this very epoch  
97 - iTimeRead = iTimeRead - nTimes * epochsToRead(i);  
98 - % New indices to read  
99 - if isAnnotOnly  
100 - iNewF = iF:(iF + 2*length(iTimeRead) - 1); 161 + iEvtChans = [];
  162 +end
  163 +% % Detect channels with inconsistent sampling frenquency
  164 +% iErrChan = find([hdr.signal(iOtherChan).sfreq] ~= hdr.signal(iOtherChan(1)).sfreq);
  165 +% iErrChan = setdiff(iErrChan, iAnnotChans);
  166 +% if ~isempty(iErrChan)
  167 +% error('Files with mixed sampling rates are not supported yet.');
  168 +% end
  169 +% Detect interrupted signals (time non-linear)
  170 +hdr.interrupted = ischar(hdr.unknown1) && (length(hdr.unknown1) >= 5) && isequal(hdr.unknown1(1:5), 'EDF+D');
  171 +if hdr.interrupted
  172 + if ImportOptions.DisplayMessages
  173 + [res, isCancel] = java_dialog('question', ...
  174 + ['Interrupted EDF file ("EDF+D") detected. It is recommended to convert it' 10 ...
  175 + 'to a continuous ("EDF+C") file first. Do you want to continue reading this' 10 ...
  176 + 'file as continuous and attempt to fix the timing of event markers?' 10 ...
  177 + 'NOTE: This may not work as intended, use at your own risk!']);
  178 + hdr.fixinterrupted = ~isCancel && strcmpi(res, 'yes');
101 else 179 else
102 - iNewF = iF:(iF + length(iTimeRead) - 1); 180 + hdr.fixinterrupted = 1;
  181 + end
  182 + if ~hdr.fixinterrupted
  183 + warning(['Interrupted EDF file ("EDF+D"): requires conversion to "EDF+C". ' 10 ...
  184 + 'Brainstorm will read this file as a continuous file ("EDF+C"), the timing of the samples after the first discontinuity will be wrong.' 10 ...
  185 + 'This may not cause any major problem unless there are time markers in the file, they might be inaccurate in all the segments >= 2.']);
103 end 186 end
104 - % Read epoch (full or partial)  
105 - F(iChanF,iNewF) = edf_read_epoch(sFile, sfid, epochsToRead(i), iTimeRead, ChannelsRange, isAnnotOnly, isBdfStatus);  
106 - % Increment marker  
107 - iF = iF + length(iTimeRead); 187 +else
  188 + hdr.fixinterrupted = 0;
108 end 189 end
109 190
110 191
111 - 192 +%% ===== CREATE BRAINSTORM SFILE STRUCTURE =====
  193 +% Initialize returned file structure
  194 +sFile = db_template('sfile');
  195 +% Add information read from header
  196 +sFile.byteorder = 'l';
  197 +sFile.filename = DataFile;
  198 +if (uint8(hdr.version(1)) == uint8(255))
  199 + sFile.format = 'EEG-BDF';
  200 + sFile.device = 'BDF';
  201 +else
  202 + sFile.format = 'EEG-EDF';
  203 + sFile.device = 'EDF';
112 end 204 end
  205 +sFile.header = hdr;
  206 +% Comment: short filename
  207 +[tmp__, sFile.comment, tmp__] = bst_fileparts(DataFile);
  208 +% No info on bad channels
  209 +sFile.channelflag = ones(hdr.nsignal,1);
  210 +% Acquisition date
  211 +sFile.acq_date = str_date(hdr.startdate);
113 212
114 213
115 214
116 -%% ===== READ ONE EPOCH =====  
117 -function F = edf_read_epoch(sFile, sfid, iEpoch, iTimes, ChannelsRange, isAnnotOnly, isBdfStatus)  
118 - % ===== COMPUTE OFFSETS =====  
119 - nTimes = sFile.header.reclen * [sFile.header.signal.sfreq];  
120 - nReadTimes = length(iTimes);  
121 - nReadChannels = double(ChannelsRange(2) - ChannelsRange(1) + 1);  
122 - iChannels = ChannelsRange(1):ChannelsRange(2);  
123 - % Check that all the channels selected have the same freq rate  
124 - if any(nTimes(iChannels) ~= nTimes(iChannels(1)))  
125 - error('Cannot read at the same signals with different sampling frequency.');  
126 - end  
127 - % Size of one value  
128 - if strcmpi(sFile.format, 'EEG-BDF')  
129 - % BDF: int24 => 3 bytes  
130 - bytesPerVal = 3;  
131 - % Reading status or regular channel  
132 - if isBdfStatus  
133 - dataClass = 'ubit24';  
134 - else  
135 - dataClass = 'bit24'; 215 +%% ===== PROCESS CHANNEL NAMES/TYPES =====
  216 +% Try to split the channel names in "TYPE NAME"
  217 +SplitType = repmat({''}, 1, hdr.nsignal);
  218 +SplitName = repmat({''}, 1, hdr.nsignal);
  219 +for i = 1:hdr.nsignal
  220 + % Removing trailing dots (eg. "Fc5." instead of "FC5", as in: https://www.physionet.org/pn4/eegmmidb/)
  221 + if (hdr.signal(i).label(end) == '.') && (length(hdr.signal(i).label) > 1)
  222 + hdr.signal(i).label(end) = [];
  223 + if (hdr.signal(i).label(end) == '.') && (length(hdr.signal(i).label) > 1)
  224 + hdr.signal(i).label(end) = [];
  225 + if (hdr.signal(i).label(end) == '.') && (length(hdr.signal(i).label) > 1)
  226 + hdr.signal(i).label(end) = [];
  227 + end
136 end 228 end
137 - else  
138 - % EDF: int16 => 2 bytes  
139 - bytesPerVal = 2;  
140 - dataClass = 'int16';  
141 - isBdfStatus = 0;  
142 end 229 end
143 - % Offset of the beginning of the recordings in the file  
144 - offsetHeader = round(sFile.header.hdrlen);  
145 - % Offset of epoch  
146 - offsetEpoch = round(iEpoch * sum(nTimes) * bytesPerVal);  
147 - % Channel offset  
148 - offsetChannel = round(sum(nTimes(1:ChannelsRange(1)-1)) * bytesPerVal);  
149 - % Time offset at the beginning and end of each channel block  
150 - offsetTimeStart = round(iTimes(1) * bytesPerVal);  
151 - offsetTimeEnd = round((nTimes(ChannelsRange(1)) - iTimes(end) - 1) * bytesPerVal);  
152 - % ALL THE "ROUND" CALLS WERE ADDED AFTER DISCOVERING THAT THERE WERE SOMETIMES ROUNDING ERRORS IN THE MULTIPLICATIONS  
153 -  
154 - % Where to start reading in the file ?  
155 - % => After the header, the number of skipped epochs, channels and time samples  
156 - offsetStart = offsetHeader + offsetEpoch + offsetChannel + offsetTimeStart;  
157 - % Number of time samples to skip after each channel  
158 - offsetSkip = offsetTimeStart + offsetTimeEnd;  
159 -  
160 -  
161 - % ===== READ DATA BLOCK =====  
162 - % Position file at the beginning of the trial  
163 - fseek(sfid, offsetStart, 'bof');  
164 - % Read annotation data (char)  
165 - if isAnnotOnly  
166 - dataClass = 'char';  
167 - nReadTimes = bytesPerVal * nReadTimes; % 1 byte instead of 2 230 + % Remove extra spaces
  231 + signalLabel = strrep(hdr.signal(i).label, ' - ', '-');
  232 + % Find space chars (label format "Type Name")
  233 + iSpace = find(signalLabel == ' ');
  234 + % Only if there is one space only
  235 + if (length(iSpace) == 1) && (iSpace >= 3)
  236 + SplitName{i} = signalLabel(iSpace+1:end);
  237 + SplitType{i} = signalLabel(1:iSpace-1);
  238 + % Accept also 2 spaces
  239 + elseif (length(iSpace) == 2) && (iSpace(1) >= 3)
  240 + SplitName{i} = strrep(signalLabel(iSpace(1)+1:end), ' ', '_');
  241 + SplitType{i} = signalLabel(1:iSpace(1)-1);
168 end 242 end
169 - % Read trial data  
170 - % => WARNING: CALL TO FREAD WITH SKIP=0 DOES NOT WORK PROPERLY  
171 - if (offsetSkip == 0)  
172 - F = fread(sfid, [nReadTimes, nReadChannels], dataClass)';  
173 - elseif (bytesPerVal == 2)  
174 - precision = sprintf('%d*%s', nReadTimes, dataClass);  
175 - F = fread(sfid, [nReadTimes, nReadChannels], precision, offsetSkip)';  
176 - % => WARNING: READING USING ubit24 SOMETIMES DOESNT WORK => DOING IT MANUALLY  
177 - elseif (bytesPerVal == 3)  
178 - % Reading each bit independently  
179 - precision = sprintf('%d*%s', 3*nReadTimes, 'uint8');  
180 - F = fread(sfid, [3*nReadTimes, nReadChannels], precision, offsetSkip)';  
181 - % Grouping the 3 bits together  
182 - F = F(:,1:3:end) + F(:,2:3:end)*256 + F(:,3:3:end)*256*256;  
183 - % 2-Complement (negative value indicated by most significant bit)  
184 - if strcmpi(dataClass, 'bit24')  
185 - iNeg = (F >= 256*256*128);  
186 - F(iNeg) = F(iNeg) - 256*256*256; 243 +end
  244 +% Remove the classification if it makes some names non unique
  245 +uniqueNames = unique(SplitName);
  246 +for i = 1:length(uniqueNames)
  247 + if ~isempty(uniqueNames{i})
  248 + iName = find(strcmpi(SplitName, uniqueNames{i}));
  249 + if (length(iName) > 1)
  250 + [SplitName{iName}] = deal('');
  251 + [SplitType{iName}] = deal('');
187 end 252 end
188 end 253 end
189 - % Check that data block was fully read  
190 - if (numel(F) < nReadTimes * nReadChannels)  
191 - % Number of full time samples that were read  
192 - nTimeTrunc = max(0, floor(numel(F) / nReadChannels) - 1);  
193 - % Error message  
194 - disp(sprintf('EDF> ERROR: File is truncated (%d time samples were read instead of %d). Padding with zeros...', nTimeTrunc, nReadTimes));  
195 - % Pad data with zeros  
196 - Ftmp = zeros(nReadTimes, nReadChannels);  
197 - F = F';  
198 - Ftmp(1:numel(F)) = F(:);  
199 - F = Ftmp'; 254 +end
  255 +
  256 +
  257 +%% ===== CREATE EMPTY CHANNEL FILE =====
  258 +ChannelMat = db_template('channelmat');
  259 +ChannelMat.Comment = [sFile.device ' channels'];
  260 +ChannelMat.Channel = repmat(db_template('channeldesc'), [1, hdr.nsignal]);
  261 +chRef = {};
  262 +% For each channel
  263 +for i = 1:hdr.nsignal
  264 + % If is the annotation channel
  265 + if ~isempty(iAnnotChans) && ismember(i, iAnnotChans)
  266 + ChannelMat.Channel(i).Type = 'EDF';
  267 + ChannelMat.Channel(i).Name = 'Annotations';
  268 + elseif ~isempty(iStatusChan) && (i == iStatusChan)
  269 + ChannelMat.Channel(i).Type = 'BDF';
  270 + ChannelMat.Channel(i).Name = 'Status';
  271 + % Regular channels
  272 + else
  273 + % If there is a pair name/type already detected
  274 + if ~isempty(SplitName{i}) && ~isempty(SplitType{i})
  275 + ChannelMat.Channel(i).Name = SplitName{i};
  276 + ChannelMat.Channel(i).Type = SplitType{i};
  277 + else
  278 + % Channel name
  279 + ChannelMat.Channel(i).Name = hdr.signal(i).label(hdr.signal(i).label ~= ' ');
  280 + % Channel type
  281 + if ~isempty(hdr.signal(i).type)
  282 + if (length(hdr.signal(i).type) == 3)
  283 + ChannelMat.Channel(i).Type = hdr.signal(i).type(hdr.signal(i).type ~= ' ');
  284 + elseif isequal(hdr.signal(i).type, 'Active Electrode') || isequal(hdr.signal(i).type, 'AgAgCl electrode')
  285 + ChannelMat.Channel(i).Type = 'EEG';
  286 + else
  287 + ChannelMat.Channel(i).Type = 'Misc';
  288 + end
  289 + else
  290 + ChannelMat.Channel(i).Type = 'EEG';
  291 + end
  292 + end
  293 + % Extract reference name (at the end of the channel name, separated with a "-", eg. "-REF")
  294 + iDash = find(ChannelMat.Channel(i).Name == '-');
  295 + if ~isempty(iDash) && (iDash(end) < length(ChannelMat.Channel(i).Name))
  296 + chRef{end+1} = ChannelMat.Channel(i).Name(iDash(end):end);
  297 + end
200 end 298 end
  299 + ChannelMat.Channel(i).Loc = [0; 0; 0];
  300 + ChannelMat.Channel(i).Orient = [];
  301 + ChannelMat.Channel(i).Weight = 1;
  302 + % ChannelMat.Channel(i).Comment = hdr.signal(i).type;
  303 +end
201 304
202 - % Processing for BDF status file  
203 - if isBdfStatus  
204 - % Mask to keep only the first 15 bits (Triggers bits)  
205 - % Bit 16 : High when new Epoch is started  
206 - % Bit 17-19 : Speed bits 0 1 2  
207 - % Bit 20 : High when CMS is within range  
208 - % Bit 21 : Speed bit 3  
209 - % Bit 22 : High when battery is low  
210 - % Bit 23 : High if ActiveTwo MK2  
211 - F = bitand(F, bin2dec('000000000111111111111111'));  
212 - % Processing for real data  
213 - elseif ~isAnnotOnly  
214 - % Convert to double  
215 - F = double(F);  
216 - % Apply gains  
217 - F = bst_bsxfun(@rdivide, F, [sFile.header.signal(iChannels).gain]');  
218 -% IN THEORY: THIS OFFSET SECTION IS USEFUL, BUT IN PRACTICE IT LOOKS LIKE THE VALUES IN ALL THE FILES ARE CENTERED ON ZERO  
219 -% % Add offset  
220 -% if isfield(sFile.header.signal, 'offset') && ~isempty(sFile.header.signal(1).offset)  
221 -% % ...  
222 -% end 305 +% If the same reference is indicated for all the channels: remove it
  306 +if (length(chRef) >= 2)
  307 + % Get the shortest reference tag
  308 + lenRef = cellfun(@length, chRef);
  309 + minLen = min(lenRef);
  310 + % Check if all the ref names are equal (up to the max length - some might be cut because the channel name is too long)
  311 + if all(cellfun(@(c)strcmpi(c(1:minLen), chRef{1}(1:minLen)), chRef))
  312 + % Remove the reference tag from all the channel names
  313 + for i = 1:length(ChannelMat.Channel)
  314 + ChannelMat.Channel(i).Name = strrep(ChannelMat.Channel(i).Name, chRef{1}, '');
  315 + ChannelMat.Channel(i).Name = strrep(ChannelMat.Channel(i).Name, chRef{1}(1:minLen), '');
  316 + end
223 end 317 end
224 end 318 end
225 319
  320 +% If there are only "Misc" and no "EEG" channels: rename to "EEG"
  321 +iMisc = find(strcmpi({ChannelMat.Channel.Type}, 'Misc'));
  322 +iEeg = find(strcmpi({ChannelMat.Channel.Type}, 'EEG'));
  323 +if ~isempty(iMisc) && isempty(iEeg)
  324 + [ChannelMat.Channel(iMisc).Type] = deal('EEG');
  325 + iEeg = iMisc;
  326 +end
  327 +
  328 +
  329 +%% ===== DETECT MULTIPLE SAMPLING RATES =====
  330 +% Use the first "EEG" channel as the reference sampling rate (or the first channel if no "EEG" channels available)
  331 +if ~isempty(iEeg) && ismember(iEeg(1), iOtherChan)
  332 + iChanFreqRef = iEeg(1);
  333 +else
  334 + iChanFreqRef = iOtherChan(1);
  335 +end
  336 +% Mark as bad channels with sampling rates different from EEG
  337 +iChanWrongRate = find([sFile.header.signal.sfreq] ~= sFile.header.signal(iChanFreqRef).sfreq);
  338 +iChanWrongRate = intersect(iChanWrongRate, iOtherChan);
  339 +if ~isempty(iChanWrongRate)
  340 + sFile.channelflag(iChanWrongRate) = -1;
  341 +end
  342 +
  343 +% Consider that the sampling rate of the file is the sampling rate of the first signal
  344 +sFile.prop.sfreq = hdr.signal(iChanFreqRef).sfreq;
  345 +sFile.prop.times = [0, hdr.signal(iChanFreqRef).nsamples * hdr.nrec - 1] ./ sFile.prop.sfreq;
  346 +sFile.prop.nAvg = 1;
  347 +
  348 +
  349 +
  350 +
  351 +%% ===== READ EDF ANNOTATION CHANNEL =====
  352 +if ~isempty(iEvtChans) % && ~isequal(ImportOptions.EventsMode, 'ignore')
  353 + % Set reading options
  354 + ImportOptions.ImportMode = 'Time';
  355 + ImportOptions.UseSsp = 0;
  356 + ImportOptions.UseCtfComp = 0;
  357 + % Read EDF annotations
  358 + if strcmpi(sFile.format, 'EEG-EDF')
  359 + evtList = {};
  360 + % In EDF+, the first annotation channel has epoch time stamps (EDF
  361 + % calls epochs records). So read all annotation channels per epoch.
  362 + for irec = 1:hdr.nrec
  363 + for ichan = 1:length(iEvtChans)
  364 + bst_progress('text', sprintf('Reading annotations... [%d%%]', round((ichan + (irec-1)*length(iEvtChans))/length(iEvtChans)/hdr.nrec*100)));
  365 + % Sample indices for the current epoch (=record)
  366 + SampleBounds = [irec-1,irec] * sFile.header.signal(iEvtChans(ichan)).nsamples - [0,1];
  367 + % Read record
  368 + F = in_fread(sFile, ChannelMat, 1, SampleBounds, iEvtChans(ichan), ImportOptions);
  369 + % Find all the TALs (Time-stamped Annotations Lists): separated with [20][0]
  370 + % Possible configurations:
  371 + % Onset[21]Duration[20]Annot1[20]Annot2[20]..AnnotN[20][0]
  372 + % Onset[20]Annot1[20]Annot2[20]..AnnotN[20][0]
  373 + % Onset[20]Annot1[20][0]
  374 + iSeparator = [-1, find((F(1:end-1) == 20) & (F(2:end) == 0))];
  375 + % Loop on all the TALs
  376 + for iAnnot = 1:length(iSeparator)-1
  377 + % Get annotation
  378 + strAnnot = char(F(iSeparator(iAnnot)+2:iSeparator(iAnnot+1)-1));
  379 + % Split in blocks with [20]
  380 + splitAnnot = str_split(strAnnot, 20);
  381 + % The first TAL in a record should be indicating the timing of the first sample of the record
  382 + if (iAnnot == 1) && (length(splitAnnot) == 1) && ~any(splitAnnot{1} == 21)
  383 + % Ignore if this is not the first channel
  384 + if (ichan > 1)
  385 + continue;
  386 + end
  387 + % Get record time stamp
  388 + t0_rec = str2double(splitAnnot{1});
  389 + if isempty(t0_rec) || isnan(t0_rec)
  390 + continue;
  391 + end
  392 + if (irec == 1)
  393 + t0_file = t0_rec;
  394 + % Find discontinuities larger than 1 sample
  395 + elseif abs(t0_rec - prev_rec - (irec - prev_irec) * hdr.reclen) > (1 / sFile.prop.sfreq)
  396 + % Brainstorm fills partial/interrupted records with zeros
  397 + bstTime = prev_rec + hdr.reclen;
  398 + timeDiff = bstTime - t0_rec;
  399 + % If we want to fix timing, apply skip to initial timestamp
  400 + if hdr.fixinterrupted
  401 + t0_file = t0_file - timeDiff;
  402 + end
  403 + % Warn user of discontinuity
  404 + if timeDiff > 0
  405 + expectMsg = 'blank data';
  406 + else
  407 + expectMsg = 'skipped data';
  408 + end
  409 + startTime = min(t0_rec - t0_file - [0, timeDiff]); % before and after t0_file adjustment
  410 + endTime = max(t0_rec - t0_file - [0, timeDiff]);
  411 + fprintf('WARNING: Found discontinuity between %.3fs and %.3fs, expect %s in between.\n', startTime, endTime, expectMsg);
  412 + % Create event for users information
  413 + if timeDiff < 0
  414 + endTime = startTime; % no extent in this case, there is skipped time.
  415 + end
  416 + evtList(end+1,:) = {'EDF+D Discontinuity', [startTime; endTime]};
  417 + end
  418 + prev_rec = t0_rec;
  419 + prev_irec = irec;
  420 + % Regular TAL: indicating a marker
  421 + else
  422 + % Split time in onset/duration
  423 + splitTime = str_split(splitAnnot{1}, 21);
  424 + % Get time
  425 + t = str2double(splitTime{1});
  426 + if isempty(t) || isnan(t)
  427 + continue;
  428 + end
  429 + % Get duration
  430 + if (length(splitTime) > 1)
  431 + duration = str2double(splitTime{2});
  432 + % Exclude 1-sample long events
  433 + if isempty(duration) || isnan(duration) || (round(duration .* sFile.prop.sfreq) <= 1)
  434 + duration = 0;
  435 + end
  436 + else
  437 + duration = 0;
  438 + end
  439 + % Unnamed event
  440 + if (length(splitAnnot) == 1) || isempty(splitAnnot{2})
  441 + splitAnnot{2} = 'Unnamed';
  442 + end
  443 + % Create one event for each label in the TAL
  444 + for iLabel = 2:length(splitAnnot)
  445 + evtList(end+1,:) = {splitAnnot{iLabel}, (t-t0_file) + [0;duration]};
  446 + end
  447 + end
  448 + end
  449 + end
  450 + end
  451 +
  452 + % If there are events: create a create an events structure
  453 + if ~isempty(evtList)
  454 + % Initialize events list
  455 + sFile.events = repmat(db_template('event'), 0);
  456 + % Events list
  457 + [uniqueEvt, iUnique] = unique(evtList(:,1));
  458 + uniqueEvt = evtList(sort(iUnique),1);
  459 + % Build events list
  460 + for iEvt = 1:length(uniqueEvt)
  461 + % Find all the occurrences of this event
  462 + iOcc = find(strcmpi(uniqueEvt{iEvt}, evtList(:,1)));
  463 + % Concatenate all times
  464 + t = [evtList{iOcc,2}];
  465 + % If second row is equal to the first one (no extended events): delete it
  466 + if all(t(1,:) == t(2,:))
  467 + t = t(1,:);
  468 + end
  469 + % Set event
  470 + sFile.events(iEvt).label = strtrim(uniqueEvt{iEvt});
  471 + sFile.events(iEvt).times = round(t .* sFile.prop.sfreq) ./ sFile.prop.sfreq;
  472 + sFile.events(iEvt).epochs = 1 + 0*t(1,:);
  473 + sFile.events(iEvt).select = 1;
  474 + sFile.events(iEvt).channels = cell(1, size(sFile.events(iEvt).times, 2));
  475 + sFile.events(iEvt).notes = cell(1, size(sFile.events(iEvt).times, 2));
  476 + end
  477 + end
  478 +
  479 + % BDF Status line
  480 + elseif strcmpi(sFile.format, 'EEG-BDF') && ~strcmpi(ImportOptions.EventsTrackMode, 'ignore')
  481 + % Ask how to read the events
  482 + [events, ImportOptions.EventsTrackMode] = process_evt_read('Compute', sFile, ChannelMat, ChannelMat.Channel(iEvtChans).Name, ImportOptions.EventsTrackMode);
  483 + if isequal(events, -1)
  484 + sFile = [];
  485 + ChannelMat = [];
  486 + return;
  487 + end
  488 + % Report the events in the file structure
  489 + sFile.events = events;
  490 + % Remove the 'Status: ' string in front of the events
  491 + for i = 1:length(sFile.events)
  492 + sFile.events(i).label = strrep(sFile.events(i).label, 'Status: ', '');
  493 + end
  494 + % Group events by time
  495 + % sFile.events = process_evt_grouptime('Compute', sFile.events);
  496 + end
  497 +end
@@ -261,9 +261,33 @@ if strcmpi(patientCode(5:end),'FRE') @@ -261,9 +261,33 @@ if strcmpi(patientCode(5:end),'FRE')
261 fprintf('\n \n MESSAGE: .. %s parameters updated ..::\n',patientCode); 261 fprintf('\n \n MESSAGE: .. %s parameters updated ..::\n',patientCode);
262 end 262 end
263 end 263 end
  264 +val_flag = 0;
  265 +if strcmpi(patientCode(5:end),'VAL')
  266 + load('/gin/data/database/02-raw/stim_parameters-ftract-val.mat','stim_params')
  267 + Loc = find(ismember(stim_params.PCode, patientCode), 1);
  268 + if ~isempty(Loc)
  269 + fre_flag = 1;
  270 + Frq = stim_params.Freq{Loc};
  271 + Amp = stim_params.Ampl{Loc};
  272 + Pul = stim_params.Pulse{Loc};
  273 + for n = 1:length(KeepEvent)
  274 + undsc = strfind(Notes{KeepEvent(n)},'_');
  275 + if ~isempty(undsc)
  276 + Notes{KeepEvent(n)} = strcat(Notes{KeepEvent(n)}(1:undsc(1)),Amp,'_',Frq,'_',Pul);
  277 + end
  278 + Notes{KeepEvent(n)} = char(Notes{KeepEvent(n)});
  279 + evt(KeepEvent(n)).type = Notes{KeepEvent(n)};
  280 + end
  281 + D = events(D,1,evt);
  282 + D2 = clone(D, D.fnamedat, [D.nchannels D.nsamples D.ntrials]);
  283 + D2(:,:,:) = D(:,:,:);
  284 + save(D2);
  285 + fprintf(&#