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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sarthak Tayal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# edf2csv
|
|
2
|
+
|
|
3
|
+
Convert an EDF, EDF+ or BDF recording to CSV with one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx edf2csv recording.edf
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it. You get a `recording_csv/` folder you can open in pandas, R, MATLAB, or
|
|
10
|
+
Excel. Nothing is uploaded, nothing is installed permanently, and the numbers are
|
|
11
|
+
not altered on the way through.
|
|
12
|
+
|
|
13
|
+
Full documentation: **[edf2csv.vercel.app](https://edf2csv.vercel.app)**
|
|
14
|
+
|
|
15
|
+
## What you get
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
recording_csv/
|
|
19
|
+
├── signals.csv the data, one row per time point
|
|
20
|
+
├── channels.csv each channel's unit, sampling rate and range
|
|
21
|
+
├── annotations.csv EDF+ events (only if the file has them)
|
|
22
|
+
└── metadata.json recording details
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```csv
|
|
26
|
+
time_s,FP1-F7,F7-T7,T7-P7
|
|
27
|
+
0.000000,-17.778,39.267,-3.712
|
|
28
|
+
0.003906,0.195,0.195,0.195
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`time_s` is seconds from the start. Columns are the channel names from the file.
|
|
32
|
+
|
|
33
|
+
## Check before you convert
|
|
34
|
+
|
|
35
|
+
An hour of 23-channel EEG makes a 159 MB CSV with 921,600 rows, which only just
|
|
36
|
+
fits inside Excel's 1,048,576-row limit. Two hours does not. `--info` shows you what
|
|
37
|
+
you'd get, without writing anything:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx edf2csv recording.edf --info
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Want a piece instead of the whole thing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx edf2csv recording.edf --start 30m --duration 5m
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
-i, --info Show what's in the file, convert nothing
|
|
53
|
+
-o, --out <dir> Where to write it
|
|
54
|
+
-c, --channels <list> Only these channels, e.g. "EEG Fpz-Cz,ECG"
|
|
55
|
+
--start <time> Start here (30s, 5m, 1h30m, 00:30:00)
|
|
56
|
+
--duration <time> How much to convert
|
|
57
|
+
--end <time> Or stop here instead
|
|
58
|
+
--annotations-only Just the EDF+ events
|
|
59
|
+
--decimals <n> Force a number of decimal places
|
|
60
|
+
--checksum Put a SHA-256 of the input in metadata.json
|
|
61
|
+
-f, --force Overwrite the output folder
|
|
62
|
+
-q, --quiet Less output
|
|
63
|
+
--json Machine-readable summary on stdout
|
|
64
|
+
-h, --help Help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Exit codes: `0` fine, `1` couldn't read or write the file, `2` bad command.
|
|
68
|
+
|
|
69
|
+
## What it won't do to your data
|
|
70
|
+
|
|
71
|
+
**It won't resample.** Some EDF files mix rates: EEG at 256 Hz, temperature at
|
|
72
|
+
1 Hz. Squeezing those into one table means inventing samples, so each rate gets its
|
|
73
|
+
own file (`signals_256hz.csv`, `signals_1hz.csv`). A 1 Hz channel over 3 seconds
|
|
74
|
+
gets 3 rows, because that's how many readings exist.
|
|
75
|
+
|
|
76
|
+
For comparison, `mne.io.read_raw_edf` turns those same 3 readings into 768
|
|
77
|
+
interpolated values with no warning. That's the right call for MNE, which needs one
|
|
78
|
+
uniform array to run its analysis. It's the wrong call for a converter.
|
|
79
|
+
|
|
80
|
+
**It won't convert units.** Microvolts stay microvolts.
|
|
81
|
+
|
|
82
|
+
**It won't hide gaps.** EDF+D recordings have real breaks in time. They show up as
|
|
83
|
+
a jump in `time_s` rather than being closed up.
|
|
84
|
+
|
|
85
|
+
**It won't stay quiet about problems.** Truncated files, headers that contradict
|
|
86
|
+
the data, duplicate channel names, calibration that can't be applied. You get told,
|
|
87
|
+
in plain words, before you rely on the output.
|
|
88
|
+
|
|
89
|
+
## Is it right?
|
|
90
|
+
|
|
91
|
+
Values are checked against [pyEDFlib](https://github.com/holgern/pyedflib). On the
|
|
92
|
+
recordings used for testing, 129,536 sample values came out **bit-for-bit
|
|
93
|
+
identical**. Not close. Identical.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## When to use something else
|
|
100
|
+
|
|
101
|
+
This is a converter. It is the right tool when you want the numbers out of an EDF
|
|
102
|
+
file and into something else. It is the wrong tool for several neighbouring jobs, and
|
|
103
|
+
saying so is cheaper than you finding out later.
|
|
104
|
+
|
|
105
|
+
- **Analysing the signal.** Filtering, epoching, re-referencing, ICA, spectral work:
|
|
106
|
+
use [MNE-Python](https://mne.tools). It is excellent and built for exactly this.
|
|
107
|
+
- **Reading EDF inside Python.** [pyEDFlib](https://github.com/holgern/pyedflib) hands
|
|
108
|
+
you arrays directly, with no CSV in the middle. edf2csv is checked against it.
|
|
109
|
+
- **Writing EDF files.** This only reads them.
|
|
110
|
+
- **Viewing a recording.** [EDFbrowser](https://www.teuniz.net/edfbrowser/) is a real
|
|
111
|
+
viewer and will beat a spreadsheet every time.
|
|
112
|
+
- **Huge recordings you will analyse repeatedly.** CSV is a poor archival format; an
|
|
113
|
+
EDF roughly quadruples in size as text. Convert a window, or read the file directly.
|
|
114
|
+
|
|
115
|
+
Use edf2csv when the destination is a spreadsheet, a colleague who does not use
|
|
116
|
+
Python, a quick look at part of a recording, or a pipeline that speaks CSV.
|
|
117
|
+
|
|
118
|
+
## Notes
|
|
119
|
+
|
|
120
|
+
Needs Node 20+. No runtime dependencies. MIT licensed.
|
|
121
|
+
|
|
122
|
+
Reads EDF, EDF+ and BDF/BDF+ (BioSemi 24-bit). No filtering, no artifact removal,
|
|
123
|
+
no AI, no network calls. It reads a file and writes files.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output for the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain text with no colour codes, so piping to a file or a log
|
|
5
|
+
* produces exactly what appeared on screen.
|
|
6
|
+
*/
|
|
7
|
+
import type { Diagnostic } from '../edf/errors.js';
|
|
8
|
+
import type { EdfFile } from '../edf/reader.js';
|
|
9
|
+
import type { ConversionPlan } from '../convert/plan.js';
|
|
10
|
+
import type { ConvertResult } from '../convert/run.js';
|
|
11
|
+
/** The `--info` view: what is in this recording, and what would converting it produce. */
|
|
12
|
+
export declare function formatInfo(file: EdfFile, plan: ConversionPlan): string;
|
|
13
|
+
/** One line per diagnostic, prefixed so warnings are greppable. */
|
|
14
|
+
export declare function formatDiagnostics(diagnostics: readonly Diagnostic[]): string;
|
|
15
|
+
export declare function formatSummary(result: ConvertResult): string;
|
|
16
|
+
export declare function summaryJson(result: ConvertResult): string;
|
|
17
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAuBvD,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CA0DtE;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAO5E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAU3D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAczD"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output for the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain text with no colour codes, so piping to a file or a log
|
|
5
|
+
* produces exactly what appeared on screen.
|
|
6
|
+
*/
|
|
7
|
+
import { describeFormat, formatRate, formatWallClock } from '../edf/header.js';
|
|
8
|
+
import { formatBytes, formatDuration } from '../format/number.js';
|
|
9
|
+
function table(rows, alignRight) {
|
|
10
|
+
if (rows.length === 0)
|
|
11
|
+
return '';
|
|
12
|
+
const width = [];
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
row.forEach((cell, i) => {
|
|
15
|
+
width[i] = Math.max(width[i] ?? 0, cell.length);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return rows
|
|
19
|
+
.map((row) => row
|
|
20
|
+
.map((cell, i) => {
|
|
21
|
+
const w = width[i] ?? 0;
|
|
22
|
+
return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);
|
|
23
|
+
})
|
|
24
|
+
.join(' ')
|
|
25
|
+
.trimEnd())
|
|
26
|
+
.join('\n');
|
|
27
|
+
}
|
|
28
|
+
/** The `--info` view: what is in this recording, and what would converting it produce. */
|
|
29
|
+
export function formatInfo(file, plan) {
|
|
30
|
+
const { header } = file;
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push(`File ${file.path}`);
|
|
33
|
+
lines.push(`Format ${describeFormat(header)}`);
|
|
34
|
+
lines.push(`Recorded ${formatWallClock(header.startDateTime)?.replace('T', ' ') ??
|
|
35
|
+
`${header.startDateRaw} ${header.startTimeRaw} (unparseable)`}`);
|
|
36
|
+
lines.push(`Duration ${formatDuration(file.durationSeconds)} (${file.recordCount} records of ${header.recordDuration}s)`);
|
|
37
|
+
const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;
|
|
38
|
+
if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {
|
|
39
|
+
lines.push(`Time span ${formatDuration(elapsedSpan)} (includes discontinuities)`);
|
|
40
|
+
}
|
|
41
|
+
lines.push(`Size ${formatBytes(file.fileSize)}`);
|
|
42
|
+
if (header.patientId)
|
|
43
|
+
lines.push(`Patient ${header.patientId}`);
|
|
44
|
+
if (header.recordingId)
|
|
45
|
+
lines.push(`Recording ${header.recordingId}`);
|
|
46
|
+
const signals = file.dataSignals;
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`Channels ${signals.length} signal${signals.length === 1 ? '' : 's'}${file.annotationSignals.length > 0 ? ` + ${file.annotationSignals.length} annotation channel` : ''}`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
const rows = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];
|
|
51
|
+
const fileFor = new Map();
|
|
52
|
+
for (const group of plan.groups) {
|
|
53
|
+
for (const channel of group.channels)
|
|
54
|
+
fileFor.set(channel.signal.index, group.fileName);
|
|
55
|
+
}
|
|
56
|
+
for (const signal of signals) {
|
|
57
|
+
rows.push([
|
|
58
|
+
String(signal.index),
|
|
59
|
+
plan.columnNames.get(signal.index) ?? '',
|
|
60
|
+
signal.label,
|
|
61
|
+
signal.physicalDimension,
|
|
62
|
+
`${formatRate(signal.samplingRate)} Hz`,
|
|
63
|
+
`${signal.physicalMin} to ${signal.physicalMax}`,
|
|
64
|
+
fileFor.get(signal.index) ?? '(not selected)',
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
lines.push(table(rows, new Set([0])));
|
|
68
|
+
lines.push('');
|
|
69
|
+
if (plan.groups.length > 1) {
|
|
70
|
+
lines.push(`Sampling rates differ, so channels are written to ${plan.groups.length} files, one per rate. ` +
|
|
71
|
+
`No channel is resampled.`);
|
|
72
|
+
}
|
|
73
|
+
lines.push(`Would write ${plan.estimate.rows.toLocaleString('en-US')} rows, roughly ${formatBytes(plan.estimate.bytes)}.`);
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
/** One line per diagnostic, prefixed so warnings are greppable. */
|
|
77
|
+
export function formatDiagnostics(diagnostics) {
|
|
78
|
+
return diagnostics
|
|
79
|
+
.map((d) => {
|
|
80
|
+
const head = `${d.severity === 'warning' ? 'warning' : 'note'}: ${d.message}`;
|
|
81
|
+
return d.hint ? `${head}\n ${d.hint}` : head;
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
}
|
|
85
|
+
export function formatSummary(result) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
const rows = [];
|
|
88
|
+
for (const file of result.files) {
|
|
89
|
+
rows.push([` ${file.name}`, file.rows.toLocaleString('en-US'), file.name.endsWith('.csv') ? 'rows' : '']);
|
|
90
|
+
}
|
|
91
|
+
lines.push(`Wrote ${result.outputDir}`);
|
|
92
|
+
lines.push(table(rows, new Set([1])));
|
|
93
|
+
lines.push(`Done in ${(result.elapsedMs / 1000).toFixed(1)}s.`);
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
export function summaryJson(result) {
|
|
97
|
+
return JSON.stringify({
|
|
98
|
+
output_dir: result.outputDir,
|
|
99
|
+
files: result.files,
|
|
100
|
+
annotations: result.annotationCount,
|
|
101
|
+
duration_seconds: result.file.durationSeconds,
|
|
102
|
+
records: result.file.recordCount,
|
|
103
|
+
elapsed_ms: result.elapsedMs,
|
|
104
|
+
warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),
|
|
105
|
+
}, null, 2);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Command-line entry point.
|
|
4
|
+
*
|
|
5
|
+
* Output discipline: anything that is the *result* of a command goes to stdout
|
|
6
|
+
* (`--info`'s channel table, `--json`'s summary). Progress, warnings and the
|
|
7
|
+
* conversion summary go to stderr, so a conversion can be run in a pipeline without
|
|
8
|
+
* its chatter contaminating the data.
|
|
9
|
+
*/
|
|
10
|
+
import { defaultOutputDir } from './convert/run.js';
|
|
11
|
+
export declare function main(argv: readonly string[]): Promise<number>;
|
|
12
|
+
export { defaultOutputDir };
|
|
13
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAUH,OAAO,EAA4B,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAuE9E,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqLnE;AAqCD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Command-line entry point.
|
|
4
|
+
*
|
|
5
|
+
* Output discipline: anything that is the *result* of a command goes to stdout
|
|
6
|
+
* (`--info`'s channel table, `--json`'s summary). Progress, warnings and the
|
|
7
|
+
* conversion summary go to stderr, so a conversion can be run in a pipeline without
|
|
8
|
+
* its chatter contaminating the data.
|
|
9
|
+
*/
|
|
10
|
+
import { parseArgs } from 'node:util';
|
|
11
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
12
|
+
import { realpathSync } from 'node:fs';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
import { EdfError } from './edf/errors.js';
|
|
15
|
+
import { EdfFile } from './edf/reader.js';
|
|
16
|
+
import { buildPlan } from './convert/plan.js';
|
|
17
|
+
import { ConversionError, convert, defaultOutputDir } from './convert/run.js';
|
|
18
|
+
import { ChannelSelectionError } from './convert/channels.js';
|
|
19
|
+
import { TimeRangeError, parseTimeSpec } from './convert/time-range.js';
|
|
20
|
+
import { deriveRecordStarts } from './convert/timing.js';
|
|
21
|
+
import { formatDiagnostics, formatInfo, formatSummary, summaryJson } from './cli/report.js';
|
|
22
|
+
import { VERSION } from './version.js';
|
|
23
|
+
const USAGE = `edf2csv ${VERSION}
|
|
24
|
+
Convert EDF, EDF+ and BDF recordings to CSV
|
|
25
|
+
|
|
26
|
+
Usage
|
|
27
|
+
edf2csv <recording.edf> [options]
|
|
28
|
+
|
|
29
|
+
Options
|
|
30
|
+
-i, --info Show the recording's structure and estimated output size,
|
|
31
|
+
without converting anything
|
|
32
|
+
-o, --out <dir> Output directory (default: <recording>_csv beside the input)
|
|
33
|
+
-c, --channels <list> Only these channels, comma-separated. Use #N to pick a
|
|
34
|
+
channel by position when two share a label
|
|
35
|
+
--start <time> Begin at this offset (30s, 5m, 1h30m, 00:30:00)
|
|
36
|
+
--duration <time> Convert this much
|
|
37
|
+
--end <time> Stop at this offset (instead of --duration)
|
|
38
|
+
--annotations-only Write only the EDF+ annotations, no signal data
|
|
39
|
+
--decimals <n> Fix the decimal places instead of deriving them per channel
|
|
40
|
+
--checksum Record a SHA-256 of the input in metadata.json
|
|
41
|
+
-f, --force Overwrite the output directory if it exists
|
|
42
|
+
-q, --quiet Suppress the summary; warnings and errors still print
|
|
43
|
+
--json Print a machine-readable summary to stdout
|
|
44
|
+
-h, --help Show this help
|
|
45
|
+
-V, --version Show the version
|
|
46
|
+
|
|
47
|
+
Output
|
|
48
|
+
A directory containing signals.csv, channels.csv, metadata.json, and
|
|
49
|
+
annotations.csv when the recording carries EDF+ annotations. Channels recorded
|
|
50
|
+
at different sampling rates are written to separate files, never resampled.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
edf2csv recording.edf
|
|
54
|
+
edf2csv recording.edf --info
|
|
55
|
+
edf2csv recording.edf --channels "EEG Fpz-Cz,ECG" --out ./converted
|
|
56
|
+
edf2csv recording.edf --start 30m --duration 5m
|
|
57
|
+
edf2csv recording.edf --annotations-only
|
|
58
|
+
`;
|
|
59
|
+
/** A problem with how the command was invoked, as opposed to a problem with the file. */
|
|
60
|
+
class OptionError extends Error {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'OptionError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const EXIT_OK = 0;
|
|
67
|
+
const EXIT_ERROR = 1;
|
|
68
|
+
const EXIT_USAGE = 2;
|
|
69
|
+
/**
|
|
70
|
+
* Piping into a consumer that exits early (`| head -1`) closes our stdout, and the
|
|
71
|
+
* next write raises EPIPE. That is normal in a shell pipeline, not an error worth a
|
|
72
|
+
* stack trace, so it is swallowed while anything else still surfaces.
|
|
73
|
+
*/
|
|
74
|
+
function ignoreBrokenPipe(stream) {
|
|
75
|
+
stream.on('error', (error) => {
|
|
76
|
+
if (error.code === 'EPIPE') {
|
|
77
|
+
process.exitCode = 0;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function main(argv) {
|
|
84
|
+
ignoreBrokenPipe(process.stdout);
|
|
85
|
+
ignoreBrokenPipe(process.stderr);
|
|
86
|
+
let values;
|
|
87
|
+
let positionals;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = parseArgs({
|
|
90
|
+
args: [...argv],
|
|
91
|
+
allowPositionals: true,
|
|
92
|
+
strict: true,
|
|
93
|
+
options: {
|
|
94
|
+
info: { type: 'boolean', short: 'i' },
|
|
95
|
+
out: { type: 'string', short: 'o' },
|
|
96
|
+
channels: { type: 'string', short: 'c', multiple: true },
|
|
97
|
+
start: { type: 'string' },
|
|
98
|
+
duration: { type: 'string' },
|
|
99
|
+
end: { type: 'string' },
|
|
100
|
+
'annotations-only': { type: 'boolean' },
|
|
101
|
+
decimals: { type: 'string' },
|
|
102
|
+
checksum: { type: 'boolean' },
|
|
103
|
+
force: { type: 'boolean', short: 'f' },
|
|
104
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
105
|
+
json: { type: 'boolean' },
|
|
106
|
+
help: { type: 'boolean', short: 'h' },
|
|
107
|
+
version: { type: 'boolean', short: 'V' },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
values = parsed.values;
|
|
111
|
+
positionals = parsed.positionals;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
process.stderr.write(`${message(error)}\n\nRun edf2csv --help to see the options.\n`);
|
|
115
|
+
return EXIT_USAGE;
|
|
116
|
+
}
|
|
117
|
+
if (values['help'] === true) {
|
|
118
|
+
process.stdout.write(USAGE);
|
|
119
|
+
return EXIT_OK;
|
|
120
|
+
}
|
|
121
|
+
if (values['version'] === true) {
|
|
122
|
+
process.stdout.write(`${VERSION}\n`);
|
|
123
|
+
return EXIT_OK;
|
|
124
|
+
}
|
|
125
|
+
if (positionals.length === 0) {
|
|
126
|
+
process.stderr.write(`No input file given.\n\n${USAGE}`);
|
|
127
|
+
return EXIT_USAGE;
|
|
128
|
+
}
|
|
129
|
+
if (positionals.length > 1) {
|
|
130
|
+
process.stderr.write(`Expected one input file but got ${positionals.length}: ${positionals.join(', ')}\n` +
|
|
131
|
+
`Convert them one at a time, or use a shell loop.\n`);
|
|
132
|
+
return EXIT_USAGE;
|
|
133
|
+
}
|
|
134
|
+
const input = positionals[0];
|
|
135
|
+
const quiet = values['quiet'] === true;
|
|
136
|
+
const asJson = values['json'] === true;
|
|
137
|
+
try {
|
|
138
|
+
const channels = splitChannels(values['channels']);
|
|
139
|
+
const start = optionalTime(values['start'], '--start');
|
|
140
|
+
const duration = optionalTime(values['duration'], '--duration');
|
|
141
|
+
const end = optionalTime(values['end'], '--end');
|
|
142
|
+
const decimals = optionalDecimals(values['decimals']);
|
|
143
|
+
if (values['info'] === true) {
|
|
144
|
+
const file = await EdfFile.open(input);
|
|
145
|
+
try {
|
|
146
|
+
const annotationData = file.annotationSignals.length > 0
|
|
147
|
+
? await file.readAnnotations()
|
|
148
|
+
: { annotations: [], recordStarts: [], malformed: 0 };
|
|
149
|
+
const timing = deriveRecordStarts(file, annotationData);
|
|
150
|
+
const plan = buildPlan({
|
|
151
|
+
signals: file.header.signals,
|
|
152
|
+
recordDuration: file.header.recordDuration,
|
|
153
|
+
recordCount: file.recordCount,
|
|
154
|
+
hasAnnotationChannel: file.annotationSignals.length > 0,
|
|
155
|
+
recordStarts: timing.starts,
|
|
156
|
+
}, {
|
|
157
|
+
channels,
|
|
158
|
+
start,
|
|
159
|
+
duration,
|
|
160
|
+
end,
|
|
161
|
+
decimals,
|
|
162
|
+
annotationsOnly: values['annotations-only'] === true,
|
|
163
|
+
});
|
|
164
|
+
plan.diagnostics.push(...timing.diagnostics);
|
|
165
|
+
process.stdout.write(`${formatInfo(file, plan)}\n`);
|
|
166
|
+
const diagnostics = [...file.diagnostics, ...plan.diagnostics];
|
|
167
|
+
if (diagnostics.length > 0) {
|
|
168
|
+
process.stderr.write(`\n${formatDiagnostics(diagnostics)}\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await file.close();
|
|
173
|
+
}
|
|
174
|
+
return EXIT_OK;
|
|
175
|
+
}
|
|
176
|
+
const showProgress = !quiet && !asJson && process.stderr.isTTY === true;
|
|
177
|
+
let lastTick = 0;
|
|
178
|
+
const destination = typeof values['out'] === 'string' ? values['out'] : defaultOutputDir(input);
|
|
179
|
+
/*
|
|
180
|
+
Interrupting a conversion leaves a CSV that stops mid-recording but is still
|
|
181
|
+
perfectly well-formed, so nothing about the file itself reveals that half the
|
|
182
|
+
data is missing. Saying so on the way out is the whole point of a tool that
|
|
183
|
+
claims it will not go quiet when something is wrong.
|
|
184
|
+
*/
|
|
185
|
+
const onInterrupt = (signal) => {
|
|
186
|
+
if (showProgress)
|
|
187
|
+
process.stderr.write('\r\u001b[K');
|
|
188
|
+
process.stderr.write(`\ninterrupted (${signal}): the conversion stopped part way through.\n` +
|
|
189
|
+
` Files already written to "${destination}" are incomplete and should not be used.\n`);
|
|
190
|
+
// 128 + signal number, the conventional exit status for dying to a signal.
|
|
191
|
+
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
192
|
+
};
|
|
193
|
+
process.once('SIGINT', onInterrupt);
|
|
194
|
+
process.once('SIGTERM', onInterrupt);
|
|
195
|
+
const result = await convert(input, {
|
|
196
|
+
outputDir: typeof values['out'] === 'string' ? values['out'] : undefined,
|
|
197
|
+
channels,
|
|
198
|
+
start,
|
|
199
|
+
duration,
|
|
200
|
+
end,
|
|
201
|
+
decimals,
|
|
202
|
+
annotationsOnly: values['annotations-only'] === true,
|
|
203
|
+
checksum: values['checksum'] === true,
|
|
204
|
+
force: values['force'] === true,
|
|
205
|
+
onProgress: showProgress
|
|
206
|
+
? (progress) => {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
if (now - lastTick < 100)
|
|
209
|
+
return;
|
|
210
|
+
lastTick = now;
|
|
211
|
+
const percent = progress.recordsTotal === 0 ? 100 : Math.floor((progress.recordsDone / progress.recordsTotal) * 100);
|
|
212
|
+
process.stderr.write(`\r converting… ${percent}%`);
|
|
213
|
+
}
|
|
214
|
+
: undefined,
|
|
215
|
+
});
|
|
216
|
+
process.off('SIGINT', onInterrupt);
|
|
217
|
+
process.off('SIGTERM', onInterrupt);
|
|
218
|
+
if (showProgress)
|
|
219
|
+
process.stderr.write('\r\u001b[K');
|
|
220
|
+
if (result.diagnostics.length > 0 && !asJson) {
|
|
221
|
+
process.stderr.write(`${formatDiagnostics(result.diagnostics)}\n\n`);
|
|
222
|
+
}
|
|
223
|
+
if (asJson) {
|
|
224
|
+
process.stdout.write(`${summaryJson(result)}\n`);
|
|
225
|
+
}
|
|
226
|
+
else if (!quiet) {
|
|
227
|
+
process.stderr.write(`${formatSummary(result)}\n`);
|
|
228
|
+
}
|
|
229
|
+
return EXIT_OK;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error instanceof EdfError || error instanceof ConversionError) {
|
|
233
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
234
|
+
if (error.hint)
|
|
235
|
+
process.stderr.write(` ${error.hint}\n`);
|
|
236
|
+
return EXIT_ERROR;
|
|
237
|
+
}
|
|
238
|
+
if (error instanceof ChannelSelectionError ||
|
|
239
|
+
error instanceof TimeRangeError ||
|
|
240
|
+
error instanceof OptionError) {
|
|
241
|
+
process.stderr.write(`error: ${error.message}\n`);
|
|
242
|
+
return EXIT_USAGE;
|
|
243
|
+
}
|
|
244
|
+
process.stderr.write(`error: ${message(error)}\n`);
|
|
245
|
+
return EXIT_ERROR;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function splitChannels(raw) {
|
|
249
|
+
if (raw === undefined)
|
|
250
|
+
return undefined;
|
|
251
|
+
const list = Array.isArray(raw) ? raw : [String(raw)];
|
|
252
|
+
const terms = list.flatMap((entry) => entry.split(',')).map((t) => t.trim()).filter((t) => t !== '');
|
|
253
|
+
// Returning undefined here would mean "no --channels given" and convert everything,
|
|
254
|
+
// which is the opposite of what someone passing an empty list is asking for.
|
|
255
|
+
if (terms.length === 0) {
|
|
256
|
+
throw new OptionError('--channels was given but lists no channel names.');
|
|
257
|
+
}
|
|
258
|
+
return terms;
|
|
259
|
+
}
|
|
260
|
+
function optionalTime(raw, option) {
|
|
261
|
+
if (raw === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
return parseTimeSpec(String(raw), option);
|
|
264
|
+
}
|
|
265
|
+
function optionalDecimals(raw) {
|
|
266
|
+
if (raw === undefined)
|
|
267
|
+
return undefined;
|
|
268
|
+
const text = String(raw).trim();
|
|
269
|
+
// Number('') is 0, which would quietly round every physical value to a whole number.
|
|
270
|
+
if (text === '') {
|
|
271
|
+
throw new OptionError('--decimals needs a number, for example --decimals 3.');
|
|
272
|
+
}
|
|
273
|
+
const value = Number(text);
|
|
274
|
+
if (!Number.isInteger(value) || value < 0 || value > 15) {
|
|
275
|
+
throw new OptionError(`--decimals must be a whole number between 0 and 15, got "${String(raw)}".`);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
function message(error) {
|
|
280
|
+
return error instanceof Error ? error.message : String(error);
|
|
281
|
+
}
|
|
282
|
+
export { defaultOutputDir };
|
|
283
|
+
/**
|
|
284
|
+
* Whether this file was executed rather than imported.
|
|
285
|
+
*
|
|
286
|
+
* npm installs a bin as a symlink (node_modules/.bin/edf2csv -> ../edf2csv/dist/cli.js),
|
|
287
|
+
* and that is the path `npx` runs. In that case process.argv[1] is the symlink while
|
|
288
|
+
* import.meta.url is already resolved to the real file, so comparing the two directly
|
|
289
|
+
* reports "imported" and the command silently does nothing. Both sides are resolved
|
|
290
|
+
* through realpath before comparing.
|
|
291
|
+
*/
|
|
292
|
+
function isMainModule() {
|
|
293
|
+
const entry = process.argv[1];
|
|
294
|
+
if (entry === undefined)
|
|
295
|
+
return false;
|
|
296
|
+
try {
|
|
297
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// An unreadable or deleted entry path is not a reason to refuse to run.
|
|
301
|
+
return pathToFileURL(entry).href === import.meta.url;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const invokedDirectly = isMainModule();
|
|
305
|
+
if (invokedDirectly) {
|
|
306
|
+
main(process.argv.slice(2))
|
|
307
|
+
.then((code) => {
|
|
308
|
+
process.exitCode = code;
|
|
309
|
+
})
|
|
310
|
+
.catch((error) => {
|
|
311
|
+
process.stderr.write(`error: ${message(error)}\n`);
|
|
312
|
+
process.exitCode = EXIT_ERROR;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|