edf2csv 0.7.221 → 0.7.223
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/reader.d.ts +12 -12
- package/dist/edf/reader.js +12 -12
- package/dist/edf/reader.js.map +1 -1
- package/package.json +1 -1
package/dist/edf/reader.d.ts
CHANGED
|
@@ -98,18 +98,6 @@ export declare class EdfFile {
|
|
|
98
98
|
offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number;
|
|
99
99
|
/** The annotation channel's raw bytes for one record in a batch. */
|
|
100
100
|
annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array;
|
|
101
|
-
/**
|
|
102
|
-
* Read every EDF+ annotation in the file, plus the start time each record declares.
|
|
103
|
-
*
|
|
104
|
-
* Only the annotation channel is read, seeking straight to it inside each record
|
|
105
|
-
* rather than pulling whole records through memory. On a multi-gigabyte recording
|
|
106
|
-
* that is the difference between a few kilobytes of I/O and all of it.
|
|
107
|
-
*
|
|
108
|
-
* The whole file is always scanned, never just the records inside a requested
|
|
109
|
-
* window: writers are not obliged to store an annotation in the record its onset
|
|
110
|
-
* falls in, and some put every annotation in the first record. Reading only the
|
|
111
|
-
* window's records would drop those entirely.
|
|
112
|
-
*/
|
|
113
101
|
/**
|
|
114
102
|
* Where this continuous recording begins, from the first record that says.
|
|
115
103
|
*
|
|
@@ -178,6 +166,18 @@ export declare class EdfFile {
|
|
|
178
166
|
malformedTimekeeping: number;
|
|
179
167
|
malformedTimekeepingWithText: number;
|
|
180
168
|
}>;
|
|
169
|
+
/**
|
|
170
|
+
* Read every EDF+ annotation in the file, plus the start time each record declares.
|
|
171
|
+
*
|
|
172
|
+
* Only the annotation channel is read, seeking straight to it inside each record
|
|
173
|
+
* rather than pulling whole records through memory. On a multi-gigabyte recording
|
|
174
|
+
* that is the difference between a few kilobytes of I/O and all of it.
|
|
175
|
+
*
|
|
176
|
+
* The whole file is always scanned, never just the records inside a requested
|
|
177
|
+
* window: writers are not obliged to store an annotation in the record its onset
|
|
178
|
+
* falls in, and some put every annotation in the first record. Reading only the
|
|
179
|
+
* window's records would drop those entirely.
|
|
180
|
+
*/
|
|
181
181
|
readAnnotations(): Promise<{
|
|
182
182
|
annotations: Annotation[];
|
|
183
183
|
recordStarts: (number | null)[];
|
package/dist/edf/reader.js
CHANGED
|
@@ -312,18 +312,6 @@ export class EdfFile {
|
|
|
312
312
|
const start = this.offsetOf(batch, recordOffset, signal);
|
|
313
313
|
return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);
|
|
314
314
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Read every EDF+ annotation in the file, plus the start time each record declares.
|
|
317
|
-
*
|
|
318
|
-
* Only the annotation channel is read, seeking straight to it inside each record
|
|
319
|
-
* rather than pulling whole records through memory. On a multi-gigabyte recording
|
|
320
|
-
* that is the difference between a few kilobytes of I/O and all of it.
|
|
321
|
-
*
|
|
322
|
-
* The whole file is always scanned, never just the records inside a requested
|
|
323
|
-
* window: writers are not obliged to store an annotation in the record its onset
|
|
324
|
-
* falls in, and some put every annotation in the first record. Reading only the
|
|
325
|
-
* window's records would drop those entirely.
|
|
326
|
-
*/
|
|
327
315
|
/**
|
|
328
316
|
* Where this continuous recording begins, from the first record that says.
|
|
329
317
|
*
|
|
@@ -414,6 +402,18 @@ export class EdfFile {
|
|
|
414
402
|
}
|
|
415
403
|
return { origin: null, ...counts };
|
|
416
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Read every EDF+ annotation in the file, plus the start time each record declares.
|
|
407
|
+
*
|
|
408
|
+
* Only the annotation channel is read, seeking straight to it inside each record
|
|
409
|
+
* rather than pulling whole records through memory. On a multi-gigabyte recording
|
|
410
|
+
* that is the difference between a few kilobytes of I/O and all of it.
|
|
411
|
+
*
|
|
412
|
+
* The whole file is always scanned, never just the records inside a requested
|
|
413
|
+
* window: writers are not obliged to store an annotation in the record its onset
|
|
414
|
+
* falls in, and some put every annotation in the first record. Reading only the
|
|
415
|
+
* window's records would drop those entirely.
|
|
416
|
+
*/
|
|
417
417
|
async readAnnotations() {
|
|
418
418
|
this.#assertOpen();
|
|
419
419
|
const annotations = [];
|
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 { readInt16LE } from './bytes.js';\nimport { counted } from '../format/list.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 /*\n A sentence, and advice under it, like the destination-side twin.\n\n `Cannot read \"rec.edf\": no such file` was the one diagnostic this tool prints that does not\n end in a full stop — 68 of its 69 do — and the only member of its family with nothing\n indented under it. `Cannot create \"out\": part of the path does not exist.` has carried\n \"Check the path exists and that you have permission to write there.\" since the destination\n errors were given sentences, the mid-conversion UNREADABLE beside it carries one too, and\n this is the form a mistyped path actually reaches.\n */\n static readonly #UNREADABLE_HINT =\n 'Check the path is spelled the way it is on disk and that you have permission to read it.';\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${path}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\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(\n 'UNREADABLE',\n `Cannot read \"${path}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\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 * That mitigation covers records that could not be read, and not records that said nothing:\n * an empty annotation slot is not a TAL that failed, so nothing is counted and nothing is\n * raised. Twenty records whose only timekeeping entry is in record 16 therefore convert with\n * `time_s` from the origin it states and are reported here as beginning at zero, in silence\n * on both sides — and `--start` and `--end` are read against that same clock. The bound\n * stays, since it is what makes `--info` a header read on a file of any size; what was\n * wrong was the account of what it costs, which every page giving it said was a warning.\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 * All three counters, not one. A first-position TAL may carry events after the start time,\n * and when it cannot be parsed those go with it — which is what `malformedTimekeepingWithText`\n * counts and what decides whether the warning says \"No event was lost\" or names the events\n * that were. Counting only the first meant `--info` took the first sentence every time: it\n * announced that a record had lost its position and that nothing else had gone, over a file\n * whose conversion said, correctly, that an event had gone with it. One file, two answers,\n * and the confident one was `--info`, which is the command run first to find out what a\n * conversion will say.\n *\n * `malformed` comes back for the same reason one sentence further on: that hint ends \"and is\n * counted above\", which is only true where the entry warning is printed too.\n *\n * All three are of the records this actually read, which is as far as the first record that\n * states a time — so they are lower bounds on the file, as `malformedTimekeeping` has been\n * since it was returned at all. A conversion reads every record and may count more. What\n * they must not be is inconsistent with each other, which is what a hard-coded zero made\n * them.\n */\n async scanOrigin(): Promise<{\n origin: number | null;\n malformed: number;\n malformedTimekeeping: number;\n malformedTimekeepingWithText: number;\n }> {\n this.#assertOpen();\n\n const counts = { malformed: 0, malformedTimekeeping: 0, malformedTimekeepingWithText: 0 };\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, ...counts };\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, ...counts };\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, ...counts };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n counts.malformed += decoded.malformed;\n counts.malformedTimekeeping += decoded.malformedTimekeeping;\n counts.malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, ...counts };\n }\n }\n return { origin: null, ...counts };\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 // EPERM beside EACCES, because everywhere else in this codebase that reads an errno pairs\n // the two, and ENOTDIR because a path that runs through a regular file — `rec.edf/inner`,\n // which a shell completes and a script builds by joining — is otherwise the one input\n // failure that answers in errno text while its output-side twin answers in a sentence.\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\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 ` +\n `${counted(actual, 'byte')} ${actual === 1 ? 'was' : 'were'} available; the file appears ` +\n `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 { readInt16LE } from './bytes.js';\nimport { counted } from '../format/list.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 /*\n A sentence, and advice under it, like the destination-side twin.\n\n `Cannot read \"rec.edf\": no such file` was the one diagnostic this tool prints that does not\n end in a full stop — 68 of its 69 do — and the only member of its family with nothing\n indented under it. `Cannot create \"out\": part of the path does not exist.` has carried\n \"Check the path exists and that you have permission to write there.\" since the destination\n errors were given sentences, the mid-conversion UNREADABLE beside it carries one too, and\n this is the form a mistyped path actually reaches.\n */\n static readonly #UNREADABLE_HINT =\n 'Check the path is spelled the way it is on disk and that you have permission to read it.';\n\n static async open(path: string): Promise<EdfFile> {\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${path}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\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(\n 'UNREADABLE',\n `Cannot read \"${path}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\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 * 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 * That mitigation covers records that could not be read, and not records that said nothing:\n * an empty annotation slot is not a TAL that failed, so nothing is counted and nothing is\n * raised. Twenty records whose only timekeeping entry is in record 16 therefore convert with\n * `time_s` from the origin it states and are reported here as beginning at zero, in silence\n * on both sides — and `--start` and `--end` are read against that same clock. The bound\n * stays, since it is what makes `--info` a header read on a file of any size; what was\n * wrong was the account of what it costs, which every page giving it said was a warning.\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 * All three counters, not one. A first-position TAL may carry events after the start time,\n * and when it cannot be parsed those go with it — which is what `malformedTimekeepingWithText`\n * counts and what decides whether the warning says \"No event was lost\" or names the events\n * that were. Counting only the first meant `--info` took the first sentence every time: it\n * announced that a record had lost its position and that nothing else had gone, over a file\n * whose conversion said, correctly, that an event had gone with it. One file, two answers,\n * and the confident one was `--info`, which is the command run first to find out what a\n * conversion will say.\n *\n * `malformed` comes back for the same reason one sentence further on: that hint ends \"and is\n * counted above\", which is only true where the entry warning is printed too.\n *\n * All three are of the records this actually read, which is as far as the first record that\n * states a time — so they are lower bounds on the file, as `malformedTimekeeping` has been\n * since it was returned at all. A conversion reads every record and may count more. What\n * they must not be is inconsistent with each other, which is what a hard-coded zero made\n * them.\n */\n async scanOrigin(): Promise<{\n origin: number | null;\n malformed: number;\n malformedTimekeeping: number;\n malformedTimekeepingWithText: number;\n }> {\n this.#assertOpen();\n\n const counts = { malformed: 0, malformedTimekeeping: 0, malformedTimekeepingWithText: 0 };\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, ...counts };\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, ...counts };\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, ...counts };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n counts.malformed += decoded.malformed;\n counts.malformedTimekeeping += decoded.malformedTimekeeping;\n counts.malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, ...counts };\n }\n }\n return { origin: null, ...counts };\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 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 // EPERM beside EACCES, because everywhere else in this codebase that reads an errno pairs\n // the two, and ENOTDIR because a path that runs through a regular file — `rec.edf/inner`,\n // which a shell completes and a script builds by joining — is otherwise the one input\n // failure that answers in errno text while its output-side twin answers in a sentence.\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\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 ` +\n `${counted(actual, 'byte')} ${actual === 1 ? 'was' : 'were'} available; the file appears ` +\n `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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edf2csv",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.223",
|
|
4
4
|
"description": "Convert EDF, EDF+ and BDF biosignal recordings (European Data Format) to CSV from the command line. Local, streaming, and never resamples or alters units.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"edf",
|