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,232 @@
|
|
|
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 { TICKS_PER_SECOND } from '../constants.js';
|
|
19
|
+
import { EdfChannelNotFoundError } from '../errors.js';
|
|
20
|
+
import { secondsToTicks, ticksToSeconds } from '../tal/ticks.js';
|
|
21
|
+
const NO_RANGES = Object.freeze([]);
|
|
22
|
+
/** Exact: 10^7 is far below 2^53. */
|
|
23
|
+
const TICKS_PER_SECOND_FLOAT = Number(TICKS_PER_SECOND);
|
|
24
|
+
/** `b` must be positive. Bigint `/` truncates toward zero, so negatives need the correction. */
|
|
25
|
+
function floorDiv(a, b) {
|
|
26
|
+
const quotient = a / b;
|
|
27
|
+
return a % b === 0n || a > 0n ? quotient : quotient - 1n;
|
|
28
|
+
}
|
|
29
|
+
function ceilDiv(a, b) {
|
|
30
|
+
const quotient = a / b;
|
|
31
|
+
return a % b === 0n || a < 0n ? quotient : quotient + 1n;
|
|
32
|
+
}
|
|
33
|
+
/** Clamping in bigint first, because `Number()` on a large bigint silently loses digits. */
|
|
34
|
+
function clampToInt(value, low, high) {
|
|
35
|
+
if (value <= BigInt(low))
|
|
36
|
+
return low;
|
|
37
|
+
if (value >= BigInt(high))
|
|
38
|
+
return high;
|
|
39
|
+
return Number(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The record duration in exact ticks, recovered from the timeline's float.
|
|
43
|
+
*
|
|
44
|
+
* `resolveTimeWindow`'s signature takes no header, so `header.recordDurationTicks` is not
|
|
45
|
+
* available here. The round-trip is exact anyway: the seconds value was itself produced from a
|
|
46
|
+
* header field of at most a few decimals, and `secondsToTicks` rounds to the nearest tick, so
|
|
47
|
+
* every duration a file can declare below ~10^9 s comes back to the tick it came from.
|
|
48
|
+
*/
|
|
49
|
+
function recordDurationTicksOf(timeline) {
|
|
50
|
+
return secondsToTicks(timeline.recordDurationSeconds);
|
|
51
|
+
}
|
|
52
|
+
function signalAt(header, signalIndex) {
|
|
53
|
+
const signal = header.signals[signalIndex];
|
|
54
|
+
if (signal !== undefined)
|
|
55
|
+
return signal;
|
|
56
|
+
throw new EdfChannelNotFoundError(`signalIndex ${signalIndex} is not one of the ${header.signals.length} signals in this ` +
|
|
57
|
+
'header, so trimToWindow() cannot know how many samples per record it holds. Next: pass ' +
|
|
58
|
+
'the header the chunk was read with.', { selector: signalIndex, availableLabels: header.signals.map((s) => s.label) });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The records a window needs, one `RecordRange` per contiguous run it overlaps, in time order.
|
|
62
|
+
*
|
|
63
|
+
* Empty when the window falls entirely inside a gap, entirely outside the recording, or has a
|
|
64
|
+
* non-positive duration — the interval is half-open, so a zero-length window contains no time
|
|
65
|
+
* and therefore no samples.
|
|
66
|
+
*
|
|
67
|
+
* Ranges are RECORD-ALIGNED and are therefore usually wider than the window: a record is the
|
|
68
|
+
* smallest unit the file can be read by, and `trimToWindow` is how a caller narrows the samples
|
|
69
|
+
* afterwards.
|
|
70
|
+
*
|
|
71
|
+
* With `index.segments` present (`coverage === 'complete'`) the answer is exact. With a probed
|
|
72
|
+
* index it is exact only while the file is contiguous, which is precisely what
|
|
73
|
+
* `spanSeconds === coveredSeconds` states; when it is not, the records a window maps to depend
|
|
74
|
+
* on onsets nobody has read, and this function refuses rather than guessing them.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveTimeWindow(timeline, index, startSeconds, durationSeconds) {
|
|
77
|
+
const recordCount = timeline.recordCount;
|
|
78
|
+
if (recordCount <= 0)
|
|
79
|
+
return NO_RANGES;
|
|
80
|
+
const windowStartTicks = secondsToTicks(startSeconds);
|
|
81
|
+
const windowDurationTicks = secondsToTicks(durationSeconds);
|
|
82
|
+
if (windowDurationTicks <= 0n)
|
|
83
|
+
return NO_RANGES;
|
|
84
|
+
const windowEndTicks = windowStartTicks + windowDurationTicks;
|
|
85
|
+
const durationTicks = recordDurationTicksOf(timeline);
|
|
86
|
+
const segments = index.segments;
|
|
87
|
+
if (segments !== undefined) {
|
|
88
|
+
const ranges = [];
|
|
89
|
+
for (const segment of segments) {
|
|
90
|
+
const segmentCount = segment.records.count;
|
|
91
|
+
if (segmentCount <= 0)
|
|
92
|
+
continue;
|
|
93
|
+
const segmentStartTicks = segment.startTicks;
|
|
94
|
+
// A zero record duration puts every record of the segment at one instant, so the segment
|
|
95
|
+
// is either wholly inside the window or wholly outside it.
|
|
96
|
+
if (durationTicks === 0n) {
|
|
97
|
+
if (windowStartTicks <= segmentStartTicks && segmentStartTicks < windowEndTicks) {
|
|
98
|
+
ranges.push({ start: segment.records.start, count: segmentCount });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const segmentEndTicks = segmentStartTicks + BigInt(segmentCount) * durationTicks;
|
|
103
|
+
if (segmentEndTicks <= windowStartTicks || segmentStartTicks >= windowEndTicks)
|
|
104
|
+
continue;
|
|
105
|
+
const firstOffset = clampToInt(floorDiv(windowStartTicks - segmentStartTicks, durationTicks), 0, segmentCount - 1);
|
|
106
|
+
const lastOffset = clampToInt(ceilDiv(windowEndTicks - segmentStartTicks, durationTicks) - 1n, 0, segmentCount - 1);
|
|
107
|
+
if (lastOffset < firstOffset)
|
|
108
|
+
continue;
|
|
109
|
+
ranges.push({
|
|
110
|
+
start: segment.records.start + firstOffset,
|
|
111
|
+
count: lastOffset - firstOffset + 1,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze(ranges);
|
|
115
|
+
}
|
|
116
|
+
if (timeline.spanSeconds !== timeline.coveredSeconds) {
|
|
117
|
+
throw new RangeError(`resolveTimeWindow() cannot map seconds to records on this file: its ${recordCount} ` +
|
|
118
|
+
`records span ${timeline.spanSeconds} s but cover only ${timeline.coveredSeconds} s, so ` +
|
|
119
|
+
'it contains at least one gap, and a probed index knows where neither the gap nor the ' +
|
|
120
|
+
'records after it start. Next: await buildRecordIndex(recording) and pass the index it ' +
|
|
121
|
+
'returns, or locate the window with index.locate(seconds).');
|
|
122
|
+
}
|
|
123
|
+
// Contiguous as far as the probes can tell: record r starts at exactly r * recordDuration.
|
|
124
|
+
if (durationTicks === 0n) {
|
|
125
|
+
return windowStartTicks <= 0n && windowEndTicks > 0n
|
|
126
|
+
? Object.freeze([{ start: 0, count: recordCount }])
|
|
127
|
+
: NO_RANGES;
|
|
128
|
+
}
|
|
129
|
+
if (windowEndTicks <= 0n)
|
|
130
|
+
return NO_RANGES;
|
|
131
|
+
if (windowStartTicks >= BigInt(recordCount) * durationTicks)
|
|
132
|
+
return NO_RANGES;
|
|
133
|
+
const first = clampToInt(floorDiv(windowStartTicks, durationTicks), 0, recordCount - 1);
|
|
134
|
+
const last = clampToInt(ceilDiv(windowEndTicks, durationTicks) - 1n, 0, recordCount - 1);
|
|
135
|
+
if (last < first)
|
|
136
|
+
return NO_RANGES;
|
|
137
|
+
return Object.freeze([{ start: first, count: last - first + 1 }]);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Sample `firstIndex` of the chunk, in seconds, as an exact rational.
|
|
141
|
+
*
|
|
142
|
+
* The sample sits at `chunkStart + firstIndex * recordDuration / samplesPerRecord`, and that
|
|
143
|
+
* division is usually not a whole number of ticks. The whole part goes through `ticksToSeconds`
|
|
144
|
+
* and only the remainder is divided, so the sub-tick part costs one rounding instead of poisoning
|
|
145
|
+
* the seconds and the ticks together. Bigint `/` and `%` agree in sign, so a negative chunk start
|
|
146
|
+
* (a pre-stimulus window) sums correctly.
|
|
147
|
+
*/
|
|
148
|
+
function sampleStartSeconds(chunkStartTicks, firstIndex, durationTicks, samplesPerRecord) {
|
|
149
|
+
const scaled = chunkStartTicks * samplesPerRecord + firstIndex * durationTicks;
|
|
150
|
+
const wholeTicks = scaled / samplesPerRecord;
|
|
151
|
+
const remainder = scaled % samplesPerRecord;
|
|
152
|
+
return (ticksToSeconds(wholeTicks) +
|
|
153
|
+
Number(remainder) / (Number(samplesPerRecord) * TICKS_PER_SECOND_FLOAT));
|
|
154
|
+
}
|
|
155
|
+
function countOutOfDigitalRange(digital, signal) {
|
|
156
|
+
const low = Math.min(signal.digitalMinimum, signal.digitalMaximum);
|
|
157
|
+
const high = Math.max(signal.digitalMinimum, signal.digitalMaximum);
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (let i = 0; i < digital.length; i += 1) {
|
|
160
|
+
// biome-ignore lint/style/noNonNullAssertion: i is bounded by digital.length.
|
|
161
|
+
const value = digital[i];
|
|
162
|
+
if (value < low || value > high)
|
|
163
|
+
count += 1;
|
|
164
|
+
}
|
|
165
|
+
return count;
|
|
166
|
+
}
|
|
167
|
+
function trimmed(chunkSignal, signal, firstIndex, sampleCount, startSeconds) {
|
|
168
|
+
const digital = chunkSignal.digital.subarray(firstIndex, firstIndex + sampleCount);
|
|
169
|
+
const keptEverything = firstIndex === 0 && digital.length === chunkSignal.digital.length;
|
|
170
|
+
return {
|
|
171
|
+
signalIndex: chunkSignal.signalIndex,
|
|
172
|
+
// From the view, so the count and the data cannot disagree even if the chunk they came from
|
|
173
|
+
// declared a length its array did not have.
|
|
174
|
+
sampleCount: digital.length,
|
|
175
|
+
digital,
|
|
176
|
+
firstSampleIndex: chunkSignal.firstSampleIndex + firstIndex,
|
|
177
|
+
startSeconds,
|
|
178
|
+
// Re-counted only when it can have changed and only when there is something to find: a
|
|
179
|
+
// chunk with no out-of-range samples cannot acquire one by being narrowed.
|
|
180
|
+
outOfDigitalRangeCount: keptEverything || chunkSignal.outOfDigitalRangeCount === 0
|
|
181
|
+
? chunkSignal.outOfDigitalRangeCount
|
|
182
|
+
: countOutOfDigitalRange(digital, signal),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The exact per-signal trim of a record-aligned chunk to `[startSeconds, startSeconds + duration)`.
|
|
187
|
+
*
|
|
188
|
+
* Sample `j` of the chunk starts at `chunkSignal.startSeconds + j * recordDuration /
|
|
189
|
+
* samplesPerRecord`, so the samples inside the window are those with
|
|
190
|
+
* `j * recordDuration >= relativeStart * samplesPerRecord` and
|
|
191
|
+
* `j * recordDuration < relativeEnd * samplesPerRecord`. Both comparisons are integer bigint
|
|
192
|
+
* products of on-disk quantities — no division, no sample rate, no float bound — which is what
|
|
193
|
+
* makes the boundary sample the same one every time and on every platform.
|
|
194
|
+
*
|
|
195
|
+
* The chunk must be one contiguous run of records (what `readWindow` returns), because that is
|
|
196
|
+
* what makes the sample grid uniform across it.
|
|
197
|
+
*
|
|
198
|
+
* `digital` in the result is a SUBARRAY view of the input's, so trimming allocates nothing and
|
|
199
|
+
* the two share memory. A window that only partly overlaps the chunk is clamped to the samples
|
|
200
|
+
* that exist; one that misses it entirely yields a zero-length result rather than an error.
|
|
201
|
+
*/
|
|
202
|
+
export function trimToWindow(header, chunkSignal, startSeconds, durationSeconds) {
|
|
203
|
+
const signal = signalAt(header, chunkSignal.signalIndex);
|
|
204
|
+
const samplesPerRecord = signal.samplesPerRecord;
|
|
205
|
+
const durationTicks = header.recordDurationTicks;
|
|
206
|
+
const available = Math.min(chunkSignal.sampleCount, chunkSignal.digital.length);
|
|
207
|
+
const windowStartTicks = secondsToTicks(startSeconds);
|
|
208
|
+
const windowDurationTicks = secondsToTicks(durationSeconds);
|
|
209
|
+
// The chunk's own start is a float only because `EdfChunkSignal` publishes seconds; it was
|
|
210
|
+
// produced from exact ticks by `ticksToSeconds`, and rounding back to the nearest tick recovers
|
|
211
|
+
// them for any recording shorter than ~28.5 years.
|
|
212
|
+
const chunkStartTicks = secondsToTicks(chunkSignal.startSeconds);
|
|
213
|
+
// Nothing advances in time within the chunk: a zero record duration puts every sample at the
|
|
214
|
+
// chunk's start instant, and a signal with no samples per record has no grid at all. The chunk
|
|
215
|
+
// is then either wholly inside the window or wholly outside it.
|
|
216
|
+
if (durationTicks === 0n || samplesPerRecord === 0) {
|
|
217
|
+
const inside = windowDurationTicks > 0n &&
|
|
218
|
+
windowStartTicks <= chunkStartTicks &&
|
|
219
|
+
chunkStartTicks < windowStartTicks + windowDurationTicks;
|
|
220
|
+
return inside
|
|
221
|
+
? trimmed(chunkSignal, signal, 0, available, chunkSignal.startSeconds)
|
|
222
|
+
: trimmed(chunkSignal, signal, 0, 0, chunkSignal.startSeconds);
|
|
223
|
+
}
|
|
224
|
+
const samplesPerRecordTicks = BigInt(samplesPerRecord);
|
|
225
|
+
const relativeStartTicks = windowStartTicks - chunkStartTicks;
|
|
226
|
+
const relativeEndTicks = relativeStartTicks + windowDurationTicks;
|
|
227
|
+
const firstIndex = clampToInt(ceilDiv(relativeStartTicks * samplesPerRecordTicks, durationTicks), 0, available);
|
|
228
|
+
const lastIndex = clampToInt(ceilDiv(relativeEndTicks * samplesPerRecordTicks, durationTicks) - 1n, -1, available - 1);
|
|
229
|
+
const sampleCount = lastIndex < firstIndex ? 0 : lastIndex - firstIndex + 1;
|
|
230
|
+
return trimmed(chunkSignal, signal, firstIndex, sampleCount, sampleStartSeconds(chunkStartTicks, BigInt(firstIndex), durationTicks, samplesPerRecordTicks));
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window.js","sourceRoot":"","sources":["../../src/time/window.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every public data shape in edfcore.
|
|
3
|
+
*
|
|
4
|
+
* Layer 0. Types only — this module emits no runtime code, so any layer may import it without
|
|
5
|
+
* creating a dependency edge.
|
|
6
|
+
*
|
|
7
|
+
* Two conventions run through the whole file:
|
|
8
|
+
*
|
|
9
|
+
* 1. A field that may be absent is declared `T | undefined` and is always present as a key.
|
|
10
|
+
* Optional (`?`) is reserved for *options* the caller passes in. Reading a result should
|
|
11
|
+
* never require knowing whether a key exists.
|
|
12
|
+
* 2. Anything that can be checked against the file is exposed twice — as parsed value and as
|
|
13
|
+
* the raw bytes it came from. edfcore never destroys evidence.
|
|
14
|
+
*/
|
|
15
|
+
import type { EdfDiagnosticCode, EdfSeverity } from './diagnostics/codes.js';
|
|
16
|
+
export type { EdfDiagnosticCode, EdfKnownDiagnosticCode, EdfSeverity, } from './diagnostics/codes.js';
|
|
17
|
+
export interface AbortSignalLike {
|
|
18
|
+
readonly aborted: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface BlobLike {
|
|
21
|
+
readonly size: number;
|
|
22
|
+
slice(start?: number, end?: number): BlobLike;
|
|
23
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
24
|
+
}
|
|
25
|
+
export interface HttpResponseLike {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly headers: {
|
|
28
|
+
get(name: string): string | null;
|
|
29
|
+
};
|
|
30
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `globalThis.fetch` is assignable to this.
|
|
34
|
+
*
|
|
35
|
+
* `signal` is deliberately absent from `init`: naming it would drag in the real `AbortSignal`
|
|
36
|
+
* by parameter contravariance, which is the exact DOM dependency these shims exist to avoid.
|
|
37
|
+
* It is still passed at runtime.
|
|
38
|
+
*/
|
|
39
|
+
export type FetchLike = (url: string, init: {
|
|
40
|
+
headers: Record<string, string>;
|
|
41
|
+
method?: string;
|
|
42
|
+
}) => Promise<HttpResponseLike>;
|
|
43
|
+
export interface ReadOptions {
|
|
44
|
+
readonly signal?: AbortSignalLike;
|
|
45
|
+
/** Refuse before allocating rather than dying inside it. Default 256 MiB. */
|
|
46
|
+
readonly maxMaterializeBytes?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ParseOptions {
|
|
49
|
+
/**
|
|
50
|
+
* When true the first would-be diagnostic throws `EdfFormatError` carrying it, so every
|
|
51
|
+
* `diagnostics` array is consequently empty. Check order is pinned and tested, which is what
|
|
52
|
+
* makes error identity stable across refactors.
|
|
53
|
+
*/
|
|
54
|
+
readonly strict?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export type OpenOptions = ParseOptions & ReadOptions;
|
|
57
|
+
/**
|
|
58
|
+
* A random-access byte range reader.
|
|
59
|
+
*
|
|
60
|
+
* CONTRACT, verified on every call including user-supplied sources: `read` resolves with
|
|
61
|
+
* EXACTLY `length` bytes or rejects. It never pads and never truncates. The returned array is
|
|
62
|
+
* owned by the caller, so a caching implementation must hand back a copy.
|
|
63
|
+
*/
|
|
64
|
+
export interface ByteSource {
|
|
65
|
+
readonly byteLength: number;
|
|
66
|
+
read(offset: number, length: number, options?: ReadOptions): Promise<Uint8Array>;
|
|
67
|
+
close?(): Promise<void> | void;
|
|
68
|
+
}
|
|
69
|
+
export interface HttpSourceOptions extends ReadOptions {
|
|
70
|
+
readonly fetch?: FetchLike;
|
|
71
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
72
|
+
readonly byteLength?: number;
|
|
73
|
+
readonly maxConcurrency?: number;
|
|
74
|
+
/** Server answered 200 instead of 206. Default false, i.e. throw rather than silently
|
|
75
|
+
* buffering gigabytes the caller did not ask for. */
|
|
76
|
+
readonly allowFullDownload?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface CacheOptions {
|
|
79
|
+
/** Block size in bytes. Default 1 MiB. Blocks are byte-aligned, not record-aligned: the
|
|
80
|
+
* cache is format-independent by construction and never sees a header to learn a record
|
|
81
|
+
* size from. Round this to a multiple of `header.recordByteLength` yourself if you want
|
|
82
|
+
* block boundaries to fall on record boundaries. */
|
|
83
|
+
readonly blockBytes?: number;
|
|
84
|
+
/** LRU budget. Default 64 MiB. */
|
|
85
|
+
readonly maxBytes?: number;
|
|
86
|
+
}
|
|
87
|
+
export type EdfVariant = 'EDF' | 'EDF+C' | 'EDF+D' | 'BDF' | 'BDF+C' | 'BDF+D';
|
|
88
|
+
export interface EdfCalendarDate {
|
|
89
|
+
readonly year: number;
|
|
90
|
+
/** 1-12. Not a JavaScript month index. */
|
|
91
|
+
readonly month: number;
|
|
92
|
+
readonly day: number;
|
|
93
|
+
}
|
|
94
|
+
export interface EdfClockTime {
|
|
95
|
+
readonly hour: number;
|
|
96
|
+
readonly minute: number;
|
|
97
|
+
readonly second: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* EDF records local time at the patient with no timezone, so edfcore never produces a `Date`:
|
|
101
|
+
* a `Date` silently applies the reader's zone, and is worst exactly at DST boundaries.
|
|
102
|
+
*/
|
|
103
|
+
export interface EdfStartTime {
|
|
104
|
+
/** From the `dd.mm.yy` header field, through the 1985-2084 rule. */
|
|
105
|
+
readonly headerDate: EdfCalendarDate | undefined;
|
|
106
|
+
/** From the EDF+ recording-identification `Startdate` subfield — the only unambiguous
|
|
107
|
+
* four-digit year, and the only way past 2084. */
|
|
108
|
+
readonly recordingIdDate: EdfCalendarDate | undefined;
|
|
109
|
+
readonly resolvedDate: EdfCalendarDate | undefined;
|
|
110
|
+
readonly dateSource: 'headerField' | 'recordingIdField' | 'none';
|
|
111
|
+
readonly clock: EdfClockTime;
|
|
112
|
+
readonly secondsSinceMidnight: number;
|
|
113
|
+
}
|
|
114
|
+
export interface EdfPatientId {
|
|
115
|
+
readonly raw: string;
|
|
116
|
+
readonly conformant: boolean;
|
|
117
|
+
readonly code: string | undefined;
|
|
118
|
+
readonly sex: 'F' | 'M' | undefined;
|
|
119
|
+
readonly birthDate: EdfCalendarDate | undefined;
|
|
120
|
+
readonly name: string | undefined;
|
|
121
|
+
readonly extraSubfields: readonly string[];
|
|
122
|
+
}
|
|
123
|
+
export interface EdfRecordingId {
|
|
124
|
+
readonly raw: string;
|
|
125
|
+
readonly conformant: boolean;
|
|
126
|
+
readonly startDate: EdfCalendarDate | undefined;
|
|
127
|
+
readonly investigationCode: string | undefined;
|
|
128
|
+
readonly technicianCode: string | undefined;
|
|
129
|
+
readonly equipmentCode: string | undefined;
|
|
130
|
+
readonly extraSubfields: readonly string[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `physical = bitValue * (offset + digital)`.
|
|
134
|
+
*
|
|
135
|
+
* This is EDFlib's exact expression, kept verbatim for float64 bit-parity with pyEDFlib.
|
|
136
|
+
* It is *not* the numerically better form, and it must not be "simplified" — see
|
|
137
|
+
* `src/decode/physical.ts`.
|
|
138
|
+
*/
|
|
139
|
+
export interface EdfScale {
|
|
140
|
+
readonly bitValue: number;
|
|
141
|
+
readonly offset: number;
|
|
142
|
+
}
|
|
143
|
+
/** Header text exactly as written, before trimming or interpretation. */
|
|
144
|
+
export interface EdfRawSignalFields {
|
|
145
|
+
readonly label: string;
|
|
146
|
+
readonly transducerType: string;
|
|
147
|
+
readonly physicalDimension: string;
|
|
148
|
+
readonly physicalMinimum: string;
|
|
149
|
+
readonly physicalMaximum: string;
|
|
150
|
+
readonly digitalMinimum: string;
|
|
151
|
+
readonly digitalMaximum: string;
|
|
152
|
+
readonly prefiltering: string;
|
|
153
|
+
readonly samplesPerRecord: string;
|
|
154
|
+
readonly reserved: string;
|
|
155
|
+
}
|
|
156
|
+
export interface EdfRawHeaderFields {
|
|
157
|
+
readonly version: string;
|
|
158
|
+
readonly patientId: string;
|
|
159
|
+
readonly recordingId: string;
|
|
160
|
+
readonly startDate: string;
|
|
161
|
+
readonly startTime: string;
|
|
162
|
+
readonly headerByteLength: string;
|
|
163
|
+
readonly reserved: string;
|
|
164
|
+
readonly recordCount: string;
|
|
165
|
+
readonly recordDuration: string;
|
|
166
|
+
readonly signalCount: string;
|
|
167
|
+
}
|
|
168
|
+
export interface EdfSignal {
|
|
169
|
+
readonly index: number;
|
|
170
|
+
readonly kind: 'data' | 'annotations';
|
|
171
|
+
/** Trimmed. `raw.label` keeps the padding. */
|
|
172
|
+
readonly label: string;
|
|
173
|
+
readonly transducerType: string;
|
|
174
|
+
readonly prefiltering: string;
|
|
175
|
+
/** Trimmed. `raw.physicalDimension` keeps the padding and the exact bytes. */
|
|
176
|
+
readonly physicalDimension: string;
|
|
177
|
+
/** Normalised for comparison only: the several encodings of micro all become `u`. */
|
|
178
|
+
readonly unit: string;
|
|
179
|
+
readonly physicalMinimum: number;
|
|
180
|
+
/** MAY be less than the minimum: that is a negative amplifier gain, it is legal, and
|
|
181
|
+
* edfcore never "fixes" it. */
|
|
182
|
+
readonly physicalMaximum: number;
|
|
183
|
+
readonly digitalMinimum: number;
|
|
184
|
+
readonly digitalMaximum: number;
|
|
185
|
+
/** Authoritative. Sample indexing uses this, never a rate. */
|
|
186
|
+
readonly samplesPerRecord: number;
|
|
187
|
+
/** Derived. `undefined` exactly when `recordDurationSeconds === 0`, which is legal.
|
|
188
|
+
* Never index by this. */
|
|
189
|
+
readonly sampleRateHz: number | undefined;
|
|
190
|
+
readonly sampleCount: number;
|
|
191
|
+
/** `undefined` when scaling is impossible or unsafe: a degenerate or inverted digital
|
|
192
|
+
* range, a degenerate physical range, or a log-transformed channel. `toPhysical` then
|
|
193
|
+
* throws `EdfScalingError`; `decodeDigital` keeps working. */
|
|
194
|
+
readonly scale: EdfScale | undefined;
|
|
195
|
+
/** Byte offset of this signal's block within one data record. */
|
|
196
|
+
readonly recordByteOffset: number;
|
|
197
|
+
readonly recordByteLength: number;
|
|
198
|
+
readonly raw: EdfRawSignalFields;
|
|
199
|
+
}
|
|
200
|
+
export interface EdfHeader {
|
|
201
|
+
readonly variant: EdfVariant;
|
|
202
|
+
readonly continuity: 'continuous' | 'discontinuous';
|
|
203
|
+
readonly bytesPerSample: 2 | 3;
|
|
204
|
+
/** Always the computed `256 * (ns + 1)`, never the declared value. */
|
|
205
|
+
readonly headerByteLength: number;
|
|
206
|
+
/** What the file claims at offset 184. Kept so a mismatch stays visible. */
|
|
207
|
+
readonly declaredHeaderByteLength: number;
|
|
208
|
+
readonly recordByteLength: number;
|
|
209
|
+
readonly dataByteLength: number;
|
|
210
|
+
/** MAY legitimately be 0 — then never divide by it. */
|
|
211
|
+
readonly recordDurationSeconds: number;
|
|
212
|
+
readonly recordDurationTicks: bigint;
|
|
213
|
+
/** Resolved and non-negative. */
|
|
214
|
+
readonly recordCount: number;
|
|
215
|
+
/** Verbatim. `-1` means the writer never closed the file. */
|
|
216
|
+
readonly declaredRecordCount: number;
|
|
217
|
+
readonly recordCountSource: 'headerField' | 'sourceByteLength';
|
|
218
|
+
readonly startTime: EdfStartTime;
|
|
219
|
+
readonly patient: EdfPatientId;
|
|
220
|
+
readonly recording: EdfRecordingId;
|
|
221
|
+
readonly signals: readonly EdfSignal[];
|
|
222
|
+
readonly dataSignalIndices: readonly number[];
|
|
223
|
+
readonly annotationSignalIndices: readonly number[];
|
|
224
|
+
/** The full 44 reserved bytes, verbatim. */
|
|
225
|
+
readonly reserved: string;
|
|
226
|
+
readonly raw: EdfRawHeaderFields;
|
|
227
|
+
/** The whole header, for hexdumps and bug reports. */
|
|
228
|
+
readonly rawBytes: Uint8Array;
|
|
229
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
230
|
+
}
|
|
231
|
+
/** Start plus count, never start plus end: there is no inclusive/exclusive ambiguity. */
|
|
232
|
+
export interface RecordRange {
|
|
233
|
+
readonly start: number;
|
|
234
|
+
readonly count: number;
|
|
235
|
+
}
|
|
236
|
+
export interface EdfSegment {
|
|
237
|
+
readonly index: number;
|
|
238
|
+
readonly records: RecordRange;
|
|
239
|
+
readonly startSeconds: number;
|
|
240
|
+
readonly startTicks: bigint;
|
|
241
|
+
readonly durationSeconds: number;
|
|
242
|
+
readonly endSeconds: number;
|
|
243
|
+
}
|
|
244
|
+
export interface EdfGap {
|
|
245
|
+
readonly beforeSegmentIndex: number;
|
|
246
|
+
readonly afterSegmentIndex: number;
|
|
247
|
+
readonly startSeconds: number;
|
|
248
|
+
readonly endSeconds: number;
|
|
249
|
+
readonly durationSeconds: number;
|
|
250
|
+
}
|
|
251
|
+
export interface EdfLocation {
|
|
252
|
+
readonly recordIndex: number;
|
|
253
|
+
readonly recordStartSeconds: number;
|
|
254
|
+
readonly offsetInRecordSeconds: number;
|
|
255
|
+
}
|
|
256
|
+
export interface EdfTimeline {
|
|
257
|
+
readonly recordCount: number;
|
|
258
|
+
readonly recordDurationSeconds: number;
|
|
259
|
+
/** Sub-second start carried by record 0's timekeeping TAL. In [0, 1). */
|
|
260
|
+
readonly startOffsetSeconds: number;
|
|
261
|
+
readonly startOffsetTicks: bigint;
|
|
262
|
+
/** Last record end minus first record start. Includes gaps. */
|
|
263
|
+
readonly spanSeconds: number;
|
|
264
|
+
/** Sum of record durations. Equals `spanSeconds` exactly when the file is contiguous. */
|
|
265
|
+
readonly coveredSeconds: number;
|
|
266
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
267
|
+
}
|
|
268
|
+
export interface EdfRecordIndex {
|
|
269
|
+
/** `probed` = record 0 and the last record only. `complete` = every record verified. */
|
|
270
|
+
readonly coverage: 'probed' | 'complete';
|
|
271
|
+
readonly recordCount: number;
|
|
272
|
+
/** Present only when `coverage === 'complete'`. Deliberately absent otherwise: no property
|
|
273
|
+
* on this object may ever read as "continuous" when we have not checked. */
|
|
274
|
+
readonly segments: readonly EdfSegment[] | undefined;
|
|
275
|
+
readonly gaps: readonly EdfGap[] | undefined;
|
|
276
|
+
/** One targeted read of that record's annotation region. Memoised. */
|
|
277
|
+
onsetTicks(recordIndex: number, options?: ReadOptions): Promise<bigint>;
|
|
278
|
+
/** O(log recordCount) probes. Onsets are monotonic; any observed violation is fatal. */
|
|
279
|
+
locate(seconds: number, options?: ReadOptions): Promise<EdfLocation | undefined>;
|
|
280
|
+
}
|
|
281
|
+
export interface BuildIndexOptions extends ParseOptions, ReadOptions {
|
|
282
|
+
readonly onProgress?: (done: number, total: number) => void;
|
|
283
|
+
}
|
|
284
|
+
export interface EdfChunkSignal {
|
|
285
|
+
readonly signalIndex: number;
|
|
286
|
+
/** The truth. Never padded to a round number. */
|
|
287
|
+
readonly sampleCount: number;
|
|
288
|
+
readonly digital: Int32Array;
|
|
289
|
+
/** Index of the first sample on this signal's own sample grid. */
|
|
290
|
+
readonly firstSampleIndex: number;
|
|
291
|
+
/** Start of this signal's samples. A record-aligned read gives every signal the same
|
|
292
|
+
* value, since they all begin at the same record; it becomes genuinely per-signal after
|
|
293
|
+
* `trimToWindow`, where each signal's own sample grid decides the boundary. */
|
|
294
|
+
readonly startSeconds: number;
|
|
295
|
+
/** Counted during decode, so it costs nothing. A non-zero count means the declared
|
|
296
|
+
* digital range is wrong, not that the samples are. */
|
|
297
|
+
readonly outOfDigitalRangeCount: number;
|
|
298
|
+
}
|
|
299
|
+
export interface EdfChunk {
|
|
300
|
+
readonly records: RecordRange;
|
|
301
|
+
readonly startSeconds: number;
|
|
302
|
+
readonly durationSeconds: number;
|
|
303
|
+
readonly byteOffset: number;
|
|
304
|
+
/** Bytes actually read from the source. Makes overread visible instead of invisible. */
|
|
305
|
+
readonly byteLength: number;
|
|
306
|
+
readonly signals: readonly EdfChunkSignal[];
|
|
307
|
+
readonly precededByGap: EdfGap | undefined;
|
|
308
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
309
|
+
}
|
|
310
|
+
export interface EdfAnnotation {
|
|
311
|
+
/** Verbatim on-disk value, relative to the header startdate/starttime (EDF+ 2.2.4). */
|
|
312
|
+
readonly onsetSecondsFromHeaderStart: number;
|
|
313
|
+
/** Rebased to the first record's true start — the EDFlib/pyEDFlib/MNE convention. */
|
|
314
|
+
readonly onsetSecondsFromFirstRecord: number;
|
|
315
|
+
/** Exact, in 100 ns units. Compare event times with this, never with the floats. */
|
|
316
|
+
readonly onsetTicks: bigint;
|
|
317
|
+
/** The original digits, so precision is never lost to a round-trip. */
|
|
318
|
+
readonly onsetRaw: string;
|
|
319
|
+
readonly durationSeconds: number | undefined;
|
|
320
|
+
readonly durationTicks: bigint | undefined;
|
|
321
|
+
readonly durationRaw: string | undefined;
|
|
322
|
+
/** Verbatim. Never trimmed, never case-folded. */
|
|
323
|
+
readonly text: string;
|
|
324
|
+
/** From the EDF+ `description@@channel` convention. */
|
|
325
|
+
readonly channelLabel: string | undefined;
|
|
326
|
+
readonly signalIndex: number;
|
|
327
|
+
readonly recordIndex: number;
|
|
328
|
+
readonly byteOffsetInRecord: number;
|
|
329
|
+
readonly textEncoding: 'utf-8' | 'latin-1-fallback';
|
|
330
|
+
}
|
|
331
|
+
export interface EdfAnnotationsResult {
|
|
332
|
+
/** Timekeeping TALs and empty texts excluded. Stable sort by
|
|
333
|
+
* (onsetTicks, signalIndex, byteOffsetInRecord). */
|
|
334
|
+
readonly annotations: readonly EdfAnnotation[];
|
|
335
|
+
/** One entry per record in the decoded range. This is the primitive the timeline is
|
|
336
|
+
* built from. */
|
|
337
|
+
readonly recordOnsetTicks: BigInt64Array;
|
|
338
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
339
|
+
}
|
|
340
|
+
export interface EdfDiagnostic {
|
|
341
|
+
readonly code: EdfDiagnosticCode;
|
|
342
|
+
readonly severity: EdfSeverity;
|
|
343
|
+
/** Names the field, the raw bytes as written, the rule, and an actionable next step. */
|
|
344
|
+
readonly message: string;
|
|
345
|
+
readonly field: string | undefined;
|
|
346
|
+
readonly byteOffset: number | undefined;
|
|
347
|
+
readonly byteLength: number | undefined;
|
|
348
|
+
readonly rawBytes: Uint8Array | undefined;
|
|
349
|
+
readonly raw: string | undefined;
|
|
350
|
+
readonly expected: string | undefined;
|
|
351
|
+
readonly actual: string | undefined;
|
|
352
|
+
readonly signalIndex: number | undefined;
|
|
353
|
+
readonly recordIndex: number | undefined;
|
|
354
|
+
/** A spec clause, e.g. 'EDF+ additional specification 5'. Verifiable and stable, unlike a
|
|
355
|
+
* claim about how some other library behaves. */
|
|
356
|
+
readonly specReference: string | undefined;
|
|
357
|
+
}
|
|
358
|
+
export interface EdfRecording {
|
|
359
|
+
readonly source: ByteSource;
|
|
360
|
+
readonly header: EdfHeader;
|
|
361
|
+
readonly timeline: EdfTimeline;
|
|
362
|
+
readonly index: EdfRecordIndex;
|
|
363
|
+
}
|
|
364
|
+
export interface RecordSelection {
|
|
365
|
+
readonly records: RecordRange;
|
|
366
|
+
/** Required, with no "all signals" default: a 256-channel file must never be read
|
|
367
|
+
* wholesale because an argument was omitted. */
|
|
368
|
+
readonly signalIndices: readonly number[];
|
|
369
|
+
}
|
|
370
|
+
export interface WindowSelection {
|
|
371
|
+
readonly startSeconds: number;
|
|
372
|
+
readonly durationSeconds: number;
|
|
373
|
+
readonly signalIndices: readonly number[];
|
|
374
|
+
}
|
|
375
|
+
export interface DecodeAnnotationsOptions extends ParseOptions {
|
|
376
|
+
/** Defaults to every annotation signal. Only the first carries timekeeping. */
|
|
377
|
+
readonly signalIndices?: readonly number[];
|
|
378
|
+
}
|
|
379
|
+
/** Header-only triage. Reads at most 128 KiB and never throws on malformed content. */
|
|
380
|
+
export interface EdfInspection {
|
|
381
|
+
readonly ok: boolean;
|
|
382
|
+
readonly variant: EdfVariant | undefined;
|
|
383
|
+
readonly header: EdfHeader | undefined;
|
|
384
|
+
readonly byteLength: number;
|
|
385
|
+
readonly bytesRead: number;
|
|
386
|
+
readonly headerBytes: Uint8Array | undefined;
|
|
387
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
388
|
+
}
|
|
389
|
+
export interface ValidateOptions extends ReadOptions {
|
|
390
|
+
/** Reuse a completed index so conformance costs one traversal, not two. */
|
|
391
|
+
readonly index?: EdfRecordIndex;
|
|
392
|
+
readonly scanSamples?: boolean;
|
|
393
|
+
readonly onProgress?: (done: number, total: number) => void;
|
|
394
|
+
}
|
|
395
|
+
export interface ObservedSignalStats {
|
|
396
|
+
readonly signalIndex: number;
|
|
397
|
+
readonly observedDigitalMin: number;
|
|
398
|
+
readonly observedDigitalMax: number;
|
|
399
|
+
readonly outOfDigitalRangeCount: number;
|
|
400
|
+
readonly sampleCount: number;
|
|
401
|
+
}
|
|
402
|
+
export interface ValidationReport {
|
|
403
|
+
readonly ok: boolean;
|
|
404
|
+
readonly diagnostics: readonly EdfDiagnostic[];
|
|
405
|
+
readonly recordsScanned: number;
|
|
406
|
+
readonly bytesRead: number;
|
|
407
|
+
readonly signalStats: readonly ObservedSignalStats[];
|
|
408
|
+
}
|
|
409
|
+
//# sourceMappingURL=types.d.ts.map
|