Skip to content

Commit d29ebde

Browse files
committed
Added optional time-stamp post-processing to the LSL inlet. This can be used to get accurate time-synching even when the raw time stamps of a stream are heavily jittered.
1 parent 8cc0bef commit d29ebde

23 files changed

+909
-21
lines changed

include/lsl_c.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ typedef enum {
107107
cft_undefined = 0 /* Can not be transmitted. */
108108
} lsl_channel_format_t;
109109

110+
/**
111+
* Post-processing options for stream inlets.
112+
*/
113+
typedef enum {
114+
proc_none = 0, /* No automatic post-processing; return the ground-truth time stamps for manual post-processing */
115+
/* (this is the default behavior of the inlet). */
116+
proc_clocksync = 1, /* Perform automatic clock synchronization; equivalent to manually adding the time_correction() value */
117+
/* to the received time stamps. */
118+
proc_dejitter = 2, /* Remove jitter from time stamps. This will apply a smoothing algorithm to the received time stamps; */
119+
/* the smoothing needs to see a minimum number of samples (30-120 seconds worst-case) until the remaining */
120+
/* jitter is consistently below 1ms. */
121+
proc_monotonize = 4, /* Force the time-stamps to be monotonically ascending (only makes sense if timestamps are dejittered). */
122+
proc_threadsafe = 8, /* Post-processing is thread-safe (same inlet can be read from by multiple threads); uses somewhat more CPU. */
123+
proc_ALL = 1|2|4|8 /* The combination of all possible post-processing options. */
124+
} processing_options_t;
110125

111126
/**
112127
* Possible error codes.
@@ -656,6 +671,18 @@ extern LIBLSL_C_API void lsl_close_stream(lsl_inlet in);
656671
*/
657672
extern LIBLSL_C_API double lsl_time_correction(lsl_inlet in, double timeout, int *ec);
658673

674+
/**
675+
* Set post-processing flags to use. By default, the inlet performs NO post-processing and returns the
676+
* ground-truth time stamps, which can then be manually synchronized using time_correction(), and then
677+
* smoothed/dejittered if desired. This function allows automating these two and possibly more operations.
678+
* Warning: when you enable this, you will no longer receive or be able to recover the original time stamps.
679+
* @param in The lsl_inlet object to act on.
680+
* @param flags An integer that is the result of bitwise OR'ing one or more options from processing_options_t
681+
* together (e.g., post_clocksync|post_dejitter); a good setting is to use post_ALL.
682+
* @return The error code: if nonzero, can be lsl_argument_error if an unknown flag was passed in.
683+
*/
684+
extern LIBLSL_C_API int lsl_set_postprocessing(lsl_inlet in, unsigned flags);
685+
659686

660687
/* === Pulling a sample from the inlet === */
661688

@@ -784,6 +811,20 @@ extern LIBLSL_C_API unsigned lsl_samples_available(lsl_inlet in);
784811
*/
785812
extern LIBLSL_C_API unsigned lsl_was_clock_reset(lsl_inlet in);
786813

814+
/**
815+
* Override the half-time (forget factor) of the time-stamp smoothing.
816+
* The default is 90 seconds unless a different value is set in the config file.
817+
* Using a longer window will yield lower jitter in the time stamps, but longer
818+
* windows will have trouble tracking changes in the clock rate (usually due to
819+
* temperature changes); the default is able to track changes up to 10
820+
* degrees C per minute sufficiently well.
821+
* @param in The lsl_inlet object to act on.
822+
* @param value The new value, in seconds. This is the time after which a past sample
823+
* will be weighted by 1/2 in the exponential smoothing window.
824+
* @return The error code: if nonzero, can be lsl_argument_error if an unknown flag was passed in.
825+
*/
826+
extern LIBLSL_C_API int lsl_smoothing_halftime(lsl_inlet in, float value);
827+
787828

788829

789830
/* ============================ */

include/lsl_cpp.h

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ namespace lsl {
6868
cf_undefined = 0 // Can not be transmitted.
6969
};
7070

71+
/**
72+
* Post-processing options for stream inlets.
73+
*/
74+
enum processing_options_t {
75+
post_none = 0, // No automatic post-processing; return the ground-truth time stamps for manual post-processing
76+
// (this is the default behavior of the inlet).
77+
post_clocksync = 1, // Perform automatic clock synchronization; equivalent to manually adding the time_correction() value
78+
// to the received time stamps.
79+
post_dejitter = 2, // Remove jitter from time stamps. This will apply a smoothing algorithm to the received time stamps;
80+
// the smoothing needs to see a minimum number of samples (30-120 seconds worst-case) until the remaining
81+
// jitter is consistently below 1ms.
82+
post_monotonize = 4, // Force the time-stamps to be monotonically ascending (only makes sense if timestamps are dejittered).
83+
post_threadsafe = 8, // Post-processing is thread-safe (same inlet can be read from by multiple threads); uses somewhat more CPU.
84+
post_ALL = 1|2|4|8 // The combination of all possible post-processing options.
85+
};
7186

7287
/**
7388
* Protocol version.
@@ -722,7 +737,7 @@ namespace lsl {
722737

723738
/**
724739
* Retrieve an estimated time correction offset for the given stream.
725-
* The first call to this function takes several miliseconds until a reliable first estimate is obtained.
740+
* The first call to this function takes several milliseconds until a reliable first estimate is obtained.
726741
* Subsequent calls are instantaneous (and rely on periodic background updates).
727742
* The precision of these estimates should be below 1 ms (empirically within +/-0.2 ms).
728743
* @timeout Timeout to acquire the first time-correction estimate (default: no timeout).
@@ -732,6 +747,15 @@ namespace lsl {
732747
*/
733748
double time_correction(double timeout=FOREVER) { int ec=0; double res = lsl_time_correction(obj,timeout,&ec); check_error(ec); return res; }
734749

750+
/**
751+
* Set post-processing flags to use. By default, the inlet performs NO post-processing and returns the
752+
* ground-truth time stamps, which can then be manually synchronized using time_correction(), and then
753+
* smoothed/dejittered if desired. This function allows automating these two and possibly more operations.
754+
* Warning: when you enable this, you will no longer receive or be able to recover the original time stamps.
755+
* @param flags An integer that is the result of bitwise OR'ing one or more options from processing_options_t
756+
* together (e.g., post_clocksync|post_dejitter); the default is to enable all options.
757+
*/
758+
void set_postprocessing(unsigned flags=post_ALL) { check_error(lsl_set_postprocessing(obj,flags)); }
735759

736760
// =======================================
737761
// === Pulling a sample from the inlet ===
@@ -986,6 +1010,16 @@ namespace lsl {
9861010
* hot-swapped or restarted in between two measurements.
9871011
*/
9881012
bool was_clock_reset() { return lsl_was_clock_reset(obj) != 0; }
1013+
1014+
/**
1015+
* Override the half-time (forget factor) of the time-stamp smoothing.
1016+
* The default is 90 seconds unless a different value is set in the config file.
1017+
* Using a longer window will yield lower jitter in the time stamps, but longer
1018+
* windows will have trouble tracking changes in the clock rate (usually due to
1019+
* temperature changes); the default is able to track changes up to 10
1020+
* degrees C per minute sufficiently well.
1021+
*/
1022+
void smoothing_halftime(float value) { check_error(lsl_smoothing_halftime(obj,value)); }
9891023
private:
9901024
// The inlet is a non-copyable object.
9911025
stream_inlet(const stream_inlet &rhs);

project/code.blocks/liblsl/liblsl.cbp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@
136136
<Unit filename="../../../src/stream_outlet_impl.h" />
137137
<Unit filename="../../../src/tcp_server.cpp" />
138138
<Unit filename="../../../src/tcp_server.h" />
139+
<Unit filename="../../../src/time_postprocessor.cpp" />
140+
<Unit filename="../../../src/time_postprocessor.h" />
139141
<Unit filename="../../../src/time_receiver.cpp" />
140142
<Unit filename="../../../src/time_receiver.h" />
141143
<Unit filename="../../../src/udp_server.cpp" />

project/vs2008/liblsl/liblsl.vcproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,14 @@
512512
RelativePath="..\..\..\src\tcp_server.h"
513513
>
514514
</File>
515+
<File
516+
RelativePath="..\..\..\src\time_postprocessor.cpp"
517+
>
518+
</File>
519+
<File
520+
RelativePath="..\..\..\src\time_postprocessor.h"
521+
>
522+
</File>
515523
<File
516524
RelativePath="..\..\..\src\time_receiver.cpp"
517525
>

project/vs2008/vs2008.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HandleMetaDataC", "..\..\ex
104104
{06C12A6B-BF5B-413F-94CA-4EAA289ED93B} = {06C12A6B-BF5B-413F-94CA-4EAA289ED93B}
105105
EndProjectSection
106106
EndProject
107+
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SyncTest", "..\..\testing\SyncTest\SyncTest.vcproj", "{0607EE81-5039-4054-8486-AD219DDAFDBC}"
108+
EndProject
107109
Global
108110
GlobalSection(SolutionConfigurationPlatforms) = preSolution
109111
Debug|Win32 = Debug|Win32
@@ -272,6 +274,12 @@ Global
272274
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|Win32.Build.0 = Release|Win32
273275
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|x64.ActiveCfg = Release|x64
274276
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|x64.Build.0 = Release|x64
277+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|Win32.ActiveCfg = Debug|Win32
278+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|Win32.Build.0 = Debug|Win32
279+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|x64.ActiveCfg = Debug|Win32
280+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|Win32.ActiveCfg = Release|Win32
281+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|Win32.Build.0 = Release|Win32
282+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|x64.ActiveCfg = Release|Win32
275283
EndGlobalSection
276284
GlobalSection(SolutionProperties) = preSolution
277285
HideSolutionNode = FALSE
@@ -296,5 +304,6 @@ Global
296304
{B83D5DB3-A216-469A-AD1B-55D49ED7A330} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
297305
{3E7C2E18-2728-4FD5-AA37-A9D2F823611C} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
298306
{13976433-8151-4C16-A2D6-5B29E77B932F} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
307+
{0607EE81-5039-4054-8486-AD219DDAFDBC} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
299308
EndGlobalSection
300309
EndGlobal

project/vs2013/liblsl/liblsl.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
<ClInclude Include="..\..\..\src\stream_inlet_impl.h" />
230230
<ClInclude Include="..\..\..\src\stream_outlet_impl.h" />
231231
<ClInclude Include="..\..\..\src\tcp_server.h" />
232+
<ClInclude Include="..\..\..\src\time_postprocessor.h" />
232233
<ClInclude Include="..\..\..\src\time_receiver.h" />
233234
<ClInclude Include="..\..\..\src\udp_server.h" />
234235
</ItemGroup>
@@ -293,6 +294,7 @@
293294
<ClCompile Include="..\..\..\src\stream_info_impl.cpp" />
294295
<ClCompile Include="..\..\..\src\stream_outlet_impl.cpp" />
295296
<ClCompile Include="..\..\..\src\tcp_server.cpp" />
297+
<ClCompile Include="..\..\..\src\time_postprocessor.cpp" />
296298
<ClCompile Include="..\..\..\src\time_receiver.cpp" />
297299
<ClCompile Include="..\..\..\src\udp_server.cpp" />
298300
</ItemGroup>

project/vs2013/liblsl/liblsl.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@
138138
<ClInclude Include="..\..\..\src\legacy\legacy_abi.h">
139139
<Filter>Source\legacy</Filter>
140140
</ClInclude>
141+
<ClInclude Include="..\..\..\src\time_postprocessor.h">
142+
<Filter>Source</Filter>
143+
</ClInclude>
141144
</ItemGroup>
142145
<ItemGroup>
143146
<ClCompile Include="..\..\..\src\lsl_continuous_resolver_c.cpp">
@@ -326,5 +329,8 @@
326329
<ClCompile Include="..\..\..\src\legacy\legacy_abi.cpp">
327330
<Filter>Source\legacy</Filter>
328331
</ClCompile>
332+
<ClCompile Include="..\..\..\src\time_postprocessor.cpp">
333+
<Filter>Source</Filter>
334+
</ClCompile>
329335
</ItemGroup>
330336
</Project>

project/vs2013/vs2013.sln

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 2013
4-
VisualStudioVersion = 12.0.31101.0
4+
VisualStudioVersion = 12.0.21005.1
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "C++ Examples", "C++ Examples", "{F2FBBA98-5B99-4184-A044-C0F3D5D14546}"
77
EndProject
@@ -49,6 +49,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HandleMetaData", "..\..\exa
4949
EndProject
5050
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HandleMetaDataC", "..\..\examples\C\HandleMetaDataC\HandleMetaDataC.vcxproj", "{B83D5DB3-A216-469A-AD1B-55D49ED7A330}"
5151
EndProject
52+
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SyncTest", "..\..\testing\SyncTest\SyncTest.vcxproj", "{0607EE81-5039-4054-8486-AD219DDAFDBC}"
53+
EndProject
5254
Global
5355
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5456
Debug|Win32 = Debug|Win32
@@ -217,6 +219,12 @@ Global
217219
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|Win32.Build.0 = Release|Win32
218220
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|x64.ActiveCfg = Release|x64
219221
{B83D5DB3-A216-469A-AD1B-55D49ED7A330}.Release|x64.Build.0 = Release|x64
222+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|Win32.ActiveCfg = Debug|Win32
223+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|Win32.Build.0 = Debug|Win32
224+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Debug|x64.ActiveCfg = Debug|Win32
225+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|Win32.ActiveCfg = Release|Win32
226+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|Win32.Build.0 = Release|Win32
227+
{0607EE81-5039-4054-8486-AD219DDAFDBC}.Release|x64.ActiveCfg = Release|Win32
220228
EndGlobalSection
221229
GlobalSection(SolutionProperties) = preSolution
222230
HideSolutionNode = FALSE
@@ -233,13 +241,14 @@ Global
233241
{DF1CBFF5-7FEF-4BF0-A9D0-952340D2D9A8} = {F2FBBA98-5B99-4184-A044-C0F3D5D14546}
234242
{5354EE8B-1461-49F9-933C-32E4ED09F82C} = {F2FBBA98-5B99-4184-A044-C0F3D5D14546}
235243
{BD645E08-AFC1-4B25-845B-D93FD7A23E48} = {F2FBBA98-5B99-4184-A044-C0F3D5D14546}
244+
{8C7D68B0-6618-46A4-A892-FD0A63EB9D70} = {F2FBBA98-5B99-4184-A044-C0F3D5D14546}
236245
{024A5CAB-8739-4517-B0E5-2A2CD97469E1} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
237246
{21B5E60F-C822-4C0C-91AA-08DB039CF7C8} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
238247
{F900E3A0-83DC-4507-A0C4-9F2D06286027} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
239248
{7CA24E05-4823-458C-8911-DA13D12E80E6} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
249+
{B83D5DB3-A216-469A-AD1B-55D49ED7A330} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
240250
{3E7C2E18-2728-4FD5-AA37-A9D2F823611C} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
241251
{13976433-8151-4C16-A2D6-5B29E77B932F} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
242-
{8C7D68B0-6618-46A4-A892-FD0A63EB9D70} = {F2FBBA98-5B99-4184-A044-C0F3D5D14546}
243-
{B83D5DB3-A216-469A-AD1B-55D49ED7A330} = {E8E6A310-4D2D-4275-B3BB-BFFDB35D9A07}
252+
{0607EE81-5039-4054-8486-AD219DDAFDBC} = {8A81A852-D9A8-4A6D-A6B0-4BF5BB6A6664}
244253
EndGlobalSection
245254
EndGlobal

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ set (sources
2222
stream_info_impl.cpp
2323
stream_outlet_impl.cpp
2424
tcp_server.cpp
25+
time_postprocessor.cpp
2526
time_receiver.cpp
2627
udp_server.cpp
2728
pugixml/pugixml.cpp

src/api_config.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ void api_config::load_from_file(const std::string &filename) {
173173
outlet_buffer_reserve_samples_ = pt.get("tuning.OutletBufferReserveSamples",128);
174174
inlet_buffer_reserve_ms_ = pt.get("tuning.InletBufferReserveMs",5000);
175175
inlet_buffer_reserve_samples_ = pt.get("tuning.InletBufferReserveSamples",128);
176+
smoothing_halftime_ = pt.get("tuning.SmoothingHalftime",90.0f);
176177

177178
} catch(std::exception &e) {
178179
std::cerr << "Error parsing config file " << filename << " (" << e.what() << "). Rolling back to defaults." << std::endl;

0 commit comments

Comments
 (0)