-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathft_read_event.m
2642 lines (2392 loc) · 107 KB
/
ft_read_event.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
function [event] = ft_read_event(filename, varargin)
% FT_READ_EVENT reads all events from an EEG, MEG or other time series dataset and
% returns them in a common data-independent structure. The supported formats are
% listed in the accompanying FT_READ_HEADER function.
%
% Use as
% [event] = ft_read_event(filename, ...)
%
% Additional options should be specified in key-value pairs and can be
% 'dataformat' = string
% 'headerformat' = string
% 'eventformat' = string
% 'header' = header structure, see FT_READ_HEADER
% 'detectflank' = string, can be 'up', 'updiff', 'down', 'downdiff', 'both', 'any', 'biton', 'bitoff' (default is system specific)
% 'trigshift' = integer, number of samples to shift from flank to detect trigger value (default = 0)
% 'chanindx' = list with channel numbers for trigger detection, specify -1 in case you don't want to detect triggers (default is automatic)
% 'threshold' = threshold for analog trigger channels (default is system specific)
% 'tolerance' = tolerance in samples when merging Neuromag analogue trigger channels (default = 1, meaning that a shift of one sample in both directions is compensated for)
% 'blocking' = wait for the selected number of events (default = 'no')
% 'timeout' = amount of time in seconds to wait when blocking (default = 5)
% 'password' = password structure for encrypted data set (only for dhn_med10, mayo_mef30 and mayo_mef21)
% 'readbids' = 'yes', no', or 'ifmakessense', whether to read information from the BIDS sidecar files (default = 'ifmakessense')
%
% This function returns an event structure with the following fields
% event.type = string
% event.sample = expressed in samples, the first sample of a recording is 1
% event.value = number or string
% event.offset = expressed in samples
% event.duration = expressed in samples
% event.timestamp = expressed in timestamp units, which vary over systems (optional)
%
% You can specify optional arguments as key-value pairs for filtering the events,
% e.g. to select only events of a specific type, of a specific value, or events
% between a specific begin and end sample. This event filtering is especially usefull
% for real-time processing. See FT_FILTER_EVENT for more details.
%
% Some data formats have trigger channels that are sampled continuously with the same
% rate as the electrophysiological data. The default is to detect only the up-going
% TTL flanks. The trigger events will correspond with the first sample where the TTL
% value is up. This behavior can be changed using the 'detectflank' option, which
% also allows for detecting the down-going flank or both. In case of detecting the
% down-going flank, the sample number of the event will correspond with the first
% sample at which the TTF went down, and the value will correspond to the TTL value
% just prior to going down.
%
% To use an external reading function, you can specify an external function as the
% 'eventformat' option. This function should take the filename and the headeras
% input arguments. Please check the code of this function for details, and search for
% BIDS_TSV as example.
%
% The event type and sample fields are always defined, other fields are present but
% can be empty, depending on the type of event file. Events are sorted by the sample
% on which they occur. After reading the event structure, you can use the following
% tricks to extract information about those events in which you are interested.
%
% Determine the different event types
% unique({event.type})
%
% Get the index of all trial events
% find(strcmp('trial', {event.type}))
%
% Make a vector with all triggers that occurred on the backpanel
% [event(find(strcmp('backpanel trigger', {event.type}))).value]
%
% Find the events that occurred in trial 26
% t=26; samples_trials = [event(find(strcmp('trial', {event.type}))).sample];
% find([event.sample]>samples_trials(t) & [event.sample]<samples_trials(t+1))
%
% The list of supported file formats can be found in FT_READ_HEADER.
%
% See also FT_READ_HEADER, FT_READ_DATA, FT_WRITE_EVENT, FT_FILTER_EVENT
% Copyright (C) 2004-2021 Robert Oostenveld
%
% This file is part of FieldTrip, see http://www.fieldtriptoolbox.org
% for the documentation and details.
%
% FieldTrip is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% FieldTrip is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with FieldTrip. If not, see <http://www.gnu.org/licenses/>.
%
% $Id$
global event_queue % for fcdc_global
persistent sock % for fcdc_tcp
persistent db_blob % for fcdc_mysql
if isempty(db_blob)
db_blob = 0;
end
if iscell(filename)
% use recursion to read the events from multiple files
ft_warning('concatenating events from %d files', numel(filename));
hdr = ft_getopt(varargin, 'header');
if isempty(hdr)
% read the combined and individual file headers
combined = ft_read_header(filename, varargin{:});
else
% use the combined and individual file headers that were read previously
combined = hdr;
end
hdr = combined.orig;
allhdr = cat(1, hdr{:});
if numel(unique([allhdr.label]))==sum([allhdr.nChans])
% each file has different channels, concatenate along the channel dimension
% sample indices are the same for all files
for i=1:numel(filename)
varargin = ft_setopt(varargin, 'header', hdr{i});
event{i} = ft_read_event(filename{i}, varargin{:});
end
else
% each file has the same channels, concatenate along the time dimension
% this requires careful bookkeeping of the sample indices
nsmp = nan(size(filename));
for i=1:numel(filename)
nsmp(i) = hdr{i}.nSamples*hdr{i}.nTrials;
end
offset = [0 cumsum(nsmp(1:end-1))];
event = cell(size(filename));
for i=1:numel(filename)
varargin = ft_setopt(varargin, 'header', hdr{i});
event{i} = ft_read_event(filename{i}, varargin{:});
for j=1:numel(event{i})
% add the offset due to the previous files
event{i}(j).sample = event{i}(j).sample + offset(i);
end
end
end
if numel(event)>1
event = appendstruct(event{:});
else
% APPENDSTRUCT fails if there is only one
event = event{1};
end
% return the concatenated events
return
end
% optionally get the data from the URL and make a temporary local copy
filename = fetch_url(filename);
% get the options
hdr = ft_getopt(varargin, 'header');
detectflank = ft_getopt(varargin, 'detectflank', 'up', true); % note that emptymeaningful=true
denoise = ft_getopt(varargin, 'denoise', []);
trigshift = ft_getopt(varargin, 'trigshift'); % default is assigned in subfunction
trigpadding = ft_getopt(varargin, 'trigpadding'); % default is assigned in subfunction
headerformat = ft_getopt(varargin, 'headerformat');
dataformat = ft_getopt(varargin, 'dataformat');
eventformat = ft_getopt(varargin, 'eventformat');
threshold = ft_getopt(varargin, 'threshold'); % this is used for analog channels
tolerance = ft_getopt(varargin, 'tolerance', 1);
checkmaxfilter = ft_getopt(varargin, 'checkmaxfilter'); % will be passed to FT_READ_HEADER
readbids = ft_getopt(varargin, 'readbids', 'ifmakessense'); % will be passed to FT_READ_HEADER
chanindx = ft_getopt(varargin, 'chanindx'); % this allows to override the automatic trigger channel detection (useful for Yokogawa & Ricoh, and for EDF with variable sampling rate)
trigindx = ft_getopt(varargin, 'trigindx'); % deprecated, use chanindx instead
triglabel = ft_getopt(varargin, 'triglabel'); % deprecated, use chanindx instead
password = ft_getopt(varargin, 'password', struct([]));
combinebinary = ft_getopt(varargin, 'combinebinary'); % this is an option for yokogawa data
% for backward compatibility, added by Robert in Sept 2019
if ~isempty(trigindx)
ft_warning('please use ''chanindx'' instead of ''trigindx''')
chanindx = trigindx;
end
% for backward compatibility with https://github.com/fieldtrip/fieldtrip/issues/1585
if islogical(readbids)
% it should be either yes/no/ifmakessense
if readbids
readbids = 'yes';
else
readbids = 'no';
end
end
% for backward compatibility, added by Robert in Sept 2019
if ~isempty(triglabel)
ft_error('please use ''chanindx'' instead of ''triglabel''')
% FIXME it would be possible to read the header and search for the corresponding
% channels. However this was only implemented for Artinis oxy3 and oxy4 formats,
% and never supported for other formats. I did not find it in use anywhere or
% documented on the website. If this were to be re-enabled, it should be done
% consistently for all data formats as 'chanlabel', and probably also when
% reading data using FT_READ_DATA.
end
if isempty(eventformat)
% only do the autodetection if the format was not specified
eventformat = ft_filetype(filename);
end
if iscell(eventformat)
% this happens for datasets specified as cell-array for concatenation
eventformat = eventformat{1};
end
% this allows to read only events in a certain range, supported for selected data formats only
flt_type = ft_getopt(varargin, 'type');
flt_value = ft_getopt(varargin, 'value');
flt_minsample = ft_getopt(varargin, 'minsample');
flt_maxsample = ft_getopt(varargin, 'maxsample');
flt_mintimestamp = ft_getopt(varargin, 'mintimestamp');
flt_maxtimestamp = ft_getopt(varargin, 'maxtimestamp');
flt_minnumber = ft_getopt(varargin, 'minnumber');
flt_maxnumber = ft_getopt(varargin, 'maxnumber');
% this allows blocking reads to avoid having to poll many times for online processing
blocking = ft_getopt(varargin, 'blocking', false); % true or false
timeout = ft_getopt(varargin, 'timeout', 5); % seconds
% convert from 'yes'/'no' into boolean
blocking = istrue(blocking);
if any(strcmp(eventformat, {'brainvision_eeg', 'brainvision_dat'}))
[p, f] = fileparts(filename);
filename = fullfile(p, [f '.vhdr']);
eventformat = 'brainvision_vhdr';
end
if strcmp(eventformat, 'brainvision_vhdr')
% read the header file belonging to the dataset and try to determine the corresponding marker file
eventformat = 'brainvision_vmrk';
if ~isempty(hdr)
vhdr = hdr.orig;
else
vhdr = read_brainvision_vhdr(filename);
end
% replace the filename with the filename of the markerfile
if ~isfield(vhdr, 'MarkerFile') || isempty(vhdr.MarkerFile)
filename = [];
else
[p, f, x] = fileparts(filename);
filename = fullfile(p, vhdr.MarkerFile);
end
end
if (strcmp(readbids, 'yes') || strcmp(readbids, 'ifmakessense')) && ~isempty(filename)
% deal with data that is organized according to BIDS
% data in a BIDS tsv file (like physio and stim) will be explicitly dealt with in BIDS_TSV
[p, f, x] = fileparts(filename);
isbids = startsWith(f, 'sub-') && ~strcmp(x, '.tsv');
if isbids
% find the corresponding events.tsv file, due to inheritance it can be at a higher level
eventsfile = bids_sidecar(filename, 'events');
if ~isempty(eventsfile)
% read the events from the BIDS events.tsv file rather than from the datafile
filename = eventsfile;
eventformat = 'events_tsv';
end
end
end
% start with an empty event structure
event = [];
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% read the events with the low-level reading function
% please maintain this list in alphabetical order
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
switch eventformat
case {'4d' '4d_pdf', '4d_m4d', '4d_xyz'}
if isempty(hdr)
hdr = ft_read_header(filename, 'headerformat', eventformat);
end
if isempty(chanindx)
% auto-detect the trigger channels
chanindx = match_str(hdr.label, 'TRIGGER');
end
% read the trigger channels and do flank detection
if isfield(hdr, 'orig') && isfield(hdr.orig, 'config_data') && (strcmp(hdr.orig.config_data.site_name, 'Glasgow') || strcmp(hdr.orig.config_data.site_name, 'Marseille')),
trigger = read_trigger(filename, 'header', hdr, 'dataformat', '4d', 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fix4d8192', true);
else
trigger = read_trigger(filename, 'header', hdr, 'dataformat', '4d', 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fix4d8192', false);
end
event = appendstruct(event, trigger);
respindx = match_str(hdr.label, 'RESPONSE');
if ~isempty(respindx)
response = read_trigger(filename, 'header', hdr, 'dataformat', '4d', 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', respindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding);
event = appendstruct(event, response);
end
case 'bci2000_dat'
% this requires the load_bcidat mex file to be present on the path
ft_hastoolbox('BCI2000', 1);
if isempty(hdr)
hdr = ft_read_header(filename);
end
if isfield(hdr.orig, 'signal') && isfield(hdr.orig, 'states')
% assume that the complete data is stored in the header, this speeds up subsequent read operations
signal = hdr.orig.signal;
states = hdr.orig.states;
parameters = hdr.orig.parameters;
total_samples = hdr.orig.total_samples;
else
[signal, states, parameters, total_samples] = load_bcidat(filename);
end
list = fieldnames(states);
% loop over all states and detect the flanks, the following code was taken from read_trigger
for i=1:length(list)
channel = list{i};
trig = double(getfield(states, channel));
pad = trig(1);
trigshift = 0;
begsample = 1;
switch detectflank
case 'up'
% convert the trigger into an event with a value at a specific sample
for j=find(diff([pad trig(:)'])>0)
event(end+1).type = channel;
event(end ).sample = j + begsample - 1; % assign the sample at which the trigger has gone down
event(end ).value = trig(j+trigshift); % assign the trigger value just _after_ going up
end
case 'down'
% convert the trigger into an event with a value at a specific sample
for j=find(diff([pad trig(:)'])<0)
event(end+1).type = channel;
event(end ).sample = j + begsample - 1; % assign the sample at which the trigger has gone down
event(end ).value = trig(j-1-trigshift); % assign the trigger value just _before_ going down
end
case 'both'
% convert the trigger into an event with a value at a specific sample
for j=find(diff([pad trig(:)'])>0)
event(end+1).type = [channel '_up']; % distinguish between up and down flank
event(end ).sample = j + begsample - 1; % assign the sample at which the trigger has gone down
event(end ).value = trig(j+trigshift); % assign the trigger value just _after_ going up
end
% convert the trigger into an event with a value at a specific sample
for j=find(diff([pad trig(:)'])<0)
event(end+1).type = [channel '_down']; % distinguish between up and down flank
event(end ).sample = j + begsample - 1; % assign the sample at which the trigger has gone down
event(end ).value = trig(j-1-trigshift); % assign the trigger value just _before_ going down
end
otherwise
ft_error('incorrect specification of ''detectflank''');
end
end
case 'besa_besa'
% read the header
if isempty(hdr)
hdr = ft_read_header(filename);
end
if ~isempty(detectflank) % parse the trigger channel (indicated by chanindx) for events
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'threshold', threshold);
elseif issubfield(hdr, 'orig.events') && ~isempty(hdr.orig.events.offsets) % FIXME: add support for reading in events from the datafile
else
event = [];
end
case {'besa_avr', 'besa_swf'}
if isempty(hdr)
hdr = ft_read_header(filename);
end
event(end+1).type = 'average';
event(end ).sample = 1;
event(end ).duration = hdr.nSamples;
event(end ).offset = -hdr.nSamplesPre;
event(end ).value = [];
case {'biosemi_bdf', 'bham_bdf'}
% read the header, required to determine the stimulus channels and trial specification
if isempty(hdr)
hdr = ft_read_header(filename);
end
% specify the range to search for triggers, default is the complete file
if ~isempty(flt_minsample)
begsample = flt_minsample;
else
begsample = 1;
end
if ~isempty(flt_maxsample)
endsample = flt_maxsample;
else
endsample = hdr.nSamples*hdr.nTrials;
end
if isempty(detectflank)
detectflank = 'up';
end
if ~strcmp(detectflank, 'up')
if strcmp(detectflank, 'both')
ft_warning('only up-going flanks are supported for Biosemi');
detectflank = 'up';
else
ft_error('only up-going flanks are supported for Biosemi');
% FIXME the next section on trigger detection should be merged with the
% READ_CTF_TRIGGER (which also does masking with bit-patterns) into the
% READ_TRIGGER function
end
end
% find the STATUS channel and read the values from it
schan = find(strcmpi(hdr.label,'STATUS'));
sdata = ft_read_data(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', begsample, 'endsample', endsample, 'chanindx', schan);
if ft_platform_supports('int32_logical_operations')
% convert to 32-bit integer representation and only preserve the lowest 24 bits
sdata = bitand(int32(sdata), 2^24-1);
else
% find indices of negative numbers
bit24i = find(sdata < 0);
% make number positive and preserve bits 0-22
sdata(bit24i) = bitcmp(abs(sdata(bit24i))-1,24);
% re-insert the sign bit on its original location, i.e. bit24
sdata(bit24i) = sdata(bit24i)+(2^(24-1));
% typecast the data to ensure that the status channel is represented in 32 bits
sdata = uint32(sdata);
end
byte1 = 2^8 - 1;
byte2 = 2^16 - 1 - byte1;
byte3 = 2^24 - 1 - byte1 - byte2;
% get the respective status and trigger bits
trigger = bitand(sdata, bitor(byte1, byte2)); % this is contained in the lower two bytes
epoch = int8(bitget(sdata, 16+1));
cmrange = int8(bitget(sdata, 20+1));
battery = int8(bitget(sdata, 22+1));
% determine when the respective status bits go up or down
flank_trigger = diff([0 trigger]);
flank_epoch = diff([0 epoch ]);
flank_cmrange = diff([0 cmrange]);
flank_battery = diff([0 battery]);
for i=find(flank_trigger>0)
event(end+1).type = 'STATUS';
event(end ).sample = i + begsample - 1;
event(end ).value = double(trigger(i));
end
for i=find(flank_epoch==1)
event(end+1).type = 'Epoch';
event(end ).sample = i;
end
for i=find(flank_cmrange==1)
event(end+1).type = 'CM_in_range';
event(end ).sample = i;
end
for i=find(flank_cmrange==-1)
event(end+1).type = 'CM_out_of_range';
event(end ).sample = i;
end
for i=find(flank_battery==1)
event(end+1).type = 'Battery_low';
event(end ).sample = i;
end
for i=find(flank_battery==-1)
event(end+1).type = 'Battery_ok';
event(end ).sample = i;
end
case {'biosig', 'gdf'}
% FIXME it would be nice to figure out how sopen/sread return events
% for all possible fileformats that can be processed with biosig
%
% This section of code is opaque with respect to the gdf file being a
% single file or the first out of a sequence with postfix _1, _2, ...
% because it uses private/read_trigger which again uses ft_read_data
if isempty(hdr)
hdr = ft_read_header(filename);
end
% the following applies to Biosemi data that is stored in the gdf format
if ~isempty(chanindx)
chanindx = find(strcmp(hdr.label, 'STATUS'));
end
event = [];
if length(chanindx)==1
% represent the rising flanks in the STATUS (or user specified) channel as events
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixbiosemi', true);
else
ft_warning('data does not have a STATUS channel');
end
% make an attempt to get the events from the BIOSIG hdr
if isfield(hdr.orig, 'EVENT')
% this is code that has been inspired by eeglab's biosig2eeglabevent
event_hdr = biosig2fieldtripevent(hdr.orig.EVENT);
else
event_hdr = [];
end
if ~isempty(event) && ~isempty(event_hdr)
% merge the two structs
event = appendstruct(event(:), event_hdr(:));
smp = [event.sample];
[srt, indx] = sort(smp);
event = event(indx);
elseif isempty(event) && ~isempty(event_hdr)
event = event_hdr(:);
end
case 'AnyWave'
event = read_ah5_markers(hdr, filename);
case 'brainvision_vmrk'
if ~isempty(filename)
event = read_brainvision_vmrk(filename);
else
% the user specified a BrainVision dataset without a marker file
event = [];
end
case 'bucn_nirs'
event = read_bucn_nirsevent(filename);
case 'ced_son'
% check that the required low-level toolbox is available
ft_hastoolbox('neuroshare', 1);
orig = read_ced_son(filename,'readevents','yes');
event = struct('type', {orig.events.type},...
'sample', {orig.events.sample},...
'value', {orig.events.value},...
'offset', {orig.events.offset},...
'duration', {orig.events.duration});
case 'ced_spike6mat'
if isempty(hdr)
hdr = ft_read_header(filename);
end
if isempty(chanindx)
for i = 1:numel(hdr.orig)
if ~any(isfield(hdr.orig{i}, {'units', 'scale'}))
chanindx = [chanindx i];
end
end
end
if ~isempty(chanindx)
% read the trigger channels and do flank detection
trigger = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank);
event = appendstruct(event, trigger);
end
case {'ctf_ds', 'ctf_meg4', 'ctf_res4', 'ctf_old'}
% obtain the dataset name
if ft_filetype(filename, 'ctf_meg4') || ft_filetype(filename, 'ctf_res4')
filename = fileparts(filename);
end
[path, name, ext] = fileparts(filename);
headerfile = fullfile(path, [name ext], [name '.res4']);
datafile = fullfile(path, [name ext], [name '.meg4']);
classfile = fullfile(path, [name ext], 'ClassFile.cls');
markerfile = fullfile(path, [name ext], 'MarkerFile.mrk');
% in case ctf_old was specified as eventformat, the other reading functions should also know about that
if strcmp(eventformat, 'ctf_old')
dataformat = 'ctf_old';
headerformat = 'ctf_old';
end
% read the header, required to determine the stimulus channels and trial specification
if isempty(hdr)
hdr = ft_read_header(headerfile, 'headerformat', headerformat);
end
try
% read the trigger codes from the STIM channel, useful for (pseudo) continuous data
% this splits the trigger channel into the lowers and highest 16 bits,
% corresponding with the front and back panel of the electronics cabinet at the Donders Centre
[backpanel, frontpanel] = read_ctf_trigger(filename);
for i=find(backpanel(:)')
event(end+1).type = 'backpanel trigger';
event(end ).sample = i;
event(end ).value = backpanel(i);
end
for i=find(frontpanel(:)')
event(end+1).type = 'frontpanel trigger';
event(end ).sample = i;
event(end ).value = frontpanel(i);
end
end
if isempty(chanindx)
% determine the trigger channels from the header
if isfield(hdr, 'orig') && isfield(hdr.orig, 'sensType')
origSensType = hdr.orig.sensType;
elseif isfield(hdr, 'orig') && isfield(hdr.orig, 'res4')
origSensType = [hdr.orig.res4.senres.sensorTypeIndex];
else
origSensType = [];
end
% meg channels are 5, refmag 0, refgrad 1, adcs 18, trigger 11 or 20, eeg 9
chanindx = find(origSensType==11 | origSensType==20);
end
if ~isempty(chanindx)
% read the trigger channels and do flank detection
trigger = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixctf', true);
event = appendstruct(event, trigger);
end
if isfield(hdr, 'orig') && isfield(hdr.orig, 'res4') && isfield(hdr.orig.res4, 'preTrigPts') && hdr.orig.res4.preTrigPts>0
% it looks like the data is properly epoched
if length(chanindx)==1
dat = ft_read_data(filename, 'chanindx', chanindx, 'begtrial', 1, 'endtrial', hdr.orig.res4.no_trials);
trg = squeeze(dat(1,hdr.orig.res4.preTrigPts+1,:))/65536; % the division shifts it by 16 bits
for i=1:hdr.nTrials
event(end+1).type = 'trial';
event(end ).sample = (i-1)*hdr.nSamples + 1;
event(end ).offset = -hdr.nSamplesPre;
event(end ).duration = hdr.nSamples;
event(end ).value = trg(i);
end
else
% trial events without values will still be constructed further down, as part of the generic code
ft_warning('cannot construct trials with corresponding trigger values')
end % if a single trigger channel
end % if preTrigPts>0
% read the classification file and make an event for each classified trial
[condNumbers, condLabels] = read_ctf_cls(classfile);
if ~isempty(condNumbers)
Ncond = length(condLabels);
for i=1:Ncond
for j=1:length(condNumbers{i})
event(end+1).type = 'classification';
event(end ).value = condLabels{i};
event(end ).sample = (condNumbers{i}(j)-1)*hdr.nSamples + 1;
event(end ).offset = -hdr.nSamplesPre;
event(end ).duration = hdr.nSamples;
end
end
end
if exist(markerfile,'file')
% read the marker file and make an event for each marker
% this depends on the readmarkerfile function that I got from Tom Holroyd
% I have not tested this myself extensively, since at the FCDC we
% don't use the marker files
mrk = readmarkerfile(filename);
for i=1:mrk.number_markers
for j=1:mrk.number_samples(i)
% determine the location of the marker, expressed in samples
trialnum = mrk.trial_times{i}(j,1);
synctime = mrk.trial_times{i}(j,2);
begsample = (trialnum-1)*hdr.nSamples + 1; % of the trial, relative to the start of the datafile
endsample = (trialnum )*hdr.nSamples; % of the trial, relative to the start of the datafile
offset = round(synctime*hdr.Fs); % this is the offset (in samples) relative to time t=0 for this trial
offset = offset + hdr.nSamplesPre; % and time t=0 corrsponds with the nSamplesPre'th sample
% store this marker as an event
event(end+1).type = mrk.marker_names{i};
event(end ).value = [];
event(end ).sample = begsample + offset;
event(end ).duration = 0;
event(end ).offset = offset;
end
end
end
case 'ctf_shm'
% contact Robert Oostenveld if you are interested in real-time acquisition on the CTF system
% read the events from shared memory
event = read_shm_event(filename, varargin{:});
case {'curry_dat', 'curry_cdt'}
if isempty(hdr)
hdr = ft_read_header(filename);
end
event = [];
for i=1:size(hdr.orig.events, 2)
event(i).type = 'trigger';
event(i).value = hdr.orig.events(2, i);
event(i).sample = hdr.orig.events(1, i);
event(i).offset = hdr.orig.events(3, i)-hdr.orig.events(1, i);
event(i).duration = hdr.orig.events(4, i)-hdr.orig.events(1, i);
end
case 'dataq_wdq'
if isempty(hdr)
hdr = ft_read_header(filename, 'headerformat', 'dataq_wdq');
end
trigger = read_wdq_data(filename, hdr.orig, 'lowbits');
[ix, iy] = find(trigger>1); %it seems as if the value of 1 is meaningless
for i=1:numel(ix)
event(i).type = num2str(ix(i));
event(i).value = trigger(ix(i),iy(i));
event(i).sample = iy(i);
end
case 'dhn_med10'
if isempty(hdr)
hdr = ft_read_header(filename, 'password', password);
end
event = read_dhn_med10(filename, password, false, hdr);
case 'edf'
% read the header
if isempty(hdr)
hdr = ft_read_header(filename, 'headerformat', headerformat);
end
if ~isempty(detectflank) && ~isempty(chanindx) % parse the trigger channels for events
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'threshold', threshold);
else
event = [];
end
if issubfield(hdr, 'orig.annotation') && ~isempty(hdr.orig.annotation) % EDF itself does not contain events, but EDF+ does define an annotation channel
% read the data of the annotation channel as 16 bit
evt = read_edf(filename, hdr);
% undo the faulty calibration
evt = (evt - hdr.orig.Off(hdr.orig.annotation)) ./ hdr.orig.Cal(hdr.orig.annotation);
% convert the 16 bit format into the separate bytes
evt = typecast(int16(evt), 'uint8');
% construct the Time-stamped Annotations Lists (TAL), see http://www.edfplus.info/specs/edfplus.html#tal
tal = tokenize(char(evt), char(0), true);
% the startdate/time of a file is specified in the EDF+ header
% fields 'startdate of recording' and 'starttime of recording'.
% These fields must indicate the absolute second in which the
% start of the first data record falls. So, the first TAL in
% the first data record always starts with +0.X, indicating
% that the first data record starts a fraction, X, of a second
% after the startdate/time that is specified in the EDF+
% header. If X=0, then the .X may be omitted. Onset must start
% with a '+' or a '-' character and specifies the amount of
% seconds by which the onset of the annotated event follows
% ('+') or precedes ('-') the startdate/time of the file,
% that is specified in the header.
% determine millisecond aspect of starttime (always within
% first data record), remove from remaining timestamps
tok = tokenize(tal{1}, char(20));
millisecond_start = -1*str2double(tok{1});
for i=1:length(tal)
% the unprintable characters 20 and 21 are used as separators between time, duration and the annotation
% duration can be skipped in which case its preceding 21 must also be skipped
tok = tokenize(tal{i}, char(20)); % split time and annotation
if any(tok{1}==21)
% the time and duration are specified
dum = tokenize(tok{1}, char(21)); % split time and duration
time = str2double(dum{1}) + millisecond_start;
duration = str2double(dum{2}) + millisecond_start;
else
% only the time is specified
time = str2double(tok{1}) + millisecond_start;
duration = [];
end
% there can be multiple annotations per time, the last cell is always empty
for j=2:length(tok)-1
anot = char(tok{j});
% represent the annotation as event
event(end+1).type = 'annotation';
event(end ).value = anot;
event(end ).sample = round(time*hdr.Fs + 1); % expressed in samples, first sample in the file is 1
event(end ).duration = round(duration*hdr.Fs); % expressed in samples
event(end ).timestamp = time; % in seconds, relative to the start of the recording
event(end ).offset = 0;
end
end
end
case 'eeglab_set'
if isempty(hdr)
hdr = ft_read_header(filename);
end
event = read_eeglabevent(filename, 'header', hdr);
case 'eeglab_erp'
if isempty(hdr)
hdr = ft_read_header(filename);
end
event = read_erplabevent(filename, 'header', hdr);
case 'eep_avr'
% check that the required low-level toolbox is available
ft_hastoolbox('eeprobe', 1);
% the headerfile and datafile are the same
if isempty(hdr)
hdr = ft_read_header(filename);
end
event(end+1).type = 'average';
event(end ).sample = 1;
event(end ).duration = hdr.nSamples;
event(end ).offset = -hdr.nSamplesPre;
event(end ).value = [];
case {'eep_cnt' 'eep_trg'}
% check that the required low-level toolbox is available
ft_hastoolbox('eeprobe', 1);
% this requires the header from the cnt file and the triggers from the trg file
if strcmp(eventformat, 'eep_cnt')
trgfile = [filename(1:(end-3)), 'trg'];
cntfile = filename;
elseif strcmp(eventformat, 'eep_trg')
cntfile = [filename(1:(end-3)), 'cnt'];
trgfile = filename;
end
if exist(trgfile, 'file')
trg = read_eep_trg(trgfile);
else
ft_warning('The corresponding "%s" file was not found, cannot read in trigger information. No events can be read in.', trgfile);
trg = []; % make it empty, needed below
end
if isempty(hdr)
if exist(cntfile, 'file')
hdr = ft_read_header(cntfile);
else
ft_warning('The corresponding "%s" file was not found, cannot read in header information. No events can be read in.', cntfile);
hdr = []; % remains empty, needed below
end
end
if ~isempty(trg) && ~isempty(hdr)
% translate the EEProbe trigger codes to events
for i=1:length(trg)
event(i).type = 'trigger';
event(i).sample = trg(i).offset;
event(i).value = trg(i).type;
event(i).offset = 0;
event(i).duration = 0;
end
end
case 'egi_egis'
if isempty(hdr)
hdr = ft_read_header(filename);
end
fhdr = hdr.orig.fhdr;
chdr = hdr.orig.chdr;
ename = hdr.orig.ename;
cnames = hdr.orig.cnames;
fcom = hdr.orig.fcom;
ftext = hdr.orig.ftext;
eventCount=0;
for cel=1:fhdr(18)
for trial=1:chdr(cel,2)
eventCount=eventCount+1;
event(eventCount).type = 'trial';
event(eventCount).sample = (eventCount-1)*hdr.nSamples + 1;
event(eventCount).offset = -hdr.nSamplesPre;
event(eventCount).duration = hdr.nSamples;
event(eventCount).value = cnames{cel};
end
end
case 'egi_egia'
if isempty(hdr)
hdr = ft_read_header(filename);
end
fhdr = hdr.orig.fhdr;
chdr = hdr.orig.chdr;
ename = hdr.orig.ename;
cnames = hdr.orig.cnames;
fcom = hdr.orig.fcom;
ftext = hdr.orig.ftext;
eventCount=0;
for cel=1:fhdr(18)
for subject=1:chdr(cel,2)
eventCount=eventCount+1;
event(eventCount).type = 'trial';
event(eventCount).sample = (eventCount-1)*hdr.nSamples + 1;
event(eventCount).offset = -hdr.nSamplesPre;
event(eventCount).duration = hdr.nSamples;
event(eventCount).value = ['Sub' sprintf('%03d',subject) cnames{cel}];
end
end
case 'egi_sbin'
if ~exist('segHdr','var')
[EventCodes, segHdr, eventData] = read_sbin_events(filename);
end
if ~exist('header_array','var')
[header_array, CateNames, CatLengths, preBaseline] = read_sbin_header(filename);
end
if isempty(hdr)
hdr = ft_read_header(filename,'headerformat','egi_sbin');
end
version = header_array(1);
unsegmented = ~mod(version, 2);
eventCount=0;
if unsegmented
[evType,sampNum] = find(eventData);
for k = 1:length(evType)
event(k).sample = sampNum(k);
event(k).offset = [];
event(k).duration = 0;
event(k).type = 'trigger';
event(k).value = char(EventCodes(evType(k),:));
end
else
for theEvent=1:size(eventData,1)
for segment=1:hdr.nTrials
if any(eventData(theEvent,((segment-1)*hdr.nSamples +1):segment*hdr.nSamples))
eventCount=eventCount+1;
event(eventCount).sample = min(find(eventData(theEvent,((segment-1)*hdr.nSamples +1):segment*hdr.nSamples))) +(segment-1)*hdr.nSamples;
event(eventCount).offset = -hdr.nSamplesPre;
event(eventCount).duration = length(find(eventData(theEvent,((segment-1)*hdr.nSamples +1):segment*hdr.nSamples )>0))-1;
event(eventCount).type = 'trigger';
event(eventCount).value = char(EventCodes(theEvent,:));
end
end
end
end
if ~unsegmented
for segment=1:hdr.nTrials % cell information
eventCount=eventCount+1;
event(eventCount).type = 'trial';
event(eventCount).sample = (segment-1)*hdr.nSamples + 1;
event(eventCount).offset = -hdr.nSamplesPre;
event(eventCount).duration = hdr.nSamples;
event(eventCount).value = char([CateNames{segHdr(segment,1)}(1:CatLengths(segHdr(segment,1)))]);
end
end
case 'egi_mff_v1'
% The following represents the code that was written by Ingrid, Robert
% and Giovanni to get started with the EGI mff dataset format. It might
% not support all details of the file formats.
%
% An alternative implementation has been provided by EGI, this is
% released as fieldtrip/external/egi_mff and referred further down in
% this function as 'egi_mff_v2'.
%
% An more recent implementation has been provided by EGI and Arno Delorme, this
% is released as https://github.com/arnodelorme/mffmatlabio and referred further
% down in this function as 'egi_mff_v3'.
if isempty(hdr)
% use the corresponding code to read the header
hdr = ft_read_header(filename, 'headerformat', eventformat);
end
if ~usejava('jvm')
ft_error('the xml2struct requires MATLAB to be running with the Java virtual machine (JVM)');
% an alternative implementation which does not require the JVM but runs much slower is
% available from http://www.mathworks.com/matlabcentral/fileexchange/6268-xml4mat-v2-0
end
% get event info from xml files
ws = ft_warning('off', 'MATLAB:REGEXP:deprecated'); % due to some small code xml2struct
xmlfiles = dir( fullfile(filename, '*.xml'));
disp('reading xml files to obtain event info... This might take a while if many events/triggers are present')
if isempty(xmlfiles)
xml=struct([]);
else
xml=[];
end
for i = 1:numel(xmlfiles)
if strcmpi(xmlfiles(i).name(1:6), 'Events')
fieldname = strrep(xmlfiles(i).name(1:end-4), ' ', '_');
filename_xml = fullfile(filename, xmlfiles(i).name);
xml.(fieldname) = xml2struct(filename_xml);
end
end
ft_warning(ws); % revert the warning state
% construct info needed for FieldTrip Event
eventNames = fieldnames(xml);
begTime = hdr.orig.xml.info.recordTime;
begTime(11) = ' '; begTime(end-5:end) = [];
begSDV = datenum(begTime);
% find out if there are epochs in this dataset
if isfield(hdr.orig.xml,'epochs') && length(hdr.orig.xml.epochs) > 1
Msamp2offset = nan(2,size(hdr.orig.epochdef,1),1+max(hdr.orig.epochdef(:,2)-hdr.orig.epochdef(:,1)));
for iEpoch = 1:size(hdr.orig.epochdef,1)
nSampEpoch = hdr.orig.epochdef(iEpoch,2)-hdr.orig.epochdef(iEpoch,1)+1;
Msamp2offset(1,iEpoch,1:nSampEpoch) = hdr.orig.epochdef(iEpoch,1):hdr.orig.epochdef(iEpoch,2); %sample number in samples
Msamp2offset(2,iEpoch,1:nSampEpoch) = hdr.orig.epochdef(iEpoch,3):hdr.orig.epochdef(iEpoch,3)+nSampEpoch-1; %offset in samples
end
end