edf2csv 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/cli/report.d.ts +17 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +107 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +315 -0
- package/dist/cli.js.map +1 -0
- package/dist/convert/channels.d.ts +40 -0
- package/dist/convert/channels.d.ts.map +1 -0
- package/dist/convert/channels.js +134 -0
- package/dist/convert/channels.js.map +1 -0
- package/dist/convert/plan.d.ts +70 -0
- package/dist/convert/plan.d.ts.map +1 -0
- package/dist/convert/plan.js +127 -0
- package/dist/convert/plan.js.map +1 -0
- package/dist/convert/run.d.ts +48 -0
- package/dist/convert/run.d.ts.map +1 -0
- package/dist/convert/run.js +358 -0
- package/dist/convert/run.js.map +1 -0
- package/dist/convert/time-range.d.ts +63 -0
- package/dist/convert/time-range.d.ts.map +1 -0
- package/dist/convert/time-range.js +188 -0
- package/dist/convert/time-range.js.map +1 -0
- package/dist/convert/timing.d.ts +18 -0
- package/dist/convert/timing.d.ts.map +1 -0
- package/dist/convert/timing.js +69 -0
- package/dist/convert/timing.js.map +1 -0
- package/dist/edf/annotations.d.ts +43 -0
- package/dist/edf/annotations.d.ts.map +1 -0
- package/dist/edf/annotations.js +88 -0
- package/dist/edf/annotations.js.map +1 -0
- package/dist/edf/errors.d.ts +27 -0
- package/dist/edf/errors.d.ts.map +1 -0
- package/dist/edf/errors.js +23 -0
- package/dist/edf/errors.js.map +1 -0
- package/dist/edf/header.d.ts +115 -0
- package/dist/edf/header.d.ts.map +1 -0
- package/dist/edf/header.js +383 -0
- package/dist/edf/header.js.map +1 -0
- package/dist/edf/reader.d.ts +75 -0
- package/dist/edf/reader.d.ts.map +1 -0
- package/dist/edf/reader.js +224 -0
- package/dist/edf/reader.js.map +1 -0
- package/dist/edf/scale.d.ts +46 -0
- package/dist/edf/scale.d.ts.map +1 -0
- package/dist/edf/scale.js +76 -0
- package/dist/edf/scale.js.map +1 -0
- package/dist/format/csv.d.ts +34 -0
- package/dist/format/csv.d.ts.map +1 -0
- package/dist/format/csv.js +136 -0
- package/dist/format/csv.js.map +1 -0
- package/dist/format/number.d.ts +42 -0
- package/dist/format/number.d.ts.map +1 -0
- package/dist/format/number.js +108 -0
- package/dist/format/number.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDF / EDF+ header parsing.
|
|
3
|
+
*
|
|
4
|
+
* Layout (all fields are ASCII, left-justified, space-padded):
|
|
5
|
+
*
|
|
6
|
+
* fixed header, 256 bytes
|
|
7
|
+
* 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)
|
|
8
|
+
* 8 80 patient identification
|
|
9
|
+
* 88 80 recording identification
|
|
10
|
+
* 168 8 start date dd.mm.yy
|
|
11
|
+
* 176 8 start time hh.mm.ss
|
|
12
|
+
* 184 8 number of bytes in the header record
|
|
13
|
+
* 192 44 reserved ('EDF+C' / 'EDF+D' live here)
|
|
14
|
+
* 236 8 number of data records (-1 if unknown)
|
|
15
|
+
* 244 8 duration of a data record, in seconds (may be fractional)
|
|
16
|
+
* 252 4 number of signals (ns)
|
|
17
|
+
*
|
|
18
|
+
* signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:
|
|
19
|
+
* all ns labels, then all ns transducer types, and so on.
|
|
20
|
+
* ns * 16 label
|
|
21
|
+
* ns * 80 transducer type
|
|
22
|
+
* ns * 8 physical dimension
|
|
23
|
+
* ns * 8 physical minimum
|
|
24
|
+
* ns * 8 physical maximum
|
|
25
|
+
* ns * 8 digital minimum
|
|
26
|
+
* ns * 8 digital maximum
|
|
27
|
+
* ns * 80 prefiltering
|
|
28
|
+
* ns * 8 number of samples in each data record
|
|
29
|
+
* ns * 32 reserved
|
|
30
|
+
*/
|
|
31
|
+
import { EdfError } from './errors.js';
|
|
32
|
+
/** Label the EDF+ spec reserves for the annotations channel. */
|
|
33
|
+
export const ANNOTATIONS_LABEL = 'EDF Annotations';
|
|
34
|
+
/** BDF+ uses its own spelling for the same channel. */
|
|
35
|
+
export const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';
|
|
36
|
+
export const FIXED_HEADER_BYTES = 256;
|
|
37
|
+
export const SIGNAL_HEADER_BYTES = 256;
|
|
38
|
+
const dec = (buf, start, len) => buf.subarray(start, start + len).toString('latin1');
|
|
39
|
+
/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */
|
|
40
|
+
const trimField = (s) => s.replace(/[\0\s]+$/u, '').replace(/^\s+/u, '');
|
|
41
|
+
function parseNumberField(raw, field, { integer = false, sawComma } = {}) {
|
|
42
|
+
let text = trimField(raw);
|
|
43
|
+
// Some writers emit a comma decimal separator despite the spec requiring '.'.
|
|
44
|
+
if (text.includes(',') && !text.includes('.')) {
|
|
45
|
+
text = text.replace(',', '.');
|
|
46
|
+
if (sawComma)
|
|
47
|
+
sawComma.value = true;
|
|
48
|
+
}
|
|
49
|
+
if (text === '') {
|
|
50
|
+
throw new EdfError('BAD_HEADER_FIELD', `Header field "${field}" is empty.`);
|
|
51
|
+
}
|
|
52
|
+
const n = Number(text);
|
|
53
|
+
if (!Number.isFinite(n)) {
|
|
54
|
+
throw new EdfError('BAD_HEADER_FIELD', `Header field "${field}" is not a number (found ${JSON.stringify(text)}).`, 'The file may be truncated, byte-shifted, or not an EDF file at all.');
|
|
55
|
+
}
|
|
56
|
+
if (integer && !Number.isInteger(n)) {
|
|
57
|
+
throw new EdfError('BAD_HEADER_FIELD', `Header field "${field}" must be a whole number (found ${JSON.stringify(text)}).`);
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999
|
|
63
|
+
* and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.
|
|
64
|
+
*/
|
|
65
|
+
function resolveStartDateTime(dateRaw, timeRaw) {
|
|
66
|
+
const d = /^(\d{2})[.\-/](\d{2})[.\-/](\d{2})$/u.exec(trimField(dateRaw));
|
|
67
|
+
const t = /^(\d{2})[.:\-](\d{2})[.:\-](\d{2})$/u.exec(trimField(timeRaw));
|
|
68
|
+
if (!d || !t)
|
|
69
|
+
return null;
|
|
70
|
+
const dd = Number(d[1]);
|
|
71
|
+
const mm = Number(d[2]);
|
|
72
|
+
const yy = Number(d[3]);
|
|
73
|
+
const hh = Number(t[1]);
|
|
74
|
+
const mi = Number(t[2]);
|
|
75
|
+
const ss = Number(t[3]);
|
|
76
|
+
if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60)
|
|
77
|
+
return null;
|
|
78
|
+
const year = yy >= 85 ? 1900 + yy : 2000 + yy;
|
|
79
|
+
const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));
|
|
80
|
+
// Reject dates that rolled over, e.g. 31.02.
|
|
81
|
+
if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd)
|
|
82
|
+
return null;
|
|
83
|
+
return date;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse the fixed 256-byte header plus the per-signal header block.
|
|
87
|
+
*
|
|
88
|
+
* @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.
|
|
89
|
+
* @param fileSize Total size of the file on disk, used to derive the real record count.
|
|
90
|
+
*/
|
|
91
|
+
export function parseHeader(buf, fileSize) {
|
|
92
|
+
const diagnostics = [];
|
|
93
|
+
const sawComma = { value: false };
|
|
94
|
+
if (buf.length < FIXED_HEADER_BYTES) {
|
|
95
|
+
throw new EdfError('FILE_TOO_SMALL', `File is ${fileSize} bytes; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`);
|
|
96
|
+
}
|
|
97
|
+
// BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores
|
|
98
|
+
// 3-byte samples instead of 2. Everything else about the layout is identical.
|
|
99
|
+
const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';
|
|
100
|
+
const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));
|
|
101
|
+
const patientId = trimField(dec(buf, 8, 80));
|
|
102
|
+
const recordingId = trimField(dec(buf, 88, 80));
|
|
103
|
+
const startDateRaw = trimField(dec(buf, 168, 8));
|
|
104
|
+
const startTimeRaw = trimField(dec(buf, 176, 8));
|
|
105
|
+
const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {
|
|
106
|
+
integer: true,
|
|
107
|
+
sawComma,
|
|
108
|
+
});
|
|
109
|
+
const reserved = trimField(dec(buf, 192, 44));
|
|
110
|
+
const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {
|
|
111
|
+
integer: true,
|
|
112
|
+
sawComma,
|
|
113
|
+
});
|
|
114
|
+
const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {
|
|
115
|
+
sawComma,
|
|
116
|
+
});
|
|
117
|
+
const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {
|
|
118
|
+
integer: true,
|
|
119
|
+
sawComma,
|
|
120
|
+
});
|
|
121
|
+
if (signalCount <= 0) {
|
|
122
|
+
throw new EdfError('INVALID_SIGNAL_COUNT', `Header declares ${signalCount} signals; expected at least 1.`);
|
|
123
|
+
}
|
|
124
|
+
if (!(recordDuration > 0)) {
|
|
125
|
+
throw new EdfError('INVALID_RECORD_DURATION', `Header declares a data record duration of ${recordDuration}s; expected a positive number.`);
|
|
126
|
+
}
|
|
127
|
+
const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;
|
|
128
|
+
if (buf.length < expectedHeaderBytes) {
|
|
129
|
+
throw new EdfError('FILE_TOO_SMALL', `File declares ${signalCount} signals, which needs a ${expectedHeaderBytes}-byte header, ` +
|
|
130
|
+
`but the file is only ${fileSize} bytes.`);
|
|
131
|
+
}
|
|
132
|
+
if (headerBytes !== expectedHeaderBytes) {
|
|
133
|
+
diagnostics.push({
|
|
134
|
+
code: 'HEADER_BYTES_MISMATCH',
|
|
135
|
+
severity: 'warning',
|
|
136
|
+
message: `Header says it is ${headerBytes} bytes, but ${signalCount} signals require ` +
|
|
137
|
+
`${expectedHeaderBytes} bytes. Using the value computed from the signal count.`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Signal headers are field-major: all labels, then all transducers, and so on.
|
|
141
|
+
const base = FIXED_HEADER_BYTES;
|
|
142
|
+
const readField = (offsetUnits, width, i) => dec(buf, base + offsetUnits * signalCount + i * width, width);
|
|
143
|
+
// EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the
|
|
144
|
+
// same thing, so both are normalised to a single continuity marker.
|
|
145
|
+
const continuityTag = /^(?:EDF|BDF)\+([CD])/u.exec(reserved);
|
|
146
|
+
const continuity = continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';
|
|
147
|
+
const signals = [];
|
|
148
|
+
let byteOffsetInRecord = 0;
|
|
149
|
+
const bytesPerSample = isBdf ? 3 : 2;
|
|
150
|
+
const seenLabels = new Map();
|
|
151
|
+
for (let i = 0; i < signalCount; i++) {
|
|
152
|
+
const label = trimField(readField(0, 16, i));
|
|
153
|
+
const transducer = trimField(readField(16, 80, i));
|
|
154
|
+
const physicalDimension = trimField(readField(96, 8, i));
|
|
155
|
+
const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {
|
|
156
|
+
sawComma,
|
|
157
|
+
});
|
|
158
|
+
const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {
|
|
159
|
+
sawComma,
|
|
160
|
+
});
|
|
161
|
+
const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {
|
|
162
|
+
integer: true,
|
|
163
|
+
sawComma,
|
|
164
|
+
});
|
|
165
|
+
const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {
|
|
166
|
+
integer: true,
|
|
167
|
+
sawComma,
|
|
168
|
+
});
|
|
169
|
+
const prefiltering = trimField(readField(136, 80, i));
|
|
170
|
+
const samplesPerRecord = parseNumberField(readField(216, 8, i), `samples per record (signal ${i})`, { integer: true, sawComma });
|
|
171
|
+
const sigReserved = trimField(readField(224, 32, i));
|
|
172
|
+
if (samplesPerRecord < 0) {
|
|
173
|
+
throw new EdfError('BAD_HEADER_FIELD', `Signal ${i} ("${label}") declares ${samplesPerRecord} samples per record.`);
|
|
174
|
+
}
|
|
175
|
+
const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;
|
|
176
|
+
signals.push({
|
|
177
|
+
index: i,
|
|
178
|
+
label,
|
|
179
|
+
transducer,
|
|
180
|
+
physicalDimension,
|
|
181
|
+
physicalMin,
|
|
182
|
+
physicalMax,
|
|
183
|
+
digitalMin,
|
|
184
|
+
digitalMax,
|
|
185
|
+
prefiltering,
|
|
186
|
+
samplesPerRecord,
|
|
187
|
+
reserved: sigReserved,
|
|
188
|
+
isAnnotations,
|
|
189
|
+
samplingRate: samplesPerRecord / recordDuration,
|
|
190
|
+
byteOffsetInRecord,
|
|
191
|
+
});
|
|
192
|
+
byteOffsetInRecord += samplesPerRecord * bytesPerSample;
|
|
193
|
+
if (!isAnnotations) {
|
|
194
|
+
if (label === '') {
|
|
195
|
+
diagnostics.push({
|
|
196
|
+
code: 'EMPTY_LABEL',
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
message: `Signal ${i} has no label. It will appear as "signal_${i}".`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Collected rather than reported here: a label repeated five times should
|
|
203
|
+
// produce one warning naming all five, not four near-identical pairs.
|
|
204
|
+
const seen = seenLabels.get(label);
|
|
205
|
+
if (seen)
|
|
206
|
+
seen.push(i);
|
|
207
|
+
else
|
|
208
|
+
seenLabels.set(label, [i]);
|
|
209
|
+
}
|
|
210
|
+
if (samplesPerRecord === 0) {
|
|
211
|
+
diagnostics.push({
|
|
212
|
+
code: 'NO_SAMPLES',
|
|
213
|
+
severity: 'warning',
|
|
214
|
+
message: `Signal ${i} ("${label}") carries no samples at all (0 per data record).`,
|
|
215
|
+
hint: 'It is described in channels.csv but left out of the converted data.',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (digitalMax === digitalMin) {
|
|
219
|
+
diagnostics.push({
|
|
220
|
+
code: 'DEGENERATE_DIGITAL_RANGE',
|
|
221
|
+
severity: 'warning',
|
|
222
|
+
message: `Signal ${i} ("${label}") has digital minimum equal to digital maximum ` +
|
|
223
|
+
`(${digitalMin}), so its values cannot be scaled.`,
|
|
224
|
+
hint: 'Its samples are written as the physical minimum. Treat this channel with suspicion.',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (physicalMax === physicalMin) {
|
|
228
|
+
diagnostics.push({
|
|
229
|
+
code: 'DEGENERATE_PHYSICAL_RANGE',
|
|
230
|
+
severity: 'warning',
|
|
231
|
+
message: `Signal ${i} ("${label}") has physical minimum equal to physical maximum ` +
|
|
232
|
+
`(${physicalMin}), so every sample converts to the same value.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (physicalMax < physicalMin) {
|
|
236
|
+
diagnostics.push({
|
|
237
|
+
code: 'INVERTED_PHYSICAL_RANGE',
|
|
238
|
+
severity: 'warning',
|
|
239
|
+
message: `Signal ${i} ("${label}") declares physical minimum ${physicalMin} above physical ` +
|
|
240
|
+
`maximum ${physicalMax}, which inverts its polarity.`,
|
|
241
|
+
hint: 'The values are converted exactly as the header specifies, inversion included.',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const [label, indices] of seenLabels) {
|
|
247
|
+
if (indices.length < 2)
|
|
248
|
+
continue;
|
|
249
|
+
diagnostics.push({
|
|
250
|
+
code: 'DUPLICATE_LABEL',
|
|
251
|
+
severity: 'warning',
|
|
252
|
+
message: `${indices.length} signals share the label "${label}" (positions ${indices.join(', ')}).`,
|
|
253
|
+
hint: 'Their columns are suffixed with the signal number so they stay distinguishable.',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const recordBytes = byteOffsetInRecord;
|
|
257
|
+
if (recordBytes <= 0) {
|
|
258
|
+
throw new EdfError('NO_SAMPLES', 'No signal in this file carries any samples (every channel declares 0 samples per record).');
|
|
259
|
+
}
|
|
260
|
+
if (sawComma.value) {
|
|
261
|
+
diagnostics.push({
|
|
262
|
+
code: 'COMMA_DECIMAL',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',
|
|
265
|
+
hint: 'They were read as decimal points. Check the values in the channel table.',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const dataBytes = fileSize - expectedHeaderBytes;
|
|
269
|
+
if (dataBytes < 0) {
|
|
270
|
+
throw new EdfError('FILE_TOO_SMALL', `File is smaller than its own header.`);
|
|
271
|
+
}
|
|
272
|
+
const recordCount = Math.floor(dataBytes / recordBytes);
|
|
273
|
+
const trailingBytes = dataBytes - recordCount * recordBytes;
|
|
274
|
+
if (recordCount === 0) {
|
|
275
|
+
throw new EdfError('NO_DATA_RECORDS', 'The file contains a header but no complete data record.', 'The recording was probably interrupted before any data was written.');
|
|
276
|
+
}
|
|
277
|
+
if (declaredRecordCount === -1) {
|
|
278
|
+
diagnostics.push({
|
|
279
|
+
code: 'RECORD_COUNT_UNKNOWN',
|
|
280
|
+
severity: 'warning',
|
|
281
|
+
message: `The header does not say how many data records the file has (-1), which the spec allows ` +
|
|
282
|
+
`for recordings still in progress. Using the ${recordCount} records the file actually contains.`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (declaredRecordCount !== recordCount) {
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
code: 'RECORD_COUNT_MISMATCH',
|
|
288
|
+
severity: 'warning',
|
|
289
|
+
message: `The header declares ${declaredRecordCount} data records but the file contains ` +
|
|
290
|
+
`${recordCount}. Converting the ${recordCount} records that are present.`,
|
|
291
|
+
hint: declaredRecordCount > recordCount
|
|
292
|
+
? 'The recording looks truncated. It may have been cut short or copied incompletely.'
|
|
293
|
+
: 'The file is longer than its header claims.',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (trailingBytes > 0) {
|
|
297
|
+
diagnostics.push({
|
|
298
|
+
code: 'TRAILING_BYTES',
|
|
299
|
+
severity: 'warning',
|
|
300
|
+
message: `${trailingBytes} bytes after the last complete data record were ignored.`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const isEdfPlus = continuity !== null;
|
|
304
|
+
if (continuity === 'EDF+D') {
|
|
305
|
+
diagnostics.push({
|
|
306
|
+
code: 'DISCONTINUOUS',
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
message: `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +
|
|
309
|
+
`not contiguous in time.`,
|
|
310
|
+
hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const dataSignals = signals.filter((s) => !s.isAnnotations);
|
|
314
|
+
if (dataSignals.length === 0) {
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
code: 'NO_SIGNAL_CHANNELS',
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
message: 'This file has no signal channels; it contains only EDF+ annotations.',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const rates = new Set(dataSignals.map((s) => s.samplingRate));
|
|
322
|
+
if (rates.size > 1) {
|
|
323
|
+
diagnostics.push({
|
|
324
|
+
code: 'MIXED_SAMPLING_RATES',
|
|
325
|
+
severity: 'warning',
|
|
326
|
+
message: `Channels use ${rates.size} different sampling rates ` +
|
|
327
|
+
`(${[...rates].sort((a, b) => b - a).map((r) => `${formatRate(r)} Hz`).join(', ')}).`,
|
|
328
|
+
hint: 'They are written to one file per rate so no channel is resampled.',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
header: {
|
|
333
|
+
version,
|
|
334
|
+
patientId,
|
|
335
|
+
recordingId,
|
|
336
|
+
startDateRaw,
|
|
337
|
+
startTimeRaw,
|
|
338
|
+
startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw),
|
|
339
|
+
headerBytes: expectedHeaderBytes,
|
|
340
|
+
reserved,
|
|
341
|
+
isEdfPlus,
|
|
342
|
+
isBdf,
|
|
343
|
+
continuity,
|
|
344
|
+
declaredRecordCount,
|
|
345
|
+
recordDuration,
|
|
346
|
+
signalCount,
|
|
347
|
+
signals,
|
|
348
|
+
bytesPerSample,
|
|
349
|
+
recordBytes,
|
|
350
|
+
},
|
|
351
|
+
recordCount,
|
|
352
|
+
trailingBytes,
|
|
353
|
+
diagnostics,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The recording start as a zone-less wall clock, "YYYY-MM-DDTHH:MM:SS".
|
|
358
|
+
*
|
|
359
|
+
* EDF stores the start time as local wall-clock digits with no timezone anywhere in
|
|
360
|
+
* the format. `startDateTime` is built with Date.UTC purely so those digits survive a
|
|
361
|
+
* round trip unshifted, which makes it a carrier for the wall clock rather than a
|
|
362
|
+
* real instant. Serialising it with `toISOString()` would append a Z and assert UTC,
|
|
363
|
+
* and any reader converting to local time would then shift the recording by their own
|
|
364
|
+
* offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because
|
|
365
|
+
* the file genuinely does not say which zone it meant.
|
|
366
|
+
*/
|
|
367
|
+
export function formatWallClock(date) {
|
|
368
|
+
if (!date)
|
|
369
|
+
return null;
|
|
370
|
+
return date.toISOString().slice(0, 19);
|
|
371
|
+
}
|
|
372
|
+
/** "EDF", "EDF+ (EDF+D)", "BDF", "BDF+ (EDF+C)". */
|
|
373
|
+
export function describeFormat(header) {
|
|
374
|
+
const base = header.isBdf ? 'BDF' : 'EDF';
|
|
375
|
+
if (!header.isEdfPlus)
|
|
376
|
+
return base;
|
|
377
|
+
return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;
|
|
378
|
+
}
|
|
379
|
+
/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */
|
|
380
|
+
export function formatRate(hz) {
|
|
381
|
+
return Number.isInteger(hz) ? String(hz) : String(Number(hz.toFixed(6)));
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunked reader for EDF / EDF+ files.
|
|
3
|
+
*
|
|
4
|
+
* Data records are read in batches sized by a byte budget rather than all at once,
|
|
5
|
+
* so peak memory stays flat regardless of how long the recording is. A 4 GB file
|
|
6
|
+
* and a 4 MB file use the same working set.
|
|
7
|
+
*/
|
|
8
|
+
import type { Diagnostic } from './errors.js';
|
|
9
|
+
import type { EdfHeader, EdfSignal } from './header.js';
|
|
10
|
+
import type { Annotation } from './annotations.js';
|
|
11
|
+
/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */
|
|
12
|
+
export declare const DEFAULT_CHUNK_BYTES: number;
|
|
13
|
+
export interface RecordBatch {
|
|
14
|
+
/** Index of the first record in this batch, relative to the whole file. */
|
|
15
|
+
firstRecordIndex: number;
|
|
16
|
+
recordCount: number;
|
|
17
|
+
/**
|
|
18
|
+
* Raw record bytes, `recordCount * header.recordBytes` long.
|
|
19
|
+
*
|
|
20
|
+
* The buffer is reused between iterations. Copy anything you need to keep past
|
|
21
|
+
* the current loop turn.
|
|
22
|
+
*/
|
|
23
|
+
data: Buffer;
|
|
24
|
+
}
|
|
25
|
+
export interface ReadRecordsOptions {
|
|
26
|
+
/** First record to read, inclusive. Defaults to 0. */
|
|
27
|
+
startRecord?: number;
|
|
28
|
+
/** Last record to read, exclusive. Defaults to the file's record count. */
|
|
29
|
+
endRecord?: number;
|
|
30
|
+
chunkBytes?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class EdfFile {
|
|
33
|
+
#private;
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly fileSize: number;
|
|
36
|
+
readonly header: EdfHeader;
|
|
37
|
+
/** Records actually present in the file, which may differ from the header's claim. */
|
|
38
|
+
readonly recordCount: number;
|
|
39
|
+
readonly trailingBytes: number;
|
|
40
|
+
readonly diagnostics: Diagnostic[];
|
|
41
|
+
private constructor();
|
|
42
|
+
static open(path: string): Promise<EdfFile>;
|
|
43
|
+
/** Signal channels, excluding the EDF+ annotations channel. */
|
|
44
|
+
get dataSignals(): EdfSignal[];
|
|
45
|
+
get annotationSignals(): EdfSignal[];
|
|
46
|
+
/** Total recording duration in seconds, based on records actually present. */
|
|
47
|
+
get durationSeconds(): number;
|
|
48
|
+
/** Read a half-open range of records in batches. */
|
|
49
|
+
readRecords(options?: ReadRecordsOptions): AsyncGenerator<RecordBatch>;
|
|
50
|
+
/** Read one sample as its raw digital value. */
|
|
51
|
+
sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number;
|
|
52
|
+
/** Byte offset of a signal's samples within a batch. */
|
|
53
|
+
offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number;
|
|
54
|
+
/** The annotation channel's raw bytes for one record in a batch. */
|
|
55
|
+
annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Buffer;
|
|
56
|
+
/**
|
|
57
|
+
* Read every EDF+ annotation in the file, plus the start time each record declares.
|
|
58
|
+
*
|
|
59
|
+
* Only the annotation channel is read, seeking straight to it inside each record
|
|
60
|
+
* rather than pulling whole records through memory. On a multi-gigabyte recording
|
|
61
|
+
* that is the difference between a few kilobytes of I/O and all of it.
|
|
62
|
+
*
|
|
63
|
+
* The whole file is always scanned, never just the records inside a requested
|
|
64
|
+
* window: writers are not obliged to store an annotation in the record its onset
|
|
65
|
+
* falls in, and some put every annotation in the first record. Reading only the
|
|
66
|
+
* window's records would drop those entirely.
|
|
67
|
+
*/
|
|
68
|
+
readAnnotations(): Promise<{
|
|
69
|
+
annotations: Annotation[];
|
|
70
|
+
recordStarts: (number | null)[];
|
|
71
|
+
malformed: number;
|
|
72
|
+
}>;
|
|
73
|
+
close(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,QAAkB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,OAAO;;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAKnC,OAAO;WAkBM,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuDjD,+DAA+D;IAC/D,IAAI,WAAW,IAAI,SAAS,EAAE,CAE7B;IAED,IAAI,iBAAiB,IAAI,SAAS,EAAE,CAEnC;IAED,8EAA8E;IAC9E,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,oDAAoD;IAC7C,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,cAAc,CAAC,WAAW,CAAC;IAiCjF,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;IAmBlG,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM;IAI7E,oEAAoE;IACpE,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM;IAKpF;;;;;;;;;;;OAWG;IACG,eAAe,IAAI,OAAO,CAAC;QAC/B,WAAW,EAAE,UAAU,EAAE,CAAC;QAC1B,YAAY,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;QAChC,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAoCI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAS7B"}
|