Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## [0.2.0] - 2022-02-23
## [UNRELEASED] - 2025-01-08
### ✨ Added
- Add dejitter function ([#13](https://github.com/cbrnr/XDF.jl/pull/13) by [Alberto Barradas](https://github.com/abcsds))

## [0.2.0] - 2022-02-23
- 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))
- 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))

Expand Down
59 changes: 57 additions & 2 deletions src/XDF.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Authors: Clemens Brunner
# Authors: Clemens Brunner, Alberto Barradas
# License: BSD (3-clause)

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

end
"""
dejitter(stream::Dict, max_time::Float64=1, max_samples::Int=500)
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.
args:
stream: Dict
Stream dictionary.
max_time: Float64
Maximum time difference between two consecutive samples (default: 1 second).
max_samples: Int
Maximum number of samples in a segment (default: 500 samples).
return:
Dict: Stream dictionary with updated timestamps.

Example:
```julia
stream = read_xdf(Downloads.download("https://github.com/xdf-modules/example-files/blob/master/data_with_clock_resets.xdf?raw=true"))[2]
stream = dejitter(stream, 1.0, 500) # process segments with a maximum time difference of 1 second or 500 samples
stream["segments"] # list of segments
stream["nominal_srate"] # recalculated nominal sampling rate
```
"""
function dejitter(stream::Dict; max_time::Float64=1.0, max_samples::Int=500)
srate = stream["srate"]
if srate == 0
@warn "Attempting to dejitter marker streams or streams with zero sampling rate. Skipping."
return stream
end
nsamples = size(stream["data"], 1)
if nsamples == 0
@warn "Attempting to dejitter empty stream. Skipping."
return stream
end
stream["nominal_srate"] = 0 # Recalculated if possible
stream["segments"] = []
time = stream["time"]
breaks = [1; findall(diff(time) .> min.(max_time, max_samples .* (1 / srate)))]
seg_starts = breaks
seg_ends = [breaks[2:end] .- 1; nsamples]
for (start, stop) in zip(seg_starts, seg_ends)
push!(stream["segments"], (start, stop))
idx = [start:stop;]
X = hcat(ones(length(idx)), time[idx])
y = time[idx]
coefs = X \ y
stream["time"][idx] = coefs[1] .+ coefs[2] .* time[idx]
end
# Recalculate nominal sampling rate
counts = (seg_ends .- seg_starts) .+ 1
durations = diff([time[seg_starts]; time[seg_ends[end]]])
stream["nominal_srate"] = sum(counts) / sum(durations)
if stream["srate"] != 0 && abs(stream["srate"] - stream["nominal_srate"]) > 1e-1
@warn "After dejittering: Nominal sampling rate differs from specified rate: $(stream["nominal_srate"]) vs. $(stream["srate"]) Hz"
end
return stream
end
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ end
@test startswith(streams[2]["footer"], "<?xml version=\"1.0\"?>")
@test endswith(streams[2]["footer"], "</clock_offsets></info>")
@test size(streams[2]["data"]) == (27815, 8)
d_stream = XDF.dejitter(streams[2])
@test d_stream["segments"][1] == (1, 12875)
@test d_stream["segments"][2] == (12876, 27815)
end
end