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.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/bytes/latin1.d.ts +43 -0
  4. package/dist/bytes/latin1.d.ts.map +1 -0
  5. package/dist/bytes/latin1.js +84 -0
  6. package/dist/bytes/latin1.js.map +1 -0
  7. package/dist/bytes/numbers.d.ts +45 -0
  8. package/dist/bytes/numbers.d.ts.map +1 -0
  9. package/dist/bytes/numbers.js +87 -0
  10. package/dist/bytes/numbers.js.map +1 -0
  11. package/dist/bytes/view.d.ts +27 -0
  12. package/dist/bytes/view.d.ts.map +1 -0
  13. package/dist/bytes/view.js +53 -0
  14. package/dist/bytes/view.js.map +1 -0
  15. package/dist/constants.d.ts +113 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +83 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/decode/digital.d.ts +49 -0
  20. package/dist/decode/digital.d.ts.map +1 -0
  21. package/dist/decode/digital.js +180 -0
  22. package/dist/decode/digital.js.map +1 -0
  23. package/dist/decode/physical.d.ts +37 -0
  24. package/dist/decode/physical.d.ts.map +1 -0
  25. package/dist/decode/physical.js +178 -0
  26. package/dist/decode/physical.js.map +1 -0
  27. package/dist/diagnostics/codes.d.ts +38 -0
  28. package/dist/diagnostics/codes.d.ts.map +1 -0
  29. package/dist/diagnostics/codes.js +134 -0
  30. package/dist/diagnostics/codes.js.map +1 -0
  31. package/dist/diagnostics/collector.d.ts +76 -0
  32. package/dist/diagnostics/collector.d.ts.map +1 -0
  33. package/dist/diagnostics/collector.js +95 -0
  34. package/dist/diagnostics/collector.js.map +1 -0
  35. package/dist/diagnostics/format.d.ts +21 -0
  36. package/dist/diagnostics/format.d.ts.map +1 -0
  37. package/dist/diagnostics/format.js +125 -0
  38. package/dist/diagnostics/format.js.map +1 -0
  39. package/dist/errors.d.ts +143 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +135 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/header/dates.d.ts +101 -0
  44. package/dist/header/dates.d.ts.map +1 -0
  45. package/dist/header/dates.js +378 -0
  46. package/dist/header/dates.js.map +1 -0
  47. package/dist/header/fields.d.ts +105 -0
  48. package/dist/header/fields.d.ts.map +1 -0
  49. package/dist/header/fields.js +363 -0
  50. package/dist/header/fields.js.map +1 -0
  51. package/dist/header/identification.d.ts +47 -0
  52. package/dist/header/identification.d.ts.map +1 -0
  53. package/dist/header/identification.js +156 -0
  54. package/dist/header/identification.js.map +1 -0
  55. package/dist/header/lookup.d.ts +33 -0
  56. package/dist/header/lookup.d.ts.map +1 -0
  57. package/dist/header/lookup.js +67 -0
  58. package/dist/header/lookup.js.map +1 -0
  59. package/dist/header/parse.d.ts +32 -0
  60. package/dist/header/parse.d.ts.map +1 -0
  61. package/dist/header/parse.js +319 -0
  62. package/dist/header/parse.js.map +1 -0
  63. package/dist/header/scale.d.ts +68 -0
  64. package/dist/header/scale.d.ts.map +1 -0
  65. package/dist/header/scale.js +154 -0
  66. package/dist/header/scale.js.map +1 -0
  67. package/dist/header/signals.d.ts +64 -0
  68. package/dist/header/signals.d.ts.map +1 -0
  69. package/dist/header/signals.js +374 -0
  70. package/dist/header/signals.js.map +1 -0
  71. package/dist/header/variant.d.ts +67 -0
  72. package/dist/header/variant.d.ts.map +1 -0
  73. package/dist/header/variant.js +180 -0
  74. package/dist/header/variant.js.map +1 -0
  75. package/dist/index.d.ts +46 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +59 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/inspect.d.ts +31 -0
  80. package/dist/inspect.d.ts.map +1 -0
  81. package/dist/inspect.js +165 -0
  82. package/dist/inspect.js.map +1 -0
  83. package/dist/io/blob.d.ts +14 -0
  84. package/dist/io/blob.d.ts.map +1 -0
  85. package/dist/io/blob.js +29 -0
  86. package/dist/io/blob.js.map +1 -0
  87. package/dist/io/bytes.d.ts +11 -0
  88. package/dist/io/bytes.d.ts.map +1 -0
  89. package/dist/io/bytes.js +26 -0
  90. package/dist/io/bytes.js.map +1 -0
  91. package/dist/io/cached.d.ts +19 -0
  92. package/dist/io/cached.d.ts.map +1 -0
  93. package/dist/io/cached.js +129 -0
  94. package/dist/io/cached.js.map +1 -0
  95. package/dist/io/http.d.ts +20 -0
  96. package/dist/io/http.d.ts.map +1 -0
  97. package/dist/io/http.js +225 -0
  98. package/dist/io/http.js.map +1 -0
  99. package/dist/io/read.d.ts +42 -0
  100. package/dist/io/read.d.ts.map +1 -0
  101. package/dist/io/read.js +119 -0
  102. package/dist/io/read.js.map +1 -0
  103. package/dist/io/source.d.ts +33 -0
  104. package/dist/io/source.d.ts.map +1 -0
  105. package/dist/io/source.js +72 -0
  106. package/dist/io/source.js.map +1 -0
  107. package/dist/node.d.ts +70 -0
  108. package/dist/node.d.ts.map +1 -0
  109. package/dist/node.js +109 -0
  110. package/dist/node.js.map +1 -0
  111. package/dist/record-index.d.ts +58 -0
  112. package/dist/record-index.d.ts.map +1 -0
  113. package/dist/record-index.js +290 -0
  114. package/dist/record-index.js.map +1 -0
  115. package/dist/recording.d.ts +57 -0
  116. package/dist/recording.d.ts.map +1 -0
  117. package/dist/recording.js +178 -0
  118. package/dist/recording.js.map +1 -0
  119. package/dist/tal/annotations.d.ts +36 -0
  120. package/dist/tal/annotations.d.ts.map +1 -0
  121. package/dist/tal/annotations.js +437 -0
  122. package/dist/tal/annotations.js.map +1 -0
  123. package/dist/tal/grammar.d.ts +111 -0
  124. package/dist/tal/grammar.d.ts.map +1 -0
  125. package/dist/tal/grammar.js +324 -0
  126. package/dist/tal/grammar.js.map +1 -0
  127. package/dist/tal/ticks.d.ts +69 -0
  128. package/dist/tal/ticks.d.ts.map +1 -0
  129. package/dist/tal/ticks.js +137 -0
  130. package/dist/tal/ticks.js.map +1 -0
  131. package/dist/time/segments.d.ts +33 -0
  132. package/dist/time/segments.d.ts.map +1 -0
  133. package/dist/time/segments.js +87 -0
  134. package/dist/time/segments.js.map +1 -0
  135. package/dist/time/timeline.d.ts +62 -0
  136. package/dist/time/timeline.d.ts.map +1 -0
  137. package/dist/time/timeline.js +227 -0
  138. package/dist/time/timeline.js.map +1 -0
  139. package/dist/time/window.d.ts +54 -0
  140. package/dist/time/window.d.ts.map +1 -0
  141. package/dist/time/window.js +232 -0
  142. package/dist/time/window.js.map +1 -0
  143. package/dist/types.d.ts +409 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +16 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/validate.d.ts +53 -0
  148. package/dist/validate.d.ts.map +1 -0
  149. package/dist/validate.js +483 -0
  150. package/dist/validate.js.map +1 -0
  151. package/package.json +77 -0
  152. package/src/bytes/latin1.ts +87 -0
  153. package/src/bytes/numbers.ts +115 -0
  154. package/src/bytes/view.ts +59 -0
  155. package/src/constants.ts +96 -0
  156. package/src/decode/digital.ts +258 -0
  157. package/src/decode/physical.ts +235 -0
  158. package/src/diagnostics/codes.ts +209 -0
  159. package/src/diagnostics/collector.ts +129 -0
  160. package/src/diagnostics/format.ts +143 -0
  161. package/src/errors.ts +208 -0
  162. package/src/header/dates.ts +453 -0
  163. package/src/header/fields.ts +455 -0
  164. package/src/header/identification.ts +196 -0
  165. package/src/header/lookup.ts +81 -0
  166. package/src/header/parse.ts +398 -0
  167. package/src/header/scale.ts +268 -0
  168. package/src/header/signals.ts +550 -0
  169. package/src/header/variant.ts +239 -0
  170. package/src/index.ts +151 -0
  171. package/src/inspect.ts +181 -0
  172. package/src/io/blob.ts +31 -0
  173. package/src/io/bytes.ts +29 -0
  174. package/src/io/cached.ts +136 -0
  175. package/src/io/http.ts +299 -0
  176. package/src/io/read.ts +152 -0
  177. package/src/io/source.ts +87 -0
  178. package/src/node.ts +138 -0
  179. package/src/record-index.ts +389 -0
  180. package/src/recording.ts +234 -0
  181. package/src/tal/annotations.ts +574 -0
  182. package/src/tal/grammar.ts +540 -0
  183. package/src/tal/ticks.ts +172 -0
  184. package/src/time/segments.ts +109 -0
  185. package/src/time/timeline.ts +290 -0
  186. package/src/time/window.ts +304 -0
  187. package/src/types.ts +490 -0
  188. 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"}