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 |