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,18 @@
|
|
|
1
|
+
import type { Diagnostic } from '../edf/errors.js';
|
|
2
|
+
import type { EdfFile } from '../edf/reader.js';
|
|
3
|
+
export interface AnnotationTimingData {
|
|
4
|
+
recordStarts: (number | null)[];
|
|
5
|
+
malformed: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the true start time of every data record.
|
|
9
|
+
*
|
|
10
|
+
* Continuous recordings need no table because their record positions are
|
|
11
|
+
* arithmetic. EDF+D recordings carry their positions in the annotation channel;
|
|
12
|
+
* missing or malformed timekeeping entries are reported before falling back.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deriveRecordStarts(file: EdfFile, annotationData: AnnotationTimingData): {
|
|
15
|
+
starts: Float64Array | null;
|
|
16
|
+
diagnostics: Diagnostic[];
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,cAAc,EAAE,oBAAoB,GACnC;IAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAmE5D"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the true start time of every data record.
|
|
3
|
+
*
|
|
4
|
+
* Continuous recordings need no table because their record positions are
|
|
5
|
+
* arithmetic. EDF+D recordings carry their positions in the annotation channel;
|
|
6
|
+
* missing or malformed timekeeping entries are reported before falling back.
|
|
7
|
+
*/
|
|
8
|
+
export function deriveRecordStarts(file, annotationData) {
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
if (annotationData.malformed > 0) {
|
|
11
|
+
diagnostics.push({
|
|
12
|
+
code: 'ANNOTATION_DECODE_FAILED',
|
|
13
|
+
severity: 'warning',
|
|
14
|
+
message: `${annotationData.malformed} annotation entr${annotationData.malformed === 1 ? 'y was' : 'ies were'} ` +
|
|
15
|
+
`unreadable and could not be exported.`,
|
|
16
|
+
hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (file.header.continuity !== 'EDF+D')
|
|
20
|
+
return { starts: null, diagnostics };
|
|
21
|
+
if (file.annotationSignals.length === 0) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
code: 'DISCONTINUOUS',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
message: 'This file is marked discontinuous but has no annotation channel, so where its ' +
|
|
26
|
+
'records sit in time is not recorded anywhere.',
|
|
27
|
+
hint: 'Times are written as if the records were contiguous. Any gaps are lost.',
|
|
28
|
+
});
|
|
29
|
+
return { starts: null, diagnostics };
|
|
30
|
+
}
|
|
31
|
+
const starts = new Float64Array(file.recordCount);
|
|
32
|
+
const missing = [];
|
|
33
|
+
for (let i = 0; i < file.recordCount; i++) {
|
|
34
|
+
const declared = annotationData.recordStarts[i];
|
|
35
|
+
if (declared === null || declared === undefined) {
|
|
36
|
+
missing.push(i);
|
|
37
|
+
starts[i] = i * file.header.recordDuration;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
starts[i] = declared;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
const shown = missing.slice(0, 5).join(', ');
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
code: 'ANNOTATION_DECODE_FAILED',
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
message: `${missing.length} of ${file.recordCount} data records carry no readable timekeeping ` +
|
|
49
|
+
`annotation (record${missing.length === 1 ? '' : 's'} ${shown}` +
|
|
50
|
+
`${missing.length > 5 ? ', …' : ''}), so their true position in time is unknown.`,
|
|
51
|
+
hint: 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
let outOfOrder = 0;
|
|
55
|
+
for (let i = 1; i < starts.length; i++) {
|
|
56
|
+
if (starts[i] < starts[i - 1])
|
|
57
|
+
outOfOrder++;
|
|
58
|
+
}
|
|
59
|
+
if (outOfOrder > 0) {
|
|
60
|
+
diagnostics.push({
|
|
61
|
+
code: 'DISCONTINUOUS',
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
message: `${outOfOrder} data record${outOfOrder === 1 ? '' : 's'} start earlier than the record before it.`,
|
|
64
|
+
hint: 'Rows are written in file order, so the time column will not increase monotonically.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { starts, diagnostics };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDF+ annotation (TAL) decoding.
|
|
3
|
+
*
|
|
4
|
+
* The annotations channel stores UTF-8 text in place of samples. Its bytes are a
|
|
5
|
+
* run of Time-stamped Annotation Lists, each terminated by a NUL, with the rest
|
|
6
|
+
* of the channel NUL-padded:
|
|
7
|
+
*
|
|
8
|
+
* +<onset>[<0x15><duration>]<0x14><text><0x14>...<0x00>
|
|
9
|
+
*
|
|
10
|
+
* The first TAL of every data record must carry that record's start time and no
|
|
11
|
+
* text; that is how an EDF+D file states where each record actually sits in time.
|
|
12
|
+
*
|
|
13
|
+
* +1.25<0x15>0.5<0x14>Seizure onset<0x14><0x00>
|
|
14
|
+
*/
|
|
15
|
+
declare const SEP_TEXT = 20;
|
|
16
|
+
declare const SEP_DURATION = 21;
|
|
17
|
+
declare const TAL_END = 0;
|
|
18
|
+
export interface Annotation {
|
|
19
|
+
/** Seconds from the start of the recording. */
|
|
20
|
+
onset: number;
|
|
21
|
+
/** Seconds, or null when the TAL omitted a duration. */
|
|
22
|
+
duration: number | null;
|
|
23
|
+
text: string;
|
|
24
|
+
/** Index of the data record this annotation was stored in. */
|
|
25
|
+
recordIndex: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DecodedRecordAnnotations {
|
|
28
|
+
/** Record start time in seconds, from the leading timekeeping TAL. */
|
|
29
|
+
recordStart: number | null;
|
|
30
|
+
annotations: Annotation[];
|
|
31
|
+
/** Non-empty chunks that were not valid TALs, so the caller can report them. */
|
|
32
|
+
malformed: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Decode one data record's annotation bytes.
|
|
36
|
+
*
|
|
37
|
+
* Malformed TALs are skipped rather than thrown, because a single bad annotation
|
|
38
|
+
* should not cost the user an entire conversion. The count of skipped chunks is
|
|
39
|
+
* returned so the caller can tell the user rather than losing them in silence.
|
|
40
|
+
*/
|
|
41
|
+
export declare function decodeRecordAnnotations(bytes: Buffer, recordIndex: number): DecodedRecordAnnotations;
|
|
42
|
+
export { SEP_TEXT, SEP_DURATION, TAL_END };
|
|
43
|
+
//# sourceMappingURL=annotations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/edf/annotations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,QAAA,MAAM,QAAQ,KAAO,CAAC;AACtB,QAAA,MAAM,YAAY,KAAO,CAAC;AAC1B,QAAA,MAAM,OAAO,IAAO,CAAC;AAKrB,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,sEAAsE;IACtE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,wBAAwB,CA2B1B;AA2CD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDF+ annotation (TAL) decoding.
|
|
3
|
+
*
|
|
4
|
+
* The annotations channel stores UTF-8 text in place of samples. Its bytes are a
|
|
5
|
+
* run of Time-stamped Annotation Lists, each terminated by a NUL, with the rest
|
|
6
|
+
* of the channel NUL-padded:
|
|
7
|
+
*
|
|
8
|
+
* +<onset>[<0x15><duration>]<0x14><text><0x14>...<0x00>
|
|
9
|
+
*
|
|
10
|
+
* The first TAL of every data record must carry that record's start time and no
|
|
11
|
+
* text; that is how an EDF+D file states where each record actually sits in time.
|
|
12
|
+
*
|
|
13
|
+
* +1.25<0x15>0.5<0x14>Seizure onset<0x14><0x00>
|
|
14
|
+
*/
|
|
15
|
+
const SEP_TEXT = 0x14; // separates onset/duration from text, and text from text
|
|
16
|
+
const SEP_DURATION = 0x15; // separates onset from duration
|
|
17
|
+
const TAL_END = 0x00;
|
|
18
|
+
const TEXT_SEP_CHAR = String.fromCharCode(SEP_TEXT);
|
|
19
|
+
const DURATION_SEP_CHAR = String.fromCharCode(SEP_DURATION);
|
|
20
|
+
/**
|
|
21
|
+
* Decode one data record's annotation bytes.
|
|
22
|
+
*
|
|
23
|
+
* Malformed TALs are skipped rather than thrown, because a single bad annotation
|
|
24
|
+
* should not cost the user an entire conversion. The count of skipped chunks is
|
|
25
|
+
* returned so the caller can tell the user rather than losing them in silence.
|
|
26
|
+
*/
|
|
27
|
+
export function decodeRecordAnnotations(bytes, recordIndex) {
|
|
28
|
+
const annotations = [];
|
|
29
|
+
let recordStart = null;
|
|
30
|
+
let isFirstTal = true;
|
|
31
|
+
let malformed = 0;
|
|
32
|
+
let start = 0;
|
|
33
|
+
for (let i = 0; i <= bytes.length; i++) {
|
|
34
|
+
if (i !== bytes.length && bytes[i] !== TAL_END)
|
|
35
|
+
continue;
|
|
36
|
+
if (i > start) {
|
|
37
|
+
const chunk = bytes.subarray(start, i);
|
|
38
|
+
const parsed = parseTal(chunk, recordIndex);
|
|
39
|
+
if (parsed) {
|
|
40
|
+
if (isFirstTal) {
|
|
41
|
+
recordStart = parsed.onset;
|
|
42
|
+
isFirstTal = false;
|
|
43
|
+
}
|
|
44
|
+
for (const annotation of parsed.annotations)
|
|
45
|
+
annotations.push(annotation);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
malformed++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
start = i + 1;
|
|
52
|
+
}
|
|
53
|
+
return { recordStart, annotations, malformed };
|
|
54
|
+
}
|
|
55
|
+
function parseTal(chunk, recordIndex) {
|
|
56
|
+
// The onset must be explicitly signed; anything else is not a TAL.
|
|
57
|
+
const first = chunk[0];
|
|
58
|
+
if (first !== 0x2b /* + */ && first !== 0x2d /* - */)
|
|
59
|
+
return null;
|
|
60
|
+
const text = chunk.toString('utf8');
|
|
61
|
+
const parts = text.split(TEXT_SEP_CHAR);
|
|
62
|
+
const head = parts[0] ?? '';
|
|
63
|
+
let onsetText = head;
|
|
64
|
+
let durationText = null;
|
|
65
|
+
const durationSep = head.indexOf(DURATION_SEP_CHAR);
|
|
66
|
+
if (durationSep >= 0) {
|
|
67
|
+
onsetText = head.slice(0, durationSep);
|
|
68
|
+
durationText = head.slice(durationSep + 1);
|
|
69
|
+
}
|
|
70
|
+
const onset = Number(onsetText);
|
|
71
|
+
if (!Number.isFinite(onset))
|
|
72
|
+
return null;
|
|
73
|
+
let duration = null;
|
|
74
|
+
if (durationText !== null && durationText !== '') {
|
|
75
|
+
const d = Number(durationText);
|
|
76
|
+
duration = Number.isFinite(d) ? d : null;
|
|
77
|
+
}
|
|
78
|
+
const annotations = [];
|
|
79
|
+
for (const raw of parts.slice(1)) {
|
|
80
|
+
// A trailing separator yields an empty segment; a timekeeping TAL is all empty.
|
|
81
|
+
if (raw === '')
|
|
82
|
+
continue;
|
|
83
|
+
annotations.push({ onset, duration, text: raw, recordIndex });
|
|
84
|
+
}
|
|
85
|
+
return { onset, annotations };
|
|
86
|
+
}
|
|
87
|
+
export { SEP_TEXT, SEP_DURATION, TAL_END };
|
|
88
|
+
//# sourceMappingURL=annotations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.js","sourceRoot":"","sources":["../../src/edf/annotations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error and diagnostic types.
|
|
3
|
+
*
|
|
4
|
+
* The distinction that matters here: an EdfError means we cannot produce
|
|
5
|
+
* trustworthy output and must stop. A Diagnostic with severity 'warning' means
|
|
6
|
+
* we can continue, but the user needs to know something about their data that
|
|
7
|
+
* they would not otherwise see.
|
|
8
|
+
*/
|
|
9
|
+
export type DiagnosticCode = 'MIXED_SAMPLING_RATES' | 'DISCONTINUOUS' | 'RECORD_COUNT_UNKNOWN' | 'RECORD_COUNT_MISMATCH' | 'TRAILING_BYTES' | 'DEGENERATE_DIGITAL_RANGE' | 'DEGENERATE_PHYSICAL_RANGE' | 'INVERTED_PHYSICAL_RANGE' | 'DUPLICATE_LABEL' | 'EMPTY_LABEL' | 'NONSTANDARD_UNIT' | 'NO_ANNOTATIONS' | 'ANNOTATION_DECODE_FAILED' | 'COMMA_DECIMAL' | 'LARGE_OUTPUT' | 'NO_SIGNAL_CHANNELS' | 'NO_SAMPLES' | 'STALE_OUTPUT' | 'HEADER_BYTES_MISMATCH' | 'NONPRINTABLE_LABEL';
|
|
10
|
+
export interface Diagnostic {
|
|
11
|
+
code: DiagnosticCode;
|
|
12
|
+
severity: 'warning' | 'info';
|
|
13
|
+
message: string;
|
|
14
|
+
/** What the user can do about it. Omitted when there is nothing useful to say. */
|
|
15
|
+
hint?: string;
|
|
16
|
+
}
|
|
17
|
+
export type EdfErrorCode = 'FILE_TOO_SMALL' | 'BAD_HEADER_FIELD' | 'NO_DATA_RECORDS' | 'INVALID_SIGNAL_COUNT' | 'INVALID_RECORD_DURATION' | 'NO_SAMPLES' | 'UNREADABLE';
|
|
18
|
+
/**
|
|
19
|
+
* A fatal problem with the recording itself. Carries a stable `code` so the CLI
|
|
20
|
+
* can map it to an exit status, and a `hint` so the user is not left guessing.
|
|
21
|
+
*/
|
|
22
|
+
export declare class EdfError extends Error {
|
|
23
|
+
readonly code: EdfErrorCode;
|
|
24
|
+
readonly hint: string | undefined;
|
|
25
|
+
constructor(code: EdfErrorCode, message: string, hint?: string);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/edf/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,cAAc,GACtB,sBAAsB,GACtB,eAAe,GACf,sBAAsB,GACtB,uBAAuB,GACvB,gBAAgB,GAChB,0BAA0B,GAC1B,2BAA2B,GAC3B,yBAAyB,GACzB,iBAAiB,GACjB,aAAa,GACb,kBAAkB,GAClB,gBAAgB,GAChB,0BAA0B,GAC1B,eAAe,GACf,cAAc,GACd,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,uBAAuB,GACvB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,YAAY,GACpB,gBAAgB,GAChB,kBAAkB,GAClB,iBAAiB,GACjB,sBAAsB,GACtB,yBAAyB,GACzB,YAAY,GACZ,YAAY,CAAC;AAEjB;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEtB,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAM/D"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error and diagnostic types.
|
|
3
|
+
*
|
|
4
|
+
* The distinction that matters here: an EdfError means we cannot produce
|
|
5
|
+
* trustworthy output and must stop. A Diagnostic with severity 'warning' means
|
|
6
|
+
* we can continue, but the user needs to know something about their data that
|
|
7
|
+
* they would not otherwise see.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A fatal problem with the recording itself. Carries a stable `code` so the CLI
|
|
11
|
+
* can map it to an exit status, and a `hint` so the user is not left guessing.
|
|
12
|
+
*/
|
|
13
|
+
export class EdfError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
hint;
|
|
16
|
+
constructor(code, message, hint) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'EdfError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.hint = hint;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/edf/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAyCH;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,IAAI,CAAe;IACnB,IAAI,CAAqB;IAElC,YAAY,IAAkB,EAAE,OAAe,EAAE,IAAa;QAC5D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,115 @@
|
|
|
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 type { Diagnostic } from './errors.js';
|
|
32
|
+
/** Label the EDF+ spec reserves for the annotations channel. */
|
|
33
|
+
export declare const ANNOTATIONS_LABEL = "EDF Annotations";
|
|
34
|
+
/** BDF+ uses its own spelling for the same channel. */
|
|
35
|
+
export declare const BDF_ANNOTATIONS_LABEL = "BDF Annotations";
|
|
36
|
+
export declare const FIXED_HEADER_BYTES = 256;
|
|
37
|
+
export declare const SIGNAL_HEADER_BYTES = 256;
|
|
38
|
+
export interface EdfSignal {
|
|
39
|
+
/** Position in the file, 0-based. Stable even when labels collide. */
|
|
40
|
+
index: number;
|
|
41
|
+
label: string;
|
|
42
|
+
transducer: string;
|
|
43
|
+
physicalDimension: string;
|
|
44
|
+
physicalMin: number;
|
|
45
|
+
physicalMax: number;
|
|
46
|
+
digitalMin: number;
|
|
47
|
+
digitalMax: number;
|
|
48
|
+
prefiltering: string;
|
|
49
|
+
samplesPerRecord: number;
|
|
50
|
+
reserved: string;
|
|
51
|
+
/** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */
|
|
52
|
+
isAnnotations: boolean;
|
|
53
|
+
/** samplesPerRecord / recordDuration, in Hz. */
|
|
54
|
+
samplingRate: number;
|
|
55
|
+
/** Byte offset of this signal's samples within one data record. */
|
|
56
|
+
byteOffsetInRecord: number;
|
|
57
|
+
}
|
|
58
|
+
export interface EdfHeader {
|
|
59
|
+
version: string;
|
|
60
|
+
patientId: string;
|
|
61
|
+
recordingId: string;
|
|
62
|
+
/** Raw 'dd.mm.yy' as written in the file. */
|
|
63
|
+
startDateRaw: string;
|
|
64
|
+
/** Raw 'hh.mm.ss' as written in the file. */
|
|
65
|
+
startTimeRaw: string;
|
|
66
|
+
/** Resolved start instant, or null when the file's date/time fields are unusable. */
|
|
67
|
+
startDateTime: Date | null;
|
|
68
|
+
headerBytes: number;
|
|
69
|
+
reserved: string;
|
|
70
|
+
isEdfPlus: boolean;
|
|
71
|
+
/** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */
|
|
72
|
+
isBdf: boolean;
|
|
73
|
+
/** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */
|
|
74
|
+
continuity: 'EDF+C' | 'EDF+D' | null;
|
|
75
|
+
/** As declared in the header. -1 means "unknown", which the spec permits. */
|
|
76
|
+
declaredRecordCount: number;
|
|
77
|
+
recordDuration: number;
|
|
78
|
+
signalCount: number;
|
|
79
|
+
signals: EdfSignal[];
|
|
80
|
+
bytesPerSample: number;
|
|
81
|
+
recordBytes: number;
|
|
82
|
+
}
|
|
83
|
+
/** Everything derived by combining the header with the file's real size. */
|
|
84
|
+
export interface EdfHeaderInfo {
|
|
85
|
+
header: EdfHeader;
|
|
86
|
+
/** Record count implied by the actual file size — the one we trust for reading. */
|
|
87
|
+
recordCount: number;
|
|
88
|
+
/** Bytes after the last complete data record. */
|
|
89
|
+
trailingBytes: number;
|
|
90
|
+
diagnostics: Diagnostic[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse the fixed 256-byte header plus the per-signal header block.
|
|
94
|
+
*
|
|
95
|
+
* @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.
|
|
96
|
+
* @param fileSize Total size of the file on disk, used to derive the real record count.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseHeader(buf: Buffer, fileSize: number): EdfHeaderInfo;
|
|
99
|
+
/**
|
|
100
|
+
* The recording start as a zone-less wall clock, "YYYY-MM-DDTHH:MM:SS".
|
|
101
|
+
*
|
|
102
|
+
* EDF stores the start time as local wall-clock digits with no timezone anywhere in
|
|
103
|
+
* the format. `startDateTime` is built with Date.UTC purely so those digits survive a
|
|
104
|
+
* round trip unshifted, which makes it a carrier for the wall clock rather than a
|
|
105
|
+
* real instant. Serialising it with `toISOString()` would append a Z and assert UTC,
|
|
106
|
+
* and any reader converting to local time would then shift the recording by their own
|
|
107
|
+
* offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because
|
|
108
|
+
* the file genuinely does not say which zone it meant.
|
|
109
|
+
*/
|
|
110
|
+
export declare function formatWallClock(date: Date | null): string | null;
|
|
111
|
+
/** "EDF", "EDF+ (EDF+D)", "BDF", "BDF+ (EDF+C)". */
|
|
112
|
+
export declare function describeFormat(header: EdfHeader): string;
|
|
113
|
+
/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */
|
|
114
|
+
export declare function formatRate(hz: number): string;
|
|
115
|
+
//# sourceMappingURL=header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.d.ts","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,uDAAuD;AACvD,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAEvD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,aAAa,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,KAAK,EAAE,OAAO,CAAC;IACf,wEAAwE;IACxE,UAAU,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACrC,6EAA6E;IAC7E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAgED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAgUxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAIxD;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7C"}
|