diff --git a/toolbox/process/functions/process_sync_recordings.m b/toolbox/process/functions/process_sync_recordings.m
new file mode 100644
index 000000000..a5ccc62db
--- /dev/null
+++ b/toolbox/process/functions/process_sync_recordings.m
@@ -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
event type ' ...
+ 'which is available in all datasets.
'];
+ 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
+ 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
+