Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronise multiple signal #660

Merged
merged 17 commits into from
Feb 9, 2024
282 changes: 282 additions & 0 deletions toolbox/process/functions/process_sync_recordings.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
function varargout = process_sync_recordings(varargin)
% process_sync_recordings: Synchronize multiple signals based on common event

% @=============================================================================
% This function is part of the Brainstorm software:
% https://neuroimage.usc.edu/brainstorm
%
% Copyright (c) University of Southern California & McGill University
% This software is distributed under the terms of the GNU General Public License
% as published by the Free Software Foundation. Further details on the GPLv3
% license can be found at http://www.gnu.org/copyleft/gpl.html.
%
% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
%
% For more information type "brainstorm license" at command prompt.
% =============================================================================@
%
% Authors: Edouard Delaire, 2021-2023
% Raymundo Cassani, 2024

eval(macro_method);
end


%% ===== GET DESCRIPTION =====
function sProcess = GetDescription()
% Description the process
sProcess.Comment = 'Synchronyze files';
sProcess.Category = 'Custom';
sProcess.SubGroup = 'Synchronize';
sProcess.Index = 681;
% Definition of the input accepted by this process
sProcess.InputTypes = {'data', 'raw'};
sProcess.OutputTypes = {'data', 'raw'};
sProcess.nInputs = 1;
sProcess.nMinFiles = 2;

%Description of options
sProcess.options.inputs.Comment = ['For synchronization, please choose an<BR>event type ' ...
'which is available in all datasets.<BR><BR>'];
sProcess.options.inputs.Type = 'label';

% Source Event name for synchronization
sProcess.options.src.Comment = 'Sync event name: ';
sProcess.options.src.Type = 'text';
sProcess.options.src.Value = '';

end


%% ===== FORMAT COMMENT =====
function Comment = FormatComment(sProcess) %#ok<DEFNU>
Comment = sProcess.Comment;
end


%% ===== RUN =====
function OutputFiles = Run(sProcess, sInputs)
OutputFiles = {};

% === Sync event management === %
syncEventName = sProcess.options.src.Value;
nInputs = length(sInputs);
sEvtSync = repmat(db_template('event'), 1, nInputs);
sOldTiming = cell(1, nInputs);
fs = zeros(1, nInputs);

% Check: Same FileType for all files
is_raw = strcmp({sInputs.FileType},'raw');
if ~all(is_raw) && ~all(~is_raw)
bst_error('Please do not mix continous (raw) and imported data', 'Synchronize signal', 0);
return;
end
is_raw = is_raw(1);

bst_progress('start', 'Synchronizing files', 'Loading data...', 0, 3*nInputs);

% Get Time vector, events and sampling frequency for each file
for iInput = 1:nInputs
if strcmp(sInputs(iInput).FileType, 'data') % Imported data structure
sData = in_bst_data(sInputs(iInput).FileName, 'Time', 'Events');
sOldTiming{iInput}.Time = sData.Time;
sOldTiming{iInput}.Events = sData.Events;
elseif strcmp(sInputs(iInput).FileType, 'raw') % Continuous data file
sDataRaw = in_bst_data(sInputs(iInput).FileName, 'Time', 'F');
sOldTiming{iInput}.Time = sDataRaw.Time;
sOldTiming{iInput}.Events = sDataRaw.F.events;
end
fs(iInput) = 1/(sOldTiming{iInput}.Time(2) - sOldTiming{iInput}.Time(1)); % in Hz
iSyncEvt = strcmp({sOldTiming{iInput}.Events.label}, syncEventName);
if any(iSyncEvt)
sEvtSync(iInput) = sOldTiming{iInput}.Events(iSyncEvt);
end
end

% Check: Sync event must be present in all files
if any(~(strcmp({sEvtSync.label}, syncEventName)))
bst_error(['Sync event ("' syncEventName '") must be present in all files'], 'Synchronize signal', 0);
return;
end

% Check: Sync event must be simple event
if any(cellfun(@(x) size(x,1), {sEvtSync.times}) ~= 1)
bst_error(['Sync event ("' syncEventName '") must be simple event in all the files'], 'Synchronize signal', 0);
return;
end

bst_progress('inc', nInputs);
bst_progress('text', 'Synchronizing...');

% First Input is the one wiht highest sampling frequency
[~, im] = max(fs);
sInputs([1, im]) = sInputs([im, 1]);
sEvtSync([1, im]) = sEvtSync([im, 1]);
sOldTiming([1, im]) = sOldTiming([im, 1]);
fs([1, im]) = fs([im, 1]);

% Compute shifiting between file i and first file
new_times = cell(1,nInputs);
new_times{1} = sOldTiming{1}.Time;
mean_shifting = zeros(1, nInputs);
for iInput = 2:nInputs
if size(sEvtSync(iInput).times, 2) == size(sEvtSync(1).times, 2)
shifting = sEvtSync(iInput).times - sEvtSync(1).times;
mean_shifting(iInput) = mean(shifting);
offsetStd = std(shifting);
else
bst_report('Warning', sProcess, sInputs, 'Files doesnt have the same number of sync events. Using approximation');
% Cross-correlate trigger signals; need to be at the same sampling frequency
tmp_fs = max(fs(iInput), fs(1));
tmp_time_a = sOldTiming{iInput}.Time(1):1/tmp_fs:sOldTiming{iInput}.Time(end);
tmp_time_b = sOldTiming{1}.Time(1):1/tmp_fs:sOldTiming{1}.Time(end);

blocA = zeros(1 , length(tmp_time_a));
for i_event = 1:size(sEvtSync(iInput).times,2)
i_intra_event = panel_time('GetTimeIndices', tmp_time_a, sEvtSync(iInput).times(i_event) + [0 1]');
blocA(1,i_intra_event) = 1;
end

blocB = zeros(1 , length(tmp_time_b));
for i_event = 1:size(sEvtSync(1).times,2)
i_intra_event = panel_time('GetTimeIndices', tmp_time_b, sEvtSync(1).times(i_event) + [0 1]');
blocB(1,i_intra_event) = 1;
end

[c,lags] = xcorr(blocA,blocB);
[~,colum] = max(c);

mean_shifting(iInput) = lags(colum) / tmp_fs;
offsetStd = 0;
end
new_times{iInput} = sOldTiming{iInput}.Time - mean_shifting(iInput);
disp(sprintf('Lag difference between %s and %s : %.2f ms (std: %.2f ms)', ...
sInputs(1).Condition, sInputs(iInput).Condition, mean_shifting(iInput)*1000, offsetStd*1000));
end

% New start and new end
new_start = max(cellfun(@(x)min(x), new_times));
new_end = min(cellfun(@(x)max(x), new_times));

% Compute new time vectors, and new events times
sNewTiming = sOldTiming;
pool_events = [];
for iInput = 1:nInputs
index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
sNewTiming{iInput}.Time = new_times{iInput}(index) - new_times{iInput}(index(1));
tmp_events = sNewTiming{iInput}.Events;
for i_event = 1:length(tmp_events)
% Update event times
tmp_events(i_event).times = tmp_events(i_event).times - mean_shifting(iInput) - new_times{iInput}(index(1));
% Remove events outside new time range
timeRange = [sNewTiming{iInput}.Time(1), sNewTiming{iInput}.Time(end)];
iEventTimesDel = all(or(tmp_events(i_event).times < timeRange(1), tmp_events(i_event).times > timeRange(2)), 1);
tmp_events(i_event).times(:,iEventTimesDel) = [];
tmp_events(i_event).epochs(iEventTimesDel) = [];
if ~isempty(tmp_events(i_event).channels)
tmp_events(i_event).channels(iEventTimesDel) = [];
end
if ~isempty(tmp_events(i_event).notes)
tmp_events(i_event).notes(iEventTimesDel) = [];
end
if ~isempty(tmp_events(i_event).reactTimes)
tmp_events(i_event).reactTimes(iEventTimesDel) = [];
end
% Clip values to time range
tmp_events(i_event).times(tmp_events(i_event).times < timeRange(1)) = timeRange(1);
tmp_events(i_event).times(tmp_events(i_event).times > timeRange(2)) = timeRange(2);
% Aggregate eventes across files
if isempty(pool_events)
pool_events = tmp_events(i_event);
elseif ~strcmp(tmp_events(i_event).label,syncEventName) || (strcmp(tmp_events(i_event).label,syncEventName) && ~any(strcmp({pool_events.label},syncEventName)))
pool_events = [pool_events tmp_events(i_event)];
end
end
end
% Update polled events
for iInput = 1:nInputs
sNewTiming{iInput}.Events = pool_events;
end

bst_progress('inc', nInputs);
bst_progress('text', 'Saving files...');

% Save sync data to file
for iInput = 1:nInputs
if ~is_raw
% Load original data
sDataSync = in_bst_data(sInputs(iInput).FileName);
% Set new time and events
sDataSync.Comment = [sDataSync.Comment ' | Synchronized '];
sDataSync.Time = sNewTiming{iInput}.Time;
sDataSync.Events = sNewTiming{iInput}.Events;
% Update data
index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
sDataSync.F = sDataSync.F(:,index);
% History: List of sync files
sDataSync = bst_history('add', sDataSync, 'sync', ['List of synchronized files (event = "', syncEventName , '"):']);
for ix = 1:nInputs
sDataSync = bst_history('add', sDataSync, 'sync', [' - ' sInputs(ix).FileName]);
end
% Save data
OutputFile = bst_process('GetNewFilename', bst_fileparts(sInputs(iInput).FileName), 'data_sync');
sDataSync.FileName = file_short(OutputFile);
bst_save(OutputFile, sDataSync, 'v7');
% Register in database
db_add_data(sInputs(iInput).iStudy, OutputFile, sDataSync);
else
% New raw condition
newCondition = [sInputs(iInput).Condition '_synced'];
iNewStudy = db_add_condition(sInputs(iInput).SubjectName, newCondition);
sNewStudy = bst_get('Study', iNewStudy);
newStudyPath = bst_fileparts(file_fullpath(sNewStudy.FileName));
% Save channel definition
ChannelMat = in_bst_channel(sInputs(iInput).ChannelFile);
[~, iChannelStudy] = bst_get('ChannelForStudy', iNewStudy);
db_set_channel(iChannelStudy, ChannelMat, 0, 0);
% Link to raw file
OutputFile = bst_process('GetNewFilename', bst_fileparts(sNewStudy.FileName), 'data_raw_sync');
% Raw file
[~, rawBaseOut, rawBaseExt] = bst_fileparts(newStudyPath);
rawBaseOut = strrep([rawBaseOut rawBaseExt], '@raw', '');
RawFileOut = bst_fullfile(newStudyPath, [rawBaseOut '.bst']);
% Load original link to raw data
sDataRawSync = in_bst_data(sInputs(iInput).FileName, 'F');
sFileIn = sDataRawSync.F;
% Set new time and events
sFileIn.events = sNewTiming{iInput}.Events;
sFileIn.header.nsamples = length( sNewTiming{iInput}.Time);
sFileIn.prop.times = [ sNewTiming{iInput}.Time(1), sNewTiming{iInput}.Time(end)];
sFileOut = out_fopen(RawFileOut, 'BST-BIN', sFileIn, ChannelMat);
% Set Output sFile structure
sDataSync = in_bst(sInputs(iInput).FileName, [], 1, 1, 'no');
sOutMat = rmfield(sDataSync, 'F');
sOutMat.format = 'BST-BIN';
sOutMat.DataType = 'raw';
sOutMat.F = sFileOut;
sOutMat.Comment = [sDataSync.Comment ' | Synchronized'];
% History: List of sync files
sOutMat = bst_history('add', sOutMat, 'sync', ['List of synchronized files (event = "', syncEventName , '"):']);
for ix = 1:nInputs
sOutMat = bst_history('add', sOutMat, 'sync', [' - ' sInputs(ix).FileName]);
end
% Update raw data
index = panel_time('GetTimeIndices', new_times{iInput}, [new_start, new_end]);
sDataSync.F = sDataSync.F(:,index);
% Save new link to raw .mat file
bst_save(OutputFile, sOutMat, 'v6');
% Write block
out_fwrite(sFileOut, ChannelMat, 1, [], [], sDataSync.F);
% Register in BST database
db_add_data(iNewStudy, OutputFile, sOutMat);
end
OutputFiles{iInput} = OutputFile;
bst_progress('inc', 1);
end
bst_progress('stop');
end