sccn / labstreaminglayer

LabStreamingLayer super repository comprising submodules for LSL and associated apps.
Other
522 stars 157 forks source link

Synchronize different streams (LSL + muse-lsl) #114

Closed DominiqueMakowski closed 6 months ago

DominiqueMakowski commented 1 year ago

First, thanks a lot for your work, I'm new to this system but I have been mindblown by the possibilities that it enables.

Now, I'm trying to stream from 2 different devices, a Muse-2 EEG system and a BITalino for physiological signals.

The Muse stream is sent via muse-lsl, and the Bitalino stream via the OpenSignals app.

I managed to select both streams in LabRecorder, and record some data.

Unfortunately, the timestamps appear different in the resulting xdf (here's the footer):

Muse 2 EEG

{'info': defaultdict(list,
             {'first_timestamp': ['1688462154.307189'],
              'last_timestamp': ['1688462162.287789'],
              'sample_count': ['416'],
              'clock_offsets': [defaultdict(list,
                           {'offset': [defaultdict(list,
                                         {'time': ['71365.49933895'],
                                          'value': ['-4.249995981808752e-06']}),
                             defaultdict(list,
                                         {'time': ['71370.5003176'],
                                          'value': ['-8.900002285372466e-06']})]})]})}

BITalino

{'info': defaultdict(list,
             {'first_timestamp': ['71359.94191930001'],
              'last_timestamp': ['71367.8835576'],
              'sample_count': ['8095'],
              'clock_offsets': [defaultdict(list,
                           {'offset': [defaultdict(list,
                                         {'time': ['71365.49836495001'],
                                          'value': ['-1.665000309003517e-05']}),
                             defaultdict(list,
                                         {'time': ['71370.49929815'],
                                          'value': ['-7.049995474517345e-06']})]})]})}

The first one is in Unix time (since 1970) and the second one I suspect is in local machine time. My question is how to synchronize these signals? How to create the "0" initial onset time.

Question 2. Additionally, the value of the first_timestamp in the footer does not match the first timestamp in the timestamp vector:

streams[2]["footer"]

{'info': defaultdict(list,
             {'first_timestamp': ['71359.94191930001'],
              'last_timestamp': ['71367.8835576']
})}
streams[2]["time_stamps"]

array([71359.8673961 , 71359.8683957 , 71359.8693953 , ...,
       71367.95716853, 71367.95816813, 71367.95916773])

I would like to create a timestamp for all signals that start from 0 (onset of the signal) to the end of the recording in seconds, but I am not sure which value to "trust" from the recording onset.

Hope that makes sense, Thanks a lot for any pointers!

cboulay commented 1 year ago

Please try using one of the xdf importers. Either pyxdf or xdf-Matlab. Those take care of the synchronization for you.

Note: do not use the timestamp in the header for synchronization. The time the inlet was opened does not tell you the time of the first sample.

chkothe commented 1 year ago

This does look to me like the Muse integration that you're using is not using the default LSL clock (which reports local time) but the unix clock. There's another piece of software that does that too (OpenVibe). There is a thread that discusses a solution in the OpenVibe context, which is to place a config file in the respective app folder that's then used by liblsl and causes it to ignore the (non-default) time stamps that the client submits. This way, everything uses the same clock domain.

DominiqueMakowski commented 1 year ago

I do use pyxdf to open the file (saved using the BIDS format in LabRecorder):

pyxdf.load_xdf("sub-P001/ses-S001/eeg/sub-P001_ses-S001_task-Default_run-001_eeg.xdf")

Do I need to add an option to synchronize the signals? Where will the synchronization "show", in the first timestamp of each stream?

DominiqueMakowski commented 1 year ago

Thanks @chkothe!

It seems like the stream command from muse-lsl uses the Muse constructor:

https://github.com/alexandrebarachant/muse-lsl/blob/f899ba3b7e8c54bb73630f478e114d724bf715f0/muselsl/stream.py#L218C7-L219

which in turns does have a time_func parameter:

https://github.com/alexandrebarachant/muse-lsl/blob/f899ba3b7e8c54bb73630f478e114d724bf715f0/muselsl/muse.py#L25

which is unfortunately not modifiable from stream(). This time_func is by default set to Python's base time.time(), and I suspect that I should modify that to get the time from pylsl is that correct?

cboulay commented 1 year ago

Ah sorry I’m travelling so I didn’t look closely. Christian is probably correct that the Muse integration is using the wrong clock. Do you have a link to there exact app that you downloaded and ran?

cboulay commented 1 year ago

Oh you are ahead of me, crossed streams:). You are correct.

chkothe commented 1 year ago

Yeah you can either just not pass in a time, or you can use pylsl.local_clock() for that if you want to explicitly pass it

chkothe commented 1 year ago

Might make a PR to Barachant's repo, hopefully he merges it in!

DominiqueMakowski commented 1 year ago

@chkothe I can try to do that if you want :)

DominiqueMakowski commented 1 year ago

Done: https://github.com/alexandrebarachant/muse-lsl/pull/197

it seems to work, but I'll give it another try with LabRecorder and everything on Friday when I'm back at the lab. Don't hesitate to check the changelog in case I missed something obvious :) thanks!

cboulay commented 6 months ago

Closing because there's nothing to do here -- upstream PR needs to be fixed.