Skip to content

Commit 0646a6f

Browse files
committed
Add dejitter
1 parent 4e5f278 commit 0646a6f

File tree

4 files changed

+64
-2
lines changed

4 files changed

+64
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [UNRELEASED] - 2025-01-08
2+
- Add CI workflow for versions 1.11, 1.6 (LTS), and "pre".
3+
- Format [blue](https://github.com/JuliaDiff/BlueStyle)
4+
- Add dejitter function
5+
16
## [0.2.0] - 2022-02-23
27
- Add support for string markers and string streams ([#2](https://github.com/cbrnr/XDF.jl/pull/2) by [Alberto Barradas](https://github.com/abcsds) and [Clemens Brunner](https://github.com/cbrnr))
38
- Make header and footer XML available in "xml" key ([#4](https://github.com/cbrnr/XDF.jl/pull/4) by [Alberto Barradas](https://github.com/abcsds))

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ streams = read_xdf("minimal.xdf")
2020
## Current status
2121
This package is currently in an early stage, so here's an overview of what doesn't work (yet):
2222

23-
- [ ] Dejittering of streams with regular sampling rates is not available yet
2423
- [ ] Loading only specific streams does not work yet
2524

2625
If you have a feature request or found a bug, please open a new issue and let me know. I'd be especially interested in making the code more efficient, because this is basically my first Julia project. Currently, the function is passing through the file twice: the first pass reads everything except sample chunks, whereas the second pass reads samples into preallocated arrays. I'm not sure if this is ideal, the code would be much simpler if it used just a simple pass (but then sample arrays will need to be concatenated).

src/XDF.jl

Lines changed: 56 additions & 1 deletion
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
@@ -185,4 +185,59 @@ function sync_clock(time::Array{Float64,1}, offsets::Array{Float64,2})
185185
return time .+ (coefs[1] .+ coefs[2] .* time)
186186
end
187187

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

test/runtests.jl

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

0 commit comments

Comments
 (0)