Read EDF files
in JavaScript.
Read EEG and biosignal files directly in JavaScript and TypeScript. Supports EDF, EDF+, BDF and BDF+, in the browser and in Node.
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,
});
if (chunk === undefined) throw new Error('no records cover that window');
const [series] = chunk.signals;
if (series === undefined) throw new Error('no signal in that chunk');
const microvolts = toPhysical(fp1, series.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 |
Those rows describe the packages as published to npm. The strongest prior art is @epicurrents/edf-reader, whose repository does random access and BDF and whose published artifact predates a good deal of that work; it is worth using if you want a whole viewer framework.See the full comparison, which says which claims that survey verified and which it did not.
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.
// Reading across a gap needs an index that has seen every onset; the one openEdf
// returns has seen two, and says so rather than guessing.
const index = await buildRecordIndex(recording);
const chunks = await readWindow({ ...recording, index }, {
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.