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,67 @@
1
+ /**
2
+ * Finding a signal by name.
3
+ *
4
+ * Layer 2. Two functions and a predicate, and the reason they exist rather than being left to
5
+ * the caller: deleting name lookup does not delete `signals.find(s => s.label === label)`, which
6
+ * silently returns the first of a duplicate pair. CHB-MIT ships `'T8-P8'` twice, and picking one
7
+ * of them by accident is how the wrong channel ends up in a paper.
8
+ *
9
+ * Matching is exact on the TRIMMED label and is case-sensitive. Nothing else is normalised: EDF
10
+ * labels are electrode names, `'Fp1'` and `'FP1'` are written by different systems, and edfcore
11
+ * has no montage vocabulary to decide they are the same thing.
12
+ */
13
+ import { trimEdfField } from '../bytes/latin1.js';
14
+ import { BDF_ANNOTATIONS_LABEL, EDF_ANNOTATIONS_LABEL } from '../constants.js';
15
+ import { EdfAmbiguousChannelError, EdfChannelNotFoundError } from '../errors.js';
16
+ /**
17
+ * True for the two reserved annotation labels, matched on the trimmed text and case-sensitively.
18
+ *
19
+ * Both are accepted for either family: the label identifies the channel's ROLE, and a BDF+ file
20
+ * written by an EDF+ library carries `'EDF Annotations'` — refusing to recognise it there would
21
+ * expose a text channel as if it held samples.
22
+ */
23
+ export function isAnnotationLabel(label) {
24
+ const trimmed = trimEdfField(label);
25
+ return trimmed === EDF_ANNOTATIONS_LABEL || trimmed === BDF_ANNOTATIONS_LABEL;
26
+ }
27
+ /** Every signal with this label, in signal order. Empty when none matches. */
28
+ export function findSignals(header, label) {
29
+ const wanted = trimEdfField(label);
30
+ return Object.freeze(header.signals.filter((signal) => signal.label === wanted));
31
+ }
32
+ function quoteLabels(header) {
33
+ return header.signals.map((signal) => JSON.stringify(signal.label)).join(', ');
34
+ }
35
+ /**
36
+ * One signal, by index or by label.
37
+ *
38
+ * A numeric selector indexes `header.signals` directly. A label that matches nothing throws
39
+ * `EdfChannelNotFoundError` carrying every available label; a label that matches more than one
40
+ * signal throws `EdfAmbiguousChannelError` carrying the indices, because there is no answer
41
+ * edfcore could return that would not be a guess.
42
+ */
43
+ export function getSignal(header, selector) {
44
+ if (typeof selector === 'number') {
45
+ const signal = header.signals[selector];
46
+ if (signal !== undefined)
47
+ return signal;
48
+ throw new EdfChannelNotFoundError(`signal index ${selector} is outside the ${header.signals.length} signals this file ` +
49
+ `declares. Labels, in signal order: ${quoteLabels(header)}. Next: pass an index in ` +
50
+ `0..${header.signals.length - 1}, or a label.`, { selector, availableLabels: header.signals.map((signal) => signal.label) });
51
+ }
52
+ const matches = findSignals(header, selector);
53
+ const first = matches[0];
54
+ if (first === undefined) {
55
+ throw new EdfChannelNotFoundError(`no signal is labelled ${JSON.stringify(trimEdfField(selector))} in this file. Labels, ` +
56
+ `in signal order: ${quoteLabels(header)}. Matching is exact on the trimmed label and ` +
57
+ 'is case-sensitive. Next: pass one of those labels, or select by index.', { selector, availableLabels: header.signals.map((signal) => signal.label) });
58
+ }
59
+ if (matches.length === 1)
60
+ return first;
61
+ const matchingIndices = matches.map((signal) => signal.index);
62
+ throw new EdfAmbiguousChannelError(`label ${JSON.stringify(trimEdfField(selector))} matches ${matches.length} signals ` +
63
+ `(indices ${matchingIndices.join(', ')}), so getSignal cannot choose one — returning the ` +
64
+ 'first is how the wrong channel ends up in a paper. Next: call findSignals() to get them ' +
65
+ 'all, or select by index.', { label: trimEdfField(selector), matchingIndices });
66
+ }
67
+ //# sourceMappingURL=lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lookup.js","sourceRoot":"","sources":["../../src/header/lookup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `parseHeader` — the orchestrator.
3
+ *
4
+ * Layer 2. Sole owner of header validation ORDER. Every check below is numbered, and the numbers
5
+ * are the contract: under `strict` the FIRST would-be diagnostic throws, so the order decides
6
+ * which error a broken file reports, and a test pins it. Moving a check is a behavioural change
7
+ * even when every individual check is unaltered.
8
+ *
9
+ * The order is not arbitrary. Each step establishes something the next one needs:
10
+ *
11
+ * 1. at least 256 bytes 8. the record size, hence the file geometry
12
+ * 2. the variant, hence sample width 9. the record count, which needs 8 and the source size
13
+ * 3. ns, before any ns-sized work 10. the EDF+ annotations requirement
14
+ * 4. the whole header is present 11. the text fields, which nothing else depends on
15
+ * 5. the declared header size (loses)
16
+ * 6. the record duration
17
+ * 7. the per-signal blocks
18
+ *
19
+ * `sourceByteLength` is required and positional because two of those steps genuinely need the
20
+ * size of the file the header came from: recovering `recordCount = -1`, and telling a truncated
21
+ * file from a complete one.
22
+ */
23
+ import type { EdfHeader, ParseOptions } from '../types.js';
24
+ /**
25
+ * Parse an EDF/BDF header.
26
+ *
27
+ * `headerBytes` must hold at least `256 * (ns + 1)` bytes; anything beyond that is ignored, so a
28
+ * caller that over-read is free to pass its whole buffer. `sourceByteLength` is the size of the
29
+ * file those bytes came from — pass `bytes.byteLength` for an in-memory file.
30
+ */
31
+ export declare function parseHeader(headerBytes: Uint8Array, sourceByteLength: number, options?: ParseOptions): EdfHeader;
32
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/header/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoK3D;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,UAAU,EACvB,gBAAgB,EAAE,MAAM,EACxB,OAAO,CAAC,EAAE,YAAY,GACrB,SAAS,CA6LX"}
@@ -0,0 +1,319 @@
1
+ /**
2
+ * `parseHeader` — the orchestrator.
3
+ *
4
+ * Layer 2. Sole owner of header validation ORDER. Every check below is numbered, and the numbers
5
+ * are the contract: under `strict` the FIRST would-be diagnostic throws, so the order decides
6
+ * which error a broken file reports, and a test pins it. Moving a check is a behavioural change
7
+ * even when every individual check is unaltered.
8
+ *
9
+ * The order is not arbitrary. Each step establishes something the next one needs:
10
+ *
11
+ * 1. at least 256 bytes 8. the record size, hence the file geometry
12
+ * 2. the variant, hence sample width 9. the record count, which needs 8 and the source size
13
+ * 3. ns, before any ns-sized work 10. the EDF+ annotations requirement
14
+ * 4. the whole header is present 11. the text fields, which nothing else depends on
15
+ * 5. the declared header size (loses)
16
+ * 6. the record duration
17
+ * 7. the per-signal blocks
18
+ *
19
+ * `sourceByteLength` is required and positional because two of those steps genuinely need the
20
+ * size of the file the header came from: recovering `recordCount = -1`, and telling a truncated
21
+ * file from a complete one.
22
+ */
23
+ import { trimEdfField } from '../bytes/latin1.js';
24
+ import { copyBytes } from '../bytes/view.js';
25
+ import { EDF_HEADER_BLOCK_BYTES, EDF_RECOMMENDED_MAX_RECORD_BYTES, HEADER_FIELDS, SIGNAL_FIELD_WIDTHS, } from '../constants.js';
26
+ import { DiagnosticSink, fatalError } from '../diagnostics/collector.js';
27
+ import { parseUnsignedTicks, secondsToTicks } from '../tal/ticks.js';
28
+ import { resolveStartTime } from './dates.js';
29
+ import { checkDeclaredHeaderByteLength, describeFixedField, fixedFieldSpecReference, parseDeclaredRecordCount, parseRecordDuration, parseSignalCount, readRawHeaderFields, reportNonAsciiHeaderFields, } from './fields.js';
30
+ import { parsePatientId, parseRecordingId } from './identification.js';
31
+ import { buildSignals, parseSignalHeaders, signalFieldOffset } from './signals.js';
32
+ import { detectVariant } from './variant.js';
33
+ /**
34
+ * How many data records the file actually contains.
35
+ *
36
+ * Four outcomes, and every one of them is reported: the count was unknown and was recovered from
37
+ * the source length; the file is shorter than the count claims; the file ends part-way through a
38
+ * record; the file carries bytes beyond the records it declares. Nothing is ever padded into
39
+ * existence — a record that is not entirely present is not a record.
40
+ */
41
+ function resolveRecordCount(input, sink) {
42
+ const { raw, declared, headerByteLength, recordByteLength, sourceByteLength } = input;
43
+ const availableDataBytes = Math.max(0, sourceByteLength - headerByteLength);
44
+ const wholeRecords = Math.floor(availableDataBytes / recordByteLength);
45
+ const partialBytes = availableDataBytes - wholeRecords * recordByteLength;
46
+ const description = describeFixedField('recordCount');
47
+ const specReference = fixedFieldSpecReference('recordCount');
48
+ const field = 'recordCount';
49
+ const byteOffset = HEADER_FIELDS.recordCount.offset;
50
+ const byteLength = HEADER_FIELDS.recordCount.length;
51
+ const reportPartialFinalRecord = () => {
52
+ if (partialBytes === 0)
53
+ return;
54
+ sink.report({
55
+ code: 'PARTIAL_FINAL_RECORD',
56
+ message: `the source ends ${partialBytes} bytes into data record ${wholeRecords}, which is ` +
57
+ `${recordByteLength} bytes long, so that record is incomplete. ${specReference}: every ` +
58
+ `data record in a file has the same size. Next: header.recordCount is ${wholeRecords} ` +
59
+ 'and only whole records are exposed — edfcore never zero-pads a record into existence, ' +
60
+ 'because the padding would decode as real samples.',
61
+ field: 'dataRecords',
62
+ byteOffset: headerByteLength + wholeRecords * recordByteLength,
63
+ byteLength: partialBytes,
64
+ raw,
65
+ expected: `${recordByteLength} bytes`,
66
+ actual: `${partialBytes} bytes`,
67
+ specReference,
68
+ });
69
+ };
70
+ // -1 is the sanctioned "the writer never closed the file" value; an unreadable field and any
71
+ // other negative count mean the same thing, and the same recovery is the truthful answer.
72
+ if (Number.isNaN(declared) || declared < 0) {
73
+ const declaredText = Number.isNaN(declared)
74
+ ? ', which is not a whole number of data records'
75
+ : `, i.e. ${declared}`;
76
+ sink.report({
77
+ code: 'RECORD_COUNT_RECOVERED',
78
+ message: `${description} is ${JSON.stringify(raw)}${declaredText}, so the count was recovered ` +
79
+ `from the source length: floor((${sourceByteLength} - ${headerByteLength}) / ` +
80
+ `${recordByteLength}) = ${wholeRecords} whole data records. ${specReference}: -1 means ` +
81
+ 'the writer never closed the file. Next: header.recordCount is ' +
82
+ `${wholeRecords} and header.recordCountSource is "sourceByteLength"; if that looks ` +
83
+ "wrong, check that parseHeader's sourceByteLength argument is the true file size.",
84
+ field,
85
+ byteOffset,
86
+ byteLength,
87
+ raw,
88
+ expected: 'a whole number of data records',
89
+ actual: String(declared),
90
+ specReference,
91
+ });
92
+ reportPartialFinalRecord();
93
+ return { recordCount: wholeRecords, recordCountSource: 'sourceByteLength' };
94
+ }
95
+ if (declared > wholeRecords) {
96
+ sink.report({
97
+ code: 'TRUNCATED_FILE',
98
+ message: `${description} is ${JSON.stringify(raw)}, i.e. ${declared} data records of ` +
99
+ `${recordByteLength} bytes, which needs ` +
100
+ `${headerByteLength + declared * recordByteLength} bytes; the source is ` +
101
+ `${sourceByteLength} bytes and holds ${wholeRecords} whole records. ${specReference}. ` +
102
+ `Next: header.recordCount is ${wholeRecords} and header.recordCountSource is ` +
103
+ '"sourceByteLength"; the missing records are not readable, and edfcore will not ' +
104
+ 'fabricate them.',
105
+ field,
106
+ byteOffset,
107
+ byteLength,
108
+ raw,
109
+ expected: `at most ${wholeRecords} data records`,
110
+ actual: String(declared),
111
+ specReference,
112
+ });
113
+ reportPartialFinalRecord();
114
+ return { recordCount: wholeRecords, recordCountSource: 'sourceByteLength' };
115
+ }
116
+ const extraBytes = availableDataBytes - declared * recordByteLength;
117
+ if (extraBytes > 0) {
118
+ if (declared === wholeRecords) {
119
+ reportPartialFinalRecord();
120
+ }
121
+ else {
122
+ sink.report({
123
+ code: 'TRAILING_BYTES',
124
+ message: `the source carries ${extraBytes} bytes beyond the ${declared} data records the ` +
125
+ `header declares, which end at byte ${headerByteLength + declared * recordByteLength} ` +
126
+ `of ${sourceByteLength}. ${specReference}. Next: those bytes are never decoded; ` +
127
+ 'header.recordCount stays at the declared count and header.dataByteLength counts ' +
128
+ 'only the declared records.',
129
+ field: 'dataRecords',
130
+ byteOffset: headerByteLength + declared * recordByteLength,
131
+ byteLength: extraBytes,
132
+ raw,
133
+ expected: `${declared * recordByteLength} data bytes`,
134
+ actual: `${availableDataBytes} data bytes`,
135
+ specReference,
136
+ });
137
+ }
138
+ }
139
+ return { recordCount: declared, recordCountSource: 'headerField' };
140
+ }
141
+ /**
142
+ * The record duration in exact 100 ns ticks.
143
+ *
144
+ * Taken from the DIGITS on disk rather than from the parsed float, so `'0.1'` is 1000000 ticks
145
+ * and not whatever `0.1 * 1e7` rounds to. `tal/ticks.ts` owns that conversion; it is listed a
146
+ * layer above this one but depends on nothing except the tick constant, and reimplementing exact
147
+ * decimal-to-tick parsing here to respect the table would be the worse trade.
148
+ *
149
+ * The float path is the fallback for the spellings the tick grammar does not admit — `'+1'`,
150
+ * `'1E3'` — which `parseRecordDuration` has already accepted and proved finite and non-negative.
151
+ */
152
+ function recordDurationTicksOf(raw, seconds) {
153
+ const exact = parseUnsignedTicks(trimEdfField(raw));
154
+ return exact.ok ? exact.ticks : secondsToTicks(seconds);
155
+ }
156
+ /**
157
+ * Parse an EDF/BDF header.
158
+ *
159
+ * `headerBytes` must hold at least `256 * (ns + 1)` bytes; anything beyond that is ignored, so a
160
+ * caller that over-read is free to pass its whole buffer. `sourceByteLength` is the size of the
161
+ * file those bytes came from — pass `bytes.byteLength` for an in-memory file.
162
+ */
163
+ export function parseHeader(headerBytes, sourceByteLength, options) {
164
+ // A caller bug, not a file defect, so it is a plain RangeError: EdfFormatError would claim the
165
+ // bytes are wrong when what is wrong is the number describing them.
166
+ if (!Number.isSafeInteger(sourceByteLength) || sourceByteLength < 0) {
167
+ throw new RangeError(`parseHeader(): sourceByteLength must be a non-negative safe integer, received ` +
168
+ `${sourceByteLength}. Next: pass the byte length of the whole file — bytes.byteLength ` +
169
+ 'for an in-memory file, or source.byteLength for a ByteSource.');
170
+ }
171
+ const sink = new DiagnosticSink(options);
172
+ // ---- 1. The fixed header must be present at all. --------------------------------------
173
+ if (headerBytes.length < EDF_HEADER_BLOCK_BYTES) {
174
+ throw fatalError({
175
+ code: 'SOURCE_TOO_SMALL',
176
+ message: `the header is ${headerBytes.length} bytes, but every EDF and BDF file begins with a ` +
177
+ `${EDF_HEADER_BLOCK_BYTES}-byte fixed header. EDF specification, header record bytes ` +
178
+ '0-255. Next: check that the whole file reached edfcore — an empty file, a truncated ' +
179
+ 'download and a directory read all land here.',
180
+ field: 'header',
181
+ byteOffset: 0,
182
+ byteLength: headerBytes.length,
183
+ expected: `at least ${EDF_HEADER_BLOCK_BYTES} bytes`,
184
+ actual: `${headerBytes.length} bytes`,
185
+ specReference: 'EDF specification, header record bytes 0-255',
186
+ });
187
+ }
188
+ const raw = readRawHeaderFields(headerBytes);
189
+ // ---- 2. Version block and reserved marker: the family, the sample width, the dialect. ----
190
+ const variant = detectVariant(headerBytes, sink);
191
+ // ---- 3. ns, before any ns-sized allocation and before trusting the field at offset 184. --
192
+ const signalCount = parseSignalCount(raw.signalCount, sink);
193
+ // ---- 4. The whole header must be present, now that its size is known. -------------------
194
+ const headerByteLength = EDF_HEADER_BLOCK_BYTES * (signalCount + 1);
195
+ if (headerBytes.length < headerByteLength) {
196
+ throw fatalError({
197
+ code: 'SOURCE_TOO_SMALL',
198
+ message: `the header of a file declaring ${signalCount} signals is ${EDF_HEADER_BLOCK_BYTES} * ` +
199
+ `(${signalCount} + 1) = ${headerByteLength} bytes, but only ${headerBytes.length} ` +
200
+ 'bytes are available. EDF specification, header record. Next: read ' +
201
+ `${headerByteLength} bytes from offset 0 and parse those; if the file itself is that ` +
202
+ 'short, the signal count at offset 252 is not the one the file was written with.',
203
+ field: 'header',
204
+ byteOffset: 0,
205
+ byteLength: headerBytes.length,
206
+ expected: `${headerByteLength} bytes`,
207
+ actual: `${headerBytes.length} bytes`,
208
+ specReference: 'EDF specification, header record bytes 184-191 (number of bytes in header)',
209
+ });
210
+ }
211
+ // ---- 5. The declared header size. The computed one always wins. -------------------------
212
+ const declaredHeaderByteLength = checkDeclaredHeaderByteLength(raw.headerByteLength, headerByteLength, sink);
213
+ // ---- 6. The record duration. May be fractional, and may legitimately be 0. --------------
214
+ const recordDurationSeconds = parseRecordDuration(raw.recordDuration, sink);
215
+ const recordDurationTicks = recordDurationTicksOf(raw.recordDuration, recordDurationSeconds);
216
+ // ---- 7. The field-major per-signal blocks. ----------------------------------------------
217
+ const parsed = parseSignalHeaders({ headerBytes, signalCount, variant, recordDurationSeconds }, sink);
218
+ const recordByteLength = parsed.recordByteLength;
219
+ // ---- 8. The record size, which every data offset in the file steps by. ------------------
220
+ if (recordByteLength === 0) {
221
+ const samplesBlockOffset = signalFieldOffset('samplesPerRecord', signalCount, 0);
222
+ throw fatalError({
223
+ code: 'RECORD_SIZE_ZERO',
224
+ message: `every one of the ${signalCount} signals declares 0 samples per data record, so a data ` +
225
+ 'record is 0 bytes long and the file has no record size to step by — record N and ' +
226
+ 'record N+1 would begin at the same byte. EDF specification, data records. Next: the ' +
227
+ `per-signal sample counts live at offset ${samplesBlockOffset}, ` +
228
+ `${SIGNAL_FIELD_WIDTHS.samplesPerRecord} bytes per signal; header.raw keeps them as ` +
229
+ 'written.',
230
+ field: 'samplesPerRecord',
231
+ byteOffset: samplesBlockOffset,
232
+ byteLength: signalCount * SIGNAL_FIELD_WIDTHS.samplesPerRecord,
233
+ expected: 'at least one signal with a non-zero number of samples per record',
234
+ actual: '0 bytes per data record',
235
+ specReference: 'EDF specification, data records',
236
+ });
237
+ }
238
+ if (recordByteLength > EDF_RECOMMENDED_MAX_RECORD_BYTES) {
239
+ sink.report({
240
+ code: 'RECORD_SIZE_ABOVE_RECOMMENDED',
241
+ message: `a data record is ${recordByteLength} bytes (${variant.bytesPerSample} bytes per ` +
242
+ `sample x ${recordByteLength / variant.bytesPerSample} samples), above the ` +
243
+ `${EDF_RECOMMENDED_MAX_RECORD_BYTES}-byte record size the EDF specification ` +
244
+ 'recommends. EDF specification, data records (recommended maximum record size). Next: ' +
245
+ 'nothing — the file is read normally, but reads are record-aligned, so this is also ' +
246
+ 'the smallest amount of data any read can return.',
247
+ field: 'recordByteLength',
248
+ expected: `at most ${EDF_RECOMMENDED_MAX_RECORD_BYTES} bytes`,
249
+ actual: `${recordByteLength} bytes`,
250
+ specReference: 'EDF specification, data records',
251
+ });
252
+ }
253
+ // ---- 9. The record count, recovered or verified against the real file size. -------------
254
+ const declaredRecordCount = parseDeclaredRecordCount(raw.recordCount, sink);
255
+ const { recordCount, recordCountSource } = resolveRecordCount({
256
+ raw: raw.recordCount,
257
+ declared: declaredRecordCount,
258
+ headerByteLength,
259
+ recordByteLength,
260
+ sourceByteLength,
261
+ }, sink);
262
+ // ---- 10. EDF+ without an annotations signal has no per-record timing to report. ---------
263
+ if (variant.isPlus && parsed.annotationSignalIndices.length === 0) {
264
+ throw fatalError({
265
+ code: 'EDFPLUS_WITHOUT_ANNOTATION_SIGNAL',
266
+ message: `the reserved field at offset ${HEADER_FIELDS.reserved.offset} declares ` +
267
+ `${JSON.stringify(variant.reservedMarker ?? '')} but no signal is labelled ` +
268
+ `${JSON.stringify(variant.annotationsLabel)}. EDF+ specification 2.2.4: every data ` +
269
+ 'record of an EDF+ file carries a timekeeping TAL in an annotations signal, and that ' +
270
+ 'is the only place a record start time exists. Without it every time edfcore reported ' +
271
+ 'would be invented. Next: read the samples as plain ' +
272
+ `${variant.family} by clearing the EDF+ marker in the reserved field, or find the ` +
273
+ 'writer that dropped the annotations channel.',
274
+ field: 'reserved',
275
+ byteOffset: HEADER_FIELDS.reserved.offset,
276
+ byteLength: HEADER_FIELDS.reserved.length,
277
+ raw: raw.reserved,
278
+ expected: `a signal labelled ${JSON.stringify(variant.annotationsLabel)}`,
279
+ actual: `${signalCount} signals, none of them annotations`,
280
+ specReference: 'EDF+ specification 2.2.4 (the "EDF Annotations" signal)',
281
+ });
282
+ }
283
+ // ---- 11. The text fields. Nothing above depends on them. --------------------------------
284
+ reportNonAsciiHeaderFields(headerBytes, sink);
285
+ const patient = parsePatientId(raw.patientId, { edfPlus: variant.isPlus }, sink);
286
+ const recording = parseRecordingId(raw.recordingId, { edfPlus: variant.isPlus }, sink);
287
+ const startTime = resolveStartTime({
288
+ rawStartDate: raw.startDate,
289
+ rawStartTime: raw.startTime,
290
+ recordingIdDate: recording.startDate,
291
+ }, sink);
292
+ return {
293
+ variant: variant.variant,
294
+ continuity: variant.continuity,
295
+ bytesPerSample: variant.bytesPerSample,
296
+ headerByteLength,
297
+ declaredHeaderByteLength,
298
+ recordByteLength,
299
+ dataByteLength: recordCount * recordByteLength,
300
+ recordDurationSeconds,
301
+ recordDurationTicks,
302
+ recordCount,
303
+ declaredRecordCount,
304
+ recordCountSource,
305
+ startTime,
306
+ patient,
307
+ recording,
308
+ signals: buildSignals(parsed.signals, recordCount),
309
+ dataSignalIndices: parsed.dataSignalIndices,
310
+ annotationSignalIndices: parsed.annotationSignalIndices,
311
+ reserved: raw.reserved,
312
+ raw,
313
+ // A copy, not a view: the caller owns the buffer it read into and is free to reuse it, and
314
+ // a header that quietly changed under a hexdump would be worse than no hexdump.
315
+ rawBytes: copyBytes(headerBytes, 0, headerByteLength),
316
+ diagnostics: sink.diagnostics,
317
+ };
318
+ }
319
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/header/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Digital-to-physical scale, and the decision to refuse one.
3
+ *
4
+ * Layer 2. Sole owner of `EdfScale` construction and of the four conditions under which a
5
+ * signal gets `scale: undefined` instead of a fabricated gain.
6
+ *
7
+ * The expression is EDFlib's, verbatim:
8
+ *
9
+ * bitValue = (physicalMaximum - physicalMinimum) / (digitalMaximum - digitalMinimum)
10
+ * offset = physicalMaximum / bitValue - digitalMaximum
11
+ * physical = bitValue * (offset + digital)
12
+ *
13
+ * The textbook `physicalMinimum + (digital - digitalMinimum) * gain` form is numerically
14
+ * *better* and is deliberately not used: it shifts up to ~45% of samples by one ULP on
15
+ * asymmetric ranges, which would forfeit float64 bit-parity with pyEDFlib for a divergence ten
16
+ * orders of magnitude below the quantisation floor. Do not "simplify" this.
17
+ *
18
+ * `physicalMinimum > physicalMaximum` is legal — it is how a negative amplifier gain is
19
+ * written — and produces a negative `bitValue`. The two are never swapped: a silent polarity
20
+ * flip is a clinically wrong result that looks perfectly normal.
21
+ */
22
+ import type { DiagnosticSink } from '../diagnostics/collector.js';
23
+ import type { EdfRawSignalFields, EdfScale } from '../types.js';
24
+ /** Byte offsets of the per-signal fields a scaling diagnostic points at. */
25
+ export interface ScaleFieldOffsets {
26
+ readonly physicalDimension: number;
27
+ readonly physicalMinimum: number;
28
+ readonly physicalMaximum: number;
29
+ readonly digitalMinimum: number;
30
+ readonly digitalMaximum: number;
31
+ }
32
+ export interface ScaleInput {
33
+ readonly signalIndex: number;
34
+ /** Trimmed label, for the message. */
35
+ readonly label: string;
36
+ /** Physical dimension exactly as written; `'Filtered'` is refused after trimming. */
37
+ readonly physicalDimension: string;
38
+ readonly physicalMinimum: number;
39
+ readonly physicalMaximum: number;
40
+ readonly digitalMinimum: number;
41
+ readonly digitalMaximum: number;
42
+ /** The raw per-signal field text, so a message can quote the bytes as written. */
43
+ readonly raw: EdfRawSignalFields | undefined;
44
+ /** Where those fields live in the header. */
45
+ readonly byteOffsets: ScaleFieldOffsets | undefined;
46
+ }
47
+ /**
48
+ * Collapse the encodings of micro to `'u'` so units can be compared.
49
+ *
50
+ * For comparison ONLY: `signal.physicalDimension` stays exactly as the file wrote it. Nothing
51
+ * else is touched — case is meaningful (`mV` is not `MV`), and edfcore does not normalise units
52
+ * to SI volts.
53
+ */
54
+ export declare function normaliseUnit(physicalDimension: string): string;
55
+ /**
56
+ * The scale for one signal, or `undefined` when edfcore refuses to invent one.
57
+ *
58
+ * The refusals are checked in this fixed order: degenerate digital range, degenerate physical
59
+ * range, inverted digital range, log-transformed channel, and finally a derived gain that is not
60
+ * a usable float64 number. Each is deferred-fatal — `decodeDigital` keeps working on the signal
61
+ * and `toPhysical` throws `EdfScalingError`.
62
+ *
63
+ * A non-finite input returns `undefined` without a diagnostic: the only way to get here with
64
+ * one is a numeric field that failed its grammar, which the caller has already reported
65
+ * against the field itself.
66
+ */
67
+ export declare function buildScale(input: ScaleInput, sink: DiagnosticSink): EdfScale | undefined;
68
+ //# sourceMappingURL=scale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../src/header/scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAYhE,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qFAAqF;IACrF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAE/D;AA6CD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,QAAQ,GAAG,SAAS,CA4IxF"}