Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn when reading discontinuous EDF+ files #77

Open
palday opened this issue Oct 19, 2023 · 1 comment
Open

Warn when reading discontinuous EDF+ files #77

palday opened this issue Oct 19, 2023 · 1 comment

Comments

@palday
Copy link
Member

palday commented Oct 19, 2023

Due to the way EDF+ implements the discontinuous extension, a naive EDF reader can still open the file but treats the signals as continuous. This is a potential footgun -- I only noticed it because the annotations referred to times that were much later than the concatenated signals referred to because the annotations assumed that the discontinuity was correctly handled.

@ericphanson
Copy link
Member

we could use this check:

function is_functionally_contiguous_after_start(file::EDF.File)
    # EDF+ says: first annotations signal is such that the first
    # annotation in each list is the start time of the record
    time_idx = findfirst(x -> isa(x, EDF.AnnotationsSignal), file.signals)
    time_anns = file.signals[time_idx]
    starts = [anns[1].onset_in_seconds for anns in time_anns.records]
    # if the start of the next record is exactly seconds_per_record + the start of the previous record
    # then the file is "functionally" contiguous
    return all(==(file.header.seconds_per_record), diff(starts))
end

When this holds, the start may not be at the header's start, but after that all the records are contiguous and reading works. To get the signal start, you need

function start_offset(edf::EDF.File)
    i = findfirst(s -> s isa EDF.AnnotationsSignal, edf.signals)
    i === nothing && return Nanosecond(0)
    annos = edf.signals[i]
    # Shouldn't happen per the spec but who knows, nobody seems to care about the spec
    (isempty(annos.records) || isempty(first(annos.records))) && return Nanosecond(0)
    tal = first(first(annos.records))
    return Nanosecond(tal.onset_in_seconds * 1e9)  # convert to ns since may be non-integer
end

Then when mapping to Onda format, the header's start corresponds to the recording start, and the start_offset corresponds to the signal span's start.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants