Permalink
Cannot retrieve contributors at this time
2463 lines (2226 sloc)
102 KB
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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 | |
% return the concatenated events | |
event = appendstruct(event{:}); | |
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 | |
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([])); | |
readbids = ft_getopt(varargin, 'readbids', 'ifmakessense'); | |
combinebinary = ft_getopt(varargin, 'combinebinary'); % this is a sub-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 | |
statusindx = find(strcmp(hdr.label, 'STATUS')); | |
if length(statusindx)==1 | |
% represent the rising flanks in the STATUS channel as events | |
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', statusindx, 'detectflank', 'up', 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixbiosemi', true); | |
else | |
ft_warning('BIOSIG does not have a consistent event representation, skipping events') | |
event = []; | |
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, usefull 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 | |
% 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 '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 | |
% construct event according to FieldTrip rules | |
eventCount = 0; | |
for iXml = 1:length(eventNames) | |
eval(['eventField=isfield(xml.' eventNames{iXml} ',''event'');']) | |
if eventField | |
for iEvent = 1:length(xml.(eventNames{iXml})) | |
eventTime = xml.(eventNames{iXml})(iEvent).event.beginTime; | |
eventTime(11) = ' '; eventTime(end-5:end) = []; | |
if strcmp('-',eventTime(21)) | |
% event out of range (before recording started): do nothing. | |
else | |
eventSDV = datenum(eventTime); | |
eventOffset = round((eventSDV - begSDV)*24*60*60*hdr.Fs); %in samples, relative to start of recording | |
if eventOffset < 0 | |
% event out of range (before recording started): do nothing | |
else | |
% calculate eventSample, relative to start of epoch | |
if isfield(hdr.orig.xml,'epochs') && length(hdr.orig.xml.epochs) > 1 | |
SampIndex=[]; | |
for iEpoch = 1:size(hdr.orig.epochdef,1) | |
[dum,dum2] = intersect(squeeze(Msamp2offset(2,iEpoch,:)), eventOffset); | |
if ~isempty(dum2) | |
EpochNum = iEpoch; | |
SampIndex = dum2; | |
end | |
end | |
if ~isempty(SampIndex) | |
eventSample = Msamp2offset(1,EpochNum,SampIndex); | |
else | |
eventSample=[]; %Drop event if past end of epoch | |
end | |
else | |
eventSample = eventOffset+1; | |
end | |
if ~isempty(eventSample) | |
eventCount=eventCount+1; | |
event(eventCount).type = eventNames{iXml}(8:end); | |
event(eventCount).sample = eventSample; | |
event(eventCount).offset = 0; | |
event(eventCount).duration = str2double(xml.(eventNames{iXml})(iEvent).event.duration)./1000000000*hdr.Fs; | |
event(eventCount).value = xml.(eventNames{iXml})(iEvent).event.code; | |
event(eventCount).orig = xml.(eventNames{iXml})(iEvent).event; | |
end | |
end %if that takes care of non "-" events that are still out of range | |
end %if that takes care of "-" events, which are out of range | |
end %iEvent | |
end | |
end | |
% add "epoch" events for epoched data, i.e. data with variable length segments | |
if (hdr.nTrials==1) | |
eventCount=length(event); | |
for iEpoch=1:size(hdr.orig.epochdef,1) | |
eventCount=eventCount+1; | |
event(eventCount).type = 'epoch'; | |
event(eventCount).sample = hdr.orig.epochdef(iEpoch,1); | |
event(eventCount).duration = hdr.orig.epochdef(iEpoch,2)-hdr.orig.epochdef(iEpoch,1)+1; | |
event(eventCount).offset = hdr.orig.epochdef(iEpoch,3); | |
event(eventCount).value = []; | |
end % for | |
end % if | |
% add "trial" events for segmented data, i.e. data with constant length segments | |
if (hdr.nTrials >1) && (size(hdr.orig.epochdef,1)==hdr.nTrials) | |
cellNames=cell(hdr.nTrials,1); | |
recTime=zeros(hdr.nTrials,1); | |
epochTimes=cell(hdr.nTrials,1); | |
for iEpoch=1:hdr.nTrials | |
epochTimes{iEpoch}=hdr.orig.xml.epochs(iEpoch).epoch.beginTime; | |
recTime(iEpoch)=((str2num(hdr.orig.xml.epochs(iEpoch).epoch.beginTime)/1000)/(1000/hdr.Fs))+1; | |
end | |
for iCat=1:length(hdr.orig.xml.categories) | |
theTimes=cell(length(hdr.orig.xml.categories(iCat).cat.segments),1); | |
for i=1:length(hdr.orig.xml.categories(iCat).cat.segments) | |
theTimes{i}=hdr.orig.xml.categories(iCat).cat.segments(i).seg.beginTime; | |
end | |
epochIndex=find(ismember(epochTimes,theTimes)); | |
for i=1:length(epochIndex) | |
cellNames{epochIndex(i)}=hdr.orig.xml.categories(iCat).cat.name; | |
end | |
end | |
eventCount=length(event); | |
for iEpoch=1:hdr.nTrials | |
eventCount=eventCount+1; | |
event(eventCount).type = 'trial'; | |
event(eventCount).sample = hdr.orig.epochdef(iEpoch,1); | |
event(eventCount).offset = -hdr.nSamplesPre; | |
event(eventCount).duration = hdr.nSamples; | |
event(eventCount).value = cellNames{iEpoch}; | |
end | |
end | |
case 'egi_mff_v2' | |
% ensure that the EGI_MFF_V2 toolbox is on the path | |
ft_hastoolbox('egi_mff_v2', 1); | |
%%%%%%%%%%%%%%%%%%%%%% | |
%workaround for MATLAB bug resulting in global variables being cleared | |
globalTemp=cell(0); | |
globalList=whos('global'); | |
varList=whos; | |
for i=1:length(globalList) | |
eval(['global ' globalList(i).name ';']); | |
eval(['globalTemp{end+1}=' globalList(i).name ';']); | |
end | |
%%%%%%%%%%%%%%%%%%%%%% | |
% ensure that the JVM is running and the jar file is on the path | |
mff_setup; | |
%%%%%%%%%%%%%%%%%%%%%% | |
%workaround for MATLAB bug resulting in global variables being cleared | |
varNames={varList.name}; | |
for i=1:length(globalList) | |
eval(['global ' globalList(i).name ';']); | |
eval([globalList(i).name '=globalTemp{i};']); | |
if ~any(strcmp(globalList(i).name,varNames)) %was global variable originally out of scope? | |
eval(['clear ' globalList(i).name ';']); %clears link to global variable without affecting it | |
end | |
end | |
clear globalTemp globalList varNames varList; | |
%%%%%%%%%%%%%%%%%%%%%% | |
if isunix && filename(1)~=filesep | |
% add the full path to the dataset directory | |
filename = fullfile(pwd, filename); | |
elseif ispc && filename(2)~=':' | |
% add the full path, including drive letter | |
filename = fullfile(pwd, filename); | |
end | |
% pass the header along to speed it up, it will be read on the fly in case it is empty | |
event = read_mff_event(filename, hdr); | |
% clean up the fields in the event structure | |
fn = fieldnames(event); | |
fn = setdiff(fn, {'type', 'sample', 'value', 'offset', 'duration', 'timestamp'}); | |
for i=1:length(fn) | |
event = rmfield(event, fn{i}); | |
end | |
case {'egi_mff_v3' 'egi_mff'} % this is the default | |
ft_hastoolbox('mffmatlabio', 1); | |
event = mff_fileio_read_event(filename); | |
case 'smi_txt' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% auto-detect the trigger channels | |
chanindx = find(strcmp(hdr.label, 'Trigger')); | |
end | |
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding); | |
for i=1:numel(event) | |
event(i).timestamp = hdr.orig.timestamp(event(i).sample); | |
end | |
case 'eyelink_asc' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isfield(hdr.orig, 'input') | |
% this is inefficient, since it keeps the complete data in memory | |
% but it does speed up subsequent read operations without the user | |
% having to care about it | |
asc = hdr.orig; | |
else | |
asc = read_eyelink_asc(filename); | |
end | |
if ~isempty(asc.input) | |
timestamp = [asc.input(:).timestamp]; | |
value = [asc.input(:).value]; | |
else | |
timestamp = []; | |
value = []; | |
end | |
% note that in this dataformat the first input trigger can be before | |
% the start of the data acquisition | |
for i=1:length(timestamp) | |
event(end+1).type = 'INPUT'; | |
event(end ).sample = (timestamp(i)-hdr.FirstTimeStamp)/hdr.TimeStampPerSample + 1; | |
event(end ).timestamp = timestamp(i); | |
event(end ).value = value(i); | |
event(end ).duration = 1; | |
event(end ).offset = 0; | |
end | |
case 'fcdc_global' | |
event = event_queue; | |
case 'fcdc_buffer' | |
% read from a networked buffer for realtime analysis | |
[host, port] = filetype_check_uri(filename); | |
% SK: the following was intended to speed up, but does not work | |
% the buffer server will try to return exact indices, even | |
% if the intend here is to filter based on a maximum range. | |
% We could change the definition of GET_EVT to comply | |
% with filtering, but that might break other existing code. | |
%if isempty(flt_minnumber) && isempty(flt_maxnumber) | |
% evtsel = []; | |
%else | |
% evtsel = [0 2^32-1]; | |
% if ~isempty(flt_minnumber) | |
% evtsel(1) = flt_minnumber-1; | |
% end | |
% if ~isempty(flt_maxnumber) | |
% evtsel(2) = flt_maxnumber-1; | |
% end | |
%end | |
if blocking && isempty(flt_minnumber) && isempty(flt_maxnumber) | |
ft_warning('disabling blocking because no selection was specified'); | |
blocking = false; | |
end | |
if blocking | |
nsamples = 0; % disable waiting for samples | |
if isempty(flt_minnumber) | |
nevents = flt_maxnumber; | |
elseif isempty(flt_maxnumber) | |
nevents = flt_minnumber; | |
else | |
nevents = max(flt_minnumber, flt_maxnumber); | |
end | |
available = buffer_wait_dat([nsamples nevents timeout], host, port); | |
if available.nevents<nevents | |
ft_error('buffer timed out while waiting for %d events', nevents); | |
end | |
end | |
try | |
event = buffer('get_evt', [], host, port); | |
catch | |
if strfind(lasterr, 'the buffer returned an error') | |
% this happens if the buffer contains no events | |
% which in itself is not a problem and should not result in an error | |
event = []; | |
else | |
rethrow(lasterr); | |
end | |
end | |
case 'fcdc_buffer_offline' | |
if isfolder(filename) | |
path = filename; | |
else | |
[path, file, ext] = fileparts(filename); | |
end | |
if isempty(hdr) | |
headerfile = fullfile(path, 'header'); | |
hdr = read_buffer_offline_header(headerfile); | |
end | |
eventfile = fullfile(path, 'events'); | |
event = read_buffer_offline_events(eventfile, hdr); | |
case 'fcdc_matbin' | |
% this is multiplexed data in a *.bin file, accompanied by a MATLAB file containing the header and event | |
[path, file, ext] = fileparts(filename); | |
filename = fullfile(path, [file '.mat']); | |
% read the events from the MATLAB file | |
tmp = load(filename, 'event'); | |
if isfield(tmp, 'event') | |
event = tmp.event; | |
else | |
event = []; | |
end | |
if ~isempty(chanindx) | |
% also parse the selected channels for TTL triggers | |
trigger = 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, 'denoise', denoise); | |
event = appendstruct(event, trigger); | |
end | |
case 'fcdc_fifo' | |
fifo = filetype_check_uri(filename); | |
if ~exist(fifo,'file') | |
ft_warning('the FIFO %s does not exist; attempting to create it', fifo); | |
fid = fopen_or_error(fifo, 'r'); | |
system(sprintf('mkfifo -m 0666 %s',fifo)); | |
end | |
msg = fread(fid, inf, 'uint8'); | |
fclose(fid); | |
try | |
event = mxDeserialize(uint8(msg)); | |
catch | |
ft_warning(lasterr); | |
end | |
case 'fcdc_tcp' | |
% requires tcp/udp/ip-toolbox | |
ft_hastoolbox('TCP_UDP_IP', 1); | |
[host, port] = filetype_check_uri(filename); | |
if isempty(sock) | |
sock=pnet('tcpsocket',port); | |
end | |
con = pnet(sock, 'tcplisten'); | |
if con~=-1 | |
try | |
pnet(con,'setreadtimeout',10); | |
% read packet | |
msg=pnet(con,'readline'); %,1000,'uint8','network'); | |
if ~isempty(msg) | |
event = mxDeserialize(uint8(str2num(msg))); | |
end | |
% catch | |
% ft_warning(lasterr); | |
end | |
pnet(con,'close'); | |
end | |
con = []; | |
case 'fcdc_udp' | |
% requires tcp/udp/ip-toolbox | |
ft_hastoolbox('TCP_UDP_IP', 1); | |
[host, port] = filetype_check_uri(filename); | |
try | |
% read from localhost | |
udp=pnet('udpsocket',port); | |
% Wait/Read udp packet to read buffer | |
len=pnet(udp,'readpacket'); | |
if len>0, | |
% if packet larger then 1 byte then read maximum of 1000 doubles in network byte order | |
msg=pnet(udp,'read',1000,'uint8'); | |
if ~isempty(msg) | |
event = mxDeserialize(uint8(msg)); | |
end | |
end | |
catch | |
ft_warning(lasterr); | |
end | |
% On break or error close connection | |
pnet(udp,'close'); | |
case 'fcdc_serial' | |
% this code is moved to a separate file | |
event = read_serial_event(filename); | |
case 'fcdc_mysql' | |
% check that the required low-level toolbox is available | |
ft_hastoolbox('mysql', 1); | |
% read from a MySQL server listening somewhere else on the network | |
db_open(filename); | |
if db_blob | |
event = db_select_blob('fieldtrip.event', 'msg'); | |
else | |
event = db_select('fieldtrip.event', {'type', 'value', 'sample', 'offset', 'duration'}); | |
end | |
case 'gtec_hdf5' | |
% the header mentions trigger channels, but I don't know how they are stored | |
ft_warning('event reading for hdf5 has not yet been implemented due to a lack of a good example file'); | |
case 'gtec_mat' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% these are probably trigger channels | |
chanindx = match_str(hdr.label, {'Display', 'Target'}); | |
end | |
% use a helper function to read the trigger channels and detect the flanks | |
% pass all the other users options to the read_trigger function | |
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding); | |
case {'homer_nirs'} | |
% Homer files are MATLAB files in disguise | |
% see https://www.nitrc.org/plugins/mwiki/index.php/homer2:Homer_Input_Files#NIRS_data_file_format | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% look in nirs.s for events, this corresponds to the stimulus channels | |
chanindx = find(ft_chantype(hdr, 'stimulus')); | |
trigger = 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, 'denoise', denoise, 'fixhomer', true); | |
event = appendstruct(event, trigger); | |
if isempty(event) | |
% also look in nirs.aux for channels with analog TTL values | |
chanindx = find(ft_chantype(hdr, 'aux')); | |
trigger = 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, 'denoise', denoise, 'fixhomer', true); | |
event = appendstruct(event, trigger); | |
end | |
else | |
% use the user-supplied list of channels | |
trigger = 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, 'denoise', denoise, 'fixhomer', true); | |
event = appendstruct(event, trigger); | |
end | |
case {'itab_raw' 'itab_mhd'} | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
for i=1:hdr.orig.nsmpl | |
event(end+1).type = 'trigger'; | |
event(end ).value = hdr.orig.smpl(i).type; | |
event(end ).sample = hdr.orig.smpl(i).start + 1; | |
event(end ).duration = hdr.orig.smpl(i).ntptot; | |
event(end ).offset = -hdr.orig.smpl(i).ntppre; % number of samples prior to the trigger | |
end | |
if isempty(event) | |
ft_warning('no events found in the event table, reading the trigger channel(s)'); | |
if isempty(chanindx) | |
% auto-detect the trigger channels | |
chanindx = find(ft_chantype(hdr, 'flag')); | |
end | |
trigger = 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, 'denoise', denoise); | |
event = appendstruct(event, trigger); | |
end | |
case 'matlab' | |
% read the event structure from a MATLAB file | |
% it should either contain an "event" structure, or a FieldTrip data structure according to FT_DATATYPE_RAW | |
w = whos(matfile(filename)); | |
if any(strcmp({w.name}, 'event')) | |
event = loadvar(filename, 'event'); | |
elseif any(strcmp({w.name}, 'data')) || length(w)==1 | |
% assume that the matlab file contains a raw data structure according to FT_DATATYPE_RAW that includes trigger channels | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% try to determine the trigger channels from the header | |
chanindx = find(ft_chantype(hdr, 'trigger')); | |
else | |
% use the user-supplied list of trigger channels | |
end | |
trigger = 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, 'denoise', denoise); | |
event = appendstruct(event, trigger); | |
end | |
case {'manscan_mbi', 'manscan_mb2'} | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isfield(hdr.orig, 'epochs') && ~isempty(hdr.orig.epochs) | |
trlind = []; | |
for i = 1:numel(hdr.orig.epochs) | |
trlind = [trlind i*ones(1, diff(hdr.orig.epochs(i).samples) + 1)]; | |
end | |
else | |
trlind = ones(1, hdr.nSamples); | |
end | |
if isfield(hdr.orig, 'events') | |
for i = 1:numel(hdr.orig.events) | |
for j = 1:length(hdr.orig.events(i).samples) | |
event(end+1).type = 'trigger'; | |
event(end).value = hdr.orig.events(i).label; | |
event(end).sample = find(cumsum(trlind == hdr.orig.events(i).epochs(j))... | |
== hdr.orig.events(i).samples(j), 1, 'first'); | |
end | |
end | |
end | |
case 'mayo_mef30' | |
if isempty(hdr) | |
hdr = ft_read_header(filename, 'password', password); | |
end | |
event = read_mayo_mef30(filename, password, [], hdr); | |
case 'mayo_mef21' | |
if isempty(hdr) | |
hdr = ft_read_header(filename, 'password', password); | |
end | |
event = read_mayo_mef21(filename, password, hdr); | |
case 'mega_neurone' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
% this is fast but memory inefficient, since the header contains all data and events | |
if isfield(hdr.orig, 'event') | |
NEURONE = hdr.orig; | |
else | |
% ensure that this external toolbox is on the path | |
ft_hastoolbox('neurone', 1); | |
if filename(end)~=filesep | |
% it should end with a slash | |
filename = [filename filesep]; | |
end | |
NEURONE = readneurone(filename); | |
end | |
for i=1:numel(NEURONE.event) | |
if isnan(str2double(NEURONE.event(i).type)) | |
% there are a number of event "Types" that can happen on different "SourcePorts" | |
event(i).type = NEURONE.event(i).type; | |
event(i).sample = round(NEURONE.event(i).latency * 0.001 * NEURONE.srate + 1); | |
event(i).value = []; | |
else | |
% this seems to correspond with an external trigger code, which is best represented numerically | |
event(i).type = 'trigger'; | |
event(i).sample = round(NEURONE.event(i).latency * 0.001 * NEURONE.srate + 1); | |
event(i).value = str2double(NEURONE.event(i).type); | |
end | |
end | |
case 'micromed_trc' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isfield(hdr, 'orig') && isfield(hdr.orig, 'Trigger_Area') && isfield(hdr.orig, 'Tigger_Area_Length') | |
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); | |
event = appendstruct(event, trigger); | |
else | |
% routine that reads analog triggers in case no index is specified | |
event = read_micromed_event(filename); | |
end | |
else | |
ft_error('Not a correct event format') | |
end | |
case {'mpi_ds', 'mpi_dap'} | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
% determine the DAP files that compromise this dataset | |
if isfolder(filename) | |
ls = dir(filename); | |
dapfile = {}; | |
for i=1:length(ls) | |
if ~isempty(regexp(ls(i).name, '.dap$', 'once' )) | |
dapfile{end+1} = fullfile(filename, ls(i).name); | |
end | |
end | |
dapfile = sort(dapfile); | |
elseif iscell(filename) | |
dapfile = filename; | |
else | |
dapfile = {filename}; | |
end | |
% assume that each DAP file is accompanied by a dat file | |
% read the trigger values from the separate dat files | |
trg = []; | |
for i=1:length(dapfile) | |
datfile = [dapfile{i}(1:(end-4)) '.dat']; | |
trg = cat(1, trg, textread(datfile, '', 'headerlines', 1)); | |
end | |
% construct a event structure, one 'trialcode' event per trial | |
for i=1:length(trg) | |
event(i).type = 'trialcode'; % string | |
event(i).sample = (i-1)*hdr.nSamples + 1; % expressed in samples, first sample of file is 1 | |
event(i).value = trg(i); % number or string | |
event(i).offset = 0; % expressed in samples | |
event(i).duration = hdr.nSamples; % expressed in samples | |
end | |
case 'nervus_eeg' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
% construct a event structure from data in the header | |
maxSampleRate = max([hdr.orig.Segments.samplingRate]); | |
earliestDateTime = min([hdr.orig.Segments.dateOLE]); | |
for i=1:length(hdr.orig.Events) | |
event(i).type = hdr.orig.Events(i).IDStr; % string | |
event(i).value = hdr.orig.Events(i).label; % number or string | |
event(i).offset = 0; % expressed in samples | |
% calculate the sample value of the event, based on the highest | |
% sample rate | |
event(i).sample = (hdr.orig.Events(i).dateOLE-earliestDateTime)*3600*24*maxSampleRate; | |
if event(i).sample == 0 | |
event(i).sample = 1; | |
elseif event(i).sample > hdr.nSamples | |
event(i).sample = hdr.nSamples; | |
end | |
event(i).duration = hdr.orig.Events(i).duration*maxSampleRate; | |
end | |
% Add boundary events to indicate segments | |
originalEventCount = length(hdr.orig.Events); | |
boundaryEventCount = 1; | |
for i=1:length(hdr.orig.Segments) | |
sampleCountOfchannelsWithSameSampleRate(i,:) = hdr.orig.Segments(i).sampleCount; | |
end | |
for i=2:length(hdr.orig.Segments) | |
event(originalEventCount+boundaryEventCount).type = 'boundary'; | |
event(originalEventCount+boundaryEventCount).value = 'boundary'; | |
event(originalEventCount+boundaryEventCount).offset = 0; | |
gapDurationSeconds = seconds(hdr.orig.Segments(i).date-hdr.orig.Segments(i-1).date)-hdr.orig.Segments(i-1).duration; | |
event(originalEventCount+boundaryEventCount).duration = gapDurationSeconds*maxSampleRate; | |
event(originalEventCount+boundaryEventCount).sample = sum(sampleCountOfchannelsWithSameSampleRate(1:(i-1))); | |
%move all non-boundary events later than this segment start | |
%back by the length of the gap, since the calculation for event | |
%sample start above assumes continuous sampling without gaps | |
for j=1:originalEventCount | |
if hdr.orig.Events(j).date > hdr.orig.Segments(i).date | |
event(j).sample = event(j).sample - gapDurationSeconds*maxSampleRate; | |
end | |
end | |
boundaryEventCount = boundaryEventCount+1; | |
end | |
case {'neuromag_eve'} | |
% previously this was called babysquid_eve, now it is neuromag_eve | |
% see also http://bugzilla.fieldtriptoolbox.org/show_bug.cgi?id=2170 | |
[p, f, x] = fileparts(filename); | |
evefile = fullfile(p, [f '.eve']); | |
fiffile = fullfile(p, [f '.fif']); | |
if isempty(hdr) | |
hdr = ft_read_header(fiffile, 'headerformat', 'neuromag_mne'); | |
end | |
[smp, tim, typ, val] = read_neuromag_eve(evefile); | |
smp = smp + 1; % should be 1-offset | |
smp = smp - double(hdr.orig.raw.first_samp); % the recording to disk may start later than the actual acquisition | |
if ~isempty(smp) | |
sample = num2cell(smp); | |
value = num2cell(val); | |
offset = num2cell(zeros(size(smp))); | |
type = repmat({'unknown'}, size(typ)); | |
type(typ==0) = {'trigger'}; | |
if any(typ~=0) | |
% see the comments in read_neuromag_eve | |
ft_warning('entries in the *.eve file with a type other than 0 are represented as ''unknown''') | |
end | |
% convert to a structure array | |
event = struct('type', type, 'value', value, 'sample', sample, 'offset', offset); | |
else | |
event = []; | |
end | |
case {'neuromag_fif' 'neuromag_mne' 'neuromag_mex'} | |
if strcmp(eventformat, 'neuromag_fif') | |
% the default is to use the MNE reader for fif files | |
eventformat = 'neuromag_mne'; | |
end | |
if strcmp(eventformat, 'neuromag_mex') | |
% check that the required low-level toolbox is available | |
ft_hastoolbox('meg-pd', 1); | |
if isempty(headerformat), headerformat = eventformat; end | |
if isempty(dataformat), dataformat = eventformat; end | |
elseif strcmp(eventformat, 'neuromag_mne') | |
% check that the required low-level toolbox is available | |
ft_hastoolbox('mne', 1); | |
if isempty(headerformat), headerformat = eventformat; end | |
if isempty(dataformat), dataformat = eventformat; end | |
end | |
if isempty(hdr) | |
hdr = ft_read_header(filename, 'headerformat', headerformat, 'checkmaxfilter', checkmaxfilter, 'password', password); | |
end | |
% note below we've had to include some chunks of code that are only | |
% called if the file is an averaged file, or if the file is continuous. | |
% These are defined in hdr by read_header for neuromag_mne, but do not | |
% exist for neuromag_fif, hence we run the code anyway if the fields do | |
% not exist (this is what happened previously anyway). | |
if strcmp(eventformat, 'neuromag_mex') | |
iscontinuous = 1; | |
isaverage = 0; | |
isepoched = 0; | |
elseif strcmp(eventformat, 'neuromag_mne') | |
iscontinuous = hdr.orig.iscontinuous; | |
isaverage = hdr.orig.isaverage; | |
isepoched = hdr.orig.isepoched; | |
end | |
if iscontinuous | |
binaryindx = find(strcmp(ft_chantype(hdr), 'digital trigger')); | |
otherindx = find(strcmp(ft_chantype(hdr), 'other trigger')); | |
analogindx = find(strcmp(ft_chantype(hdr), 'analog trigger')); | |
if isempty(binaryindx) && isempty(analogindx) && isempty(otherindx) | |
% in case of problems with older systems and MNE reader we use a predefined set of channel names | |
binary = {'STI 014', 'STI 015', 'STI 016'}; | |
binaryindx = match_str(hdr.label, binary); | |
end | |
if ~isempty(chanindx) | |
% only search in the subset of channels specified by the user | |
binaryindx = intersect(binaryindx, chanindx); | |
otherindx = intersect(otherindx, chanindx); | |
analogindx = intersect(analogindx, chanindx); | |
end | |
if ~isempty(binaryindx) | |
trigger = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', binaryindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixneuromag', false); | |
event = appendstruct(event, trigger); | |
end | |
if ~isempty(otherindx) | |
trigger = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', otherindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixneuromag', false); | |
event = appendstruct(event, trigger); | |
end | |
if ~isempty(analogindx) | |
% add the triggers to the event structure based on trigger channels with the name "STI xxx" | |
% there are some issues with noise on these analog trigger | |
% channels, on older systems only | |
% read the trigger channels and do flank detection | |
trigger = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', analogindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding, 'fixneuromag', true); | |
event = appendstruct(event, trigger); | |
if ~isempty(trigger) | |
% throw out everything that is not a (real) trigger... | |
[sel1, sel2] = match_str({trigger.type}, {'STI001', 'STI002', 'STI003', 'STI004', 'STI005', 'STI006', 'STI007', 'STI008'}); | |
trigger_tmp = trigger(sel1); | |
%trigger_bits = size(unique(sel2), 1); | |
all_triggers = unique(sel2); | |
trigger_bits = max(all_triggers) - min(all_triggers) + 1; | |
% collect all samples where triggers occured... | |
all_samples = unique([trigger_tmp.sample]); | |
new_triggers = []; | |
i = 1; | |
while i <= length(all_samples) | |
cur_triggers = []; | |
j = 0; | |
% look also in adjacent samples according to tolerance | |
while (i+j <= length(all_samples)) && (all_samples(i+j) - all_samples(i) <= tolerance) | |
if j >= 1 | |
fprintf('Fixing trigger at sample %d\n', all_samples(i)); | |
end %if | |
cur_triggers = appendstruct(cur_triggers, trigger_tmp([trigger_tmp.sample] == all_samples(i+j))); | |
j = j + 1; | |
end %while | |
% construct new trigger field | |
if ~isempty(cur_triggers) | |
new_triggers(end+1).type = 'Trigger'; | |
new_triggers(end).sample = all_samples(i); | |
[sel1, sel2] = match_str({cur_triggers.type}, {'STI001', 'STI002', 'STI003', 'STI004', 'STI005', 'STI006', 'STI007', 'STI008'}); | |
new_triggers(end).value = bin2dec((dec2bin(sum(2.^(sel2-1)), trigger_bits))); | |
end %if | |
i = i + j; | |
end %while | |
event = appendstruct(event, new_triggers); | |
end %if | |
end | |
elseif isaverage | |
% the length of each average can be variable | |
nsamples = zeros(1, length(hdr.orig.evoked)); | |
for i=1:length(hdr.orig.evoked) | |
nsamples(i) = size(hdr.orig.evoked(i).epochs, 2); | |
end | |
begsample = cumsum([1 nsamples]); | |
for i=1:length(hdr.orig.evoked) | |
event(end+1).type = 'average'; | |
event(end ).sample = begsample(i); | |
event(end ).value = hdr.orig.evoked(i).comment; % this is a descriptive string | |
event(end ).offset = hdr.orig.evoked(i).first; | |
event(end ).duration = hdr.orig.evoked(i).last - hdr.orig.evoked(i).first + 1; | |
end | |
elseif isepoched | |
begsample = cumsum([1 repmat(hdr.nSamples, hdr.nTrials-1, 1)']); | |
events_id = split(split(hdr.orig.epochs.event_id, ';'), ':'); | |
if all(cellfun(@ischar, events_id(:, 1))) | |
events_label = events_id(:, 1); | |
events_code = str2num(cell2mat(events_id(:, 2))); | |
elseif all(cellfun(@isnumeric, events_id(:, 1))) | |
events_label = cell2mat(events_id(:, 1)); | |
events_code = str2num(cell2mat(events_id(:, 2))); | |
end | |
for i=1:hdr.nTrials | |
event(end+1).type = 'trial'; | |
event(end ).sample = begsample(i); | |
event(end ).value = events_label(events_code == hdr.orig.epochs.events(i, 3), :); | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
end | |
end | |
% check whether the *.fif file is accompanied by an *.eve file | |
[p, f, x] = fileparts(filename); | |
evefile = fullfile(p, [f '.eve']); | |
if exist(evefile, 'file') | |
eve = ft_read_event(evefile, 'header', hdr); | |
if ~isempty(eve) | |
fprintf('appending %d events from "%s"\n', length(eve), evefile); | |
event = appendstruct(event, eve); | |
end | |
end | |
case {'neuralynx_ttl' 'neuralynx_bin' 'neuralynx_dma' 'neuralynx_sdma'} | |
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 strcmp(eventformat, 'neuralynx_dma') | |
% read the Parallel_in channel from the DMA log file | |
ttl = read_neuralynx_dma(filename, begsample, endsample, 'ttl'); | |
elseif strcmp(eventformat, 'neuralynx_sdma') | |
% determine the separate files with the trigger and timestamp information | |
[p, f, x] = fileparts(filename); | |
ttlfile = fullfile(filename, [f '.ttl.bin']); | |
tslfile = fullfile(filename, [f '.tsl.bin']); | |
tshfile = fullfile(filename, [f '.tsh.bin']); | |
if ~exist(ttlfile) && ~exist(tslfile) && ~exist(tshfile) | |
% perhaps it is an old splitted dma dataset? | |
ttlfile = fullfile(filename, [f '.ttl']); | |
tslfile = fullfile(filename, [f '.tsl']); | |
tshfile = fullfile(filename, [f '.tsh']); | |
end | |
if ~exist(ttlfile) && ~exist(tslfile) && ~exist(tshfile) | |
% these files must be present in a splitted dma dataset | |
ft_error('could not locate the individual ttl, tsl and tsh files'); | |
end | |
% read the trigger values from the separate file | |
ttl = read_neuralynx_bin(ttlfile, begsample, endsample); | |
elseif strcmp(eventformat, 'neuralynx_ttl') | |
% determine the optional files with timestamp information | |
tslfile = [filename(1:(end-4)) '.tsl']; | |
tshfile = [filename(1:(end-4)) '.tsh']; | |
% read the triggers from a separate *.ttl file | |
ttl = read_neuralynx_ttl(filename, begsample, endsample); | |
elseif strcmp(eventformat, 'neuralynx_bin') | |
% determine the optional files with timestamp information | |
tslfile = [filename(1:(end-8)) '.tsl.bin']; | |
tshfile = [filename(1:(end-8)) '.tsh.bin']; | |
% read the triggers from a separate *.ttl.bin file | |
ttl = read_neuralynx_bin(filename, begsample, endsample); | |
end | |
ttl = int32(ttl / (2^16)); % parallel port provides int32, but word resolution is int16. Shift the bits and typecast to signed integer. | |
d1 = (diff(ttl)~=0); % determine the flanks, which can be multiple samples long (this looses one sample) | |
d2 = (diff(d1)==1); % determine the onset of the flanks (this looses one sample) | |
smp = find(d2)+2; % find the onset of the flanks, add the two samples again | |
val = ttl(smp+5); % look some samples further for the trigger value, to avoid the flank | |
clear d1 d2 ttl | |
ind = find(val~=0); % look for triggers tith a non-zero value, this avoids downgoing flanks going to zero | |
smp = smp(ind); % discard triggers with a value of zero | |
val = val(ind); % discard triggers with a value of zero | |
if ~isempty(smp) | |
% try reading the timestamps | |
if strcmp(eventformat, 'neuralynx_dma') | |
tsl = read_neuralynx_dma(filename, 1, max(smp), 'tsl'); | |
tsl = typecast(tsl(smp), 'uint32'); | |
tsh = read_neuralynx_dma(filename, 1, max(smp), 'tsh'); | |
tsh = typecast(tsh(smp), 'uint32'); | |
ts = timestamp_neuralynx(tsl, tsh); | |
elseif exist(tslfile) && exist(tshfile) | |
tsl = read_neuralynx_bin(tslfile, 1, max(smp)); | |
tsl = tsl(smp); | |
tsh = read_neuralynx_bin(tshfile, 1, max(smp)); | |
tsh = tsh(smp); | |
ts = timestamp_neuralynx(tsl, tsh); | |
else | |
ts = []; | |
end | |
% reformat the values as cell-array, since the struct function can work with those | |
type = repmat({'trigger'},size(smp)); | |
value = num2cell(val); | |
sample = num2cell(smp + begsample - 1); | |
duration = repmat({[]},size(smp)); | |
offset = repmat({[]},size(smp)); | |
if ~isempty(ts) | |
timestamp = reshape(num2cell(ts),size(smp)); | |
else | |
timestamp = repmat({[]},size(smp)); | |
end | |
% convert it into a structure array, this can be done in one go | |
event = struct('type', type, 'value', value, 'sample', sample, 'timestamp', timestamp, 'offset', offset, 'duration', duration); | |
clear type value sample timestamp offset duration | |
end | |
if (strcmp(eventformat, 'neuralynx_bin') || strcmp(eventformat, 'neuralynx_ttl')) && isfield(hdr, 'FirstTimeStamp') | |
% the header was obtained from an external dataset which could be at a different sampling rate | |
% use the timestamps to redetermine the sample numbers | |
fprintf('using sample number of the downsampled file to reposition the TTL events\n'); | |
% convert the timestamps into samples, keeping in mind the FirstTimeStamp and TimeStampPerSample | |
smp = round(double(ts - uint64(hdr.FirstTimeStamp))./hdr.TimeStampPerSample + 1); | |
for i=1:length(event) | |
% update the sample number | |
event(i).sample = smp(i); | |
end | |
end | |
case 'neuralynx_ds' | |
% read the header of the dataset | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
% the event file is contained in the dataset directory | |
if exist(fullfile(filename, 'Events.Nev')) | |
filename = fullfile(filename, 'Events.Nev'); | |
elseif exist(fullfile(filename, 'Events.nev')) | |
filename = fullfile(filename, 'Events.nev'); | |
elseif exist(fullfile(filename, 'events.Nev')) | |
filename = fullfile(filename, 'events.Nev'); | |
elseif exist(fullfile(filename, 'events.nev')) | |
filename = fullfile(filename, 'events.nev'); | |
end | |
% read the events, apply filter is applicable | |
nev = read_neuralynx_nev(filename, 'type', flt_type, 'value', flt_value, 'mintimestamp', flt_mintimestamp, 'maxtimestamp', flt_maxtimestamp, 'minnumber', flt_minnumber, 'maxnumber', flt_maxnumber); | |
% the following code should only be executed if there are events, | |
% otherwise there will be an error subtracting an uint64 from an [] | |
if ~isempty(nev) | |
% now get the values as cell-array, since the struct function can work with those | |
value = {nev.TTLValue}; | |
timestamp = {nev.TimeStamp}; | |
number = {nev.EventNumber}; | |
type = repmat({'trigger'},size(value)); | |
duration = repmat({[]},size(value)); | |
offset = repmat({[]},size(value)); | |
sample = num2cell(round(double(cell2mat(timestamp) - hdr.FirstTimeStamp)/hdr.TimeStampPerSample + 1)); | |
% convert it into a structure array | |
event = struct('type', type, 'value', value, 'sample', sample, 'timestamp', timestamp, 'duration', duration, 'offset', offset, 'number', number); | |
end | |
case {'neuralynx_nev'} | |
% instead of the directory containing the combination of nev and ncs files, the nev file was specified | |
% do NOT read the header of the dataset | |
% read the events, apply filter is applicable | |
nev = read_neuralynx_nev(filename, 'type', flt_type, 'value', flt_value, 'mintimestamp', flt_mintimestamp, 'maxtimestamp', flt_maxtimestamp, 'minnumber', flt_minnumber, 'maxnumber', flt_maxnumber); | |
% the following code should only be executed if there are events, | |
% otherwise there will be an error subtracting an uint64 from an [] | |
if ~isempty(nev) | |
% now get the values as cell-array, since the struct function can work with those | |
value = {nev.TTLValue}; | |
timestamp = {nev.TimeStamp}; | |
number = {nev.EventNumber}; | |
type = repmat({'trigger'},size(value)); | |
duration = repmat({[]},size(value)); | |
offset = repmat({[]},size(value)); | |
% since the ncs files are not specified, there is no mapping between lfp samples and timestamps | |
sample = repmat({nan}, size(value)); | |
% convert it into a structure array | |
event = struct('type', type, 'value', value, 'sample', sample, 'timestamp', timestamp, 'duration', duration, 'offset', offset, 'number', number); | |
end | |
case 'nmc_archive_k' | |
event = read_nmc_archive_k_event(filename); | |
case 'netmeg' | |
ft_warning('FieldTrip:ft_read_event:unsupported_event_format', 'reading of events for the netmeg format is not yet supported'); | |
event = []; | |
case 'neuroomega_mat' | |
hdr = ft_read_header(filename, 'headerformat', eventformat); | |
hdr_orig = hdr.orig.orig; | |
fields_orig=hdr.orig.fields; %getting digital event channels | |
% extracting time begin | |
if ismember('CANALOG_IN_1_TimeBegin',fields_orig) | |
TimeBegin = hdr_orig.('CANALOG_IN_1_TimeBegin'); | |
else | |
ft_error('CANALOG_IN_1_TimeBegin required to load events'); | |
end | |
fields_orig=fields_orig(startsWith(fields_orig,'CDIG_IN')); %compat/matlablt2016b/startsWidth.m | |
if isempty(fields_orig) | |
ft_error('No NeuroOmega events in file %s',filename); | |
end | |
rx=regexp(fields_orig,'^CDIG_IN_{1}(\d+)[a-zA-Z_]*','tokens'); | |
dig_channels=unique(cellfun(@(x) str2num(x{1}), [rx{:}])); | |
if ~ismember(detectflank,{'up','down','both'}) | |
ft_error('incorrect specification of detectflank. Use up, down or both'); | |
end | |
if ismember(detectflank,{'up','both'}) | |
for i=1:length(dig_channels) | |
channel = ['CDIG_IN_' num2str(dig_channels(i)) '_Up']; | |
data = hdr_orig.(channel); | |
t0 = hdr_orig.(['CDIG_IN_' num2str(dig_channels(i)) '_TimeBegin']) - TimeBegin; | |
Fs = hdr_orig.(['CDIG_IN_' num2str(dig_channels(i)) '_KHz']) * 1000; | |
for j=1:length(hdr_orig.(channel)) | |
event(end+1).type = channel; | |
event(end ).value = dig_channels(i); | |
event(end ).sample = t0 + data(j) ./ Fs; %events in seconds from begging of file | |
end | |
end | |
end | |
if ismember(detectflank,{'down','both'}) | |
for i=1:length(dig_channels) | |
channel = ['CDIG_IN_' num2str(dig_channels(i)) '_Down']; | |
data = hdr_orig.(channel); | |
t0 = hdr_orig.(['CDIG_IN_' num2str(dig_channels(i)) '_TimeBegin']) - TimeBegin; | |
Fs = hdr_orig.(['CDIG_IN_' num2str(dig_channels(i)) '_KHz']) * 1000; | |
for j=1:length(hdr_orig.(channel)) | |
event(end+1).type = channel; | |
event(end ).value = dig_channels(i); | |
event(end ).sample = t0 + data(j) ./ Fs; %events in seconds from begging of file | |
end | |
end | |
end | |
case 'neuroshare' % NOTE: still under development | |
% check that the required neuroshare toolbox is available | |
ft_hastoolbox('neuroshare', 1); | |
tmp = read_neuroshare(filename, 'readevent', 'yes'); | |
for i=1:length(tmp.event.timestamp) | |
event(i).type = tmp.hdr.eventinfo(i).EventType; | |
event(i).value = tmp.event.data(i); | |
event(i).timestamp = tmp.event.timestamp(i); | |
event(i).sample = tmp.event.sample(i); | |
end | |
case 'neuralynx_cds' | |
% this is a combined Neuralynx dataset with separate subdirectories for the LFP, MUA and spike channels | |
dirlist = dir(filename); | |
%haslfp = any(filetype_check_extension({dirlist.name}, 'lfp')); | |
%hasmua = any(filetype_check_extension({dirlist.name}, 'mua')); | |
%hasspike = any(filetype_check_extension({dirlist.name}, 'spike')); | |
%hastsl = any(filetype_check_extension({dirlist.name}, 'tsl')); % separate file with original TimeStampLow | |
%hastsh = any(filetype_check_extension({dirlist.name}, 'tsh')); % separate file with original TimeStampHi | |
hasttl = any(filetype_check_extension({dirlist.name}, 'ttl')); % separate file with original Parallel_in | |
hasnev = any(filetype_check_extension({dirlist.name}, 'nev')); % original Events.Nev file | |
hasmat = 0; | |
if hasttl | |
eventfile = fullfile(filename, dirlist(find(filetype_check_extension({dirlist.name}, 'ttl'))).name); | |
% read the header from the combined dataset | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
% read the events from the *.ttl file | |
event = ft_read_event(eventfile); | |
% convert the sample numbers from the dma or ttl file to the downsampled dataset | |
% assume that the *.ttl file is sampled at 32556Hz and is aligned with the rest of the data | |
for i=1:length(event) | |
event(i).sample = round((event(i).sample-1) * hdr.Fs/32556 + 1); | |
end | |
% elseif hasnev | |
% FIXME, do something here | |
% elseif hasmat | |
% FIXME, do something here | |
else | |
ft_error('no event file found'); | |
end | |
% The sample number is missingin the code below, since it is not available | |
% without looking in the continuously sampled data files. Therefore | |
% sorting the events (later in this function) based on the sample number | |
% fails and no events can be returned. | |
% | |
% case 'neuralynx_nev' | |
% [nev] = read_neuralynx_nev(filename); | |
% % select only the events with a TTL value | |
% ttl = [nev.TTLValue]; | |
% sel = find(ttl~=0); | |
% % now get the values as cell-array, since the struct function can work with those | |
% value = {nev(sel).TTLValue}; | |
% timestamp = {nev(sel).TimeStamp}; | |
% event = struct('value', value, 'timestamp', timestamp); | |
% for i=1:length(event) | |
% % assign the other fixed elements | |
% event(i).type = 'trigger'; | |
% event(i).offset = []; | |
% event(i).duration = []; | |
% event(i).sample = []; | |
% end | |
case {'neuroprax_eeg', 'neuroprax_mrk'} | |
event = []; | |
% start reading the markers, which I believe to be more like clinical annotations | |
tmp = np_readmarker (filename, 0, inf, 'samples'); | |
for i = 1:numel(tmp.marker) | |
if isempty(tmp.marker{i}) | |
break; | |
end | |
event = appendstruct(event, struct('type', tmp.markernames(i), 'sample', num2cell(tmp.marker{i}), 'value', {tmp.markertyp(i)})); | |
end | |
% if present, read the digital triggers which are present as channel in the data | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% auto-detect the trigger channels | |
chanindx = match_str(hdr.label, 'DTRIG'); | |
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); | |
event = appendstruct(event, trigger); | |
end | |
case 'nexstim_nxe' | |
event = read_nexstim_event(filename); | |
case 'nihonkohden_m00' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% in the data I tested the triggers are marked as DC offsets (deactivation of the DC channel) | |
chanindx = match_str(hdr.label, {'DC09', 'DC10', 'DC11', 'DC12'}); | |
end | |
% FIXME why is the flank detection not done using the generic read_trigger function? | |
if ~isempty(chanindx) | |
% read the trigger channels | |
trig = ft_read_data(filename, 'header', hdr, 'chanindx', chanindx); | |
% marking offset trigger latencies | |
% onlat = (diff([trig(:,1) trig],1,2)>0); | |
offlat = (diff([trig trig(:,1)],1,2)<0); | |
% onset = find(sum(double(onlat), 1)>0); | |
offset = find(sum(double(offlat),1)>0); | |
for j=1:size(offset,2) | |
value = bin2dec(num2str(flipud(offlat(:,offset(j)))')); % flipup is needed to code bin2dec properly: DC09 = +1, DC10 = +2, DC11 = +4, DC12 = +8 | |
event(end+1).type = 'down_flank'; % distinguish between up and down flank | |
event(end ).sample = offset(j); % assign the sample at which the trigger has gone down | |
event(end ).value = value; % assign the trigger value just _before_ going down | |
end | |
end | |
case 'nihonkohden_eeg' | |
ft_hastoolbox('brainstorm', 1); | |
event = read_brainstorm_event(filename); | |
case 'nimh_cortex' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
cortex = hdr.orig.trial; | |
for i=1:length(cortex) | |
% add one 'trial' event for every trial and add the trigger events | |
event(end+1).type = 'trial'; | |
event(end ).sample = nan; | |
event(end ).duration = nan; | |
event(end ).offset = nan; | |
event(end ).value = i; % use the trial number as value | |
for j=1:length(cortex(i).event) | |
event(end+1).type = 'trigger'; | |
event(end ).sample = nan; | |
event(end ).duration = nan; | |
event(end ).offset = nan; | |
event(end ).value = cortex(i).event(j); | |
end | |
end | |
case 'ns_avg' | |
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 {'ns_cnt', 'ns_cnt16', 'ns_cnt32'} | |
% read the header, the original header includes the event table | |
if isempty(hdr) | |
hdr = ft_read_header(filename, 'headerformat', eventformat); | |
end | |
% translate the event table into known FieldTrip event types | |
for i=1:numel(hdr.orig.event) | |
event(end+1).type = 'trigger'; | |
event(end ).sample = hdr.orig.event(i).offset + 1; % +1 was in EEGLAB pop_loadcnt | |
event(end ).value = hdr.orig.event(i).stimtype; | |
event(end ).offset = 0; | |
event(end ).duration = 0; | |
% the code above assumes that all events are stimulus triggers | |
% howevere, there are also interesting events possible, such as responses | |
if hdr.orig.event(i).stimtype~=0 | |
event(end+1).type = 'stimtype'; | |
event(end ).sample = hdr.orig.event(i).offset + 1; % +1 was in EEGLAB pop_loadcnt | |
event(end ).value = hdr.orig.event(i).stimtype; | |
event(end ).offset = 0; | |
event(end ).duration = 0; | |
elseif hdr.orig.event(i).keypad_accept~=0 | |
event(end+1).type = 'keypad_accept'; | |
event(end ).sample = hdr.orig.event(i).offset + 1; % +1 was in EEGLAB pop_loadcnt | |
event(end ).value = hdr.orig.event(i).keypad_accept; | |
event(end ).offset = 0; | |
event(end ).duration = 0; | |
end | |
end | |
case 'ns_eeg' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
for i=1:hdr.nTrials | |
% the *.eeg file has a fixed trigger value for each trial | |
% furthermore each trial has additional fields like accept, correct, response and rt | |
tmp = read_ns_eeg(filename, i); | |
% create an event with the trigger value | |
event(end+1).type = 'trial'; | |
event(end ).sample = (i-1)*hdr.nSamples + 1; | |
event(end ).value = tmp.sweep.type; % trigger value | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
% create an event with the boolean accept/reject code | |
event(end+1).type = 'accept'; | |
event(end ).sample = (i-1)*hdr.nSamples + 1; | |
event(end ).value = tmp.sweep.accept; % boolean value indicating accept/reject | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
% create an event with the boolean accept/reject code | |
event(end+1).type = 'correct'; | |
event(end ).sample = (i-1)*hdr.nSamples + 1; | |
event(end ).value = tmp.sweep.correct; % boolean value | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
% create an event with the boolean accept/reject code | |
event(end+1).type = 'response'; | |
event(end ).sample = (i-1)*hdr.nSamples + 1; | |
event(end ).value = tmp.sweep.response; % probably a boolean value | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
% create an event with the boolean accept/reject code | |
event(end+1).type = 'rt'; | |
event(end ).sample = (i-1)*hdr.nSamples + 1; | |
event(end ).value = tmp.sweep.rt; % time in seconds | |
event(end ).offset = -hdr.nSamplesPre; | |
event(end ).duration = hdr.nSamples; | |
end | |
case 'plexon_nex' | |
event = read_nex_event(filename); | |
case 'plexon_nex5' | |
event = read_nex5_event(filename); | |
case {'ricoh_ave', 'ricoh_con'} | |
% use the Ricoh MEG Reader toolbox for the file reading | |
ft_hastoolbox('ricoh_meg_reader', 1); | |
% the user should be able to specify the analog threshold, but the code falls back to '1.6' as default | |
% the user should be able to specify the trigger channels | |
% the user should be able to specify the flank, but the code falls back to 'up' as default | |
if isempty(detectflank) | |
detectflank = 'up'; | |
end | |
if isempty(threshold) | |
threshold = 1.6; | |
end | |
event = read_ricoh_event(filename, 'detectflank', detectflank, 'chanindx', chanindx, 'threshold', threshold); | |
case 'tmsi_poly5' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
if isempty(chanindx) | |
% auto-detect the trigger channels | |
chanindx = find(strcmp(hdr.chantype, 'trigger')); | |
end | |
if isempty(detectflank) | |
% the "Digi" value goes down from 255 to 254, or to 251 | |
detectflank = 'downdiff'; | |
end | |
if ~isempty(chanindx) | |
event = read_trigger(filename, 'header', hdr, 'dataformat', dataformat, 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', chanindx, 'detectflank', detectflank, 'denoise', denoise, 'trigshift', trigshift, 'trigpadding', trigpadding); | |
end | |
case {'yokogawa_ave', 'yokogawa_con', 'yokogawa_raw'} | |
% check that the required low-level toolbox is available | |
if ~ft_hastoolbox('yokogawa', 0) | |
ft_hastoolbox('yokogawa_meg_reader', 1); | |
end | |
% the user should be able to specify the analog threshold, but the code falls back to '1.6' as default | |
% the user should be able to specify the trigger channels | |
% the user should be able to specify the flank, but the code falls back to 'up' as default | |
% the user can specify combinebinary to be true, in which case the individual binarized trigger channels | |
% will be combined into a single trigger code | |
% the user may need to specify a trigshift~=0 to ensure that unintended asynchronicity in the TTL-pulses is avoided | |
if isempty(detectflank) | |
detectflank = 'up'; | |
end | |
if isempty(threshold) | |
threshold = 1.6; | |
end | |
event = read_yokogawa_event(filename, 'detectflank', detectflank, 'chanindx', chanindx, 'threshold', threshold, 'combinebinary', combinebinary, 'trigshift', trigshift); | |
case {'yorkinstruments_hdf5'} | |
if isempty(hdr) | |
hdr = ft_read_header(filename, 'headerformat', eventformat); | |
end | |
% read the trigger channel and do flank detection | |
trgindx = match_str(hdr.label, 'P_PORT_A'); | |
trigger = read_trigger(filename, 'header', hdr, 'dataformat', 'yorkinstruments_hdf5', 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', trgindx, 'detectflank', detectflank, 'trigshift', trigshift, 'fix4d8192', false); | |
event = appendstruct(event, trigger); | |
respindx = match_str(hdr.label, 'RESPONSE'); | |
if ~isempty(respindx) | |
response = read_trigger(filename, 'header', hdr, 'dataformat', 'yorkinstruments_hdf5', 'begsample', flt_minsample, 'endsample', flt_maxsample, 'chanindx', respindx, 'detectflank', detectflank, 'trigshift', trigshift); | |
event = appendstruct(event, response); | |
end | |
case {'artinis_oxy3' 'artinis_oxy4' 'artinis_oxy5'} | |
ft_hastoolbox('artinis', 1); | |
if strcmp(eventformat, 'artinis_oxy3') | |
if isempty(hdr) | |
hdr = read_artinis_oxy3(filename); | |
end | |
event = read_artinis_oxy3(filename, true); | |
elseif strcmp(eventformat, 'artinis_oxy4') | |
if isempty(hdr) | |
hdr = read_artinis_oxy4(filename); | |
end | |
event = read_artinis_oxy4(filename, true); | |
elseif strcmp(eventformat, 'artinis_oxy5') | |
if isempty(hdr) | |
hdr = read_artinis_oxy5(filename); | |
end | |
event = read_artinis_oxy5(filename, true); | |
end | |
if isempty(chanindx) | |
% this allows subselection of AD channels to be markes as trigger channels (for Artinis *.oxy3 data) | |
chanindx = find(ismember(hdr.label, ft_channelselection('ADC*', hdr.label))); | |
end | |
% 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, 'threshold', threshold, 'fixartinis', true); | |
% remove consecutive triggers | |
if ~isempty(trigger) | |
i = 1; | |
last_trigger_sample = trigger(i).sample; | |
while i<numel(trigger) | |
if strcmp(trigger(i).type, trigger(i+1).type) && trigger(i+1).sample-last_trigger_sample <= tolerance | |
[trigger(i).value, idx] = max([trigger(i).value, trigger(i+1).value]); | |
last_trigger_sample = trigger(i+1).sample; | |
if (idx==2) | |
trigger(i).sample = trigger(i+1).sample; | |
end | |
trigger(i+1) = []; | |
else | |
i=i+1; | |
last_trigger_sample = trigger(i).sample; | |
end | |
end | |
event = appendstruct(event, trigger); | |
end | |
case 'spmeeg_mat' | |
if isempty(hdr) | |
hdr = ft_read_header(filename); | |
end | |
event = read_spmeeg_event(filename, 'header', hdr); | |
case {'blackrock_nev'} | |
% use the NPMK toolbox for the file reading | |
ft_hastoolbox('NPMK', 1); | |
% ensure that the filename contains a full path specification, | |
% otherwise the low-level function fails | |
[p, f, x] = fileparts(filename); | |
if ~isempty(p) | |
% this is OK | |
elseif isempty(p) | |
filename = which(filename); | |
end | |
% 'noread' prevents reading of the spike waveforms | |
% 'nosave' prevents the automatic conversion of | |
% the .nev file as a .mat file | |
orig = openNEV(filename, 'noread', 'nosave'); | |
fs = orig.MetaTags.SampleRes; % sampling rate | |
timestamps = orig.Data.SerialDigitalIO.TimeStamp; | |
eventCodeTimes = double(timestamps)./double(fs); % express in seconds | |
eventCodes = double(orig.Data.SerialDigitalIO.UnparsedData); | |
% probably not necessary for all but we often have pins up | |
% FIXME: what is the consequence for the values if the pins were not 'up'? | |
% Should this be solved more generically? E.g. with an option? | |
eventCodes2 = eventCodes - min(eventCodes) + 1; | |
for k=1:numel(eventCodes2) | |
event(k).type = 'trigger'; | |
event(k).sample = eventCodeTimes(k); | |
event(k).value = eventCodes2(k); | |
event(k).duration = 1; | |
event(k).offset = []; | |
end | |
case 'presentation_log' | |
event = read_presentation_log(filename); | |
otherwise | |
if exist(eventformat, 'file') | |
% attempt to run "eventformat" as a function, this allows the user to specify an external reading function | |
% this is also used for bids_tsv, events_tsv, biopac_acq, motion_c3d, opensignals_txt, qualisys_tsv, sccn_xdf, and possibly others | |
if isempty(hdr) | |
hdr = feval(eventformat, filename); | |
end | |
event = feval(eventformat, filename, hdr); | |
else | |
ft_warning('unsupported event format "%s"', eventformat); | |
event = []; | |
end | |
end % switch eventformat | |
if ~isempty(hdr) && hdr.nTrials>1 && (isempty(event) || ~any(strcmp({event.type}, 'trial'))) | |
% the data suggests multiple trials and trial events have not yet been defined | |
% make an event for each trial according to the file header | |
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 = []; | |
end | |
end | |
if ~isempty(event) | |
% make sure that all required elements are present | |
if ~isfield(event, 'type'), ft_error('type field not defined for each event'); end | |
if ~isfield(event, 'sample'), ft_error('sample field not defined for each event'); end | |
if ~isfield(event, 'value'), for i=1:length(event), event(i).value = []; end; end | |
if ~isfield(event, 'offset'), for i=1 |