edf2csv 0.6.1 → 0.6.3
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/dist/edf/header.js +1 -1
- package/dist/edf/header.js.map +1 -1
- package/dist/edf/reader.js +5 -2
- package/dist/edf/reader.js.map +1 -1
- package/docs/CHANGELOG.md +18 -0
- package/package.json +1 -1
package/dist/edf/header.js
CHANGED
|
@@ -129,7 +129,7 @@ export function parseHeader(buf, fileSize) {
|
|
|
129
129
|
const diagnostics = [];
|
|
130
130
|
const sawComma = { value: false };
|
|
131
131
|
if (buf.length < FIXED_HEADER_BYTES) {
|
|
132
|
-
throw new EdfError('FILE_TOO_SMALL', `File is ${fileSize}
|
|
132
|
+
throw new EdfError('FILE_TOO_SMALL', `File is ${counted(fileSize, 'byte')}; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`);
|
|
133
133
|
}
|
|
134
134
|
// BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores
|
|
135
135
|
// 3-byte samples instead of 2. Everything else about the layout is identical.
|
package/dist/edf/header.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, listed } from '../format/list.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const count = Number(normaliseNumberField(dec(fixed, 252, 4)).text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`);\n }\n const n = Number(text);\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n 'The file may be truncated, byte-shifted, or not an EDF file at all.',\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n );\n }\n return n;\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n */\nfunction resolveStartDateTime(dateRaw: string, timeRaw: string): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const year = yy >= 85 ? 1900 + yy : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${fileSize} bytes; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`,\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${signalCount} signals; expected at least 1.`,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${recordDuration}s; expected a positive number.`,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${counted(signalCount, 'signal')}, which needs a ${expectedHeaderBytes}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${fileSize} bytes.`\n : `but only ${buf.length} bytes of it were handed to the parser.`),\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${headerBytes} bytes, but ${counted(signalCount, 'signal')} ` +\n `${signalCount === 1 ? 'requires' : 'require'} ${expectedHeaderBytes} bytes. ` +\n `Using the value computed from the signal count.`,\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n /*\n All four free-text fields, not the two that were checked.\n\n `transducer` and `prefiltering` are free text out of the header exactly as the label\n and the unit are, and they land in channels.csv exactly as the unit does — so an ESC\n byte in a transducer field reached the CSV raw with nothing said, and `cat\n channels.csv` would drive the terminal. That is the hazard this warning exists for,\n two columns over. 0.5.71 made it name which field carries them; this is the rest of\n the fields it can name.\n */\n const fields = [\n ['label', label],\n ['unit', physicalDimension],\n ['transducer', transducer],\n ['prefiltering', prefiltering],\n ] as const;\n const affected = fields.filter(([, text]) => [...text].some(isControlCharacter));\n const control = affected.flatMap(([, text]) => [...text].filter(isControlCharacter));\n if (control.length > 0) {\n const shown = [...new Set(control)]\n .map((c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`)\n .join(', ');\n const plural = control.length === 1 ? '' : 's';\n const inLabel = affected.some(([name]) => name === 'label');\n // \"label and unit\", not \"label, unit\" — `listed` is for long enumerations that get\n // truncated, and this is a sentence with at most four items in it.\n const names = affected.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; the other three are cells of channels.csv and nothing else.\n // Named down to the cell when there is one of them, because that is the answer to\n // \"where did it go\" — `channels.csv` alone leaves a reader scanning fourteen columns.\n const cells = affected.filter(([name]) => name !== 'label').map(([name]) => name);\n const where =\n cells.length === 1 ? `channels.csv's ${cells[0] as string} cell` : 'channels.csv';\n const lands =\n inLabel && cells.length > 0\n ? `which will appear in the CSV column name and in ${where}`\n : inLabel\n ? 'which will appear in the CSV column name'\n : `which will appear in ${where}`;\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${affected.length === 1 ? 'contains' : 'contain'} ` +\n `${control.length} control character${plural} (${shown}), ${lands} exactly as the ` +\n `header has ${control.length === 1 ? 'it' : 'them'}.`,\n hint:\n /*\n Every branch has to print a command that works.\n\n The middle one quoted the label back, which is right until the label is empty:\n an unlabelled channel got `--channels \"\"`, and that exits 2 with \"--channels was\n given but lists no channel names\". A hint whose command fails is worse than no\n hint, and this warning's whole job is to say how to reach a channel whose header\n text you cannot type. `EMPTY_LABEL` already says the position is the only way in\n for such a channel; so does this now.\n\n A comma is the third way. `--channels` separates names with one, and splits on\n every occurrence, so a channel labelled `EEG Fpz-Cz, ref` cannot be selected by\n name at all: the quoted-back advice printed `--channels \"EEG Fpz-Cz, ref\"`, which\n exits 2 with `No channel named \"EEG Fpz-Cz\"` — a channel the file does not have,\n named after half of one it does. Commas in labels are ordinary, since EDF labels\n are free text, and the CSV header quotes them; only this one hint claimed\n something about them that isn't so.\n */\n (inLabel || label === '' || label.includes(',')\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n `since ${\n inLabel\n ? 'the name cannot be typed'\n : label === ''\n ? 'it has no label'\n : 'a comma in the label would read as two names'\n }. `\n : `The column name is unaffected, so --channels \"${label}\" still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n /*\n Too large to represent, and too small — the second was silent.\n\n The gain is the span divided by the digital range, and a span of 2e-320 over 65,535\n codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The\n scaler's flat-range branch then handed every code the same physical value, and a\n channel of 65,536 distinct readings became one repeated number with nothing raised at\n all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.\n\n Both are the same fact about the header — the span cannot be turned into a mapping —\n so both get this code, and both leave the cells empty rather than filling them with a\n value the header cannot justify.\n */\n const span = physicalMax - physicalMin;\n const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;\n if (!Number.isFinite(span) || underflowed) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +\n `represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n });\n }\n\n /*\n A timestamp that is not one.\n\n EDF gives the start date and time eight characters each, and nothing stops a writer\n putting `32.13.99` and `25.61.61` there. `--info` has always echoed the raw fields with\n \"(unparseable)\" beside them, but nothing was raised: the conversion exited 0, `--strict`\n passed, and metadata.json recorded `start_datetime_local: null` with no note against it.\n\n Every other unusable header field reports itself — a degenerate digital range, a physical\n span that cannot be represented, a comma decimal separator, a header whose declared size\n disagrees with its signal count. This was the one that did not, and it is the field\n output-files points at for turning `time_s` into an absolute instant.\n */\n if (resolveStartDateTime(startDateRaw, startTimeRaw) === null) {\n diagnostics.push({\n code: 'START_TIME_UNREADABLE',\n severity: 'warning',\n message:\n `The header's start date and time (\"${startDateRaw}\" and ` +\n `\"${startTimeRaw}\") are not a date and a time, so the recording has ` +\n `no start instant.`,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and metadata.json records ' +\n 'start_datetime_local as null.',\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `${indices.length} signals share the label \"${label}\" (positions ${indices.join(', ')}).`,\n hint: 'Their columns are suffixed with the signal number so they stay distinguishable.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n throw new EdfError('FILE_TOO_SMALL', `File is smaller than its own header.`);\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n /*\n Which of the two, and with the numbers.\n\n \"The recording was probably interrupted before any data was written\" is right about an\n empty file and wrong about the other way to get here: a header declaring records larger\n than the data present. A 606 KB file holding 589 KB of samples — 60% of one record, more\n than half a million readings — was told no data was written, and the message carried no\n figures at all, so nothing in it could be checked against the file. The declared record\n size is the thing to look at, and it was the one thing not said.\n\n Still an error either way. A record is the unit the format is addressed in, and there is\n nothing smaller to convert.\n */\n const empty = dataBytes === 0;\n throw new EdfError(\n 'NO_DATA_RECORDS',\n empty\n ? 'The file contains a header and no data at all.'\n : `The file contains ${counted(dataBytes, 'byte')} of data, which is less than the ` +\n `${recordBytes} its header says one data record takes.`,\n empty\n ? 'The recording was probably interrupted before any data was written.'\n : 'Either the recording was cut short part way through its first record, or the ' +\n 'header describes records larger than the ones actually written. Check the ' +\n 'samples-per-record fields against the file size.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n message:\n `The header declares ${declaredRecordCount} data records but the file contains ` +\n `${recordCount}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n message: 'This file has no signal channels; it contains only EDF+ annotations.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${rates.size} different sampling rates ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/** \"EDF\", \"EDF+ (EDF+D)\", \"BDF\", \"BDF+ (EDF+C)\". */\nexport function describeFormat(header: EdfHeader): string {\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n if (Number.isInteger(hz)) return String(hz);\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, listed } from '../format/list.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const count = Number(normaliseNumberField(dec(fixed, 252, 4)).text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`);\n }\n const n = Number(text);\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n 'The file may be truncated, byte-shifted, or not an EDF file at all.',\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n );\n }\n return n;\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n */\nfunction resolveStartDateTime(dateRaw: string, timeRaw: string): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const year = yy >= 85 ? 1900 + yy : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${counted(fileSize, 'byte')}; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`,\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${signalCount} signals; expected at least 1.`,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${recordDuration}s; expected a positive number.`,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${counted(signalCount, 'signal')}, which needs a ${expectedHeaderBytes}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${fileSize} bytes.`\n : `but only ${buf.length} bytes of it were handed to the parser.`),\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${headerBytes} bytes, but ${counted(signalCount, 'signal')} ` +\n `${signalCount === 1 ? 'requires' : 'require'} ${expectedHeaderBytes} bytes. ` +\n `Using the value computed from the signal count.`,\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n /*\n All four free-text fields, not the two that were checked.\n\n `transducer` and `prefiltering` are free text out of the header exactly as the label\n and the unit are, and they land in channels.csv exactly as the unit does — so an ESC\n byte in a transducer field reached the CSV raw with nothing said, and `cat\n channels.csv` would drive the terminal. That is the hazard this warning exists for,\n two columns over. 0.5.71 made it name which field carries them; this is the rest of\n the fields it can name.\n */\n const fields = [\n ['label', label],\n ['unit', physicalDimension],\n ['transducer', transducer],\n ['prefiltering', prefiltering],\n ] as const;\n const affected = fields.filter(([, text]) => [...text].some(isControlCharacter));\n const control = affected.flatMap(([, text]) => [...text].filter(isControlCharacter));\n if (control.length > 0) {\n const shown = [...new Set(control)]\n .map((c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`)\n .join(', ');\n const plural = control.length === 1 ? '' : 's';\n const inLabel = affected.some(([name]) => name === 'label');\n // \"label and unit\", not \"label, unit\" — `listed` is for long enumerations that get\n // truncated, and this is a sentence with at most four items in it.\n const names = affected.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; the other three are cells of channels.csv and nothing else.\n // Named down to the cell when there is one of them, because that is the answer to\n // \"where did it go\" — `channels.csv` alone leaves a reader scanning fourteen columns.\n const cells = affected.filter(([name]) => name !== 'label').map(([name]) => name);\n const where =\n cells.length === 1 ? `channels.csv's ${cells[0] as string} cell` : 'channels.csv';\n const lands =\n inLabel && cells.length > 0\n ? `which will appear in the CSV column name and in ${where}`\n : inLabel\n ? 'which will appear in the CSV column name'\n : `which will appear in ${where}`;\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${affected.length === 1 ? 'contains' : 'contain'} ` +\n `${control.length} control character${plural} (${shown}), ${lands} exactly as the ` +\n `header has ${control.length === 1 ? 'it' : 'them'}.`,\n hint:\n /*\n Every branch has to print a command that works.\n\n The middle one quoted the label back, which is right until the label is empty:\n an unlabelled channel got `--channels \"\"`, and that exits 2 with \"--channels was\n given but lists no channel names\". A hint whose command fails is worse than no\n hint, and this warning's whole job is to say how to reach a channel whose header\n text you cannot type. `EMPTY_LABEL` already says the position is the only way in\n for such a channel; so does this now.\n\n A comma is the third way. `--channels` separates names with one, and splits on\n every occurrence, so a channel labelled `EEG Fpz-Cz, ref` cannot be selected by\n name at all: the quoted-back advice printed `--channels \"EEG Fpz-Cz, ref\"`, which\n exits 2 with `No channel named \"EEG Fpz-Cz\"` — a channel the file does not have,\n named after half of one it does. Commas in labels are ordinary, since EDF labels\n are free text, and the CSV header quotes them; only this one hint claimed\n something about them that isn't so.\n */\n (inLabel || label === '' || label.includes(',')\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n `since ${\n inLabel\n ? 'the name cannot be typed'\n : label === ''\n ? 'it has no label'\n : 'a comma in the label would read as two names'\n }. `\n : `The column name is unaffected, so --channels \"${label}\" still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n /*\n Too large to represent, and too small — the second was silent.\n\n The gain is the span divided by the digital range, and a span of 2e-320 over 65,535\n codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The\n scaler's flat-range branch then handed every code the same physical value, and a\n channel of 65,536 distinct readings became one repeated number with nothing raised at\n all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.\n\n Both are the same fact about the header — the span cannot be turned into a mapping —\n so both get this code, and both leave the cells empty rather than filling them with a\n value the header cannot justify.\n */\n const span = physicalMax - physicalMin;\n const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;\n if (!Number.isFinite(span) || underflowed) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +\n `represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n });\n }\n\n /*\n A timestamp that is not one.\n\n EDF gives the start date and time eight characters each, and nothing stops a writer\n putting `32.13.99` and `25.61.61` there. `--info` has always echoed the raw fields with\n \"(unparseable)\" beside them, but nothing was raised: the conversion exited 0, `--strict`\n passed, and metadata.json recorded `start_datetime_local: null` with no note against it.\n\n Every other unusable header field reports itself — a degenerate digital range, a physical\n span that cannot be represented, a comma decimal separator, a header whose declared size\n disagrees with its signal count. This was the one that did not, and it is the field\n output-files points at for turning `time_s` into an absolute instant.\n */\n if (resolveStartDateTime(startDateRaw, startTimeRaw) === null) {\n diagnostics.push({\n code: 'START_TIME_UNREADABLE',\n severity: 'warning',\n message:\n `The header's start date and time (\"${startDateRaw}\" and ` +\n `\"${startTimeRaw}\") are not a date and a time, so the recording has ` +\n `no start instant.`,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and metadata.json records ' +\n 'start_datetime_local as null.',\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `${indices.length} signals share the label \"${label}\" (positions ${indices.join(', ')}).`,\n hint: 'Their columns are suffixed with the signal number so they stay distinguishable.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n throw new EdfError('FILE_TOO_SMALL', `File is smaller than its own header.`);\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n /*\n Which of the two, and with the numbers.\n\n \"The recording was probably interrupted before any data was written\" is right about an\n empty file and wrong about the other way to get here: a header declaring records larger\n than the data present. A 606 KB file holding 589 KB of samples — 60% of one record, more\n than half a million readings — was told no data was written, and the message carried no\n figures at all, so nothing in it could be checked against the file. The declared record\n size is the thing to look at, and it was the one thing not said.\n\n Still an error either way. A record is the unit the format is addressed in, and there is\n nothing smaller to convert.\n */\n const empty = dataBytes === 0;\n throw new EdfError(\n 'NO_DATA_RECORDS',\n empty\n ? 'The file contains a header and no data at all.'\n : `The file contains ${counted(dataBytes, 'byte')} of data, which is less than the ` +\n `${recordBytes} its header says one data record takes.`,\n empty\n ? 'The recording was probably interrupted before any data was written.'\n : 'Either the recording was cut short part way through its first record, or the ' +\n 'header describes records larger than the ones actually written. Check the ' +\n 'samples-per-record fields against the file size.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n message:\n `The header declares ${declaredRecordCount} data records but the file contains ` +\n `${recordCount}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n message: 'This file has no signal channels; it contains only EDF+ annotations.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${rates.size} different sampling rates ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/** \"EDF\", \"EDF+ (EDF+D)\", \"BDF\", \"BDF+ (EDF+C)\". */\nexport function describeFormat(header: EdfHeader): string {\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n if (Number.isInteger(hz)) return String(hz);\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
|
package/dist/edf/reader.js
CHANGED
|
@@ -267,8 +267,11 @@ export class EdfFile {
|
|
|
267
267
|
if (bytesRead < bytes) {
|
|
268
268
|
// The file is shorter than its own size said. Quietly stopping here would
|
|
269
269
|
// hand back a conversion missing its tail with nothing to show for it.
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
//
|
|
271
|
+
// Through the shared builder rather than a second copy of its sentence: the two were
|
|
272
|
+
// character-for-character identical, which is how a wording fixed in one of them
|
|
273
|
+
// would have been fixed in only one of them.
|
|
274
|
+
throw changedWhileReading(record, bytes, bytesRead);
|
|
272
275
|
}
|
|
273
276
|
yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };
|
|
274
277
|
}
|
package/dist/edf/reader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { decodeLatin1, readInt16LE } from './bytes.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${this.fileSize} bytes to checksum but the file ended at ${at}; ` +\n `it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n if (info.isDirectory()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is a directory, not an EDF file.`);\n }\n if (!info.isFile()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is not a regular file.`);\n }\n\n /*\n Opening is a second chance to be refused, and it was the one that got through.\n\n `stat` needs the parent directory searchable and says nothing about the file's own mode,\n so a recording with no read permission passes it and fails here — the commonest\n permission failure there is. Unwrapped, it escaped as Node's own error: the CLI printed\n `error: EACCES: permission denied, open '...'` where every neighbouring failure prints\n the tool's sentence, and the library threw a plain Error whose `code` was the errno.\n\n api.md says `UNREADABLE` \"covers a missing file, a directory passed where a file was\n expected, a permission failure, and a file that changed size while being read. Branch on\n `code`, never on the message text.\" A consumer doing exactly that fell through to its\n generic handler.\n */\n const handle = await open(path, 'r').catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `readRecords: ${name} must be a whole record index, got ${value}.`,\n 'Record boundaries are the unit the file can be read in; a fractional index would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n /*\n Checked rather than handed to Buffer.alloc.\n\n `chunkBytes: NaN` came back as `RangeError: The value of \"size\" is out of range` from\n inside Node, with no mention of the option that caused it — while a fractional\n `startRecord` two lines up gets a typed EdfError naming the field. Every other option\n here is checked; this one reached the allocator.\n */\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n if (!Number.isFinite(budget) || budget < 1) {\n throw new EdfError(\n 'UNREADABLE',\n `chunkBytes must be a positive number of bytes, got ${String(options.chunkBytes)}.`,\n 'It is a ceiling on how much of the file is held at once; one record is read whatever it says.',\n );\n }\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${bytes} bytes of data at record ${record} but only ${bytesRead} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n return (await this.scanOrigin()).origin;\n }\n\n /**\n * The origin, and what the search saw on the way to it.\n *\n * `--info` takes this route for a continuous recording rather than reading every record,\n * and reported nothing when the timekeeping it read was unreadable: the count was hard-coded\n * to zero at the call site, so a file whose first TAL cannot be parsed raised\n * ANNOTATION_DECODE_FAILED when converted and nothing under `--info`. Its byte-identical\n * EDF+D twin — same bytes but for the reserved field, which has nothing to do with the\n * defect — raised it both ways, because that path reads every record and counts as it goes.\n *\n * The failure was being read and then thrown away. `readOrigin` keeps its shape for callers\n * who only want the number.\n */\n async scanOrigin(): Promise<{ origin: number | null; malformedTimekeeping: number }> {\n this.#assertOpen();\n\n let malformedTimekeeping = 0;\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, malformedTimekeeping };\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return { origin: null, malformedTimekeeping };\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return { origin: null, malformedTimekeeping };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n malformedTimekeeping += decoded.malformedTimekeeping;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, malformedTimekeeping };\n }\n }\n return { origin: null, malformedTimekeeping };\n }\n\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n /** How many of those also carried event text, so events were lost with the position. */\n malformedTimekeepingWithText: number;\n /** Events kept whose stated duration could not be read; see Annotation.duration. */\n unreadableDurations: number;\n /** Events kept whose stated duration read as a number below zero. */\n negativeDurations: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n const decoded = decodeRecordAnnotations(buffer, record, channel === timekeeping);\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n unreadableDurations += decoded.unreadableDurations;\n negativeDurations += decoded.negativeDurations;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new EdfError('UNREADABLE', 'This EDF file has already been closed.');\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n if (code === 'EACCES') return 'permission denied';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${expected} bytes of ${subject} at record ${record} but only ${actual} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { decodeLatin1, readInt16LE } from './bytes.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n `Expected ${this.fileSize} bytes to checksum but the file ended at ${at}; ` +\n `it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n if (info.isDirectory()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is a directory, not an EDF file.`);\n }\n if (!info.isFile()) {\n throw new EdfError('UNREADABLE', `\"${path}\" is not a regular file.`);\n }\n\n /*\n Opening is a second chance to be refused, and it was the one that got through.\n\n `stat` needs the parent directory searchable and says nothing about the file's own mode,\n so a recording with no read permission passes it and fails here — the commonest\n permission failure there is. Unwrapped, it escaped as Node's own error: the CLI printed\n `error: EACCES: permission denied, open '...'` where every neighbouring failure prints\n the tool's sentence, and the library threw a plain Error whose `code` was the errno.\n\n api.md says `UNREADABLE` \"covers a missing file, a directory passed where a file was\n expected, a permission failure, and a file that changed size while being read. Branch on\n `code`, never on the message text.\" A consumer doing exactly that fell through to its\n generic handler.\n */\n const handle = await open(path, 'r').catch((cause: unknown) => {\n throw new EdfError('UNREADABLE', `Cannot read \"${path}\": ${describe(cause)}`);\n });\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `readRecords: ${name} must be a whole record index, got ${value}.`,\n 'Record boundaries are the unit the file can be read in; a fractional index would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n /*\n Checked rather than handed to Buffer.alloc.\n\n `chunkBytes: NaN` came back as `RangeError: The value of \"size\" is out of range` from\n inside Node, with no mention of the option that caused it — while a fractional\n `startRecord` two lines up gets a typed EdfError naming the field. Every other option\n here is checked; this one reached the allocator.\n */\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n if (!Number.isFinite(budget) || budget < 1) {\n throw new EdfError(\n 'UNREADABLE',\n `chunkBytes must be a positive number of bytes, got ${String(options.chunkBytes)}.`,\n 'It is a ceiling on how much of the file is held at once; one record is read whatever it says.',\n );\n }\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n //\n // Through the shared builder rather than a second copy of its sentence: the two were\n // character-for-character identical, which is how a wording fixed in one of them\n // would have been fixed in only one of them.\n throw changedWhileReading(record, bytes, bytesRead);\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n return (await this.scanOrigin()).origin;\n }\n\n /**\n * The origin, and what the search saw on the way to it.\n *\n * `--info` takes this route for a continuous recording rather than reading every record,\n * and reported nothing when the timekeeping it read was unreadable: the count was hard-coded\n * to zero at the call site, so a file whose first TAL cannot be parsed raised\n * ANNOTATION_DECODE_FAILED when converted and nothing under `--info`. Its byte-identical\n * EDF+D twin — same bytes but for the reserved field, which has nothing to do with the\n * defect — raised it both ways, because that path reads every record and counts as it goes.\n *\n * The failure was being read and then thrown away. `readOrigin` keeps its shape for callers\n * who only want the number.\n */\n async scanOrigin(): Promise<{ origin: number | null; malformedTimekeeping: number }> {\n this.#assertOpen();\n\n let malformedTimekeeping = 0;\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, malformedTimekeeping };\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return { origin: null, malformedTimekeeping };\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return { origin: null, malformedTimekeeping };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n malformedTimekeeping += decoded.malformedTimekeeping;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, malformedTimekeeping };\n }\n }\n return { origin: null, malformedTimekeeping };\n }\n\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n /** How many of those also carried event text, so events were lost with the position. */\n malformedTimekeepingWithText: number;\n /** Events kept whose stated duration could not be read; see Annotation.duration. */\n unreadableDurations: number;\n /** Events kept whose stated duration read as a number below zero. */\n negativeDurations: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n const decoded = decodeRecordAnnotations(buffer, record, channel === timekeeping);\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n unreadableDurations += decoded.unreadableDurations;\n negativeDurations += decoded.negativeDurations;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new EdfError('UNREADABLE', 'This EDF file has already been closed.');\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n if (code === 'EACCES') return 'permission denied';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${expected} bytes of ${subject} at record ${record} but only ${actual} were ` +\n `available; the file appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
Notable changes to edf2csv. Versions follow [semantic versioning](https://semver.org); while the
|
|
4
4
|
major version is 0, a minor bump may contain breaking changes.
|
|
5
5
|
|
|
6
|
+
## 0.6.3
|
|
7
|
+
|
|
8
|
+
### Changed: the short-read message is built in one place, not two
|
|
9
|
+
|
|
10
|
+
`readRecords` threw a sentence it composed itself, character-for-character identical to the one
|
|
11
|
+
`changedWhileReading` builds for the annotation reader a few lines down — right down to the
|
|
12
|
+
hint. Two copies of one message is one copy too many to keep in step, and the next version
|
|
13
|
+
changes that wording, which would otherwise have meant changing it in one of the two.
|
|
14
|
+
|
|
15
|
+
## 0.6.2
|
|
16
|
+
|
|
17
|
+
### Fixed: "File is 1 bytes"
|
|
18
|
+
|
|
19
|
+
The first message a file too small to hold a header gets, and the count was not agreeing with
|
|
20
|
+
its noun. A one-byte file — the shortest thing that is not empty, and what a truncated copy or
|
|
21
|
+
a failed download leaves behind — was told `File is 1 bytes; an EDF header alone needs at least
|
|
22
|
+
256.` Through `counted`, like every other count in this parser.
|
|
23
|
+
|
|
6
24
|
## 0.6.1
|
|
7
25
|
|
|
8
26
|
### Changed: the changelog lives at `docs/CHANGELOG.md`
|
package/package.json
CHANGED