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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-level TAL state machine.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3. Sole owner of turning ONE annotation region into time-stamped annotation lists.
|
|
5
|
+
* `annotations.ts` adds provenance and EDF+ semantics on top; nothing here knows about records,
|
|
6
|
+
* signals, or timekeeping.
|
|
7
|
+
*
|
|
8
|
+
* Two rules define the module, and both are where other EDF readers go wrong:
|
|
9
|
+
*
|
|
10
|
+
* 1. Parsing is HARD-BOUNDED to `[regionStart, regionStart + regionBytes)`. A TAL with no
|
|
11
|
+
* terminating 0x00 inside the region is discarded, never continued past the bound — the bytes
|
|
12
|
+
* after it are the next signal's samples, and parsing those is how a reader invents
|
|
13
|
+
* annotations that are not in the file.
|
|
14
|
+
* 2. The region is split on the STRUCTURAL bytes (0x00, 0x14, 0x15) FIRST and each text run is
|
|
15
|
+
* decoded as UTF-8 LAST. That order is safe in exactly one direction: every byte of a
|
|
16
|
+
* multi-byte UTF-8 sequence is >= 0x80 and can never collide with a structural byte, while a
|
|
17
|
+
* string that has already been decoded can no longer be split on bytes at all.
|
|
18
|
+
*
|
|
19
|
+
* `src/tal/` is the only part of edfcore allowed to use `TextDecoder`, and only for annotation
|
|
20
|
+
* text. Header bytes go through `decodeHeaderLatin1`, for the cross-runtime reason documented in
|
|
21
|
+
* `src/bytes/latin1.ts`.
|
|
22
|
+
*
|
|
23
|
+
* Each defect is reported at most once per region, with an occurrence count: a corrupt region
|
|
24
|
+
* can hold thousands of malformed TALs, and one diagnostic per TAL would be an unbounded
|
|
25
|
+
* allocation dressed up as diligence. Nothing is hidden — `occurrences` says how many there were.
|
|
26
|
+
*/
|
|
27
|
+
import { decodeHeaderLatin1 } from '../bytes/latin1.js';
|
|
28
|
+
import { sliceBytes } from '../bytes/view.js';
|
|
29
|
+
import { TAL_FIELD_TERMINATOR, TAL_ONSET_DURATION_SEPARATOR, TAL_TERMINATOR, } from '../constants.js';
|
|
30
|
+
import { parseSignedTicks, parseUnsignedTicks } from './ticks.js';
|
|
31
|
+
/** Caps the bytes copied into a diagnostic. The full region is one hexdump from `byteOffset`. */
|
|
32
|
+
export const TAL_PREVIEW_MAX_BYTES = 48;
|
|
33
|
+
/**
|
|
34
|
+
* `recordOnsetTicks` is a `BigInt64Array`, and assigning an out-of-range bigint to one WRAPS
|
|
35
|
+
* silently instead of throwing. A value that cannot round-trip through int64 is therefore
|
|
36
|
+
* refused here rather than stored wrong. The bound is ~29,000 years, so nothing real is lost.
|
|
37
|
+
*/
|
|
38
|
+
const INT64_MIN = -(2n ** 63n);
|
|
39
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
40
|
+
/**
|
|
41
|
+
* An int64 tick count needs at most 19 digits, plus a sign, a point and 7 fractional digits.
|
|
42
|
+
* The cap exists so a corrupt region of digits cannot drive quadratic bigint work: the digits
|
|
43
|
+
* are counted before any bigint is built.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_TIMESTAMP_FIELD_CHARS = 40;
|
|
46
|
+
const CHANNEL_LABEL_SEPARATOR = '@@';
|
|
47
|
+
const ASCII_HIGH_BIT = 0x80;
|
|
48
|
+
const ASCII_DEL = 0x7f;
|
|
49
|
+
const ASCII_FIRST_PRINTABLE = 0x20;
|
|
50
|
+
/** `undefined` = not looked up yet, `null` = this runtime has no `TextDecoder`. */
|
|
51
|
+
let cachedUtf8Decoder;
|
|
52
|
+
/**
|
|
53
|
+
* A strict UTF-8 decoder, or `null`.
|
|
54
|
+
*
|
|
55
|
+
* `fatal` is what makes invalid UTF-8 detectable at all; without it the decoder substitutes
|
|
56
|
+
* U+FFFD and the file's bytes are gone. `ignoreBOM` keeps a leading U+FEFF as a character,
|
|
57
|
+
* because annotation text is exposed verbatim and a silently stripped BOM is a silently changed
|
|
58
|
+
* string. Every runtime edfcore supports has `TextDecoder`; the `null` branch exists so an
|
|
59
|
+
* exotic one degrades to Latin-1 with a diagnostic instead of throwing.
|
|
60
|
+
*/
|
|
61
|
+
function utf8Decoder() {
|
|
62
|
+
if (cachedUtf8Decoder === undefined) {
|
|
63
|
+
const Decoder = globalThis.TextDecoder;
|
|
64
|
+
cachedUtf8Decoder =
|
|
65
|
+
Decoder === undefined ? null : new Decoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
66
|
+
}
|
|
67
|
+
return cachedUtf8Decoder;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `noUncheckedIndexedAccess` types `region[i]` as `number | undefined`. -1 is not a byte value,
|
|
71
|
+
* so every structural comparison below is simply false past the end of the region.
|
|
72
|
+
*/
|
|
73
|
+
function readByte(region, index) {
|
|
74
|
+
return region[index] ?? -1;
|
|
75
|
+
}
|
|
76
|
+
function indexOfByte(region, start, end, byte) {
|
|
77
|
+
for (let i = start; i < end; i += 1) {
|
|
78
|
+
if (readByte(region, i) === byte)
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
function escapeControls(text) {
|
|
84
|
+
let out = '';
|
|
85
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
86
|
+
const code = text.charCodeAt(i);
|
|
87
|
+
out +=
|
|
88
|
+
code < ASCII_FIRST_PRINTABLE || code === ASCII_DEL
|
|
89
|
+
? `\\x${code.toString(16).padStart(2, '0')}`
|
|
90
|
+
: text.charAt(i);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Bytes as a short, escaped, single-line string for a diagnostic message.
|
|
96
|
+
*
|
|
97
|
+
* Latin-1 and not UTF-8 on purpose: this is evidence about bytes, so every byte must map to
|
|
98
|
+
* exactly one visible character even when the run is the invalid UTF-8 being complained about.
|
|
99
|
+
*/
|
|
100
|
+
export function previewBytes(bytes, offset, length) {
|
|
101
|
+
const shown = Math.min(length, TAL_PREVIEW_MAX_BYTES);
|
|
102
|
+
const text = escapeControls(decodeHeaderLatin1(sliceBytes(bytes, offset, shown)));
|
|
103
|
+
return length > shown ? `${text}...` : text;
|
|
104
|
+
}
|
|
105
|
+
function logIssue(log, code, region, offset, length, detail) {
|
|
106
|
+
const existing = log.get(code);
|
|
107
|
+
if (existing !== undefined) {
|
|
108
|
+
existing.occurrences += 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
log.set(code, {
|
|
112
|
+
code,
|
|
113
|
+
byteOffsetInRegion: offset,
|
|
114
|
+
byteLength: length,
|
|
115
|
+
occurrences: 1,
|
|
116
|
+
detail,
|
|
117
|
+
raw: previewBytes(region, offset, length),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function isAsciiRun(bytes) {
|
|
121
|
+
for (const byte of bytes) {
|
|
122
|
+
if (byte >= ASCII_HIGH_BIT)
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
function decodeTextRun(region, start, length, log) {
|
|
128
|
+
const bytes = sliceBytes(region, start, length);
|
|
129
|
+
// ASCII is UTF-8, byte for byte, and it is what almost every file contains. Taking it here
|
|
130
|
+
// means the decoder is only ever built for text that actually needs one.
|
|
131
|
+
if (isAsciiRun(bytes)) {
|
|
132
|
+
return {
|
|
133
|
+
text: decodeHeaderLatin1(bytes),
|
|
134
|
+
encoding: 'utf-8',
|
|
135
|
+
byteOffsetInRegion: start,
|
|
136
|
+
byteLength: length,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const decoder = utf8Decoder();
|
|
140
|
+
if (decoder !== null) {
|
|
141
|
+
try {
|
|
142
|
+
return {
|
|
143
|
+
text: decoder.decode(bytes),
|
|
144
|
+
encoding: 'utf-8',
|
|
145
|
+
byteOffsetInRegion: start,
|
|
146
|
+
byteLength: length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// A fatal decoder rejects invalid UTF-8 with a TypeError. Fall through to Latin-1.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
logIssue(log, 'ANNOTATION_TEXT_NOT_UTF8', region, start, length, decoder === null
|
|
154
|
+
? 'this runtime has no TextDecoder, so the text run could not be verified as UTF-8 and ' +
|
|
155
|
+
'was decoded as ISO-8859-1'
|
|
156
|
+
: 'the text run is not valid UTF-8 and was decoded as ISO-8859-1 instead');
|
|
157
|
+
return {
|
|
158
|
+
text: decodeHeaderLatin1(bytes),
|
|
159
|
+
encoding: 'latin-1-fallback',
|
|
160
|
+
byteOffsetInRegion: start,
|
|
161
|
+
byteLength: length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function outsideInt64(ticks) {
|
|
165
|
+
return ticks < INT64_MIN || ticks > INT64_MAX;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* One TAL slot at `start`, which is known to hold a non-0x00 byte.
|
|
169
|
+
*
|
|
170
|
+
* Returns `tal: undefined` for a slot that was skipped; `next` still advances past it, so the
|
|
171
|
+
* rest of the region is parsed either way. That is the whole point of `TAL_MALFORMED` being a
|
|
172
|
+
* warning: one bad TAL must not cost the file.
|
|
173
|
+
*/
|
|
174
|
+
function scanTal(region, start, ordinal, log) {
|
|
175
|
+
const regionLength = region.length;
|
|
176
|
+
const bodyEnd = indexOfByte(region, start, regionLength, TAL_TERMINATOR);
|
|
177
|
+
if (bodyEnd < 0) {
|
|
178
|
+
logIssue(log, 'TAL_TRUNCATED_AT_REGION_END', region, start, regionLength - start, 'a TAL begins here and has no terminating 0x00 inside the region, so it was discarded ' +
|
|
179
|
+
"rather than continued into the following signal's samples");
|
|
180
|
+
return { tal: undefined, next: regionLength };
|
|
181
|
+
}
|
|
182
|
+
const next = bodyEnd + 1;
|
|
183
|
+
const timestampEnd = indexOfByte(region, start, bodyEnd, TAL_FIELD_TERMINATOR);
|
|
184
|
+
if (timestampEnd < 0) {
|
|
185
|
+
logIssue(log, 'TAL_MALFORMED', region, start, bodyEnd - start, 'the timestamp is not terminated by 0x14, so the TAL was skipped');
|
|
186
|
+
return { tal: undefined, next };
|
|
187
|
+
}
|
|
188
|
+
const separator = indexOfByte(region, start, timestampEnd, TAL_ONSET_DURATION_SEPARATOR);
|
|
189
|
+
const onsetEnd = separator < 0 ? timestampEnd : separator;
|
|
190
|
+
if (onsetEnd - start > MAX_TIMESTAMP_FIELD_CHARS) {
|
|
191
|
+
logIssue(log, 'TAL_MALFORMED', region, start, onsetEnd - start, `the onset field is ${onsetEnd - start} bytes long, which no time in 100 ns ticks needs; ` +
|
|
192
|
+
'the TAL was skipped');
|
|
193
|
+
return { tal: undefined, next };
|
|
194
|
+
}
|
|
195
|
+
const onsetRaw = decodeHeaderLatin1(sliceBytes(region, start, onsetEnd - start));
|
|
196
|
+
const signedOnset = parseSignedTicks(onsetRaw);
|
|
197
|
+
let onsetTicks = signedOnset.ticks;
|
|
198
|
+
if (!signedOnset.ok) {
|
|
199
|
+
// `parseSignedTicks` fails for a missing sign and for a bad magnitude alike. Only the first
|
|
200
|
+
// is recoverable, and re-parsing without the sign is how they are told apart.
|
|
201
|
+
const unsignedOnset = parseUnsignedTicks(onsetRaw);
|
|
202
|
+
if (!unsignedOnset.ok) {
|
|
203
|
+
logIssue(log, 'TAL_MALFORMED', region, start, Math.max(onsetEnd - start, 1), `the onset "${onsetRaw}" is not ("+" / "-") 1*DIGIT [ "." 1*DIGIT ], so the TAL was ` +
|
|
204
|
+
'skipped');
|
|
205
|
+
return { tal: undefined, next };
|
|
206
|
+
}
|
|
207
|
+
onsetTicks = unsignedOnset.ticks;
|
|
208
|
+
logIssue(log, 'TAL_MALFORMED', region, start, onsetEnd - start, `the onset "${onsetRaw}" has no mandatory sign; the value is unambiguous, so the TAL was ` +
|
|
209
|
+
'kept and the onset read as positive');
|
|
210
|
+
}
|
|
211
|
+
if (outsideInt64(onsetTicks)) {
|
|
212
|
+
logIssue(log, 'TAL_MALFORMED', region, start, onsetEnd - start, `the onset "${onsetRaw}" is outside the +/-2^63 tick range edfcore can represent, so the ` +
|
|
213
|
+
'TAL was skipped');
|
|
214
|
+
return { tal: undefined, next };
|
|
215
|
+
}
|
|
216
|
+
let durationRaw;
|
|
217
|
+
let durationTicks;
|
|
218
|
+
if (separator >= 0) {
|
|
219
|
+
const durationStart = separator + 1;
|
|
220
|
+
const durationLength = timestampEnd - durationStart;
|
|
221
|
+
if (durationLength > MAX_TIMESTAMP_FIELD_CHARS) {
|
|
222
|
+
logIssue(log, 'TAL_MALFORMED', region, durationStart, durationLength, `the duration field is ${durationLength} bytes long, which no time in 100 ns ticks ` +
|
|
223
|
+
'needs; the TAL was skipped');
|
|
224
|
+
return { tal: undefined, next };
|
|
225
|
+
}
|
|
226
|
+
durationRaw = decodeHeaderLatin1(sliceBytes(region, durationStart, durationLength));
|
|
227
|
+
const duration = parseUnsignedTicks(durationRaw);
|
|
228
|
+
if (!duration.ok || outsideInt64(duration.ticks)) {
|
|
229
|
+
logIssue(log, 'TAL_MALFORMED', region, durationStart, Math.max(durationLength, 1), `0x15 is present but the duration "${durationRaw}" is not 1*DIGIT [ "." 1*DIGIT ] — a ` +
|
|
230
|
+
'duration is never signed — so the TAL was skipped');
|
|
231
|
+
return { tal: undefined, next };
|
|
232
|
+
}
|
|
233
|
+
durationTicks = duration.ticks;
|
|
234
|
+
}
|
|
235
|
+
const texts = [];
|
|
236
|
+
let runStart = timestampEnd + 1;
|
|
237
|
+
for (let i = runStart; i < bodyEnd; i += 1) {
|
|
238
|
+
if (readByte(region, i) !== TAL_FIELD_TERMINATOR)
|
|
239
|
+
continue;
|
|
240
|
+
texts.push(readTextRun(region, runStart, i, log));
|
|
241
|
+
runStart = i + 1;
|
|
242
|
+
}
|
|
243
|
+
if (runStart < bodyEnd) {
|
|
244
|
+
// The grammar terminates every text with 0x14, so a run left over here means the last text
|
|
245
|
+
// ran straight into the TAL terminator. The bytes are unambiguous, so the text is kept.
|
|
246
|
+
logIssue(log, 'TAL_MALFORMED', region, runStart, bodyEnd - runStart, 'the last text of the TAL is not terminated by 0x14; the text was kept verbatim');
|
|
247
|
+
texts.push(readTextRun(region, runStart, bodyEnd, log));
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
tal: {
|
|
251
|
+
ordinal,
|
|
252
|
+
byteOffsetInRegion: start,
|
|
253
|
+
byteLength: next - start,
|
|
254
|
+
onsetRaw,
|
|
255
|
+
onsetTicks,
|
|
256
|
+
durationRaw,
|
|
257
|
+
durationTicks,
|
|
258
|
+
texts,
|
|
259
|
+
},
|
|
260
|
+
next,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function readTextRun(region, start, end, log) {
|
|
264
|
+
if (indexOfByte(region, start, end, TAL_ONSET_DURATION_SEPARATOR) >= 0) {
|
|
265
|
+
logIssue(log, 'TAL_MALFORMED', region, start, end - start, 'a text run contains 0x15, which the grammar reserves for the onset/duration separator; ' +
|
|
266
|
+
'the text was kept verbatim');
|
|
267
|
+
}
|
|
268
|
+
return decodeTextRun(region, start, end - start, log);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Every TAL in `[regionStart, regionStart + regionBytes)`, and nothing outside it.
|
|
272
|
+
*
|
|
273
|
+
* `region = *TAL *%x00`, so a 0x00 where a TAL would start means the padding has begun. Content
|
|
274
|
+
* after that point is `TAL_REGION_NOT_NUL_TERMINATED`; parsing then RESUMES at those bytes
|
|
275
|
+
* rather than stopping, because they are still inside this signal's own region and a writer
|
|
276
|
+
* that pads between TALs would otherwise lose every annotation after the first pad. Recovery is
|
|
277
|
+
* bounded: each attempt consumes at least one byte, and a slot that does not parse is skipped
|
|
278
|
+
* to just past its 0x00.
|
|
279
|
+
*/
|
|
280
|
+
export function parseTalRegion(bytes, regionStart, regionBytes) {
|
|
281
|
+
const region = sliceBytes(bytes, regionStart, regionBytes);
|
|
282
|
+
const log = new Map();
|
|
283
|
+
const tals = [];
|
|
284
|
+
let position = 0;
|
|
285
|
+
let ordinal = 0;
|
|
286
|
+
while (position < regionBytes) {
|
|
287
|
+
if (readByte(region, position) === TAL_TERMINATOR) {
|
|
288
|
+
let scan = position;
|
|
289
|
+
while (scan < regionBytes && readByte(region, scan) === TAL_TERMINATOR)
|
|
290
|
+
scan += 1;
|
|
291
|
+
if (scan === regionBytes)
|
|
292
|
+
break;
|
|
293
|
+
logIssue(log, 'TAL_REGION_NOT_NUL_TERMINATED', region, scan, regionBytes - scan, 'the region tail after the last TAL is not all 0x00; parsing resumed at this byte');
|
|
294
|
+
// A TAL found after padding did not begin the region, so it must never be promoted into
|
|
295
|
+
// the timekeeping role: slot 0 is a position, not "the first TAL we managed to parse".
|
|
296
|
+
if (ordinal === 0)
|
|
297
|
+
ordinal = 1;
|
|
298
|
+
position = scan;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const scan = scanTal(region, position, ordinal, log);
|
|
302
|
+
if (scan.tal !== undefined)
|
|
303
|
+
tals.push(scan.tal);
|
|
304
|
+
ordinal += 1;
|
|
305
|
+
position = scan.next;
|
|
306
|
+
}
|
|
307
|
+
return { tals, issues: [...log.values()] };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* `description@@channel` split at the LAST `@@`, because the channel label is the suffix and a
|
|
311
|
+
* description is free to contain anything.
|
|
312
|
+
*
|
|
313
|
+
* A trailing `@@` with nothing after it is not a channel label: the text keeps it verbatim.
|
|
314
|
+
* `@@Fp1` with nothing before it yields an empty description and the channel — the run itself
|
|
315
|
+
* is not empty, so it is still a real annotation.
|
|
316
|
+
*/
|
|
317
|
+
export function splitChannelLabel(run) {
|
|
318
|
+
const at = run.lastIndexOf(CHANNEL_LABEL_SEPARATOR);
|
|
319
|
+
if (at < 0 || at + CHANNEL_LABEL_SEPARATOR.length >= run.length) {
|
|
320
|
+
return { text: run, channelLabel: undefined };
|
|
321
|
+
}
|
|
322
|
+
return { text: run.slice(0, at), channelLabel: run.slice(at + CHANNEL_LABEL_SEPARATOR.length) };
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=grammar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grammar.js","sourceRoot":"","sources":["../../src/tal/grammar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decimal text to exact 100 ns ticks.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3. Sole owner of the string -> tick conversion, which is the whole reason event times
|
|
5
|
+
* in edfcore compare exactly. `parseFloat`, `Number(text)` and float arithmetic appear nowhere
|
|
6
|
+
* on that path: an onset written `+0.1` and one written `+0.3` are integers here, so equality,
|
|
7
|
+
* ordering and subtraction are decided by the digits on disk rather than by float64 rounding.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* One parsed onset or duration field.
|
|
11
|
+
*
|
|
12
|
+
* `ticks` is the authoritative value; `seconds` and any float derived from it are conveniences.
|
|
13
|
+
*/
|
|
14
|
+
export interface TickParse {
|
|
15
|
+
/**
|
|
16
|
+
* The text matched the EDF+ grammar for its field. When false, `ticks` is 0n except in the
|
|
17
|
+
* one documented case in `parseSignedTicks` (a valid magnitude with the sign missing).
|
|
18
|
+
*/
|
|
19
|
+
readonly ok: boolean;
|
|
20
|
+
readonly ticks: bigint;
|
|
21
|
+
/** The input verbatim, so a caller never has to reconstruct the digits it came from. */
|
|
22
|
+
readonly raw: string;
|
|
23
|
+
/** Lossy by construction. See `ticksToSeconds`. */
|
|
24
|
+
readonly seconds: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An EDF+ onset: `("+" / "-") 1*DIGIT [ "." 1*DIGIT ]`.
|
|
28
|
+
*
|
|
29
|
+
* The sign is mandatory, so an unsigned onset is a spec violation and `ok` is false — the caller
|
|
30
|
+
* emits `TAL_MALFORMED`. The magnitude is parsed anyway and returned in `ticks`, so a lenient
|
|
31
|
+
* caller can still use the value instead of discarding an otherwise readable annotation. That is
|
|
32
|
+
* the only case where `ok: false` comes with a meaningful `ticks`.
|
|
33
|
+
*
|
|
34
|
+
* `-` applies to the WHOLE value, fraction included: `-0.5` is -5000000 ticks, not -0 plus
|
|
35
|
+
* 5000000. Splitting the sign off the fraction flips pre-stimulus event times about zero.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseSignedTicks(text: string): TickParse;
|
|
38
|
+
/**
|
|
39
|
+
* An EDF+ duration: `1*DIGIT [ "." 1*DIGIT ]`, never signed.
|
|
40
|
+
*
|
|
41
|
+
* A leading `+` or `-` fails the grammar; it is not tolerated and not stripped, because a signed
|
|
42
|
+
* duration means the writer's field layout is not the one we are reading.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseUnsignedTicks(text: string): TickParse;
|
|
45
|
+
/**
|
|
46
|
+
* Ticks as float64 seconds, for ergonomics and display.
|
|
47
|
+
*
|
|
48
|
+
* Lossy by construction — most tick values are not representable in binary floating point, and
|
|
49
|
+
* beyond 2^53 ticks (~28.5 years) even the integer part rounds. The exact value always stays
|
|
50
|
+
* available as ticks, and that is what comparisons must use.
|
|
51
|
+
*
|
|
52
|
+
* Split into whole seconds plus remainder so that only the remainder is ever divided; bigint
|
|
53
|
+
* `/` truncates toward zero and `%` keeps the dividend's sign, so both parts share a sign and
|
|
54
|
+
* the sum is correct for negative onsets.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ticksToSeconds(ticks: bigint): number;
|
|
57
|
+
/**
|
|
58
|
+
* Seconds to ticks, rounded to the NEAREST tick (ties away from zero is not required; ties go
|
|
59
|
+
* toward +Infinity, as `Math.round` does).
|
|
60
|
+
*
|
|
61
|
+
* Only for user-supplied bounds such as a time window, never for a value read from disk: a
|
|
62
|
+
* caller's `30.0` must resolve to 300000000 ticks and not to 299999999 because `30 * 1e7` landed
|
|
63
|
+
* one ULP low. On-disk values reach ticks through the digit parsers above, which never round.
|
|
64
|
+
*
|
|
65
|
+
* Throws `RangeError` for a non-finite argument: there is no tick count for NaN or Infinity, and
|
|
66
|
+
* inventing 0 would silently move a window to the file start.
|
|
67
|
+
*/
|
|
68
|
+
export declare function secondsToTicks(seconds: number): bigint;
|
|
69
|
+
//# sourceMappingURL=ticks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticks.d.ts","sourceRoot":"","sources":["../../src/tal/ticks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAwED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAOxD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAQ1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decimal text to exact 100 ns ticks.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3. Sole owner of the string -> tick conversion, which is the whole reason event times
|
|
5
|
+
* in edfcore compare exactly. `parseFloat`, `Number(text)` and float arithmetic appear nowhere
|
|
6
|
+
* on that path: an onset written `+0.1` and one written `+0.3` are integers here, so equality,
|
|
7
|
+
* ordering and subtraction are decided by the digits on disk rather than by float64 rounding.
|
|
8
|
+
*/
|
|
9
|
+
import { TICKS_PER_SECOND } from '../constants.js';
|
|
10
|
+
const ASCII_ZERO = 0x30;
|
|
11
|
+
const ASCII_NINE = 0x39;
|
|
12
|
+
const ASCII_PLUS = 0x2b;
|
|
13
|
+
const ASCII_MINUS = 0x2d;
|
|
14
|
+
const ASCII_DOT = 0x2e;
|
|
15
|
+
/** `TICKS_PER_SECOND` is 10^7, so exactly seven fractional decimal digits are representable. */
|
|
16
|
+
const TICK_FRACTION_DIGITS = 7;
|
|
17
|
+
/** Exact: 10^7 is far below 2^53, so this conversion loses nothing. */
|
|
18
|
+
const TICKS_PER_SECOND_FLOAT = Number(TICKS_PER_SECOND);
|
|
19
|
+
const MAGNITUDE_FAILED = { ok: false, ticks: 0n };
|
|
20
|
+
function isAsciiDigit(code) {
|
|
21
|
+
return code >= ASCII_ZERO && code <= ASCII_NINE;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `1*DIGIT [ "." 1*DIGIT ]` starting at `start`, accumulated digit by digit into a bigint.
|
|
25
|
+
*
|
|
26
|
+
* Fractional digits beyond the seventh are below tick resolution and are TRUNCATED, never
|
|
27
|
+
* rounded: rounding would move an event to a time that is in no file, and for a stimulus marker
|
|
28
|
+
* that is the difference between a pre- and a post-stimulus sample. The extra digits are still
|
|
29
|
+
* required to be digits, so `+1.00000000x` is rejected rather than silently truncated.
|
|
30
|
+
*/
|
|
31
|
+
function parseMagnitudeTicks(text, start) {
|
|
32
|
+
const end = text.length;
|
|
33
|
+
let i = start;
|
|
34
|
+
let whole = 0n;
|
|
35
|
+
while (i < end) {
|
|
36
|
+
const code = text.charCodeAt(i);
|
|
37
|
+
if (!isAsciiDigit(code))
|
|
38
|
+
break;
|
|
39
|
+
whole = whole * 10n + BigInt(code - ASCII_ZERO);
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
if (i === start)
|
|
43
|
+
return MAGNITUDE_FAILED;
|
|
44
|
+
let ticks = whole * TICKS_PER_SECOND;
|
|
45
|
+
if (i === end)
|
|
46
|
+
return { ok: true, ticks };
|
|
47
|
+
// Anything other than a decimal point here is a space, a second sign, a stray structural
|
|
48
|
+
// byte or trailing padding, and none of those are part of the grammar.
|
|
49
|
+
if (text.charCodeAt(i) !== ASCII_DOT)
|
|
50
|
+
return MAGNITUDE_FAILED;
|
|
51
|
+
i += 1;
|
|
52
|
+
let fractionDigits = 0;
|
|
53
|
+
let scale = TICKS_PER_SECOND;
|
|
54
|
+
while (i < end) {
|
|
55
|
+
const code = text.charCodeAt(i);
|
|
56
|
+
if (!isAsciiDigit(code))
|
|
57
|
+
return MAGNITUDE_FAILED;
|
|
58
|
+
fractionDigits += 1;
|
|
59
|
+
if (fractionDigits <= TICK_FRACTION_DIGITS) {
|
|
60
|
+
scale = scale / 10n;
|
|
61
|
+
ticks += BigInt(code - ASCII_ZERO) * scale;
|
|
62
|
+
}
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
if (fractionDigits === 0)
|
|
66
|
+
return MAGNITUDE_FAILED;
|
|
67
|
+
return { ok: true, ticks };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* An EDF+ onset: `("+" / "-") 1*DIGIT [ "." 1*DIGIT ]`.
|
|
71
|
+
*
|
|
72
|
+
* The sign is mandatory, so an unsigned onset is a spec violation and `ok` is false — the caller
|
|
73
|
+
* emits `TAL_MALFORMED`. The magnitude is parsed anyway and returned in `ticks`, so a lenient
|
|
74
|
+
* caller can still use the value instead of discarding an otherwise readable annotation. That is
|
|
75
|
+
* the only case where `ok: false` comes with a meaningful `ticks`.
|
|
76
|
+
*
|
|
77
|
+
* `-` applies to the WHOLE value, fraction included: `-0.5` is -5000000 ticks, not -0 plus
|
|
78
|
+
* 5000000. Splitting the sign off the fraction flips pre-stimulus event times about zero.
|
|
79
|
+
*/
|
|
80
|
+
export function parseSignedTicks(text) {
|
|
81
|
+
const first = text.length > 0 ? text.charCodeAt(0) : -1;
|
|
82
|
+
const negative = first === ASCII_MINUS;
|
|
83
|
+
const signed = negative || first === ASCII_PLUS;
|
|
84
|
+
const magnitude = parseMagnitudeTicks(text, signed ? 1 : 0);
|
|
85
|
+
const ticks = negative ? -magnitude.ticks : magnitude.ticks;
|
|
86
|
+
return { ok: magnitude.ok && signed, ticks, raw: text, seconds: ticksToSeconds(ticks) };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* An EDF+ duration: `1*DIGIT [ "." 1*DIGIT ]`, never signed.
|
|
90
|
+
*
|
|
91
|
+
* A leading `+` or `-` fails the grammar; it is not tolerated and not stripped, because a signed
|
|
92
|
+
* duration means the writer's field layout is not the one we are reading.
|
|
93
|
+
*/
|
|
94
|
+
export function parseUnsignedTicks(text) {
|
|
95
|
+
const magnitude = parseMagnitudeTicks(text, 0);
|
|
96
|
+
return {
|
|
97
|
+
ok: magnitude.ok,
|
|
98
|
+
ticks: magnitude.ticks,
|
|
99
|
+
raw: text,
|
|
100
|
+
seconds: ticksToSeconds(magnitude.ticks),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Ticks as float64 seconds, for ergonomics and display.
|
|
105
|
+
*
|
|
106
|
+
* Lossy by construction — most tick values are not representable in binary floating point, and
|
|
107
|
+
* beyond 2^53 ticks (~28.5 years) even the integer part rounds. The exact value always stays
|
|
108
|
+
* available as ticks, and that is what comparisons must use.
|
|
109
|
+
*
|
|
110
|
+
* Split into whole seconds plus remainder so that only the remainder is ever divided; bigint
|
|
111
|
+
* `/` truncates toward zero and `%` keeps the dividend's sign, so both parts share a sign and
|
|
112
|
+
* the sum is correct for negative onsets.
|
|
113
|
+
*/
|
|
114
|
+
export function ticksToSeconds(ticks) {
|
|
115
|
+
const wholeSeconds = ticks / TICKS_PER_SECOND;
|
|
116
|
+
const remainder = ticks % TICKS_PER_SECOND;
|
|
117
|
+
return Number(wholeSeconds) + Number(remainder) / TICKS_PER_SECOND_FLOAT;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Seconds to ticks, rounded to the NEAREST tick (ties away from zero is not required; ties go
|
|
121
|
+
* toward +Infinity, as `Math.round` does).
|
|
122
|
+
*
|
|
123
|
+
* Only for user-supplied bounds such as a time window, never for a value read from disk: a
|
|
124
|
+
* caller's `30.0` must resolve to 300000000 ticks and not to 299999999 because `30 * 1e7` landed
|
|
125
|
+
* one ULP low. On-disk values reach ticks through the digit parsers above, which never round.
|
|
126
|
+
*
|
|
127
|
+
* Throws `RangeError` for a non-finite argument: there is no tick count for NaN or Infinity, and
|
|
128
|
+
* inventing 0 would silently move a window to the file start.
|
|
129
|
+
*/
|
|
130
|
+
export function secondsToTicks(seconds) {
|
|
131
|
+
if (!Number.isFinite(seconds)) {
|
|
132
|
+
throw new RangeError(`secondsToTicks(): expected a finite number of seconds, received ${seconds}. ` +
|
|
133
|
+
'Next: check the window bound you passed in.');
|
|
134
|
+
}
|
|
135
|
+
return BigInt(Math.round(seconds * TICKS_PER_SECOND_FLOAT));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=ticks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticks.js","sourceRoot":"","sources":["../../src/tal/ticks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 type { EdfGap, EdfSegment } from '../types.js';
|
|
19
|
+
export interface Segmentation {
|
|
20
|
+
/** In time order, contiguous within each entry, and covering every record exactly once. */
|
|
21
|
+
readonly segments: readonly EdfSegment[];
|
|
22
|
+
/** One per adjacent pair of segments, so `gaps.length === segments.length - 1` (or 0). */
|
|
23
|
+
readonly gaps: readonly EdfGap[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `onsetTicks` holds one entry per record, record 0 first — the `BigInt64Array` that
|
|
27
|
+
* `decodeAnnotations` returns for a full-file range fits directly.
|
|
28
|
+
*
|
|
29
|
+
* `originTicks` is the tick value that maps to `0` seconds. It defaults to `onsetTicks[0]`,
|
|
30
|
+
* which is record 0's own onset and therefore the recording's sub-second start offset.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildSegmentation(onsetTicks: ArrayLike<bigint>, recordDurationTicks: bigint, originTicks?: bigint): Segmentation;
|
|
33
|
+
//# sourceMappingURL=segments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segments.d.ts","sourceRoot":"","sources":["../../src/time/segments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAaD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,EAC7B,mBAAmB,EAAE,MAAM,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,YAAY,CA0Dd"}
|