/** * EDF / EDF+ header parsing. * * Layout (all fields are ASCII, left-justified, space-padded): * * fixed header, 256 bytes * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF) * 8 80 patient identification * 88 80 recording identification * 168 8 start date dd.mm.yy * 176 8 start time hh.mm.ss * 184 8 number of bytes in the header record * 192 44 reserved ('EDF+C' / 'EDF+D' live here) * 236 8 number of data records (-1 if unknown) * 244 8 duration of a data record, in seconds (may be fractional) * 252 4 number of signals (ns) * * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major: * all ns labels, then all ns transducer types, and so on. * ns * 16 label * ns * 80 transducer type * ns * 8 physical dimension * ns * 8 physical minimum * ns * 8 physical maximum * ns * 8 digital minimum * ns * 8 digital maximum * ns * 80 prefiltering * ns * 8 number of samples in each data record * ns * 32 reserved */ import type { Diagnostic } from './errors.js'; /** Label the EDF+ spec reserves for the annotations channel. */ export declare const ANNOTATIONS_LABEL = "EDF Annotations"; /** BDF+ uses its own spelling for the same channel. */ export declare const BDF_ANNOTATIONS_LABEL = "BDF Annotations"; export declare const FIXED_HEADER_BYTES = 256; export declare const SIGNAL_HEADER_BYTES = 256; export interface EdfSignal { /** Position in the file, 0-based. Stable even when labels collide. */ index: number; label: string; transducer: string; physicalDimension: string; physicalMin: number; physicalMax: number; digitalMin: number; digitalMax: number; prefiltering: string; samplesPerRecord: number; reserved: string; /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */ isAnnotations: boolean; /** samplesPerRecord / recordDuration, in Hz. */ samplingRate: number; /** Byte offset of this signal's samples within one data record. */ byteOffsetInRecord: number; } export interface EdfHeader { version: string; patientId: string; recordingId: string; /** Raw 'dd.mm.yy' as written in the file. */ startDateRaw: string; /** Raw 'hh.mm.ss' as written in the file. */ startTimeRaw: string; /** Resolved start instant, or null when the file's date/time fields are unusable. */ startDateTime: Date | null; /** * Header size in bytes, computed from the signal count rather than read from the field. * * Every data record offset is derived from this, so it has to be the one the layout * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH, * and trusting the field over the arithmetic would put every sample at the wrong offset. */ headerBytes: number; /** * What the header's own length field says, which need not be the above. * * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact * about the file, and a caller checking how a recording was written should be able to see * what it claimed rather than only what was believed. */ declaredHeaderBytes: number; reserved: string; isEdfPlus: boolean; /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */ isBdf: boolean; /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */ continuity: 'EDF+C' | 'EDF+D' | null; /** As declared in the header. -1 means "unknown", which the spec permits. */ declaredRecordCount: number; recordDuration: number; signalCount: number; signals: EdfSignal[]; bytesPerSample: number; recordBytes: number; } /** Everything derived by combining the header with the file's real size. */ export interface EdfHeaderInfo { header: EdfHeader; /** Record count implied by the actual file size — the one we trust for reading. */ recordCount: number; /** Bytes after the last complete data record. */ trailingBytes: number; diagnostics: Diagnostic[]; } /** * Whether a spreadsheet reads this field as the start of a formula rather than as text. * * `=` and `@` unconditionally; every list of these characters names two more, and this had * `-` as an exception with the reason written out on the warnings page: "a lone `-` is a * real convention for no unit ... and neither is evaluated unless what follows it parses as a * formula". Which is the condition, and it was not being applied — nothing with a leading * minus was flagged at all. A channel labelled `-2+3` opens as a column headed `1`, and * `-HYPERLINK("http://...","EEG")` is a name the spreadsheet resolves, in silence. * * So the exception is what it says it is rather than the whole character. A lone `-` is left * as text by every spreadsheet and is not flagged; a field that is entirely a number reads as * that number, which is what the header says, and is not flagged either. Anything else after * the minus is arithmetic or a name. * * And `+` takes the same exception, which it did not. It is the same rule in the spreadsheet — * Lotus compatibility, which converts a leading `+` or `-` to a formula when what follows one * parses as a formula and leaves it as text when it does not — so the two signs cannot differ * here for a reason that comes from the sign. A channel labelled `+100` was warned about as * something a spreadsheet "reads as the start of a formula rather than as text", over a cell * that opens as 100, which is what the header says; `-100` beside it said nothing, and under * `--strict` the difference was an exit code. `+1+1` is still arithmetic and still flagged. */ export declare function startsFormula(text: string): boolean; /** * How many signals the fixed header says there are, read exactly as `parseHeader` will. * * `EdfFile.open` needs this before it can know how much header to read, and it used to work * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike * every other numeric field here. A header written with a comma decimal separator, which * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was * therefore never given its signal headers at all, and the file died on a message that * contradicted itself: "needs a 768-byte header, but the file is only 848 bytes". * * Sharing the parse is what keeps the two from disagreeing again about which files are * readable. Null means "not a usable count", and the caller reads no further header — the * real error then comes from `parseHeader`, which is the one place that decides. */ export declare function peekSignalCount(fixed: Uint8Array): number | null; /** * Parse the fixed 256-byte header plus the per-signal header block. * * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes. * @param fileSize Total size of the file on disk, used to derive the real record count. */ export declare function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo; /** * The recording start as a zone-less wall clock, "YYYY-MM-DDTHH:MM:SS". * * EDF stores the start time as local wall-clock digits with no timezone anywhere in * the format. `startDateTime` is built with Date.UTC purely so those digits survive a * round trip unshifted, which makes it a carrier for the wall clock rather than a * real instant. Serialising it with `toISOString()` would append a Z and assert UTC, * and any reader converting to local time would then shift the recording by their own * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because * the file genuinely does not say which zone it meant. */ export declare function formatWallClock(date: Date | null): string | null; /** * The recording's format, as `--info`, `metadata.json` and `--json` all name it. * * `"EDF"`, `"BDF"`, or one of `"EDF+ (continuous)"`, `"EDF+ (discontinuous)"`, * `"BDF+ (continuous)"`, `"BDF+ (discontinuous)"`. A BDF+ file reports its own spelling even * though `continuity` normalises the marker to the `EDF+` form. * * This said `EDF+ (EDF+D)` and `BDF+ (EDF+C)`, which are not strings it can return — the * parenthetical is the word, not the marker. It is a one-line doc comment on a public export, * so it is what a TypeScript consumer's editor shows and what `dist/edf/header.d.ts` ships, * and the value it describes is `recording.format` in every metadata.json this tool writes. * A consumer branching on the tooltip's spelling never matches. Every documentation page had * it right; this was the only place that did not. */ export declare function describeFormat(header: EdfHeader): string; /** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */ export declare function formatRate(hz: number): string; /** * Renders a group of rates so that rates which differ read as differing. * * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double, * and belongs on screen as 300. Two rates separated by less than that round to one * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used "2 different * sampling rates (0.000001 Hz, 0.000001 Hz)" and named both files the same thing. * * That is the contradiction the exponent fallback above already removes for rates that * round away to zero; this is the same one a step further out. On a collision every rate * in the group switches to its shortest exact form, which is unique for distinct values, * rather than only the pair that collided — one column in one notation reads better than * two. */ export declare function formatRates(rates: readonly number[]): string[];