edfcore 0.2.7 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ alone does not tell you whether you were affected.
6
6
  edfcore is pre-1.0. Patch releases have carried behaviour changes where the old behaviour was a
7
7
  defect; those are called out below.
8
8
 
9
+ ## 0.2.8
10
+
11
+ - **Added** `mergeChunks(chunks)`. `readWindow` splits at every discontinuity, and joining the
12
+ pieces by hand is where the gap gets lost: concatenating two runs five minutes apart dates every
13
+ sample after the join five minutes early, with nothing in the result to say so. This refuses,
14
+ and refuses a chunk already narrowed by `trimToWindow` too — that one is invisible to a record
15
+ adjacency check.
16
+ - **Fixed** the 200,000-record call-stack test timing out under load. It ran within a few hundred
17
+ milliseconds of vitest's 5 s default on its own, so it failed intermittently in a full run and
18
+ the failure read as a regression in code it does not touch.
19
+
9
20
  ## 0.2.7
10
21
 
11
22
  - **Added** `edfcore gaps <file>`. Runs a full scan rather than reading the two probes `openEdf`
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Joining chunks that a read returned as several.
3
+ *
4
+ * Layer 7, and pure: nothing here reads. `readWindow` splits at every discontinuity, so a window
5
+ * over an EDF+D file comes back as one chunk per contiguous run. Code that then wants ONE array —
6
+ * a filter, an FFT, a CSV writer — has to join them, and joining is where the gap gets lost.
7
+ *
8
+ * Concatenating two runs separated by five minutes produces an array in which sample `i` and
9
+ * sample `i + 1` are five minutes apart. Every time derived from an index past that point is
10
+ * wrong by five minutes, and nothing in the result says so. `mergeChunks` refuses instead. A
11
+ * caller who genuinely wants the samples end to end can concatenate them in three lines and own
12
+ * the consequence; what they should not get is a helper that hides it.
13
+ *
14
+ * The refusals are caller mistakes, not file defects, so they are plain `RangeError`s — the same
15
+ * convention every option check in the package follows.
16
+ */
17
+ import type { EdfChunk } from './types.js';
18
+ /**
19
+ * One chunk covering every input chunk, or a `RangeError` explaining why they do not join.
20
+ *
21
+ * Accepts only chunks that are adjacent, in order, gapless, and read with the same signals in the
22
+ * same order. A single chunk is returned as-is, so the common continuous-file case costs nothing.
23
+ *
24
+ * The samples are copied, so the result holds a second copy of the data the inputs already hold.
25
+ * That is unavoidable — `Int32Array`s are not splices of one buffer — and it is why this is a
26
+ * separate call rather than something `readWindow` does on the way out.
27
+ */
28
+ export declare function mergeChunks(chunks: readonly EdfChunk[]): EdfChunk;
29
+ //# sourceMappingURL=chunks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../src/chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAiC,MAAM,YAAY,CAAC;AA+D1E;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,GAAG,QAAQ,CAwEjE"}
package/dist/chunks.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Joining chunks that a read returned as several.
3
+ *
4
+ * Layer 7, and pure: nothing here reads. `readWindow` splits at every discontinuity, so a window
5
+ * over an EDF+D file comes back as one chunk per contiguous run. Code that then wants ONE array —
6
+ * a filter, an FFT, a CSV writer — has to join them, and joining is where the gap gets lost.
7
+ *
8
+ * Concatenating two runs separated by five minutes produces an array in which sample `i` and
9
+ * sample `i + 1` are five minutes apart. Every time derived from an index past that point is
10
+ * wrong by five minutes, and nothing in the result says so. `mergeChunks` refuses instead. A
11
+ * caller who genuinely wants the samples end to end can concatenate them in three lines and own
12
+ * the consequence; what they should not get is a helper that hides it.
13
+ *
14
+ * The refusals are caller mistakes, not file defects, so they are plain `RangeError`s — the same
15
+ * convention every option check in the package follows.
16
+ */
17
+ import { appendDiagnostics } from './diagnostics/collector.js';
18
+ /** Reads as one line at the call site, and keeps the `chunks[i]` non-null assertions out of it. */
19
+ function at(chunks, index) {
20
+ const chunk = chunks[index];
21
+ if (chunk === undefined)
22
+ throw new RangeError(`mergeChunks: no chunk at ${index}.`);
23
+ return chunk;
24
+ }
25
+ /**
26
+ * Everything that makes two chunks joinable, checked before a byte is allocated.
27
+ *
28
+ * The record-adjacency test is the obvious one. The per-signal sample-index test is the one that
29
+ * earns its place: `trimToWindow` narrows a chunk on each signal's own grid, so two chunks can
30
+ * still be record-adjacent after a trim has removed the samples between them. Comparing
31
+ * `firstSampleIndex` against the previous chunk's end catches exactly that, per signal, which is
32
+ * the granularity at which it actually happens.
33
+ */
34
+ function assertJoinable(previous, next, index) {
35
+ if (next.precededByGap !== undefined) {
36
+ throw new RangeError(`mergeChunks: chunk ${index} is preceded by a gap of ${next.precededByGap.durationSeconds} s. ` +
37
+ 'Concatenating across it would put two samples that are seconds apart next to each other ' +
38
+ 'in one array, and every time computed from an index after the join would be wrong by the ' +
39
+ 'gap. Merge each contiguous run separately.');
40
+ }
41
+ const expectedStart = previous.records.start + previous.records.count;
42
+ if (next.records.start !== expectedStart) {
43
+ throw new RangeError(`mergeChunks: chunk ${index} starts at record ${next.records.start}, but the chunk before ` +
44
+ `it ends at ${expectedStart}. Chunks must be adjacent and in order.`);
45
+ }
46
+ if (next.signals.length !== previous.signals.length) {
47
+ throw new RangeError(`mergeChunks: chunk ${index} carries ${next.signals.length} signal(s), the chunk before it ` +
48
+ `${previous.signals.length}. Every chunk must have been read with the same signal selection.`);
49
+ }
50
+ for (let i = 0; i < next.signals.length; i += 1) {
51
+ const before = previous.signals[i];
52
+ const after = next.signals[i];
53
+ if (after.signalIndex !== before.signalIndex) {
54
+ throw new RangeError(`mergeChunks: chunk ${index} has signal ${after.signalIndex} in position ${i}, the chunk ` +
55
+ `before it signal ${before.signalIndex}. The selection must be in the same order too.`);
56
+ }
57
+ const expectedSample = before.firstSampleIndex + before.sampleCount;
58
+ if (after.firstSampleIndex !== expectedSample) {
59
+ throw new RangeError(`mergeChunks: signal ${after.signalIndex} of chunk ${index} starts at sample ` +
60
+ `${after.firstSampleIndex}, but the chunk before it ends at ${expectedSample}. ` +
61
+ 'A trimmed chunk cannot be merged with the one after it — trim after merging, not before.');
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * One chunk covering every input chunk, or a `RangeError` explaining why they do not join.
67
+ *
68
+ * Accepts only chunks that are adjacent, in order, gapless, and read with the same signals in the
69
+ * same order. A single chunk is returned as-is, so the common continuous-file case costs nothing.
70
+ *
71
+ * The samples are copied, so the result holds a second copy of the data the inputs already hold.
72
+ * That is unavoidable — `Int32Array`s are not splices of one buffer — and it is why this is a
73
+ * separate call rather than something `readWindow` does on the way out.
74
+ */
75
+ export function mergeChunks(chunks) {
76
+ if (chunks.length === 0) {
77
+ throw new RangeError('mergeChunks: nothing to merge. `readWindow` returns [] for a window that lands past the ' +
78
+ 'end of the recording, so check the length before merging.');
79
+ }
80
+ const first = at(chunks, 0);
81
+ if (chunks.length === 1)
82
+ return first;
83
+ for (let i = 1; i < chunks.length; i += 1) {
84
+ assertJoinable(at(chunks, i - 1), at(chunks, i), i);
85
+ }
86
+ const last = at(chunks, chunks.length - 1);
87
+ const signals = first.signals.map((firstSignal, position) => {
88
+ let total = 0;
89
+ let outOfRange = 0;
90
+ for (const chunk of chunks) {
91
+ const signal = chunk.signals[position];
92
+ total += signal.sampleCount;
93
+ outOfRange += signal.outOfDigitalRangeCount;
94
+ }
95
+ const digital = new Int32Array(total);
96
+ let written = 0;
97
+ for (const chunk of chunks) {
98
+ const signal = chunk.signals[position];
99
+ // `digital` may be longer than `sampleCount` — the decoder is allowed to hand back a buffer
100
+ // it sized for whole records. `sampleCount` is the truth, so the subarray is taken from it.
101
+ digital.set(signal.digital.subarray(0, signal.sampleCount), written);
102
+ written += signal.sampleCount;
103
+ }
104
+ return {
105
+ signalIndex: firstSignal.signalIndex,
106
+ sampleCount: total,
107
+ digital,
108
+ firstSampleIndex: firstSignal.firstSampleIndex,
109
+ startSeconds: firstSignal.startSeconds,
110
+ outOfDigitalRangeCount: outOfRange,
111
+ };
112
+ });
113
+ let byteLength = 0;
114
+ const diagnostics = [];
115
+ for (const chunk of chunks) {
116
+ byteLength += chunk.byteLength;
117
+ // Not `push(...chunk.diagnostics)`: a scan over a damaged annotation section reports one
118
+ // diagnostic per record, and the spread blows the call stack past ~125,000 of them (0.1.6).
119
+ appendDiagnostics(diagnostics, chunk.diagnostics);
120
+ }
121
+ return {
122
+ records: {
123
+ start: first.records.start,
124
+ count: last.records.start + last.records.count - first.records.start,
125
+ },
126
+ startSeconds: first.startSeconds,
127
+ // Two float operations against the ends, not a sum of N durations: adding the durations up
128
+ // accumulates rounding once per chunk, and the run is contiguous so the ends are the truth.
129
+ durationSeconds: last.startSeconds + last.durationSeconds - first.startSeconds,
130
+ byteOffset: first.byteOffset,
131
+ byteLength,
132
+ signals: Object.freeze(signals),
133
+ // The gap BEFORE the whole run survives the merge. It describes what precedes the first
134
+ // chunk, which is still what precedes the merged one.
135
+ precededByGap: first.precededByGap,
136
+ diagnostics: Object.freeze(diagnostics),
137
+ };
138
+ }
139
+ //# sourceMappingURL=chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.js","sourceRoot":"","sources":["../src/chunks.ts"],"names":[],"mappings":"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"}
@@ -109,5 +109,5 @@ export declare const SIGNAL_FIELD_BLOCK_OFFSETS: {
109
109
  readonly reserved: 224;
110
110
  };
111
111
  /** Published package version. Kept in sync with package.json by a test. */
112
- export declare const VERSION = "0.2.7";
112
+ export declare const VERSION = "0.2.8";
113
113
  //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -79,5 +79,5 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
79
79
  reserved: 224,
80
80
  };
81
81
  /** Published package version. Kept in sync with package.json by a test. */
82
- export const VERSION = '0.2.7';
82
+ export const VERSION = '0.2.8';
83
83
  //# sourceMappingURL=constants.js.map
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export { readHeader, readRecordBytes } from './io/read.js';
43
43
  export { buildRecordIndex, buildTimeline, contiguityOf } from './record-index.js';
44
44
  export { annotationsAt, countAnnotationsByText, filterAnnotationsByText, filterAnnotationsByTime, } from './annotations-query.js';
45
45
  export { decodeStatusWord, getStatusSignal, readTriggers } from './biosemi.js';
46
+ export { mergeChunks } from './chunks.js';
46
47
  export { envelopeOfSamples, readEnvelope, readEnvelopeAtResolution, toPhysicalEnvelope, } from './envelope.js';
47
48
  export { formatHeader } from './format-header.js';
48
49
  export { inspectEdf } from './inspect.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAOH,mFAAmF;AACnF,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,YAAY,EACV,eAAe,EACf,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,wBAAwB,EACxB,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,EACN,SAAS,EACT,aAAa,EACb,sBAAsB,EACtB,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,WAAW,EACX,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,WAAW,EACX,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,YAAY,CAAC;AAUpB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAMrB,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQnE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAM1C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAMlF,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ export { buildRecordIndex, buildTimeline, contiguityOf } from './record-index.js
56
56
  // ===========================================================================
57
57
  export { annotationsAt, countAnnotationsByText, filterAnnotationsByText, filterAnnotationsByTime, } from './annotations-query.js';
58
58
  export { decodeStatusWord, getStatusSignal, readTriggers } from './biosemi.js';
59
+ export { mergeChunks } from './chunks.js';
59
60
  export { envelopeOfSamples, readEnvelope, readEnvelopeAtResolution, toPhysicalEnvelope, } from './envelope.js';
60
61
  export { formatHeader } from './format-header.js';
61
62
  export { inspectEdf } from './inspect.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA0EH,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,8EAA8E;AAC9E,2EAA2E;AAC3E,2CAA2C;AAC3C,8EAA8E;AAE9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElF,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA0EH,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,8EAA8E;AAC9E,2EAA2E;AAC3E,2CAA2C;AAC3C,8EAA8E;AAE9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElF,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Modern, typed, zero-dependency reader for EDF, EDF+, BDF and BDF+ biosignal files. Works in browsers and Node with true random access.",
5
5
  "keywords": [
6
6
  "edf",
package/src/chunks.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Joining chunks that a read returned as several.
3
+ *
4
+ * Layer 7, and pure: nothing here reads. `readWindow` splits at every discontinuity, so a window
5
+ * over an EDF+D file comes back as one chunk per contiguous run. Code that then wants ONE array —
6
+ * a filter, an FFT, a CSV writer — has to join them, and joining is where the gap gets lost.
7
+ *
8
+ * Concatenating two runs separated by five minutes produces an array in which sample `i` and
9
+ * sample `i + 1` are five minutes apart. Every time derived from an index past that point is
10
+ * wrong by five minutes, and nothing in the result says so. `mergeChunks` refuses instead. A
11
+ * caller who genuinely wants the samples end to end can concatenate them in three lines and own
12
+ * the consequence; what they should not get is a helper that hides it.
13
+ *
14
+ * The refusals are caller mistakes, not file defects, so they are plain `RangeError`s — the same
15
+ * convention every option check in the package follows.
16
+ */
17
+
18
+ import { appendDiagnostics } from './diagnostics/collector.js';
19
+ import type { EdfChunk, EdfChunkSignal, EdfDiagnostic } from './types.js';
20
+
21
+ /** Reads as one line at the call site, and keeps the `chunks[i]` non-null assertions out of it. */
22
+ function at(chunks: readonly EdfChunk[], index: number): EdfChunk {
23
+ const chunk = chunks[index];
24
+ if (chunk === undefined) throw new RangeError(`mergeChunks: no chunk at ${index}.`);
25
+ return chunk;
26
+ }
27
+
28
+ /**
29
+ * Everything that makes two chunks joinable, checked before a byte is allocated.
30
+ *
31
+ * The record-adjacency test is the obvious one. The per-signal sample-index test is the one that
32
+ * earns its place: `trimToWindow` narrows a chunk on each signal's own grid, so two chunks can
33
+ * still be record-adjacent after a trim has removed the samples between them. Comparing
34
+ * `firstSampleIndex` against the previous chunk's end catches exactly that, per signal, which is
35
+ * the granularity at which it actually happens.
36
+ */
37
+ function assertJoinable(previous: EdfChunk, next: EdfChunk, index: number): void {
38
+ if (next.precededByGap !== undefined) {
39
+ throw new RangeError(
40
+ `mergeChunks: chunk ${index} is preceded by a gap of ${next.precededByGap.durationSeconds} s. ` +
41
+ 'Concatenating across it would put two samples that are seconds apart next to each other ' +
42
+ 'in one array, and every time computed from an index after the join would be wrong by the ' +
43
+ 'gap. Merge each contiguous run separately.',
44
+ );
45
+ }
46
+
47
+ const expectedStart = previous.records.start + previous.records.count;
48
+ if (next.records.start !== expectedStart) {
49
+ throw new RangeError(
50
+ `mergeChunks: chunk ${index} starts at record ${next.records.start}, but the chunk before ` +
51
+ `it ends at ${expectedStart}. Chunks must be adjacent and in order.`,
52
+ );
53
+ }
54
+
55
+ if (next.signals.length !== previous.signals.length) {
56
+ throw new RangeError(
57
+ `mergeChunks: chunk ${index} carries ${next.signals.length} signal(s), the chunk before it ` +
58
+ `${previous.signals.length}. Every chunk must have been read with the same signal selection.`,
59
+ );
60
+ }
61
+
62
+ for (let i = 0; i < next.signals.length; i += 1) {
63
+ const before = previous.signals[i] as EdfChunkSignal;
64
+ const after = next.signals[i] as EdfChunkSignal;
65
+ if (after.signalIndex !== before.signalIndex) {
66
+ throw new RangeError(
67
+ `mergeChunks: chunk ${index} has signal ${after.signalIndex} in position ${i}, the chunk ` +
68
+ `before it signal ${before.signalIndex}. The selection must be in the same order too.`,
69
+ );
70
+ }
71
+ const expectedSample = before.firstSampleIndex + before.sampleCount;
72
+ if (after.firstSampleIndex !== expectedSample) {
73
+ throw new RangeError(
74
+ `mergeChunks: signal ${after.signalIndex} of chunk ${index} starts at sample ` +
75
+ `${after.firstSampleIndex}, but the chunk before it ends at ${expectedSample}. ` +
76
+ 'A trimmed chunk cannot be merged with the one after it — trim after merging, not before.',
77
+ );
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * One chunk covering every input chunk, or a `RangeError` explaining why they do not join.
84
+ *
85
+ * Accepts only chunks that are adjacent, in order, gapless, and read with the same signals in the
86
+ * same order. A single chunk is returned as-is, so the common continuous-file case costs nothing.
87
+ *
88
+ * The samples are copied, so the result holds a second copy of the data the inputs already hold.
89
+ * That is unavoidable — `Int32Array`s are not splices of one buffer — and it is why this is a
90
+ * separate call rather than something `readWindow` does on the way out.
91
+ */
92
+ export function mergeChunks(chunks: readonly EdfChunk[]): EdfChunk {
93
+ if (chunks.length === 0) {
94
+ throw new RangeError(
95
+ 'mergeChunks: nothing to merge. `readWindow` returns [] for a window that lands past the ' +
96
+ 'end of the recording, so check the length before merging.',
97
+ );
98
+ }
99
+
100
+ const first = at(chunks, 0);
101
+ if (chunks.length === 1) return first;
102
+
103
+ for (let i = 1; i < chunks.length; i += 1) {
104
+ assertJoinable(at(chunks, i - 1), at(chunks, i), i);
105
+ }
106
+
107
+ const last = at(chunks, chunks.length - 1);
108
+
109
+ const signals = first.signals.map((firstSignal, position) => {
110
+ let total = 0;
111
+ let outOfRange = 0;
112
+ for (const chunk of chunks) {
113
+ const signal = chunk.signals[position] as EdfChunkSignal;
114
+ total += signal.sampleCount;
115
+ outOfRange += signal.outOfDigitalRangeCount;
116
+ }
117
+
118
+ const digital = new Int32Array(total);
119
+ let written = 0;
120
+ for (const chunk of chunks) {
121
+ const signal = chunk.signals[position] as EdfChunkSignal;
122
+ // `digital` may be longer than `sampleCount` — the decoder is allowed to hand back a buffer
123
+ // it sized for whole records. `sampleCount` is the truth, so the subarray is taken from it.
124
+ digital.set(signal.digital.subarray(0, signal.sampleCount), written);
125
+ written += signal.sampleCount;
126
+ }
127
+
128
+ return {
129
+ signalIndex: firstSignal.signalIndex,
130
+ sampleCount: total,
131
+ digital,
132
+ firstSampleIndex: firstSignal.firstSampleIndex,
133
+ startSeconds: firstSignal.startSeconds,
134
+ outOfDigitalRangeCount: outOfRange,
135
+ } satisfies EdfChunkSignal;
136
+ });
137
+
138
+ let byteLength = 0;
139
+ const diagnostics: EdfDiagnostic[] = [];
140
+ for (const chunk of chunks) {
141
+ byteLength += chunk.byteLength;
142
+ // Not `push(...chunk.diagnostics)`: a scan over a damaged annotation section reports one
143
+ // diagnostic per record, and the spread blows the call stack past ~125,000 of them (0.1.6).
144
+ appendDiagnostics(diagnostics, chunk.diagnostics);
145
+ }
146
+
147
+ return {
148
+ records: {
149
+ start: first.records.start,
150
+ count: last.records.start + last.records.count - first.records.start,
151
+ },
152
+ startSeconds: first.startSeconds,
153
+ // Two float operations against the ends, not a sum of N durations: adding the durations up
154
+ // accumulates rounding once per chunk, and the run is contiguous so the ends are the truth.
155
+ durationSeconds: last.startSeconds + last.durationSeconds - first.startSeconds,
156
+ byteOffset: first.byteOffset,
157
+ byteLength,
158
+ signals: Object.freeze(signals),
159
+ // The gap BEFORE the whole run survives the merge. It describes what precedes the first
160
+ // chunk, which is still what precedes the merged one.
161
+ precededByGap: first.precededByGap,
162
+ diagnostics: Object.freeze(diagnostics),
163
+ };
164
+ }
package/src/constants.ts CHANGED
@@ -93,4 +93,4 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
93
93
  } as const;
94
94
 
95
95
  /** Published package version. Kept in sync with package.json by a test. */
96
- export const VERSION = '0.2.7';
96
+ export const VERSION = '0.2.8';
package/src/index.ts CHANGED
@@ -171,6 +171,7 @@ export {
171
171
  filterAnnotationsByTime,
172
172
  } from './annotations-query.js';
173
173
  export { decodeStatusWord, getStatusSignal, readTriggers } from './biosemi.js';
174
+ export { mergeChunks } from './chunks.js';
174
175
  export {
175
176
  envelopeOfSamples,
176
177
  readEnvelope,