Skip to content

Commit d442a1a

Browse files
committed
Add stream dejitter function
1 parent 18e055e commit d442a1a

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

src/XDF.jl

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Authors: Clemens Brunner
1+
# Authors: Clemens Brunner, Alberto Barradas
22
# License: BSD (3-clause)
33

44
module XDF
@@ -182,4 +182,59 @@ function sync_clock(time::Array{Float64,1}, offsets::Array{Float64,2})
182182
return time .+ (coefs[1] .+ coefs[2] .* time)
183183
end
184184

185-
end
185+
"""
186+
dejitter(stream::Dict, max_time::Float64=1, max_samples::Int=500)
187+
Calculate timestamps assuming constant intervals within each continuous segment in a stream. Chooses the minimum of the time difference and the number of samples as indicator for a new segment.
188+
args:
189+
stream: Dict
190+
Stream dictionary.
191+
max_time: Float64
192+
Maximum time difference between two consecutive samples (default: 1 second).
193+
max_samples: Int
194+
Maximum number of samples in a segment (default: 500 samples).
195+
return:
196+
Dict: Stream dictionary with updated timestamps.
197+
198+
Example:
199+
```julia
200+
stream = read_xdf(Downloads.download("https://github.com/xdf-modules/example-files/blob/master/data_with_clock_resets.xdf?raw=true"))[2]
201+
stream = dejitter(stream, 1.0, 500) # process segments with a maximum time difference of 1 second or 500 samples
202+
stream["segments"] # list of segments
203+
stream["nominal_srate"] # recalculated nominal sampling rate
204+
```
205+
"""
206+
function dejitter(stream::Dict; max_time::Float64=1.0, max_samples::Int=500)
207+
srate = stream["srate"]
208+
if srate == 0
209+
@warn "Attempting to dejitter marker streams or streams with zero sampling rate. Skipping."
210+
return stream
211+
end
212+
nsamples = size(stream["data"], 1)
213+
if nsamples == 0
214+
@warn "Attempting to dejitter empty stream. Skipping."
215+
return stream
216+
end
217+
stream["nominal_srate"] = 0 # Recalculated if possible
218+
stream["segments"] = []
219+
time = stream["time"]
220+
breaks = [1; findall(diff(time) .> min.(max_time, max_samples .* (1 / srate)))]
221+
seg_starts = breaks
222+
seg_ends = [breaks[2:end] .- 1; nsamples]
223+
for (start, stop) in zip(seg_starts, seg_ends)
224+
push!(stream["segments"], (start, stop))
225+
idx = [start:stop;]
226+
X = hcat(ones(length(idx)), time[idx])
227+
y = time[idx]
228+
coefs = X \ y
229+
stream["time"][idx] = coefs[1] .+ coefs[2] .* time[idx]
230+
end
231+
# Recalculate nominal sampling rate
232+
counts = (seg_ends .- seg_starts) .+ 1
233+
durations = diff([time[seg_starts]; time[seg_ends[end]]])
234+
stream["nominal_srate"] = sum(counts) / sum(durations)
235+
if stream["srate"] != 0 && abs(stream["srate"] - stream["nominal_srate"]) > 1e-1
236+
@warn "After dejittering: Nominal sampling rate differs from specified rate: $(stream["nominal_srate"]) vs. $(stream["srate"]) Hz"
237+
end
238+
return stream
239+
end
240+
end

test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ end
6868
@test startswith(streams[2]["footer"], "<?xml version=\"1.0\"?>")
6969
@test endswith(streams[2]["footer"], "</clock_offsets></info>")
7070
@test size(streams[2]["data"]) == (27815, 8)
71+
d_stream = XDF.dejitter(streams[2])
72+
@test d_stream["segments"][1] == (1, 12875)
73+
@test d_stream["segments"][2] == (12876, 27815)
74+
end
7175
end

0 commit comments

Comments
 (0)