edfcore 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/bytes/latin1.d.ts +43 -0
- package/dist/bytes/latin1.d.ts.map +1 -0
- package/dist/bytes/latin1.js +84 -0
- package/dist/bytes/latin1.js.map +1 -0
- package/dist/bytes/numbers.d.ts +45 -0
- package/dist/bytes/numbers.d.ts.map +1 -0
- package/dist/bytes/numbers.js +87 -0
- package/dist/bytes/numbers.js.map +1 -0
- package/dist/bytes/view.d.ts +27 -0
- package/dist/bytes/view.d.ts.map +1 -0
- package/dist/bytes/view.js +53 -0
- package/dist/bytes/view.js.map +1 -0
- package/dist/constants.d.ts +113 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +83 -0
- package/dist/constants.js.map +1 -0
- package/dist/decode/digital.d.ts +49 -0
- package/dist/decode/digital.d.ts.map +1 -0
- package/dist/decode/digital.js +180 -0
- package/dist/decode/digital.js.map +1 -0
- package/dist/decode/physical.d.ts +37 -0
- package/dist/decode/physical.d.ts.map +1 -0
- package/dist/decode/physical.js +178 -0
- package/dist/decode/physical.js.map +1 -0
- package/dist/diagnostics/codes.d.ts +38 -0
- package/dist/diagnostics/codes.d.ts.map +1 -0
- package/dist/diagnostics/codes.js +134 -0
- package/dist/diagnostics/codes.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +76 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +95 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/format.d.ts +21 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +125 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/errors.d.ts +143 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/header/dates.d.ts +101 -0
- package/dist/header/dates.d.ts.map +1 -0
- package/dist/header/dates.js +378 -0
- package/dist/header/dates.js.map +1 -0
- package/dist/header/fields.d.ts +105 -0
- package/dist/header/fields.d.ts.map +1 -0
- package/dist/header/fields.js +363 -0
- package/dist/header/fields.js.map +1 -0
- package/dist/header/identification.d.ts +47 -0
- package/dist/header/identification.d.ts.map +1 -0
- package/dist/header/identification.js +156 -0
- package/dist/header/identification.js.map +1 -0
- package/dist/header/lookup.d.ts +33 -0
- package/dist/header/lookup.d.ts.map +1 -0
- package/dist/header/lookup.js +67 -0
- package/dist/header/lookup.js.map +1 -0
- package/dist/header/parse.d.ts +32 -0
- package/dist/header/parse.d.ts.map +1 -0
- package/dist/header/parse.js +319 -0
- package/dist/header/parse.js.map +1 -0
- package/dist/header/scale.d.ts +68 -0
- package/dist/header/scale.d.ts.map +1 -0
- package/dist/header/scale.js +154 -0
- package/dist/header/scale.js.map +1 -0
- package/dist/header/signals.d.ts +64 -0
- package/dist/header/signals.d.ts.map +1 -0
- package/dist/header/signals.js +374 -0
- package/dist/header/signals.js.map +1 -0
- package/dist/header/variant.d.ts +67 -0
- package/dist/header/variant.d.ts.map +1 -0
- package/dist/header/variant.js +180 -0
- package/dist/header/variant.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +31 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +165 -0
- package/dist/inspect.js.map +1 -0
- package/dist/io/blob.d.ts +14 -0
- package/dist/io/blob.d.ts.map +1 -0
- package/dist/io/blob.js +29 -0
- package/dist/io/blob.js.map +1 -0
- package/dist/io/bytes.d.ts +11 -0
- package/dist/io/bytes.d.ts.map +1 -0
- package/dist/io/bytes.js +26 -0
- package/dist/io/bytes.js.map +1 -0
- package/dist/io/cached.d.ts +19 -0
- package/dist/io/cached.d.ts.map +1 -0
- package/dist/io/cached.js +129 -0
- package/dist/io/cached.js.map +1 -0
- package/dist/io/http.d.ts +20 -0
- package/dist/io/http.d.ts.map +1 -0
- package/dist/io/http.js +225 -0
- package/dist/io/http.js.map +1 -0
- package/dist/io/read.d.ts +42 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +119 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/source.d.ts +33 -0
- package/dist/io/source.d.ts.map +1 -0
- package/dist/io/source.js +72 -0
- package/dist/io/source.js.map +1 -0
- package/dist/node.d.ts +70 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/record-index.d.ts +58 -0
- package/dist/record-index.d.ts.map +1 -0
- package/dist/record-index.js +290 -0
- package/dist/record-index.js.map +1 -0
- package/dist/recording.d.ts +57 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +178 -0
- package/dist/recording.js.map +1 -0
- package/dist/tal/annotations.d.ts +36 -0
- package/dist/tal/annotations.d.ts.map +1 -0
- package/dist/tal/annotations.js +437 -0
- package/dist/tal/annotations.js.map +1 -0
- package/dist/tal/grammar.d.ts +111 -0
- package/dist/tal/grammar.d.ts.map +1 -0
- package/dist/tal/grammar.js +324 -0
- package/dist/tal/grammar.js.map +1 -0
- package/dist/tal/ticks.d.ts +69 -0
- package/dist/tal/ticks.d.ts.map +1 -0
- package/dist/tal/ticks.js +137 -0
- package/dist/tal/ticks.js.map +1 -0
- package/dist/time/segments.d.ts +33 -0
- package/dist/time/segments.d.ts.map +1 -0
- package/dist/time/segments.js +87 -0
- package/dist/time/segments.js.map +1 -0
- package/dist/time/timeline.d.ts +62 -0
- package/dist/time/timeline.d.ts.map +1 -0
- package/dist/time/timeline.js +227 -0
- package/dist/time/timeline.js.map +1 -0
- package/dist/time/window.d.ts +54 -0
- package/dist/time/window.d.ts.map +1 -0
- package/dist/time/window.js +232 -0
- package/dist/time/window.js.map +1 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +483 -0
- package/dist/validate.js.map +1 -0
- package/package.json +77 -0
- package/src/bytes/latin1.ts +87 -0
- package/src/bytes/numbers.ts +115 -0
- package/src/bytes/view.ts +59 -0
- package/src/constants.ts +96 -0
- package/src/decode/digital.ts +258 -0
- package/src/decode/physical.ts +235 -0
- package/src/diagnostics/codes.ts +209 -0
- package/src/diagnostics/collector.ts +129 -0
- package/src/diagnostics/format.ts +143 -0
- package/src/errors.ts +208 -0
- package/src/header/dates.ts +453 -0
- package/src/header/fields.ts +455 -0
- package/src/header/identification.ts +196 -0
- package/src/header/lookup.ts +81 -0
- package/src/header/parse.ts +398 -0
- package/src/header/scale.ts +268 -0
- package/src/header/signals.ts +550 -0
- package/src/header/variant.ts +239 -0
- package/src/index.ts +151 -0
- package/src/inspect.ts +181 -0
- package/src/io/blob.ts +31 -0
- package/src/io/bytes.ts +29 -0
- package/src/io/cached.ts +136 -0
- package/src/io/http.ts +299 -0
- package/src/io/read.ts +152 -0
- package/src/io/source.ts +87 -0
- package/src/node.ts +138 -0
- package/src/record-index.ts +389 -0
- package/src/recording.ts +234 -0
- package/src/tal/annotations.ts +574 -0
- package/src/tal/grammar.ts +540 -0
- package/src/tal/ticks.ts +172 -0
- package/src/time/segments.ts +109 -0
- package/src/time/timeline.ts +290 -0
- package/src/time/window.ts +304 -0
- package/src/types.ts +490 -0
- package/src/validate.ts +610 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record onsets to segments and gaps.
|
|
3
|
+
*
|
|
4
|
+
* Layer 4. Pure and synchronous, and structural only: it reports the shape the onsets actually
|
|
5
|
+
* have and judges none of it. Monotonicity and the spacing rules belong to `time/timeline.ts`,
|
|
6
|
+
* which is their sole owner — run `assertMonotonicOnsetArray` on the same array first, and a gap
|
|
7
|
+
* can then only have a non-negative duration.
|
|
8
|
+
*
|
|
9
|
+
* Only `buildRecordIndex` calls this, because only a complete traversal has every onset. The
|
|
10
|
+
* boundary rule is the one edfcore states everywhere else: a new segment starts wherever
|
|
11
|
+
* `onset[r] !== onset[r - 1] + recordDurationTicks`, in exact ticks. Not "differs by more than an
|
|
12
|
+
* epsilon" — a float tolerance is how a one-sample overlap becomes invisible.
|
|
13
|
+
*
|
|
14
|
+
* Every second here is elapsed recording time, measured from record 0's start (see
|
|
15
|
+
* `time/timeline.ts`), so `segment.startTicks` is the rebased value and
|
|
16
|
+
* `ticksToSeconds(segment.startTicks) === segment.startSeconds` holds by construction.
|
|
17
|
+
*/
|
|
18
|
+
import { ticksToSeconds } from '../tal/ticks.js';
|
|
19
|
+
const EMPTY_SEGMENTATION = Object.freeze({
|
|
20
|
+
segments: Object.freeze([]),
|
|
21
|
+
gaps: Object.freeze([]),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* `onsetTicks` holds one entry per record, record 0 first — the `BigInt64Array` that
|
|
25
|
+
* `decodeAnnotations` returns for a full-file range fits directly.
|
|
26
|
+
*
|
|
27
|
+
* `originTicks` is the tick value that maps to `0` seconds. It defaults to `onsetTicks[0]`,
|
|
28
|
+
* which is record 0's own onset and therefore the recording's sub-second start offset.
|
|
29
|
+
*/
|
|
30
|
+
export function buildSegmentation(onsetTicks, recordDurationTicks, originTicks) {
|
|
31
|
+
const recordCount = onsetTicks.length;
|
|
32
|
+
if (recordCount === 0)
|
|
33
|
+
return EMPTY_SEGMENTATION;
|
|
34
|
+
const firstOnset = onsetTicks[0];
|
|
35
|
+
if (firstOnset === undefined)
|
|
36
|
+
return EMPTY_SEGMENTATION;
|
|
37
|
+
const origin = originTicks ?? firstOnset;
|
|
38
|
+
const segments = [];
|
|
39
|
+
const bounds = [];
|
|
40
|
+
let segmentStart = 0;
|
|
41
|
+
let segmentStartTicks = firstOnset;
|
|
42
|
+
const closeSegment = (endExclusive) => {
|
|
43
|
+
const count = endExclusive - segmentStart;
|
|
44
|
+
const startTicks = segmentStartTicks - origin;
|
|
45
|
+
const durationTicks = BigInt(count) * recordDurationTicks;
|
|
46
|
+
const endTicks = startTicks + durationTicks;
|
|
47
|
+
segments.push({
|
|
48
|
+
index: segments.length,
|
|
49
|
+
records: { start: segmentStart, count },
|
|
50
|
+
startSeconds: ticksToSeconds(startTicks),
|
|
51
|
+
startTicks,
|
|
52
|
+
durationSeconds: ticksToSeconds(durationTicks),
|
|
53
|
+
endSeconds: ticksToSeconds(endTicks),
|
|
54
|
+
});
|
|
55
|
+
bounds.push({ startTicks, endTicks });
|
|
56
|
+
};
|
|
57
|
+
for (let record = 1; record < recordCount; record += 1) {
|
|
58
|
+
const previous = onsetTicks[record - 1];
|
|
59
|
+
const current = onsetTicks[record];
|
|
60
|
+
// A missing entry cannot happen for a BigInt64Array of this length; skipping rather than
|
|
61
|
+
// asserting keeps a hand-built ArrayLike from turning a structural walk into a throw.
|
|
62
|
+
if (previous === undefined || current === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
if (current === previous + recordDurationTicks)
|
|
65
|
+
continue;
|
|
66
|
+
closeSegment(record);
|
|
67
|
+
segmentStart = record;
|
|
68
|
+
segmentStartTicks = current;
|
|
69
|
+
}
|
|
70
|
+
closeSegment(recordCount);
|
|
71
|
+
const gaps = [];
|
|
72
|
+
for (let index = 1; index < bounds.length; index += 1) {
|
|
73
|
+
const before = bounds[index - 1];
|
|
74
|
+
const after = bounds[index];
|
|
75
|
+
if (before === undefined || after === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
gaps.push({
|
|
78
|
+
beforeSegmentIndex: index - 1,
|
|
79
|
+
afterSegmentIndex: index,
|
|
80
|
+
startSeconds: ticksToSeconds(before.endTicks),
|
|
81
|
+
endSeconds: ticksToSeconds(after.startTicks),
|
|
82
|
+
durationSeconds: ticksToSeconds(after.startTicks - before.endTicks),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { segments: Object.freeze(segments), gaps: Object.freeze(gaps) };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=segments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segments.js","sourceRoot":"","sources":["../../src/time/segments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recording's time axis, built from probed record onsets.
|
|
3
|
+
*
|
|
4
|
+
* Layer 4. Pure and synchronous: it is handed onsets that someone else read, and it decides what
|
|
5
|
+
* they mean. Sole owner of what makes a timeline valid — `tal/annotations.ts` produces
|
|
6
|
+
* `recordOnsetTicks` and deliberately passes no judgement on it, so monotonicity and
|
|
7
|
+
* record-onset spacing are enforced here and nowhere else.
|
|
8
|
+
*
|
|
9
|
+
* TIME AXIS, fixed here for the whole library: `t = 0` is the START OF RECORD 0, not the header
|
|
10
|
+
* start time. Record 0's timekeeping onset is `startOffsetTicks` — in [0, 1) s — and it is the
|
|
11
|
+
* bridge back to the header clock. Every other second edfcore reports (segment, gap, chunk,
|
|
12
|
+
* window bound) is elapsed recording time. This is the EDFlib/pyEDFlib/MNE convention, and it is
|
|
13
|
+
* the one under which sample `n` of a signal sits at exactly
|
|
14
|
+
* `n * recordDuration / samplesPerRecord` with no sub-second constant to remember.
|
|
15
|
+
*/
|
|
16
|
+
import type { EdfDiagnostic, EdfHeader, EdfTimeline, ParseOptions } from '../types.js';
|
|
17
|
+
/** One record onset as observed on disk: which record, and its exact timekeeping value. */
|
|
18
|
+
export interface RecordOnsetProbe {
|
|
19
|
+
readonly recordIndex: number;
|
|
20
|
+
/** Verbatim, relative to the header start time — the axis rebasing has not happened yet. */
|
|
21
|
+
readonly onsetTicks: bigint;
|
|
22
|
+
}
|
|
23
|
+
export interface TimelineInput {
|
|
24
|
+
readonly header: EdfHeader;
|
|
25
|
+
/**
|
|
26
|
+
* Ascending by `recordIndex`, first entry record 0 and last entry record `recordCount - 1`.
|
|
27
|
+
* In practice the two probes `buildTimeline` reads; a single-record file supplies one entry
|
|
28
|
+
* that is both. Empty exactly when the file has no records.
|
|
29
|
+
*/
|
|
30
|
+
readonly probes: readonly RecordOnsetProbe[];
|
|
31
|
+
/**
|
|
32
|
+
* Diagnostics from decoding the probed records, folded into `timeline.diagnostics` so one
|
|
33
|
+
* array explains the whole timeline. A `START_OFFSET_OUT_OF_RANGE` already present here is not
|
|
34
|
+
* repeated below.
|
|
35
|
+
*/
|
|
36
|
+
readonly probeDiagnostics?: readonly EdfDiagnostic[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fatal, at any observed pair: a later record starting earlier than an earlier one makes every
|
|
40
|
+
* time-based answer for the file wrong, so edfcore refuses to derive any of them.
|
|
41
|
+
*
|
|
42
|
+
* Equal onsets are not a violation — a file with `recordDuration = 0` has every record at the
|
|
43
|
+
* same instant legally, and an insufficient advance is spacing, which is a warning.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertMonotonicOnsets(earlier: RecordOnsetProbe, later: RecordOnsetProbe): void;
|
|
46
|
+
/**
|
|
47
|
+
* `assertMonotonicOnsets` across a full onset array, which is what `buildRecordIndex` has.
|
|
48
|
+
*
|
|
49
|
+
* `firstRecordIndex` is the record `onsetTicks[0]` belongs to, so the diagnostic names the record
|
|
50
|
+
* in the file rather than a position in the array.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertMonotonicOnsetArray(onsetTicks: ArrayLike<bigint>, firstRecordIndex?: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* `EdfTimeline` from the probed onsets plus the header.
|
|
55
|
+
*
|
|
56
|
+
* `spanSeconds` and `coveredSeconds` are computed independently — last record end minus first
|
|
57
|
+
* record start, against the sum of the record durations — because their being equal is the
|
|
58
|
+
* statement "this file is contiguous as far as two reads can tell". Deriving either from the
|
|
59
|
+
* other would make that identity true by construction and worth nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildTimelineFromProbes(input: TimelineInput, options?: ParseOptions): EdfTimeline;
|
|
62
|
+
//# sourceMappingURL=timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../src/time/timeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIvF,2FAA2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACtD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAkB9F;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAC7B,gBAAgB,GAAE,MAAU,GAC3B,IAAI,CAUN;AAiID;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,WAAW,CA4DjG"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recording's time axis, built from probed record onsets.
|
|
3
|
+
*
|
|
4
|
+
* Layer 4. Pure and synchronous: it is handed onsets that someone else read, and it decides what
|
|
5
|
+
* they mean. Sole owner of what makes a timeline valid — `tal/annotations.ts` produces
|
|
6
|
+
* `recordOnsetTicks` and deliberately passes no judgement on it, so monotonicity and
|
|
7
|
+
* record-onset spacing are enforced here and nowhere else.
|
|
8
|
+
*
|
|
9
|
+
* TIME AXIS, fixed here for the whole library: `t = 0` is the START OF RECORD 0, not the header
|
|
10
|
+
* start time. Record 0's timekeeping onset is `startOffsetTicks` — in [0, 1) s — and it is the
|
|
11
|
+
* bridge back to the header clock. Every other second edfcore reports (segment, gap, chunk,
|
|
12
|
+
* window bound) is elapsed recording time. This is the EDFlib/pyEDFlib/MNE convention, and it is
|
|
13
|
+
* the one under which sample `n` of a signal sits at exactly
|
|
14
|
+
* `n * recordDuration / samplesPerRecord` with no sub-second constant to remember.
|
|
15
|
+
*/
|
|
16
|
+
import { TICKS_PER_SECOND } from '../constants.js';
|
|
17
|
+
import { DiagnosticSink, fatalError } from '../diagnostics/collector.js';
|
|
18
|
+
import { ticksToSeconds } from '../tal/ticks.js';
|
|
19
|
+
const TIMEKEEPING_SPEC = 'EDF+ specification 2.2.1 (time keeping of data records)';
|
|
20
|
+
/**
|
|
21
|
+
* Fatal, at any observed pair: a later record starting earlier than an earlier one makes every
|
|
22
|
+
* time-based answer for the file wrong, so edfcore refuses to derive any of them.
|
|
23
|
+
*
|
|
24
|
+
* Equal onsets are not a violation — a file with `recordDuration = 0` has every record at the
|
|
25
|
+
* same instant legally, and an insufficient advance is spacing, which is a warning.
|
|
26
|
+
*/
|
|
27
|
+
export function assertMonotonicOnsets(earlier, later) {
|
|
28
|
+
if (later.onsetTicks >= earlier.onsetTicks)
|
|
29
|
+
return;
|
|
30
|
+
throw fatalError({
|
|
31
|
+
code: 'TIMELINE_NOT_MONOTONIC',
|
|
32
|
+
message: `record ${later.recordIndex} starts at ${ticksToSeconds(later.onsetTicks)} s, before ` +
|
|
33
|
+
`record ${earlier.recordIndex} at ${ticksToSeconds(earlier.onsetTicks)} s ` +
|
|
34
|
+
`(${later.onsetTicks} against ${earlier.onsetTicks} ticks of 100 ns). ` +
|
|
35
|
+
'Rule: record onsets never decrease, in EDF+C or EDF+D — the timekeeping TAL of record r ' +
|
|
36
|
+
"is that record's start relative to the header start time. " +
|
|
37
|
+
'Next: every time-based answer for this file would be wrong, so edfcore derives none of ' +
|
|
38
|
+
'them; read by record index with readRecords(), or repair the timekeeping TALs.',
|
|
39
|
+
field: 'timekeeping TAL',
|
|
40
|
+
expected: `onset >= ${earlier.onsetTicks} ticks`,
|
|
41
|
+
actual: `${later.onsetTicks} ticks`,
|
|
42
|
+
recordIndex: later.recordIndex,
|
|
43
|
+
specReference: TIMEKEEPING_SPEC,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `assertMonotonicOnsets` across a full onset array, which is what `buildRecordIndex` has.
|
|
48
|
+
*
|
|
49
|
+
* `firstRecordIndex` is the record `onsetTicks[0]` belongs to, so the diagnostic names the record
|
|
50
|
+
* in the file rather than a position in the array.
|
|
51
|
+
*/
|
|
52
|
+
export function assertMonotonicOnsetArray(onsetTicks, firstRecordIndex = 0) {
|
|
53
|
+
for (let position = 1; position < onsetTicks.length; position += 1) {
|
|
54
|
+
const previous = onsetTicks[position - 1];
|
|
55
|
+
const current = onsetTicks[position];
|
|
56
|
+
if (previous === undefined || current === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
assertMonotonicOnsets({ recordIndex: firstRecordIndex + position - 1, onsetTicks: previous }, { recordIndex: firstRecordIndex + position, onsetTicks: current });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The probe array has to describe the whole file, because `spanSeconds` is defined by its two
|
|
63
|
+
* ends. A `RangeError` rather than an `EdfFormatError`: nothing here is the file's fault.
|
|
64
|
+
*/
|
|
65
|
+
function assertProbeShape(probes, recordCount) {
|
|
66
|
+
if (recordCount === 0) {
|
|
67
|
+
if (probes.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
throw new RangeError(`buildTimelineFromProbes() received ${probes.length} onset probes for a file with no data ` +
|
|
70
|
+
'records. Next: pass an empty probes array when header.recordCount is 0.');
|
|
71
|
+
}
|
|
72
|
+
const first = probes[0];
|
|
73
|
+
const last = probes[probes.length - 1];
|
|
74
|
+
if (first === undefined || last === undefined) {
|
|
75
|
+
throw new RangeError(`buildTimelineFromProbes() received no onset probes for a file with ${recordCount} data ` +
|
|
76
|
+
'records, so neither the start offset nor the span is known. Next: probe record 0 and ' +
|
|
77
|
+
`record ${recordCount - 1} and pass both.`);
|
|
78
|
+
}
|
|
79
|
+
if (first.recordIndex !== 0 || last.recordIndex !== recordCount - 1) {
|
|
80
|
+
throw new RangeError(`buildTimelineFromProbes() received probes for records ${first.recordIndex}..` +
|
|
81
|
+
`${last.recordIndex}, but the start offset comes from record 0 and the span ends at ` +
|
|
82
|
+
`record ${recordCount - 1}. Next: probe both ends of the file; intermediate probes are ` +
|
|
83
|
+
'optional.');
|
|
84
|
+
}
|
|
85
|
+
for (let i = 1; i < probes.length; i += 1) {
|
|
86
|
+
const previous = probes[i - 1];
|
|
87
|
+
const probe = probes[i];
|
|
88
|
+
if (previous === undefined || probe === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
if (probe.recordIndex <= previous.recordIndex) {
|
|
91
|
+
throw new RangeError(`buildTimelineFromProbes() received probes out of order: record ${probe.recordIndex} ` +
|
|
92
|
+
`follows record ${previous.recordIndex}. Next: sort the probes by record index.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function reportStartOffset(sink, startOffsetTicks) {
|
|
97
|
+
sink.report({
|
|
98
|
+
code: 'START_OFFSET_OUT_OF_RANGE',
|
|
99
|
+
message: `record 0 starts ${ticksToSeconds(startOffsetTicks)} s after the header start time ` +
|
|
100
|
+
`(${startOffsetTicks} ticks of 100 ns), but a sub-second start offset must be in [0, 1). ` +
|
|
101
|
+
"Rule: the onset of record 0's timekeeping TAL is the recording's sub-second start " +
|
|
102
|
+
'offset; the whole-second part of the start time lives in the header starttime field. ' +
|
|
103
|
+
'Next: the value was used as written, so the time axis still starts at record 0; compare ' +
|
|
104
|
+
'it with the header starttime, because a writer that encodes the start time twice ' +
|
|
105
|
+
'produces exactly this.',
|
|
106
|
+
field: 'timekeeping TAL',
|
|
107
|
+
expected: `0 <= onset < ${TICKS_PER_SECOND} ticks`,
|
|
108
|
+
actual: `${startOffsetTicks} ticks`,
|
|
109
|
+
recordIndex: 0,
|
|
110
|
+
specReference: TIMEKEEPING_SPEC,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The two-probe contiguity verdict.
|
|
115
|
+
*
|
|
116
|
+
* `onset[last] === onset[0] + (n - 1) * recordDuration`, in exact ticks, detects any NET drift of
|
|
117
|
+
* the timeline from two reads. It is not a proof of contiguity: a gap that a later overlap
|
|
118
|
+
* cancels exactly leaves the two ends where a contiguous file would put them, and only
|
|
119
|
+
* `buildRecordIndex()` or `validateRecording()` — which read every onset — can see that. The
|
|
120
|
+
* message says so, because a caller who believes this check is a proof will trust the wrong file.
|
|
121
|
+
*/
|
|
122
|
+
function reportDrift(sink, header, lastRecordIndex, observedTicks, expectedTicks) {
|
|
123
|
+
const driftTicks = observedTicks - expectedTicks;
|
|
124
|
+
const nominal = `record ${lastRecordIndex} starts at ${ticksToSeconds(observedTicks)} s where ` +
|
|
125
|
+
`startOffset + ${lastRecordIndex} * recordDuration is ${ticksToSeconds(expectedTicks)} s, ` +
|
|
126
|
+
`a net drift of ${ticksToSeconds(driftTicks)} s (${driftTicks} ticks of 100 ns)`;
|
|
127
|
+
const probeNote = 'Note: two probes detect any net drift of the timeline, but not a gap that an overlap ' +
|
|
128
|
+
'elsewhere cancels exactly — buildRecordIndex() reads every onset and does.';
|
|
129
|
+
if (header.continuity === 'continuous') {
|
|
130
|
+
sink.report({
|
|
131
|
+
code: 'DISCONTINUITY_IN_CONTINUOUS_FILE',
|
|
132
|
+
message: `the reserved field marks this file continuous, but ${nominal}. ` +
|
|
133
|
+
'Rule: in a continuous file every record onset is startOffset + recordIndex * ' +
|
|
134
|
+
'recordDuration; a recording with gaps is what EDF+D exists for. ' +
|
|
135
|
+
'Next: treat the file as discontinuous — buildRecordIndex() returns its segments and ' +
|
|
136
|
+
`gaps, and readWindow() then returns one chunk per contiguous run instead of ` +
|
|
137
|
+
'crossing a gap silently. ' +
|
|
138
|
+
probeNote,
|
|
139
|
+
field: 'timekeeping TAL',
|
|
140
|
+
expected: `${expectedTicks} ticks`,
|
|
141
|
+
actual: `${observedTicks} ticks`,
|
|
142
|
+
recordIndex: lastRecordIndex,
|
|
143
|
+
specReference: TIMEKEEPING_SPEC,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// A discontinuous file is allowed to spread its records out; it is never allowed to pull them
|
|
148
|
+
// together, so only negative net drift is reportable here.
|
|
149
|
+
if (driftTicks >= 0n)
|
|
150
|
+
return;
|
|
151
|
+
sink.report({
|
|
152
|
+
code: 'RECORD_ONSET_SPACING_VIOLATION',
|
|
153
|
+
message: `${nominal}, so somewhere in this file a record starts before the previous one ends. ` +
|
|
154
|
+
'Rule: consecutive record onsets are spaced by at least the record duration — a ' +
|
|
155
|
+
'discontinuous file may leave gaps between records but never overlaps them. ' +
|
|
156
|
+
'Next: the onsets were used as written and nothing was reordered; buildRecordIndex() ' +
|
|
157
|
+
'reports which records overlap. ' +
|
|
158
|
+
probeNote,
|
|
159
|
+
field: 'timekeeping TAL',
|
|
160
|
+
expected: `>= ${expectedTicks} ticks`,
|
|
161
|
+
actual: `${observedTicks} ticks`,
|
|
162
|
+
recordIndex: lastRecordIndex,
|
|
163
|
+
specReference: TIMEKEEPING_SPEC,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `EdfTimeline` from the probed onsets plus the header.
|
|
168
|
+
*
|
|
169
|
+
* `spanSeconds` and `coveredSeconds` are computed independently — last record end minus first
|
|
170
|
+
* record start, against the sum of the record durations — because their being equal is the
|
|
171
|
+
* statement "this file is contiguous as far as two reads can tell". Deriving either from the
|
|
172
|
+
* other would make that identity true by construction and worth nothing.
|
|
173
|
+
*/
|
|
174
|
+
export function buildTimelineFromProbes(input, options) {
|
|
175
|
+
const header = input.header;
|
|
176
|
+
const recordCount = header.recordCount;
|
|
177
|
+
const durationTicks = header.recordDurationTicks;
|
|
178
|
+
assertProbeShape(input.probes, recordCount);
|
|
179
|
+
// Fatal, and checked before anything is derived: a non-monotonic timeline has no valid span,
|
|
180
|
+
// no valid offset, and no valid answer to any question a caller could ask of this object.
|
|
181
|
+
for (let i = 1; i < input.probes.length; i += 1) {
|
|
182
|
+
const previous = input.probes[i - 1];
|
|
183
|
+
const probe = input.probes[i];
|
|
184
|
+
if (previous === undefined || probe === undefined)
|
|
185
|
+
continue;
|
|
186
|
+
assertMonotonicOnsets(previous, probe);
|
|
187
|
+
}
|
|
188
|
+
const sink = new DiagnosticSink(options);
|
|
189
|
+
const priorDiagnostics = input.probeDiagnostics ?? [];
|
|
190
|
+
const first = input.probes[0];
|
|
191
|
+
const last = input.probes[input.probes.length - 1];
|
|
192
|
+
if (recordCount === 0 || first === undefined || last === undefined) {
|
|
193
|
+
return {
|
|
194
|
+
recordCount: 0,
|
|
195
|
+
recordDurationSeconds: header.recordDurationSeconds,
|
|
196
|
+
startOffsetSeconds: 0,
|
|
197
|
+
startOffsetTicks: 0n,
|
|
198
|
+
spanSeconds: 0,
|
|
199
|
+
coveredSeconds: 0,
|
|
200
|
+
diagnostics: Object.freeze(priorDiagnostics.slice()),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const startOffsetTicks = first.onsetTicks;
|
|
204
|
+
const spanTicks = last.onsetTicks + durationTicks - first.onsetTicks;
|
|
205
|
+
const coveredTicks = BigInt(recordCount) * durationTicks;
|
|
206
|
+
const expectedLastTicks = first.onsetTicks + BigInt(recordCount - 1) * durationTicks;
|
|
207
|
+
// decodeAnnotations already reports this for a range that began at record 0, which is exactly
|
|
208
|
+
// how record 0 is probed. Repeating it would double every strict-mode failure and every
|
|
209
|
+
// diagnostic count a test asserts.
|
|
210
|
+
const offsetAlreadyReported = priorDiagnostics.some((diagnostic) => diagnostic.code === 'START_OFFSET_OUT_OF_RANGE');
|
|
211
|
+
if (!offsetAlreadyReported && (startOffsetTicks < 0n || startOffsetTicks >= TICKS_PER_SECOND)) {
|
|
212
|
+
reportStartOffset(sink, startOffsetTicks);
|
|
213
|
+
}
|
|
214
|
+
if (last.onsetTicks !== expectedLastTicks) {
|
|
215
|
+
reportDrift(sink, header, last.recordIndex, last.onsetTicks, expectedLastTicks);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
recordCount,
|
|
219
|
+
recordDurationSeconds: header.recordDurationSeconds,
|
|
220
|
+
startOffsetSeconds: ticksToSeconds(startOffsetTicks),
|
|
221
|
+
startOffsetTicks,
|
|
222
|
+
spanSeconds: ticksToSeconds(spanTicks),
|
|
223
|
+
coveredSeconds: ticksToSeconds(coveredTicks),
|
|
224
|
+
diagnostics: Object.freeze([...priorDiagnostics, ...sink.diagnostics]),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.js","sourceRoot":"","sources":["../../src/time/timeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows in seconds, resolved to records and then to samples.
|
|
3
|
+
*
|
|
4
|
+
* Layer 4. Pure and synchronous, both halves of it. `resolveTimeWindow` answers "which records
|
|
5
|
+
* does this window cost?" before a byte is read, so the price of a window is auditable rather
|
|
6
|
+
* than discovered; `trimToWindow` narrows a record-aligned chunk to the samples actually asked
|
|
7
|
+
* for.
|
|
8
|
+
*
|
|
9
|
+
* The window is the half-open interval `[startSeconds, startSeconds + durationSeconds)` in
|
|
10
|
+
* elapsed recording time — `t = 0` is the start of record 0, the axis `time/timeline.ts` fixes.
|
|
11
|
+
*
|
|
12
|
+
* Every comparison below is integer or rational arithmetic on ticks, records and
|
|
13
|
+
* `samplesPerRecord`. `round(t * sampleRateHz)` appears nowhere: `sampleRateHz` is derived and
|
|
14
|
+
* often not representable (256/3 Hz is a real record duration of 3 s with 256 samples), so
|
|
15
|
+
* rounding through it walks the answer off by a sample near every large `t` — which is the exact
|
|
16
|
+
* mistake edfcore exists to stop a consumer from re-implementing.
|
|
17
|
+
*/
|
|
18
|
+
import type { EdfChunkSignal, EdfHeader, EdfRecordIndex, EdfTimeline, RecordRange } from '../types.js';
|
|
19
|
+
/**
|
|
20
|
+
* The records a window needs, one `RecordRange` per contiguous run it overlaps, in time order.
|
|
21
|
+
*
|
|
22
|
+
* Empty when the window falls entirely inside a gap, entirely outside the recording, or has a
|
|
23
|
+
* non-positive duration — the interval is half-open, so a zero-length window contains no time
|
|
24
|
+
* and therefore no samples.
|
|
25
|
+
*
|
|
26
|
+
* Ranges are RECORD-ALIGNED and are therefore usually wider than the window: a record is the
|
|
27
|
+
* smallest unit the file can be read by, and `trimToWindow` is how a caller narrows the samples
|
|
28
|
+
* afterwards.
|
|
29
|
+
*
|
|
30
|
+
* With `index.segments` present (`coverage === 'complete'`) the answer is exact. With a probed
|
|
31
|
+
* index it is exact only while the file is contiguous, which is precisely what
|
|
32
|
+
* `spanSeconds === coveredSeconds` states; when it is not, the records a window maps to depend
|
|
33
|
+
* on onsets nobody has read, and this function refuses rather than guessing them.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveTimeWindow(timeline: EdfTimeline, index: EdfRecordIndex, startSeconds: number, durationSeconds: number): readonly RecordRange[];
|
|
36
|
+
/**
|
|
37
|
+
* The exact per-signal trim of a record-aligned chunk to `[startSeconds, startSeconds + duration)`.
|
|
38
|
+
*
|
|
39
|
+
* Sample `j` of the chunk starts at `chunkSignal.startSeconds + j * recordDuration /
|
|
40
|
+
* samplesPerRecord`, so the samples inside the window are those with
|
|
41
|
+
* `j * recordDuration >= relativeStart * samplesPerRecord` and
|
|
42
|
+
* `j * recordDuration < relativeEnd * samplesPerRecord`. Both comparisons are integer bigint
|
|
43
|
+
* products of on-disk quantities — no division, no sample rate, no float bound — which is what
|
|
44
|
+
* makes the boundary sample the same one every time and on every platform.
|
|
45
|
+
*
|
|
46
|
+
* The chunk must be one contiguous run of records (what `readWindow` returns), because that is
|
|
47
|
+
* what makes the sample grid uniform across it.
|
|
48
|
+
*
|
|
49
|
+
* `digital` in the result is a SUBARRAY view of the input's, so trimming allocates nothing and
|
|
50
|
+
* the two share memory. A window that only partly overlaps the chunk is clamped to the samples
|
|
51
|
+
* that exist; one that misses it entirely yields a zero-length result rather than an error.
|
|
52
|
+
*/
|
|
53
|
+
export declare function trimToWindow(header: EdfHeader, chunkSignal: EdfChunkSignal, startSeconds: number, durationSeconds: number): EdfChunkSignal;
|
|
54
|
+
//# sourceMappingURL=window.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../src/time/window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,cAAc,EAEd,WAAW,EACX,WAAW,EACZ,MAAM,aAAa,CAAC;AAgDrB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,MAAM,GACtB,SAAS,WAAW,EAAE,CAuExB;AAgED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,cAAc,EAC3B,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,MAAM,GACtB,cAAc,CAiDhB"}
|