Read EDF files
in JavaScript.
edfcore opens EDF, EDF+, BDF and BDF+ recordings (EEG, sleep studies, ECG) in the browser and in Node. Read any ten seconds of a twelve-hour file without loading the rest. When a file is wrong, you get an error that names the byte.
Zero dependencies. TypeScript types included. MIT licensed.
Ten seconds out of twelve hours reads ten seconds of bytes.
An EDF file stores every channel interleaved inside fixed-size data records, so a time window maps to a contiguous run of records. edfcore reads exactly that run. Opening a file reads the header and nothing else. It doesn't scan.
import { openEdf, blobSource, getSignal, readWindow, toPhysical } from 'edfcore';
const recording = await openEdf(blobSource(file));
const fp1 = getSignal(recording.header, 'Fp1');
const [chunk] = await readWindow(recording, {
signalIndices: [fp1.index],
startSeconds: 30,
durationSeconds: 10,
});
const microvolts = toPhysical(fp1, chunk.signals[0].digital);Python has the tooling for EDF. JavaScript didn't.
This is every EDF package on npm, surveyed in mid-2026:
| Capability | Before edfcore |
|---|---|
| Ships TypeScript types | No standalone reader does |
| Random access to a time window | Nothing published — the file is loaded whole |
| BDF / 24-bit samples | No published package can read them |
| Discontinuous EDF+D | Rejected, or silently decoded as contiguous |
| Typed, located errors | console.warn and null, or bare thrown strings |
| Header validation | Essentially none |
The strongest prior art is @epicurrents/edf-reader, worth using if you want a whole viewer framework.See the full comparison.
A bad file either throws or reports itself.
One rule decides which. If edfcore can't continue without inventing something, it throws. If it can continue on what the file says, it records a diagnostic on the result. There's no third option, and nothing is written to the console.
A header that declares digitalMinimum === digitalMaximum defines no scale: the gain is a division by zero. edfcore sets signal.scale toundefined, so reading the gain without checking is a compile error andtoPhysical throws at runtime. EDFlib substitutes a gain of 1 here and returns ADC counts labelled as microvolts.
const recording = await openEdf(source);
for (const problem of recording.header.diagnostics) {
console.log(problem.code, problem.byteOffset, problem.message);
}
// DEGENERATE_DIGITAL_RANGE 3400 signal 7 "EMG Chin" declares digitalMinimum ===
// digitalMaximum === 0, so physical scale is undefined (division by zero). …Discontinuous recordings.
Sleep and long-term-monitoring studies are routinely discontinuous. Most readers either reject those files or decode them as though the gaps weren't there, producing a timeline that's wrong by hours.
readWindow always returns an array of chunks, one per contiguous run, including for continuous files. A window inside a gap returns an empty array.
// A discontinuous recording is a list of runs, never one continuous block.
const chunks = await readWindow(recording, {
signalIndices: [0],
startSeconds: 0,
durationSeconds: 7200,
});
chunks.length; // one chunk per contiguous run
chunks[1].precededByGap; // EdfGap — where the recorder stoppedTry it on your own file.
The inspector runs in your browser. Nothing is uploaded, because there's no server to upload to. That's the same reason edfcore suits clinical and research data.