edf2csv 0.5.51 → 0.5.53

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/CHANGELOG.md CHANGED
@@ -3,6 +3,48 @@
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.5.53
7
+
8
+ ### Fixed: the correctness page's layout sweep disagreed with itself about how many recordings it ran
9
+
10
+ Claim 7 says the two layouts are checked over 49 recordings. Six lines further down, the
11
+ pasted output of the command that checks it read `266 conversions compared over 48
12
+ recordings`. Both numbers were on screen at once, on the page whose whole argument is that
13
+ its figures can be reproduced by running the command printed above them.
14
+
15
+ The output block was pasted when there were 48 fixtures and never repasted. A test already
16
+ held the claim to the fixture count; it did not look at the block below it, so the two drifted
17
+ apart the moment a fixture was added.
18
+
19
+ Repasted, and the test now reads both. The recording count in that output is `names.length` —
20
+ the fixture list itself — so it is the same number in both places by construction. The
21
+ conversion and channel-sequence counts beside it genuinely do move with which windows each
22
+ recording can honour, and are left alone; so is the batch sweep's recording count, which is
23
+ how many files a seed scattered across its random trees and was never the fixture count.
24
+
25
+ ## 0.5.52
26
+
27
+ ### Fixed: a bound on one channel is not a bound on a file's channels
28
+
29
+ Every channel is handed a cache of its formatted values, because a channel has only
30
+ `digitalMax - digitalMin + 1` distinct readings and the same handful of strings then serve
31
+ millions of rows. 0.2.5 sized that cache to the range a channel declares rather than to the
32
+ whole 16-bit domain, which fixed the dense montage of 12-bit channels — and left the ceiling
33
+ at 512 KB per channel, with nothing whatever said about how many channels a file may have.
34
+
35
+ The full 16-bit range is legal, usual, and what an ordinary EEG amplifier writes. 256 such
36
+ channels claim the ceiling 256 times: 134 MB of pointer arrays reserved before a single row
37
+ is written, and it is the channel count that does it, not the file size. A 229 KB recording
38
+ exits 134 with a native V8 stack and an empty output directory under a 96 MB heap; a 7.9 MB
39
+ one needs 192 MB to finish, where the site advertises 48 MB and means it.
40
+
41
+ One budget for the conversion, 16 MB, the same shape of fix the time-offset cache got in
42
+ 0.4.23 one level over — there the unbounded count was rate groups, here it is channels.
43
+ A 32-channel montage at the full range keeps every cache it had, as do 512 channels of a
44
+ 12-bit ADC. Past that, channels format each value directly: about a quarter slower on a
45
+ recording where almost none of them are cached, and byte-for-byte the same output. The
46
+ 229 KB file now converts under 48 MB, and the 7.9 MB one holds a 28 MB working set.
47
+
6
48
  ## 0.5.51
7
49
 
8
50
  ### Fixed: 0.5.32 removed the collision refusal from `--info` and put nothing in its place
@@ -17,7 +17,7 @@ import { describeFormat, formatRates, formatWallClock } from '../edf/header.js';
17
17
  import { EdfError } from '../edf/errors.js';
18
18
  import { BufferedLineWriter, DEFAULT_FLUSH_THRESHOLD, UTF8_BOM, csvRow, escapeCsvField, } from '../format/csv.js';
19
19
  import { listed } from '../format/list.js';
20
- import { fixed, makeSampleFormatter, makeTimeFormatter, newOffsetBudget, } from '../format/number.js';
20
+ import { fixed, makeSampleFormatter, makeTimeFormatter, newOffsetBudget, newSampleCacheBudget, } from '../format/number.js';
21
21
  import { buildPlan, withoutFileRateWarning } from './plan.js';
22
22
  import { deriveRecordStarts } from './timing.js';
23
23
  import { sampleTimeIsInRange, toleranceFor } from './time-range.js';
@@ -355,6 +355,8 @@ function describeFsError(cause) {
355
355
  async function writeSignalFiles(file, plan, outputDir, recordStarts, options, onHangUp) {
356
356
  // One budget for every table in this conversion, not one per table: see OffsetBudget.
357
357
  const offsets = newOffsetBudget();
358
+ // Likewise one for every channel, not one per channel: see SampleCacheBudget.
359
+ const sampleCaches = newSampleCacheBudget();
358
360
  // Only the stdout path needs this; --out finds a full disk on its own, because it always
359
361
  // has another file to write afterwards. See auditStdout.
360
362
  const audit = outputDir === null ? auditStdout() : null;
@@ -437,7 +439,7 @@ async function writeSignalFiles(file, plan, outputDir, recordStarts, options, on
437
439
  return {
438
440
  group,
439
441
  writer,
440
- formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals)),
442
+ formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals, sampleCaches)),
441
443
  formatTime: makeTimeFormatter(group.samplesPerRecord, group.rate, group.timeDecimals, offsets),
442
444
  rows: 0,
443
445
  settled,
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/convert/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executing a conversion.\n *\n * Everything is written in a single pass over the data records. All rate groups are\n * open at once and fed from the same batch of bytes, so a file is read once no\n * matter how many output tables it produces, and memory stays flat.\n */\n\nimport { createWriteStream, fstatSync } from 'node:fs';\nimport { mkdir, readdir, stat, writeFile } from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { finished, pipeline } from 'node:stream/promises';\nimport type { Writable } from 'node:stream';\nimport { createGzip, gzipSync } from 'node:zlib';\nimport path from 'node:path';\n\nimport type { RecordBatch } from '../edf/reader.js';\nimport { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport type { Diagnostic } from '../edf/errors.js';\nimport { EdfError } from '../edf/errors.js';\nimport type { Annotation } from '../edf/annotations.js';\nimport {\n BufferedLineWriter,\n DEFAULT_FLUSH_THRESHOLD,\n UTF8_BOM,\n csvRow,\n escapeCsvField,\n} from '../format/csv.js';\nimport { listed } from '../format/list.js';\nimport {\n fixed,\n makeSampleFormatter,\n makeTimeFormatter,\n newOffsetBudget,\n} from '../format/number.js';\nimport type { SampleFormatter } from '../format/number.js';\nimport { buildPlan, withoutFileRateWarning } from './plan.js';\nimport type { ConversionPlan, PlanOptions, RateGroup } from './plan.js';\nimport { deriveRecordStarts } from './timing.js';\nimport { sampleTimeIsInRange, toleranceFor } from './time-range.js';\nimport { VERSION as TOOL_VERSION } from '../version.js';\n\nexport { TOOL_VERSION };\n\nexport type ConversionErrorCode =\n | 'OUTPUT_EXISTS'\n | 'OUTPUT_UNWRITABLE'\n | 'INPUT_OUTPUT_COLLISION'\n | 'INPUT_UNREADABLE'\n | 'UNSUPPORTED_REQUEST'\n | 'CALLBACK_FAILED'\n | 'WRITE_FAILED';\n\n/**\n * Codes that mean the command cannot be carried out as written, rather than that something\n * about the file or the destination went wrong.\n *\n * The distinction is the one the exit codes draw: 1 is \"the file or the destination is the\n * problem\", 2 is \"the command line is the problem\". A caller with a `--stdout` conflict is\n * being told to change the flags — the hints say exactly that — so filing it under 1 sent\n * scripts looking at the disk. Exit 2 already covers checks that need the header first,\n * such as a `--channels` term matching nothing.\n */\nexport const USAGE_ERROR_CODES: ReadonlySet<ConversionErrorCode> = new Set(['UNSUPPORTED_REQUEST']);\n\nexport class ConversionError extends Error {\n readonly code: ConversionErrorCode;\n readonly hint: string | undefined;\n constructor(code: ConversionErrorCode, message: string, hint?: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConversionError';\n this.code = code;\n this.hint = hint;\n }\n}\n\nexport interface ConvertOptions extends PlanOptions {\n /** Destination directory. Defaults to the input file's name without its extension. */\n outputDir?: string | undefined;\n /** Overwrite an existing output directory. */\n force?: boolean | undefined;\n /** Record a SHA-256 of the input in metadata.json. Costs one extra read of the file. */\n checksum?: boolean | undefined;\n /**\n * Write the signal CSV to stdout instead of to a directory.\n *\n * Only valid when the conversion produces exactly one signal file. In the default wide\n * layout a mixed-rate recording becomes several tables, and merging them into one stream\n * would mean inventing the samples this tool exists not to invent; `layout: 'long'` gives\n * one table for any recording, so it lifts the restriction. No sidecar files are written.\n */\n toStdout?: boolean | undefined;\n onProgress?: ((progress: ConversionProgress) => void) | undefined;\n}\n\nexport interface ConversionProgress {\n recordsDone: number;\n recordsTotal: number;\n bytesWritten: number;\n}\n\nexport interface WrittenFile {\n name: string;\n rows: number;\n}\n\nexport interface ConvertResult {\n outputDir: string;\n files: WrittenFile[];\n /**\n * True when a `--stdout` reader closed the pipe before the conversion finished.\n *\n * `edf2csv rec.edf --stdout | head -1` is an ordinary thing to type and not a failure, but\n * it is also not a conversion: the row count is rows formatted before the close was\n * noticed, which is neither the recording's total nor what the reader received.\n */\n readerHungUp: boolean;\n annotationCount: number;\n diagnostics: Diagnostic[];\n plan: ConversionPlan;\n file: EdfFile;\n elapsedMs: number;\n}\n\ninterface OpenGroup {\n group: RateGroup;\n writer: BufferedLineWriter;\n formatters: SampleFormatter[];\n formatTime: (recordStart: number, sample: number) => string;\n rows: number;\n /** Resolves once a compressed stream's bytes have reached the file behind it. */\n settled: Promise<void>;\n}\n\nexport async function convert(inputPath: string, options: ConvertOptions = {}): Promise<ConvertResult> {\n const startedAt = Date.now();\n const file = await EdfFile.open(inputPath);\n\n try {\n /*\n Hashed before a record is read, and only published if the file held still.\n\n A checksum taken afterwards cannot be trusted whichever descriptor it goes through. A\n file overwritten in place keeps its inode, so the open handle sees the new bytes too,\n and the old ones are simply gone — there is nowhere left to read what was converted.\n Taking it first at least means the hash describes the file the header was read from.\n\n What makes it a guarantee rather than a hope is the check at the end: if size or\n modification time moved at any point, the hash is dropped and the run says why. So\n `sha256` present means the file demonstrably did not change while it was read, and a\n recording still being written gets a null and a warning instead of a plausible hash of\n the wrong bytes. (A change reverted within the same modification timestamp would slip\n through; nothing short of copying the input first can close that.)\n */\n const checksumAtOpen = options.checksum === true ? await file.sha256() : null;\n\n // One pass over the annotation channel supplies everything annotation-related:\n // where each record sits in time, and the full event list. It reads the whole\n // file even when a window was requested, because an annotation inside the window\n // may be stored in a record outside it.\n const annotationData =\n file.annotationSignals.length > 0\n ? await file.readAnnotations()\n : { annotations: [] as Annotation[], recordStarts: [] as (number | null)[], malformed: 0 };\n\n const timing = deriveRecordStarts(file, annotationData);\n\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n options,\n );\n plan.diagnostics.push(...timing.diagnostics);\n\n if (options.toStdout === true) {\n if (!plan.writeSignals) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n '--stdout has no signal data to write because --annotations-only was given.',\n 'Drop one of the two flags.',\n );\n }\n /*\n No table at all is its own answer, and neither layout gave it.\n\n A recording with no signal channels — one holding only EDF+ annotations — produced\n zero rate groups. The wide layout then said \"--stdout needs exactly one table, but\n this recording produces 0, one for each sampling rate its channels use ()\", with an\n empty parenthetical, advice to narrow to one of no rates, and advice to use\n `--layout long` — which wrote zero bytes to stdout, not even a header row, and exited\n 0 while warning that \"the signal files hold their headers and no data\". There were no\n files and there was no header. The one path that was right about this is\n `--annotations-only`, which refuses outright, and this is the same situation reached\n by a different route.\n */\n if (plan.groups.length === 0) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n file.dataSignals.length === 0\n ? '--stdout has no signal data to write: this recording has no signal channels, ' +\n 'only EDF+ annotations.'\n : '--stdout has no signal data to write: nothing was selected that carries samples.',\n file.dataSignals.length === 0\n ? 'Convert to a directory to get its annotations.csv, or drop --stdout.'\n : 'Check --channels and the requested window, or convert to a directory instead.',\n );\n }\n\n // The long layout is one table whatever the rates are, so it has nothing to refuse.\n if (plan.layout !== 'long' && plan.groups.length !== 1) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n // Naming the rates is the point: the hint says to narrow the selection, and this\n // is what there is to narrow it to. The parenthetical used to repeat the count\n // that had just been given — \"produces 3 (its channels use 3 different sampling\n // rates)\" — which told nobody anything they could act on.\n `--stdout needs exactly one table, but this recording produces ${plan.groups.length}, ` +\n `one for each sampling rate its channels use ` +\n `(${listed(formatRates(plan.groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n 'Narrow it to one rate with --channels, write --layout long to get them all in ' +\n 'one table, or convert to a directory instead.',\n );\n }\n let readerHungUp = false;\n const written = await writeSignalFiles(file, plan, null, timing.starts, options, (hungUp) => {\n readerHungUp = hungUp;\n });\n if (await file.changedSinceOpen()) plan.diagnostics.push(inputChanged(false));\n return {\n outputDir: '-',\n files: written,\n readerHungUp,\n annotationCount: 0,\n diagnostics: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics],\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n const outputDir = options.outputDir ?? defaultOutputDir(inputPath);\n await assertInputDoesNotOverlapOutputs(inputPath, outputDir, file, plan, options);\n await prepareOutputDir(outputDir, options.force === true);\n\n const written: WrittenFile[] = [];\n\n if (plan.writeSignals && plan.groups.length > 0) {\n const signals = await writeSignalFiles(file, plan, outputDir, timing.starts, options);\n written.push(...signals);\n } else if (plan.writeSignals) {\n /*\n Asked for signal data and given none to write.\n\n Every channel selected carries zero samples per record, so there is no table to make\n — `edf2csv rec.edf --channels unused` writes channels.csv and metadata.json and no\n signals.csv at all. The NO_SAMPLES warning explains the channel; nothing explained the\n missing file, and the documentation says signals.csv is written unless\n --annotations-only was passed. Someone looking for it should be told where it went.\n */\n plan.diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message:\n 'No signal file was written: every channel selected carries zero samples per data ' +\n 'record, so there is nothing to put in one.',\n hint:\n 'channels.csv still describes them. Run with --info to see which channels do carry ' +\n 'samples.',\n });\n }\n\n if (options.annotationsOnly === true && file.annotationSignals.length === 0) {\n plan.diagnostics.push({\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message:\n '--annotations-only was requested but this recording has no annotation channel, ' +\n 'so there are no events to export.',\n hint: 'Plain EDF files carry no annotations. Convert without --annotations-only to get the signals.',\n });\n }\n\n let annotationsWritten = 0;\n if (file.annotationSignals.length > 0) {\n const result = await writeAnnotationsCsv(\n outputDir,\n annotationData.annotations,\n requestedAnnotationWindow(options, plan.range.recordingStartSeconds),\n options.gzip === true,\n options.bom === true,\n );\n written.push(result);\n annotationsWritten = result.rows;\n }\n\n written.push(\n await writeChannelsCsv(outputDir, file, plan, options.gzip === true, options.bom === true),\n );\n\n /*\n The file moved under the conversion. Said out loud, because nothing else shows it.\n\n The CSVs are still correct for the records that were read, and metadata.json still\n describes the file as it was opened — so the record is consistent with the output\n whatever happens here. What stops being true is that the checksum describes the bytes\n that were converted, since an in-place overwrite leaves nowhere to read them from. It\n is dropped rather than guessed at.\n */\n const changed = await file.changedSinceOpen();\n if (changed) plan.diagnostics.push(inputChanged(options.checksum === true));\n\n await writeMetadata(\n outputDir,\n inputPath,\n file,\n plan,\n written,\n annotationsWritten,\n changed ? null : checksumAtOpen,\n );\n\n const stale = await findStaleOutput(outputDir, written);\n\n return {\n outputDir,\n files: written,\n readerHungUp: false,\n annotationCount: annotationsWritten,\n diagnostics: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics, ...stale],\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n } finally {\n await file.close();\n }\n}\n\n/*\n Files this tool produces, used to spot leftovers from an earlier conversion.\n\n The rate part has to allow every shape a filename can now take, not just the plain ones:\n\n signals_256hz.csv an integer rate\n signals_12_5hz.csv a fractional rate, decimal point written as an underscore\n signals_1_000e-7hz.csv a rate small enough to need exponent form (0.2.2)\n signals_256hz.csv.gz any of the above, compressed (0.3.0)\n signals_0hz_2.csv a second group whose rate slug collided (0.2.1)\n\n The previous `[\\w.]+hz` matched neither of the last two — `-` and `+` are not word\n characters, and the collision suffix falls after the `hz`. Both were introduced by\n recent changes and both silently stopped being recognised as this tool's own output, so\n leftovers of exactly those kinds went unreported: the one situation the warning exists\n for. Requiring a digit after the underscore keeps a user's own `signals_notes.csv` out.\n*/\nconst OUTPUT_PATTERN =\n /^(signals(_\\d[\\w.+-]*hz(_\\d+)?)?\\.csv(\\.gz)?|annotations\\.csv(\\.gz)?|channels\\.csv(\\.gz)?|metadata\\.json)$/u;\n\n/**\n * Detect output from a previous run that this one did not replace.\n *\n * `--force` overwrites files but does not empty the directory, so converting a\n * mixed-rate recording and then a single-rate one into the same place leaves\n * `signals_256hz.csv` sitting next to a fresh `signals.csv`. Both look current.\n * Nothing is deleted here — the user is told, and decides.\n */\nasync function findStaleOutput(\n outputDir: string,\n written: readonly WrittenFile[],\n): Promise<Diagnostic[]> {\n const entries = await readdir(outputDir).catch(() => null);\n if (!entries) return [];\n\n // metadata.json is rewritten on every run but is not part of the reported file list.\n const fresh = new Set([...written.map((f) => f.name), 'metadata.json']);\n const stale = entries.filter((name) => OUTPUT_PATTERN.test(name) && !fresh.has(name)).sort();\n if (stale.length === 0) return [];\n\n return [\n {\n code: 'STALE_OUTPUT',\n severity: 'warning',\n message:\n `${stale.join(', ')} ${stale.length === 1 ? 'is' : 'are'} left over from an earlier ` +\n `conversion into this directory and ${stale.length === 1 ? 'was' : 'were'} not rewritten.`,\n hint: 'Delete them, or convert into a fresh directory, so the two runs do not get mixed up.',\n },\n ];\n}\n\nexport function defaultOutputDir(inputPath: string): string {\n const base = path.basename(inputPath, path.extname(inputPath));\n return path.join(path.dirname(inputPath), `${base}_csv`);\n}\n\n/** Never let an output target resolve to the recording being read, even with --force. */\nasync function assertInputDoesNotOverlapOutputs(\n inputPath: string,\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n options: ConvertOptions,\n): Promise<void> {\n /*\n The names this run will actually write, compressed ones included.\n\n The rate files come from the plan and already carry `.csv.gz` under --gzip; the sidecars\n were spelled out here and did not. So a compressed run checked two names it would never\n write and missed the two it would: a recording sitting at <outdir>/channels.csv.gz was\n overwritten by its own conversion, with --force, reported as a success. The same file\n named signals.csv.gz was refused, which is what gives the oversight away.\n */\n const suffix = options.gzip === true ? '.csv.gz' : '.csv';\n const names = new Set(plan.groups.map((group) => group.fileName));\n if (file.annotationSignals.length > 0) names.add(`annotations${suffix}`);\n names.add(`channels${suffix}`);\n names.add('metadata.json');\n\n const inputResolved = path.resolve(inputPath);\n const inputInfo = await stat(inputPath);\n\n for (const name of names) {\n const target = path.join(outputDir, name);\n const targetResolved = path.resolve(target);\n const targetInfo = await stat(target).catch(() => null);\n const samePath = targetResolved === inputResolved;\n const sameFile =\n targetInfo !== null && targetInfo.dev === inputInfo.dev && targetInfo.ino === inputInfo.ino;\n if (!samePath && !sameFile) continue;\n\n throw new ConversionError(\n 'INPUT_OUTPUT_COLLISION',\n `Output file \"${target}\" is the same file as the input recording.`,\n 'Choose a separate directory with --out. The input was not modified.',\n );\n }\n}\n\nasync function prepareOutputDir(dir: string, force: boolean): Promise<void> {\n /*\n Claim the directory with a single atomic mkdir rather than asking whether it exists\n and then creating it.\n\n Checking first left a window between the two: two conversions started together both\n saw \"not there\", both proceeded, and both opened write streams on the same signals.csv.\n Neither reported anything — both exited 0, having half-written one file between them.\n A non-recursive mkdir cannot do that. Exactly one caller creates the directory; every\n other one gets EEXIST from the filesystem and takes the already-exists path below.\n\n Parents are still created recursively, since --out ./a/b/c should work. Only the final\n component is the claim.\n */\n const parent = path.dirname(dir);\n if (parent && parent !== dir) {\n try {\n await mkdir(parent, { recursive: true });\n } catch (cause: unknown) {\n // A parent that is a regular file surfaces as EEXIST naming the parent, which reads\n // as though the destination already exists rather than as \"you cannot put a\n // directory inside a file\". Name the real obstacle instead of the errno.\n const info = await stat(parent).catch(() => null);\n if (info && !info.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": \"${parent}\" is a file, not a directory.`,\n 'Choose a destination whose parent directories are directories, with --out.',\n );\n }\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n 'Check the path exists and that you have permission to write there.',\n );\n }\n }\n\n let claimed = true;\n try {\n await mkdir(dir);\n } catch (cause: unknown) {\n if ((cause as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n 'Check the path exists and that you have permission to write there.',\n );\n }\n claimed = false;\n }\n\n if (!claimed) {\n const existing = await stat(dir).catch(() => null);\n\n // --force means \"replace my previous output\", not \"write output files into\n // whatever this happens to be\". Pointing it at a regular file needs saying so.\n if (existing && !existing.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a file, but the converted data needs a directory.`,\n 'Choose a directory with --out.',\n );\n }\n if (!force) {\n throw new ConversionError(\n 'OUTPUT_EXISTS',\n `\"${dir}\" already exists.`,\n 'Pass --force to overwrite it, or --out to choose a different directory.',\n );\n }\n }\n}\n\n/** Turn a Node filesystem error into something a person can act on. */\nfunction describeFsError(cause: unknown): string {\n const code = (cause as NodeJS.ErrnoException | undefined)?.code;\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOSPC') return 'the disk is full';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n if (code === 'EROFS') return 'the filesystem is read-only';\n if (code === 'ENAMETOOLONG') return 'the path is too long';\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nasync function writeSignalFiles(\n file: EdfFile,\n plan: ConversionPlan,\n outputDir: string | null,\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n onHangUp?: (hungUp: boolean) => void,\n): Promise<WrittenFile[]> {\n // One budget for every table in this conversion, not one per table: see OffsetBudget.\n const offsets = newOffsetBudget();\n // Only the stdout path needs this; --out finds a full disk on its own, because it always\n // has another file to write afterwards. See auditStdout.\n const audit = outputDir === null ? auditStdout() : null;\n /*\n In the long layout every group writes into one table, so they share one stream. Opening\n a stream per group on the same path is what the rate-slug collision fix in groupByRate\n was about: two writers on one file interleave rows under a header naming one of them.\n */\n let shared: {\n stream: Writable;\n settled: Promise<void>;\n target: Writable;\n writer: BufferedLineWriter;\n } | null = null;\n\n /*\n One buffer's worth of memory for the conversion, not one per table.\n\n Every group had its own writer at the default 1 MiB threshold, so pending output was\n group count × 1 MiB before anything drained. A 6.5 MB recording with 40 sampling rates\n — the header allows thousands of channels, and a research montage really does mix a\n dozen rates — died with a raw V8 heap out-of-memory and exit 134 under a 96 MB cap,\n while the site advertises 48 MB. The recording is small; it is the fan-out that is not.\n\n Split evenly with a floor, so the single-rate case, which is nearly every recording,\n keeps exactly the buffer it always had, and forty tables cost 2.5 MB rather than 40.\n The long layout shares one writer already and is unaffected either way.\n */\n const MIN_FLUSH_THRESHOLD = 8 * 1024;\n const flushThreshold = Math.max(\n MIN_FLUSH_THRESHOLD,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n /*\n The stream's own buffer is the other half of the same sum.\n\n 0.5.6 shared the line-buffer budget and left `createWriteStream` at its 64 KiB default,\n which is per stream: 200 rate groups meant 12.8 MB of stream buffer on top of 12.8 MB of\n line buffer, and an 855 KB recording still died at a 48 MB cap. Shared the same way, with\n a floor that keeps an ordinary conversion writing in useful-sized pieces.\n */\n const streamBuffer = Math.max(\n 16 * 1024,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n\n const open: OpenGroup[] = plan.groups.map((group, groupIndex) => {\n // A null directory means the single table goes to stdout. process.stdout is already a\n // writable stream, so the same buffered writer and backpressure handling apply.\n const target =\n shared?.target ??\n (outputDir === null\n ? process.stdout\n : createWriteStream(path.join(outputDir, group.fileName), {\n highWaterMark: streamBuffer,\n }));\n const { stream, settled } = shared ?? compressed(target, options.gzip === true);\n /*\n Under --gzip the writer feeds the compressor, so its byte count is the CSV before\n compression and says nothing about what reached the descriptor. The compressor's own\n output is what stdout is handed, so that is what is counted. `pipe` uses a 'data'\n listener of its own and a second one is delivered the same chunks.\n\n Once per stream, which in the long layout is once for all the groups. Attaching per\n group put N listeners on the one shared compressor, so every chunk was counted N\n times: `--stdout --layout long --gzip` on a 40-rate recording claimed 622,240 of\n 622,240 bytes where 15,556 had been written, failed with a disk-full error over a\n perfectly good file, and printed Node's MaxListenersExceededWarning to stderr on the\n way past ten. 0.5.4 fixed the same arithmetic in the uncompressed branch and left\n this one, because the uncompressed branch is where the count is a sum and this one is\n where it is a subscription.\n */\n if (audit && stream !== target && !shared) {\n stream.on('data', (chunk: Buffer) => audit.count(chunk.length));\n }\n /*\n One writer, not one per group, when the table is shared. Separate writers over one\n stream each hold their own buffer and flush on their own schedule, so the rows would\n reach the file in whatever order the buffers filled — which is not the order they\n were produced in, and the long layout's whole claim is that its rows are in time\n order.\n */\n const writer = shared?.writer ?? new BufferedLineWriter(stream, flushThreshold);\n if (plan.layout === 'long' && !shared) shared = { stream, settled, target, writer };\n\n // Only the first group writes the header of a shared table, and the mark before it.\n if (plan.layout !== 'long' || groupIndex === 0) {\n if (options.bom === true) writer.push(UTF8_BOM);\n writer.pushLine(\n plan.layout === 'long'\n ? csvRow(['time_s', 'channel', 'value'])\n : csvRow(['time_s', ...group.channels.map((c) => c.column)]),\n );\n }\n return {\n group,\n writer,\n formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals)),\n formatTime: makeTimeFormatter(\n group.samplesPerRecord,\n group.rate,\n group.timeDecimals,\n offsets,\n ),\n rows: 0,\n settled,\n };\n });\n\n try {\n const written = await streamSignalRows(file, plan, open, recordStarts, options);\n\n /*\n Checked once everything has been flushed and ended, and not when the reader hung up.\n\n `--stdout | head -1` closes the pipe on purpose, which is a shell idiom rather than a\n failure. A pipe is not a regular file, so auditStdout declines it anyway — the second\n guard is here because the cost of getting this one wrong is reporting a failure for a\n command that worked.\n */\n if (audit) {\n // Uncompressed, the writer hands its bytes straight to the descriptor; compressed,\n // they were counted on the compressor's way out.\n if (options.gzip !== true) {\n /*\n Once per writer, not once per group. The long layout gives every group the same\n writer, so counting per group multiplied its byte total by the number of rates:\n `--stdout --layout long` on a three-rate recording handed over 32,043 bytes, was\n credited with 96,129, and failed with a disk-full error for a file that was\n complete on disk. It only showed with stdout redirected to a regular file, since\n that is the one case the audit applies to — which is the command the --layout\n documentation gives.\n */\n for (const writer of new Set(open.map((entry) => entry.writer))) {\n audit.count(writer.bytesOut);\n }\n }\n if (!open.some((entry) => entry.writer.hungUp)) audit.verify();\n }\n onHangUp?.(open.some((entry) => entry.writer.hungUp));\n return written;\n } catch (cause) {\n for (const entry of open) entry.writer.destroy();\n\n /*\n Reading and writing both fail through here, and both were reported as writing.\n\n A recording that shrinks mid-conversion — still being written by the acquisition\n software, say — raises the reader's own error, which names the record and says the\n file changed size while it was being read. That precise diagnosis was then filed under\n `Writing to \"<dir>\" failed` and given the hint about freeing disk space, which sends\n someone to look at the one part of the system that was working.\n\n The reader's message and its advice are kept; only the note about partial output is\n added, since that much is true of either failure.\n */\n // A ConversionError arrived already saying what went wrong — a callback that threw, say.\n // Wrapping it again turned \"the onProgress callback threw\" into `Writing to \"out\"\n // failed: the onProgress callback threw`, under a hint about checking the destination.\n if (cause instanceof ConversionError) throw cause;\n\n if (cause instanceof EdfError) {\n const where = outputDir === null ? 'stdout' : `\"${outputDir}\"`;\n throw new ConversionError(\n 'INPUT_UNREADABLE',\n cause.message,\n `${cause.hint ? `${cause.hint} ` : ''}What was written to ${where} before it failed is ` +\n `incomplete and should not be used.`,\n );\n }\n\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to ${outputDir === null ? 'stdout' : `\"${outputDir}\"`} failed: ${detail}`,\n writeHint(cause, outputDir === null),\n );\n }\n}\n\n/**\n * One record's samples in the long layout: `time_s,channel,value`, in time order.\n *\n * The groups are merged rather than written one after another. Every sample in a record\n * falls inside that record's span, so taking the earliest next sample across the groups\n * each time leaves the whole file sorted by `time_s` — which is the only thing that makes a\n * mixed-rate long table useful, since sorting 3 million rows afterwards is the reader's\n * problem and a large one.\n *\n * Ties go to the group with the higher rate, which is the order the groups are already in.\n * Within a sample time the channels come out in the order the file declares them.\n */\n/**\n * Whether two sample times are close enough that only rounding could separate them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude. Well below any\n * real sample interval — the finest a recording can declare is bounded by its record\n * duration and samples-per-record fields — and well above the one-ULP disagreement that two\n * exact divisions of the same instant produce.\n */\nfunction nearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/** The chosen instant as it will be written, for deciding which channels share it. */\nfunction formatAt(\n open: readonly OpenGroup[],\n cursors: Int32Array,\n recordStart: number,\n earliest: number,\n): string {\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n if (recordStart + sample / entry.group.rate === earliest) {\n return entry.formatTime(recordStart, sample);\n }\n }\n return '';\n}\n\nasync function writeLongRecord(\n file: EdfFile,\n open: readonly OpenGroup[],\n batch: RecordBatch,\n recordInBatch: number,\n recordStart: number,\n range: ConversionPlan['range'],\n): Promise<void> {\n const writer = open[0]?.writer;\n if (!writer) return;\n\n const cursors = new Int32Array(open.length);\n /* Reused across sample times so a three-million-row conversion allocates one of these. */\n const due: { entry: OpenGroup; groupIndex: number; sample: number; channel: number }[] = [];\n\n for (;;) {\n let earliest = Infinity;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time < earliest) earliest = time;\n }\n if (earliest === Infinity) return;\n\n /*\n Everything at this instant, in the order the file declares its channels.\n\n Groups are ordered by rate, largest first, because that is how the wide layout names\n its files. Emitting a tie group by group therefore ordered the channels by descending\n sampling rate — so a recording declaring `slow, medium, fast` wrote `fast, medium,\n slow`, while the documentation promised file order and channels.csv listed file order.\n Signal index is the file's own order, and the only one a reader can predict.\n\n \"At this instant\" is decided on the time as written, not on the double. `s / rate` for\n two channels at one moment need not give the same double: a 0.3 s record holding 12 and\n 4 samples makes 40 Hz and 13.333… Hz, and 9/40 is 0.22500000000000000555 while 3/13.333…\n is 0.22499999999999997780. Equality missed that, so those two rows fell out in numeric\n order — `slow` before `fast`, once, in the middle of a file that was otherwise right.\n\n Two rows are at one time exactly when they carry the same `time_s`, which is the only\n definition a reader of the CSV can apply. The numeric pre-filter keeps the common case\n to one comparison; formatting happens only for candidates already within a hair.\n */\n const earliestText = formatAt(open, cursors, recordStart, earliest);\n due.length = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time !== earliest) {\n // Far away in the ordinary case; only a near-tie is worth formatting.\n if (!nearlyEqual(time, earliest)) continue;\n if (entry.formatTime(recordStart, sample) !== earliestText) continue;\n }\n cursors[g] = sample + 1;\n // Same window rule as the wide layout, with the same per-rate slack.\n if (\n !sampleTimeIsInRange(earliest, range.startSeconds, range.endSeconds, toleranceFor(entry.group.rate))\n ) {\n continue;\n }\n for (let c = 0; c < entry.group.channels.length; c++) {\n due.push({ entry, groupIndex: g, sample, channel: c });\n }\n }\n if (due.length === 0) continue;\n if (due.length > 1) {\n due.sort(\n (a, b) =>\n (a.entry.group.channels[a.channel]?.signal.index ?? 0) -\n (b.entry.group.channels[b.channel]?.signal.index ?? 0),\n );\n }\n\n for (const item of due) {\n const channel = item.entry.group.channels[item.channel];\n const format = item.entry.formatters[item.channel];\n if (!channel || !format) continue;\n if (writer.hungUp) return;\n writer.pushLine(\n `${item.entry.formatTime(recordStart, item.sample)},${escapeCsvField(channel.column)},` +\n `${format(file.sampleAt(batch, recordInBatch, channel.signal, item.sample))}`,\n );\n item.entry.rows++;\n // Flushed inside the record for the same reason the wide layout is; see there.\n if (writer.full) await writer.flush();\n }\n }\n}\n\nasync function streamSignalRows(\n file: EdfFile,\n plan: ConversionPlan,\n open: OpenGroup[],\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n): Promise<WrittenFile[]> {\n const { startSeconds, endSeconds, startRecord, endRecord } = plan.range;\n const { recordDuration } = file.header;\n let recordsDone = 0;\n\n /*\n Every destination has hung up, so nothing formatted from here on could reach anyone.\n `--stdout | head -1` is the usual way to arrive here: the reader takes one line and\n closes the pipe while the conversion is still near the start of the recording.\n\n Stopping also makes the reported row count mean what it says — rows that reached the\n consumer, rather than rows the loop went on formatting into a discarded buffer.\n */\n const allHungUp = (): boolean => open.length > 0 && open.every((entry) => entry.writer.hungUp);\n\n for await (const batch of file.readRecords({ startRecord, endRecord })) {\n if (allHungUp()) break;\n for (let r = 0; r < batch.recordCount; r++) {\n if (allHungUp()) break;\n const index = batch.firstRecordIndex + r;\n const recordStart = recordStarts ? (recordStarts[index] ?? index * recordDuration) : index * recordDuration;\n\n if (plan.layout === 'long') {\n await writeLongRecord(file, open, batch, r, recordStart, plan.range);\n recordsDone++;\n continue;\n }\n\n for (const entry of open) {\n const { group, writer, formatters, formatTime } = entry;\n const { channels, rate } = group;\n // Slack that never reaches the next sample; see toleranceFor.\n const slack = toleranceFor(rate);\n\n for (let sample = 0; sample < group.samplesPerRecord; sample++) {\n const time = recordStart + sample / rate;\n if (!sampleTimeIsInRange(time, startSeconds, endSeconds, slack)) continue;\n\n let row = formatTime(recordStart, sample);\n for (let c = 0; c < channels.length; c++) {\n const channel = channels[c];\n const format = formatters[c];\n if (!channel || !format) continue;\n row += ',' + format(file.sampleAt(batch, r, channel.signal, sample));\n }\n if (writer.hungUp) break;\n writer.pushLine(row);\n entry.rows++;\n\n /*\n Flushed inside the record, not only at the end of one.\n\n The buffer was drained once per record, so the rows of a single record piled up\n with nothing emptying them — memory followed samples-per-record rather than the\n batch size the writer exists to hold to. One record of 16,000,000 samples died\n with a heap out of memory under a 256 MB cap, while the same 32 MB of samples\n split into 16,000 records converted to the same 283 MB CSV without trouble. The\n format allows either layout and says nothing about which to expect.\n\n `full` is a synchronous read of the pending size, so the twenty million rows that\n are not at a boundary cost a comparison rather than a microtask each.\n */\n if (writer.full) await writer.flush();\n }\n await writer.maybeFlush();\n }\n\n recordsDone++;\n }\n\n if (options.onProgress) {\n /*\n A caller's callback is the caller's, and its failures are not the destination's.\n\n This ran inside the same try that turns a stream failure into WRITE_FAILED, so a\n progress callback that threw came back as `Writing to \"out\" failed: caller bug`,\n advising the reader to check a destination that was working perfectly. It is the\n same misattribution the write hints had until 0.4.36, one layer up.\n\n The original is kept as `cause`, so the stack that actually matters survives, and\n the conversion still stops — the callback threw, and carrying on writing into a\n directory whose owner has just failed is not an improvement.\n */\n try {\n options.onProgress({\n recordsDone,\n recordsTotal: endRecord - startRecord,\n // Once per writer: the long layout's groups share one, so summing per group\n // reported a figure larger than the file being written.\n bytesWritten: [...new Set(open.map((entry) => entry.writer))].reduce(\n (sum, entry) => sum + entry.charsWritten,\n 0,\n ),\n });\n } catch (cause) {\n throw new ConversionError(\n 'CALLBACK_FAILED',\n `The onProgress callback threw: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'This is the caller\\'s callback, not the recording or the destination. Whatever was ' +\n 'written before it threw is incomplete and should not be used.',\n { cause },\n );\n }\n }\n }\n\n const closed = new Set<BufferedLineWriter>();\n for (const entry of open) {\n if (closed.has(entry.writer)) continue;\n closed.add(entry.writer);\n await entry.writer.end();\n // With --gzip the writer's stream is the compressor, whose end callback fires when the\n // compressor is done rather than when the file behind it is. Awaiting only that would\n // report success with the tail of the file still in flight.\n await entry.settled;\n }\n\n // A shared table is one file, and its row count is every group's rows, not the first's.\n if (plan.layout === 'long') {\n const first = open[0];\n if (!first) return [];\n return [{ name: first.group.fileName, rows: open.reduce((sum, e) => sum + e.rows, 0) }];\n }\n return open.map((entry) => ({ name: entry.group.fileName, rows: entry.rows }));\n}\n\n/**\n * The stream rows are written to, plus a promise for the data reaching its destination.\n *\n * Compression sits between the writer and the file as a transform. Failures below it — a\n * full disk, an unwritable path — surface on the file stream, where nothing is listening,\n * so they are forwarded onto the compressor: that is the stream the writer watches, and\n * routing them there keeps one error path rather than two.\n */\nfunction compressed(target: Writable, gzip: boolean): { stream: Writable; settled: Promise<void> } {\n if (!gzip) return { stream: target, settled: Promise.resolve() };\n const compressor = createGzip();\n target.on('error', (error: Error) => compressor.destroy(error));\n\n /*\n pipe() ends its destination when the source ends, and stdout must survive the\n conversion: the writer already refuses to close it, because a closed stdout breaks\n every later write in the process. Nothing waits on stdout either — it is not this\n tool's to finish.\n */\n const toStdout = target === process.stdout;\n compressor.pipe(target, { end: !toStdout });\n if (toStdout) return { stream: compressor, settled: Promise.resolve() };\n\n /*\n A failure under the compressor rejects both this promise and the writer's own end(),\n and end() is the one awaited first. Without a handler attached here that rejection\n belonged to nobody, and Node killed the process over it: `--gzip` into an unwritable\n path printed a raw EISDIR stack trace instead of the ordinary \"Writing to ... failed\"\n message and exit 1, which is what the same path does uncompressed.\n\n Attaching the handler is enough to make it handled. Awaiting `settled` downstream still\n reports the failure in the case where end() happened to succeed.\n */\n const settled = finished(target);\n settled.catch(() => {});\n return { stream: compressor, settled };\n}\n\n/**\n * Write one of the sidecar files, reporting a failure the way the signal writer does.\n *\n * These three used to call `writeFile` bare, so a failure escaped as whatever the\n * filesystem said — `EISDIR: illegal operation on a directory, open '...'` with no hint\n * and, more importantly, no mention that the signal files had already been written. The\n * conversion stopped half-done and the message gave no sign of it.\n */\nasync function writeOutputFile(\n outputDir: string,\n name: string,\n contents: string,\n gzip = false,\n bom = false,\n): Promise<void> {\n // metadata.json never gets one: JSON.parse rejects a leading U+FEFF, so a mark there\n // would break every reader of the file to help a spreadsheet that will not open it.\n if (bom) contents = UTF8_BOM + contents;\n try {\n // The sidecars are built in memory before being written, so compressing them in memory\n // costs nothing extra. Only the signal tables are large enough to need a stream.\n await writeFile(path.join(outputDir, name), gzip ? gzipSync(contents) : contents, gzip ? undefined : 'utf8');\n } catch (cause) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing \"${name}\" to \"${outputDir}\" failed: ${detail}`,\n writeHint(cause),\n );\n }\n}\n\nasync function writeChannelsCsv(\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const includedColumns = new Set(plan.groups.flatMap((g) => g.channels.map((c) => c.signal.index)));\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n const lines = [\n csvRow([\n 'column',\n 'signal_index',\n 'label',\n 'unit',\n 'sampling_rate_hz',\n 'samples_per_record',\n 'physical_min',\n 'physical_max',\n 'digital_min',\n 'digital_max',\n 'transducer',\n 'prefiltering',\n 'output_file',\n 'converted',\n ]),\n ];\n\n for (const signal of file.header.signals) {\n if (signal.isAnnotations) continue;\n const column = plan.columnNames.get(signal.index) ?? `signal_${signal.index}`;\n lines.push(\n csvRow([\n column,\n String(signal.index),\n signal.label,\n signal.physicalDimension,\n String(signal.samplingRate),\n String(signal.samplesPerRecord),\n String(signal.physicalMin),\n String(signal.physicalMax),\n String(signal.digitalMin),\n String(signal.digitalMax),\n signal.transducer,\n signal.prefiltering,\n fileFor.get(signal.index) ?? '',\n includedColumns.has(signal.index) ? 'yes' : 'no',\n ]),\n );\n }\n\n const name = gzip ? 'channels.csv.gz' : 'channels.csv';\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: lines.length - 1 };\n}\n\n/*\n The bounds an annotation is filtered against: what the caller actually asked for, not the\n window that survived being clamped to the recording.\n\n Filtering by the resolved window meant an unbounded request still lost events. `--end 999h`\n on a three-second file clamps to 3, and `--start 0` gets its end from the recording, so\n both filtered to [0, 3) and dropped the markers at 3.0 and 3.5 that a bare invocation\n keeps — asking for more of a recording returned less of it.\n\n An end the caller did not give is unbounded, not \"the end of the data\": EDF+ lets an\n annotation sit past the last sample, which is exactly where an end-of-recording marker is.\n\n `--duration` is measured from wherever the conversion actually starts. Anchoring it at 0\n instead read the same absent `--start` two ways in two adjacent lines, and on a recording\n that does not begin at zero the two windows did not even overlap: an EDF+D file whose first\n record sits at 30 s converted its samples from [30, 35) while filtering annotations against\n (-inf, 5), so every event inside the converted window was dropped and `annotations.csv` came\n back empty. `resolveRange` has always defaulted the same missing start to the earliest\n record, which is where this now takes it from.\n*/\nfunction requestedAnnotationWindow(\n options: ConvertOptions,\n recordingStart: number,\n): { from: number; to: number } {\n const from = options.start ?? -Infinity;\n const to =\n options.end !== undefined\n ? options.end\n : options.duration !== undefined\n ? (options.start ?? recordingStart) + options.duration\n : Infinity;\n return { from, to };\n}\n\nasync function writeAnnotationsCsv(\n outputDir: string,\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const inWindow = annotations\n .filter((a) => a.onset >= window.from && a.onset < window.to)\n .sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n\n const lines = [csvRow(['onset_s', 'duration_s', 'description', 'record_index'])];\n for (const annotation of inWindow) {\n lines.push(\n csvRow([\n String(annotation.onset),\n annotation.duration === null ? '' : String(annotation.duration),\n annotation.text,\n String(annotation.recordIndex),\n ]),\n );\n }\n\n const name = gzip ? 'annotations.csv.gz' : 'annotations.csv';\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: inWindow.length };\n}\n\n/**\n * What to try next, chosen from what actually went wrong.\n *\n * Every write failure carried the same advice — \"Free up space or choose another destination\n * with --out\" — which fits exactly one errno. A directory sitting where signals.csv belongs\n * came back telling the reader to free up disk space, and so did a read-only volume, a\n * permission denial and a path too long for the filesystem. Wrong advice is worse than none:\n * it sends someone to check `df` on a disk that is fine, and the thing that is actually\n * wrong stays unexamined.\n *\n * The errno is the one piece of the failure that names the cause, so it is what picks the\n * sentence. Anything unrecognised keeps the general form rather than guessing.\n */\nfunction writeHint(cause: unknown, toStdout = false): string {\n /*\n The stdout path writes no files, and --out is the flag its user chose not to pass.\n\n Both halves of this sentence were wrong there: \"the files written so far\" named files\n that do not exist, and \"choose another destination with --out\" is advice for a different\n command — the destination is whatever the shell redirected the stream to. Same class as\n the disk-space hint this function replaced, one flag over.\n */\n const preamble = toStdout\n ? 'What reached stdout before it failed is incomplete and should not be used. '\n : 'The files written so far are incomplete and should not be used. ';\n const elsewhere = toStdout ? 'redirect it somewhere else' : 'choose another with --out';\n const code = (cause as NodeJS.ErrnoException | null)?.code;\n switch (code) {\n case 'ENOSPC':\n return `${preamble}The destination is out of space; free some up or ${elsewhere}.`;\n case 'EDQUOT':\n return `${preamble}You are over your disk quota on this filesystem; ${elsewhere}.`;\n case 'EACCES':\n case 'EPERM':\n return `${preamble}You do not have permission to write there; ${elsewhere}.`;\n case 'EROFS':\n return `${preamble}That filesystem is mounted read-only; ${elsewhere}.`;\n case 'EISDIR':\n return `${preamble}A directory is sitting where that file belongs; remove or rename it, or ${elsewhere}.`;\n case 'ENOENT':\n return `${preamble}Part of that path no longer exists; make sure nothing is removing it while the conversion runs.`;\n case 'ENAMETOOLONG':\n return `${preamble}That path is longer than the filesystem allows; ${toStdout ? 'redirect it somewhere shorter' : 'choose a shorter destination with --out'}.`;\n case 'EMFILE':\n case 'ENFILE':\n return `${preamble}Too many files are open; a recording with many sampling rates opens one output file per rate, so --channels narrows it.`;\n case 'EPIPE':\n return `${preamble}Whatever was reading the output closed it before the conversion finished.`;\n default:\n return `${preamble}Check the destination and run the conversion again.`;\n }\n}\n\n/**\n * Confirms that everything handed to a file-backed stdout actually arrived.\n *\n * `edf2csv rec.edf --stdout > out.csv` onto a volume that the output very nearly fills lost\n * the tail in silence: 94,977 of 102,400 rows on disk, the file ending mid-row, stderr\n * announcing \"Wrote 102,400 rows to stdout.\" and the process exiting 0. The same recording\n * onto the same volume through `--out` fails correctly, which is what gives it away.\n *\n * POSIX `write` returns a short count rather than an error when the disk fills partway\n * through a single call, and only the NEXT write raises ENOSPC. `--out` always has a next\n * write — channels.csv and metadata.json come after — so it always finds out. `--stdout`\n * has nothing after it, and when fd 1 is a regular file Node's stdout is a SyncWriteStream\n * whose `_write` discards the byte count `writeSync` returns, so nothing is raised at all.\n * No error means no `#failure`, so checking that alone would not have caught this.\n *\n * What can be checked is the descriptor: how much it grew against how much it was given.\n * Only for a regular file — a pipe, a terminal or a socket has no size to compare, and on\n * those a short write cannot go unreported this way. Appending (`>>`) is fine, since the\n * starting size is taken before anything is written.\n */\nfunction auditStdout(): { count: (bytes: number) => void; verify: () => void } | null {\n let startSize: number;\n try {\n const info = fstatSync(1);\n if (!info.isFile()) return null;\n startSize = info.size;\n } catch {\n // No usable descriptor to audit; the conversion is not the place to complain about it.\n return null;\n }\n\n let expected = 0;\n return {\n count: (bytes: number): void => {\n expected += bytes;\n },\n verify: (): void => {\n let landed: number;\n try {\n landed = fstatSync(1).size - startSize;\n } catch {\n return;\n }\n if (landed >= expected) return;\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to stdout failed: ${expected - landed} of ${expected} bytes did not reach ` +\n `the destination, which stopped accepting them part way through.`,\n 'What is there ends mid-row and should not be used. The destination is almost ' +\n 'certainly out of space — a short write is how a filesystem reports filling up ' +\n 'mid-write, and nothing after it raised an error because there was nothing after it.',\n );\n },\n };\n}\n\n/** Data rows across every signal table, so \"nothing was written\" is one question. */\nfunction rowsIn(written: readonly WrittenFile[]): number {\n return written.reduce((total, file) => total + file.rows, 0);\n}\n\n\n/** Raised when the input moved while it was being read. See where it is pushed. */\nfunction inputChanged(hadChecksum: boolean): Diagnostic {\n return {\n code: 'INPUT_CHANGED',\n severity: 'warning',\n message:\n 'The input changed while it was being converted, so this output covers the file as ' +\n 'it was when the conversion started, not as it is now.',\n hint: hadChecksum\n ? 'No checksum was recorded: the bytes that were converted are no longer there to ' +\n 'hash. Convert again once the recording is finished.'\n : 'Convert again once the recording is finished to pick up the rest.',\n };\n}\n\nasync function writeMetadata(\n outputDir: string,\n inputPath: string,\n file: EdfFile,\n plan: ConversionPlan,\n written: readonly WrittenFile[],\n annotationCount: number,\n /** Hash of the bytes that were converted, or null when it could not be vouched for. */\n checksum: string | null,\n): Promise<void> {\n const { header } = file;\n\n /*\n The file as it was when it was opened, not as it is now.\n\n Both of these used to come from re-opening the path once the CSVs were written, which\n describes whatever answers to that name by then rather than what was converted. A\n recording still being written grew from 2,000 records to 3,000 mid-conversion and\n metadata.json recorded `data_records: 2000` — correct, the CSV holds 2,000 — beside the\n byte count and SHA-256 of the 3,000-record file. The two halves of one provenance record\n described two different files, and the checksum covered bytes nobody had converted.\n Replacing the file at that path did the same thing more thoroughly.\n\n `file.fileSize` is the number every record count and window in this output was derived\n from, and the hash is taken over exactly those bytes through the descriptor already open\n on them, so the record describes one file throughout.\n */\n const metadata = {\n tool: { name: 'edf2csv', version: TOOL_VERSION },\n source: {\n path: path.resolve(inputPath),\n bytes: file.fileSize,\n modified: new Date(file.modifiedAtOpenMs).toISOString(),\n sha256: checksum,\n },\n recording: {\n format: describeFormat(header),\n version: header.version,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n // Zone-less on purpose: EDF records local wall-clock digits and no timezone.\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n signal_count: header.signalCount,\n annotation_channels: file.annotationSignals.length,\n },\n conversion: {\n converted_at: new Date().toISOString(),\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n records_converted: [plan.range.startRecord, plan.range.endRecord],\n annotations_written: annotationCount,\n files: written.map((f) => ({ name: f.name, rows: f.rows })),\n rate_groups: plan.groups.map((g) => ({\n file: g.fileName,\n sampling_rate_hz: g.rate,\n channels: g.channels.map((c) => c.column),\n decimals: g.channels.map((c) => c.decimals),\n })),\n },\n notes: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics].map((d) => ({\n code: d.code,\n severity: d.severity,\n message: d.message,\n })),\n };\n\n await writeOutputFile(outputDir, 'metadata.json', JSON.stringify(metadata, null, 2) + '\\n');\n}\n\n\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/convert/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executing a conversion.\n *\n * Everything is written in a single pass over the data records. All rate groups are\n * open at once and fed from the same batch of bytes, so a file is read once no\n * matter how many output tables it produces, and memory stays flat.\n */\n\nimport { createWriteStream, fstatSync } from 'node:fs';\nimport { mkdir, readdir, stat, writeFile } from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { finished, pipeline } from 'node:stream/promises';\nimport type { Writable } from 'node:stream';\nimport { createGzip, gzipSync } from 'node:zlib';\nimport path from 'node:path';\n\nimport type { RecordBatch } from '../edf/reader.js';\nimport { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport type { Diagnostic } from '../edf/errors.js';\nimport { EdfError } from '../edf/errors.js';\nimport type { Annotation } from '../edf/annotations.js';\nimport {\n BufferedLineWriter,\n DEFAULT_FLUSH_THRESHOLD,\n UTF8_BOM,\n csvRow,\n escapeCsvField,\n} from '../format/csv.js';\nimport { listed } from '../format/list.js';\nimport {\n fixed,\n makeSampleFormatter,\n makeTimeFormatter,\n newOffsetBudget,\n newSampleCacheBudget,\n} from '../format/number.js';\nimport type { SampleFormatter } from '../format/number.js';\nimport { buildPlan, withoutFileRateWarning } from './plan.js';\nimport type { ConversionPlan, PlanOptions, RateGroup } from './plan.js';\nimport { deriveRecordStarts } from './timing.js';\nimport { sampleTimeIsInRange, toleranceFor } from './time-range.js';\nimport { VERSION as TOOL_VERSION } from '../version.js';\n\nexport { TOOL_VERSION };\n\nexport type ConversionErrorCode =\n | 'OUTPUT_EXISTS'\n | 'OUTPUT_UNWRITABLE'\n | 'INPUT_OUTPUT_COLLISION'\n | 'INPUT_UNREADABLE'\n | 'UNSUPPORTED_REQUEST'\n | 'CALLBACK_FAILED'\n | 'WRITE_FAILED';\n\n/**\n * Codes that mean the command cannot be carried out as written, rather than that something\n * about the file or the destination went wrong.\n *\n * The distinction is the one the exit codes draw: 1 is \"the file or the destination is the\n * problem\", 2 is \"the command line is the problem\". A caller with a `--stdout` conflict is\n * being told to change the flags — the hints say exactly that — so filing it under 1 sent\n * scripts looking at the disk. Exit 2 already covers checks that need the header first,\n * such as a `--channels` term matching nothing.\n */\nexport const USAGE_ERROR_CODES: ReadonlySet<ConversionErrorCode> = new Set(['UNSUPPORTED_REQUEST']);\n\nexport class ConversionError extends Error {\n readonly code: ConversionErrorCode;\n readonly hint: string | undefined;\n constructor(code: ConversionErrorCode, message: string, hint?: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConversionError';\n this.code = code;\n this.hint = hint;\n }\n}\n\nexport interface ConvertOptions extends PlanOptions {\n /** Destination directory. Defaults to the input file's name without its extension. */\n outputDir?: string | undefined;\n /** Overwrite an existing output directory. */\n force?: boolean | undefined;\n /** Record a SHA-256 of the input in metadata.json. Costs one extra read of the file. */\n checksum?: boolean | undefined;\n /**\n * Write the signal CSV to stdout instead of to a directory.\n *\n * Only valid when the conversion produces exactly one signal file. In the default wide\n * layout a mixed-rate recording becomes several tables, and merging them into one stream\n * would mean inventing the samples this tool exists not to invent; `layout: 'long'` gives\n * one table for any recording, so it lifts the restriction. No sidecar files are written.\n */\n toStdout?: boolean | undefined;\n onProgress?: ((progress: ConversionProgress) => void) | undefined;\n}\n\nexport interface ConversionProgress {\n recordsDone: number;\n recordsTotal: number;\n bytesWritten: number;\n}\n\nexport interface WrittenFile {\n name: string;\n rows: number;\n}\n\nexport interface ConvertResult {\n outputDir: string;\n files: WrittenFile[];\n /**\n * True when a `--stdout` reader closed the pipe before the conversion finished.\n *\n * `edf2csv rec.edf --stdout | head -1` is an ordinary thing to type and not a failure, but\n * it is also not a conversion: the row count is rows formatted before the close was\n * noticed, which is neither the recording's total nor what the reader received.\n */\n readerHungUp: boolean;\n annotationCount: number;\n diagnostics: Diagnostic[];\n plan: ConversionPlan;\n file: EdfFile;\n elapsedMs: number;\n}\n\ninterface OpenGroup {\n group: RateGroup;\n writer: BufferedLineWriter;\n formatters: SampleFormatter[];\n formatTime: (recordStart: number, sample: number) => string;\n rows: number;\n /** Resolves once a compressed stream's bytes have reached the file behind it. */\n settled: Promise<void>;\n}\n\nexport async function convert(inputPath: string, options: ConvertOptions = {}): Promise<ConvertResult> {\n const startedAt = Date.now();\n const file = await EdfFile.open(inputPath);\n\n try {\n /*\n Hashed before a record is read, and only published if the file held still.\n\n A checksum taken afterwards cannot be trusted whichever descriptor it goes through. A\n file overwritten in place keeps its inode, so the open handle sees the new bytes too,\n and the old ones are simply gone — there is nowhere left to read what was converted.\n Taking it first at least means the hash describes the file the header was read from.\n\n What makes it a guarantee rather than a hope is the check at the end: if size or\n modification time moved at any point, the hash is dropped and the run says why. So\n `sha256` present means the file demonstrably did not change while it was read, and a\n recording still being written gets a null and a warning instead of a plausible hash of\n the wrong bytes. (A change reverted within the same modification timestamp would slip\n through; nothing short of copying the input first can close that.)\n */\n const checksumAtOpen = options.checksum === true ? await file.sha256() : null;\n\n // One pass over the annotation channel supplies everything annotation-related:\n // where each record sits in time, and the full event list. It reads the whole\n // file even when a window was requested, because an annotation inside the window\n // may be stored in a record outside it.\n const annotationData =\n file.annotationSignals.length > 0\n ? await file.readAnnotations()\n : { annotations: [] as Annotation[], recordStarts: [] as (number | null)[], malformed: 0 };\n\n const timing = deriveRecordStarts(file, annotationData);\n\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n options,\n );\n plan.diagnostics.push(...timing.diagnostics);\n\n if (options.toStdout === true) {\n if (!plan.writeSignals) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n '--stdout has no signal data to write because --annotations-only was given.',\n 'Drop one of the two flags.',\n );\n }\n /*\n No table at all is its own answer, and neither layout gave it.\n\n A recording with no signal channels — one holding only EDF+ annotations — produced\n zero rate groups. The wide layout then said \"--stdout needs exactly one table, but\n this recording produces 0, one for each sampling rate its channels use ()\", with an\n empty parenthetical, advice to narrow to one of no rates, and advice to use\n `--layout long` — which wrote zero bytes to stdout, not even a header row, and exited\n 0 while warning that \"the signal files hold their headers and no data\". There were no\n files and there was no header. The one path that was right about this is\n `--annotations-only`, which refuses outright, and this is the same situation reached\n by a different route.\n */\n if (plan.groups.length === 0) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n file.dataSignals.length === 0\n ? '--stdout has no signal data to write: this recording has no signal channels, ' +\n 'only EDF+ annotations.'\n : '--stdout has no signal data to write: nothing was selected that carries samples.',\n file.dataSignals.length === 0\n ? 'Convert to a directory to get its annotations.csv, or drop --stdout.'\n : 'Check --channels and the requested window, or convert to a directory instead.',\n );\n }\n\n // The long layout is one table whatever the rates are, so it has nothing to refuse.\n if (plan.layout !== 'long' && plan.groups.length !== 1) {\n throw new ConversionError(\n 'UNSUPPORTED_REQUEST',\n // Naming the rates is the point: the hint says to narrow the selection, and this\n // is what there is to narrow it to. The parenthetical used to repeat the count\n // that had just been given — \"produces 3 (its channels use 3 different sampling\n // rates)\" — which told nobody anything they could act on.\n `--stdout needs exactly one table, but this recording produces ${plan.groups.length}, ` +\n `one for each sampling rate its channels use ` +\n `(${listed(formatRates(plan.groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n 'Narrow it to one rate with --channels, write --layout long to get them all in ' +\n 'one table, or convert to a directory instead.',\n );\n }\n let readerHungUp = false;\n const written = await writeSignalFiles(file, plan, null, timing.starts, options, (hungUp) => {\n readerHungUp = hungUp;\n });\n if (await file.changedSinceOpen()) plan.diagnostics.push(inputChanged(false));\n return {\n outputDir: '-',\n files: written,\n readerHungUp,\n annotationCount: 0,\n diagnostics: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics],\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n const outputDir = options.outputDir ?? defaultOutputDir(inputPath);\n await assertInputDoesNotOverlapOutputs(inputPath, outputDir, file, plan, options);\n await prepareOutputDir(outputDir, options.force === true);\n\n const written: WrittenFile[] = [];\n\n if (plan.writeSignals && plan.groups.length > 0) {\n const signals = await writeSignalFiles(file, plan, outputDir, timing.starts, options);\n written.push(...signals);\n } else if (plan.writeSignals) {\n /*\n Asked for signal data and given none to write.\n\n Every channel selected carries zero samples per record, so there is no table to make\n — `edf2csv rec.edf --channels unused` writes channels.csv and metadata.json and no\n signals.csv at all. The NO_SAMPLES warning explains the channel; nothing explained the\n missing file, and the documentation says signals.csv is written unless\n --annotations-only was passed. Someone looking for it should be told where it went.\n */\n plan.diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message:\n 'No signal file was written: every channel selected carries zero samples per data ' +\n 'record, so there is nothing to put in one.',\n hint:\n 'channels.csv still describes them. Run with --info to see which channels do carry ' +\n 'samples.',\n });\n }\n\n if (options.annotationsOnly === true && file.annotationSignals.length === 0) {\n plan.diagnostics.push({\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message:\n '--annotations-only was requested but this recording has no annotation channel, ' +\n 'so there are no events to export.',\n hint: 'Plain EDF files carry no annotations. Convert without --annotations-only to get the signals.',\n });\n }\n\n let annotationsWritten = 0;\n if (file.annotationSignals.length > 0) {\n const result = await writeAnnotationsCsv(\n outputDir,\n annotationData.annotations,\n requestedAnnotationWindow(options, plan.range.recordingStartSeconds),\n options.gzip === true,\n options.bom === true,\n );\n written.push(result);\n annotationsWritten = result.rows;\n }\n\n written.push(\n await writeChannelsCsv(outputDir, file, plan, options.gzip === true, options.bom === true),\n );\n\n /*\n The file moved under the conversion. Said out loud, because nothing else shows it.\n\n The CSVs are still correct for the records that were read, and metadata.json still\n describes the file as it was opened — so the record is consistent with the output\n whatever happens here. What stops being true is that the checksum describes the bytes\n that were converted, since an in-place overwrite leaves nowhere to read them from. It\n is dropped rather than guessed at.\n */\n const changed = await file.changedSinceOpen();\n if (changed) plan.diagnostics.push(inputChanged(options.checksum === true));\n\n await writeMetadata(\n outputDir,\n inputPath,\n file,\n plan,\n written,\n annotationsWritten,\n changed ? null : checksumAtOpen,\n );\n\n const stale = await findStaleOutput(outputDir, written);\n\n return {\n outputDir,\n files: written,\n readerHungUp: false,\n annotationCount: annotationsWritten,\n diagnostics: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics, ...stale],\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n } finally {\n await file.close();\n }\n}\n\n/*\n Files this tool produces, used to spot leftovers from an earlier conversion.\n\n The rate part has to allow every shape a filename can now take, not just the plain ones:\n\n signals_256hz.csv an integer rate\n signals_12_5hz.csv a fractional rate, decimal point written as an underscore\n signals_1_000e-7hz.csv a rate small enough to need exponent form (0.2.2)\n signals_256hz.csv.gz any of the above, compressed (0.3.0)\n signals_0hz_2.csv a second group whose rate slug collided (0.2.1)\n\n The previous `[\\w.]+hz` matched neither of the last two — `-` and `+` are not word\n characters, and the collision suffix falls after the `hz`. Both were introduced by\n recent changes and both silently stopped being recognised as this tool's own output, so\n leftovers of exactly those kinds went unreported: the one situation the warning exists\n for. Requiring a digit after the underscore keeps a user's own `signals_notes.csv` out.\n*/\nconst OUTPUT_PATTERN =\n /^(signals(_\\d[\\w.+-]*hz(_\\d+)?)?\\.csv(\\.gz)?|annotations\\.csv(\\.gz)?|channels\\.csv(\\.gz)?|metadata\\.json)$/u;\n\n/**\n * Detect output from a previous run that this one did not replace.\n *\n * `--force` overwrites files but does not empty the directory, so converting a\n * mixed-rate recording and then a single-rate one into the same place leaves\n * `signals_256hz.csv` sitting next to a fresh `signals.csv`. Both look current.\n * Nothing is deleted here — the user is told, and decides.\n */\nasync function findStaleOutput(\n outputDir: string,\n written: readonly WrittenFile[],\n): Promise<Diagnostic[]> {\n const entries = await readdir(outputDir).catch(() => null);\n if (!entries) return [];\n\n // metadata.json is rewritten on every run but is not part of the reported file list.\n const fresh = new Set([...written.map((f) => f.name), 'metadata.json']);\n const stale = entries.filter((name) => OUTPUT_PATTERN.test(name) && !fresh.has(name)).sort();\n if (stale.length === 0) return [];\n\n return [\n {\n code: 'STALE_OUTPUT',\n severity: 'warning',\n message:\n `${stale.join(', ')} ${stale.length === 1 ? 'is' : 'are'} left over from an earlier ` +\n `conversion into this directory and ${stale.length === 1 ? 'was' : 'were'} not rewritten.`,\n hint: 'Delete them, or convert into a fresh directory, so the two runs do not get mixed up.',\n },\n ];\n}\n\nexport function defaultOutputDir(inputPath: string): string {\n const base = path.basename(inputPath, path.extname(inputPath));\n return path.join(path.dirname(inputPath), `${base}_csv`);\n}\n\n/** Never let an output target resolve to the recording being read, even with --force. */\nasync function assertInputDoesNotOverlapOutputs(\n inputPath: string,\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n options: ConvertOptions,\n): Promise<void> {\n /*\n The names this run will actually write, compressed ones included.\n\n The rate files come from the plan and already carry `.csv.gz` under --gzip; the sidecars\n were spelled out here and did not. So a compressed run checked two names it would never\n write and missed the two it would: a recording sitting at <outdir>/channels.csv.gz was\n overwritten by its own conversion, with --force, reported as a success. The same file\n named signals.csv.gz was refused, which is what gives the oversight away.\n */\n const suffix = options.gzip === true ? '.csv.gz' : '.csv';\n const names = new Set(plan.groups.map((group) => group.fileName));\n if (file.annotationSignals.length > 0) names.add(`annotations${suffix}`);\n names.add(`channels${suffix}`);\n names.add('metadata.json');\n\n const inputResolved = path.resolve(inputPath);\n const inputInfo = await stat(inputPath);\n\n for (const name of names) {\n const target = path.join(outputDir, name);\n const targetResolved = path.resolve(target);\n const targetInfo = await stat(target).catch(() => null);\n const samePath = targetResolved === inputResolved;\n const sameFile =\n targetInfo !== null && targetInfo.dev === inputInfo.dev && targetInfo.ino === inputInfo.ino;\n if (!samePath && !sameFile) continue;\n\n throw new ConversionError(\n 'INPUT_OUTPUT_COLLISION',\n `Output file \"${target}\" is the same file as the input recording.`,\n 'Choose a separate directory with --out. The input was not modified.',\n );\n }\n}\n\nasync function prepareOutputDir(dir: string, force: boolean): Promise<void> {\n /*\n Claim the directory with a single atomic mkdir rather than asking whether it exists\n and then creating it.\n\n Checking first left a window between the two: two conversions started together both\n saw \"not there\", both proceeded, and both opened write streams on the same signals.csv.\n Neither reported anything — both exited 0, having half-written one file between them.\n A non-recursive mkdir cannot do that. Exactly one caller creates the directory; every\n other one gets EEXIST from the filesystem and takes the already-exists path below.\n\n Parents are still created recursively, since --out ./a/b/c should work. Only the final\n component is the claim.\n */\n const parent = path.dirname(dir);\n if (parent && parent !== dir) {\n try {\n await mkdir(parent, { recursive: true });\n } catch (cause: unknown) {\n // A parent that is a regular file surfaces as EEXIST naming the parent, which reads\n // as though the destination already exists rather than as \"you cannot put a\n // directory inside a file\". Name the real obstacle instead of the errno.\n const info = await stat(parent).catch(() => null);\n if (info && !info.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": \"${parent}\" is a file, not a directory.`,\n 'Choose a destination whose parent directories are directories, with --out.',\n );\n }\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n 'Check the path exists and that you have permission to write there.',\n );\n }\n }\n\n let claimed = true;\n try {\n await mkdir(dir);\n } catch (cause: unknown) {\n if ((cause as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n 'Check the path exists and that you have permission to write there.',\n );\n }\n claimed = false;\n }\n\n if (!claimed) {\n const existing = await stat(dir).catch(() => null);\n\n // --force means \"replace my previous output\", not \"write output files into\n // whatever this happens to be\". Pointing it at a regular file needs saying so.\n if (existing && !existing.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a file, but the converted data needs a directory.`,\n 'Choose a directory with --out.',\n );\n }\n if (!force) {\n throw new ConversionError(\n 'OUTPUT_EXISTS',\n `\"${dir}\" already exists.`,\n 'Pass --force to overwrite it, or --out to choose a different directory.',\n );\n }\n }\n}\n\n/** Turn a Node filesystem error into something a person can act on. */\nfunction describeFsError(cause: unknown): string {\n const code = (cause as NodeJS.ErrnoException | undefined)?.code;\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOSPC') return 'the disk is full';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n if (code === 'EROFS') return 'the filesystem is read-only';\n if (code === 'ENAMETOOLONG') return 'the path is too long';\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nasync function writeSignalFiles(\n file: EdfFile,\n plan: ConversionPlan,\n outputDir: string | null,\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n onHangUp?: (hungUp: boolean) => void,\n): Promise<WrittenFile[]> {\n // One budget for every table in this conversion, not one per table: see OffsetBudget.\n const offsets = newOffsetBudget();\n // Likewise one for every channel, not one per channel: see SampleCacheBudget.\n const sampleCaches = newSampleCacheBudget();\n // Only the stdout path needs this; --out finds a full disk on its own, because it always\n // has another file to write afterwards. See auditStdout.\n const audit = outputDir === null ? auditStdout() : null;\n /*\n In the long layout every group writes into one table, so they share one stream. Opening\n a stream per group on the same path is what the rate-slug collision fix in groupByRate\n was about: two writers on one file interleave rows under a header naming one of them.\n */\n let shared: {\n stream: Writable;\n settled: Promise<void>;\n target: Writable;\n writer: BufferedLineWriter;\n } | null = null;\n\n /*\n One buffer's worth of memory for the conversion, not one per table.\n\n Every group had its own writer at the default 1 MiB threshold, so pending output was\n group count × 1 MiB before anything drained. A 6.5 MB recording with 40 sampling rates\n — the header allows thousands of channels, and a research montage really does mix a\n dozen rates — died with a raw V8 heap out-of-memory and exit 134 under a 96 MB cap,\n while the site advertises 48 MB. The recording is small; it is the fan-out that is not.\n\n Split evenly with a floor, so the single-rate case, which is nearly every recording,\n keeps exactly the buffer it always had, and forty tables cost 2.5 MB rather than 40.\n The long layout shares one writer already and is unaffected either way.\n */\n const MIN_FLUSH_THRESHOLD = 8 * 1024;\n const flushThreshold = Math.max(\n MIN_FLUSH_THRESHOLD,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n /*\n The stream's own buffer is the other half of the same sum.\n\n 0.5.6 shared the line-buffer budget and left `createWriteStream` at its 64 KiB default,\n which is per stream: 200 rate groups meant 12.8 MB of stream buffer on top of 12.8 MB of\n line buffer, and an 855 KB recording still died at a 48 MB cap. Shared the same way, with\n a floor that keeps an ordinary conversion writing in useful-sized pieces.\n */\n const streamBuffer = Math.max(\n 16 * 1024,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n\n const open: OpenGroup[] = plan.groups.map((group, groupIndex) => {\n // A null directory means the single table goes to stdout. process.stdout is already a\n // writable stream, so the same buffered writer and backpressure handling apply.\n const target =\n shared?.target ??\n (outputDir === null\n ? process.stdout\n : createWriteStream(path.join(outputDir, group.fileName), {\n highWaterMark: streamBuffer,\n }));\n const { stream, settled } = shared ?? compressed(target, options.gzip === true);\n /*\n Under --gzip the writer feeds the compressor, so its byte count is the CSV before\n compression and says nothing about what reached the descriptor. The compressor's own\n output is what stdout is handed, so that is what is counted. `pipe` uses a 'data'\n listener of its own and a second one is delivered the same chunks.\n\n Once per stream, which in the long layout is once for all the groups. Attaching per\n group put N listeners on the one shared compressor, so every chunk was counted N\n times: `--stdout --layout long --gzip` on a 40-rate recording claimed 622,240 of\n 622,240 bytes where 15,556 had been written, failed with a disk-full error over a\n perfectly good file, and printed Node's MaxListenersExceededWarning to stderr on the\n way past ten. 0.5.4 fixed the same arithmetic in the uncompressed branch and left\n this one, because the uncompressed branch is where the count is a sum and this one is\n where it is a subscription.\n */\n if (audit && stream !== target && !shared) {\n stream.on('data', (chunk: Buffer) => audit.count(chunk.length));\n }\n /*\n One writer, not one per group, when the table is shared. Separate writers over one\n stream each hold their own buffer and flush on their own schedule, so the rows would\n reach the file in whatever order the buffers filled — which is not the order they\n were produced in, and the long layout's whole claim is that its rows are in time\n order.\n */\n const writer = shared?.writer ?? new BufferedLineWriter(stream, flushThreshold);\n if (plan.layout === 'long' && !shared) shared = { stream, settled, target, writer };\n\n // Only the first group writes the header of a shared table, and the mark before it.\n if (plan.layout !== 'long' || groupIndex === 0) {\n if (options.bom === true) writer.push(UTF8_BOM);\n writer.pushLine(\n plan.layout === 'long'\n ? csvRow(['time_s', 'channel', 'value'])\n : csvRow(['time_s', ...group.channels.map((c) => c.column)]),\n );\n }\n return {\n group,\n writer,\n formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals, sampleCaches)),\n formatTime: makeTimeFormatter(\n group.samplesPerRecord,\n group.rate,\n group.timeDecimals,\n offsets,\n ),\n rows: 0,\n settled,\n };\n });\n\n try {\n const written = await streamSignalRows(file, plan, open, recordStarts, options);\n\n /*\n Checked once everything has been flushed and ended, and not when the reader hung up.\n\n `--stdout | head -1` closes the pipe on purpose, which is a shell idiom rather than a\n failure. A pipe is not a regular file, so auditStdout declines it anyway — the second\n guard is here because the cost of getting this one wrong is reporting a failure for a\n command that worked.\n */\n if (audit) {\n // Uncompressed, the writer hands its bytes straight to the descriptor; compressed,\n // they were counted on the compressor's way out.\n if (options.gzip !== true) {\n /*\n Once per writer, not once per group. The long layout gives every group the same\n writer, so counting per group multiplied its byte total by the number of rates:\n `--stdout --layout long` on a three-rate recording handed over 32,043 bytes, was\n credited with 96,129, and failed with a disk-full error for a file that was\n complete on disk. It only showed with stdout redirected to a regular file, since\n that is the one case the audit applies to — which is the command the --layout\n documentation gives.\n */\n for (const writer of new Set(open.map((entry) => entry.writer))) {\n audit.count(writer.bytesOut);\n }\n }\n if (!open.some((entry) => entry.writer.hungUp)) audit.verify();\n }\n onHangUp?.(open.some((entry) => entry.writer.hungUp));\n return written;\n } catch (cause) {\n for (const entry of open) entry.writer.destroy();\n\n /*\n Reading and writing both fail through here, and both were reported as writing.\n\n A recording that shrinks mid-conversion — still being written by the acquisition\n software, say — raises the reader's own error, which names the record and says the\n file changed size while it was being read. That precise diagnosis was then filed under\n `Writing to \"<dir>\" failed` and given the hint about freeing disk space, which sends\n someone to look at the one part of the system that was working.\n\n The reader's message and its advice are kept; only the note about partial output is\n added, since that much is true of either failure.\n */\n // A ConversionError arrived already saying what went wrong — a callback that threw, say.\n // Wrapping it again turned \"the onProgress callback threw\" into `Writing to \"out\"\n // failed: the onProgress callback threw`, under a hint about checking the destination.\n if (cause instanceof ConversionError) throw cause;\n\n if (cause instanceof EdfError) {\n const where = outputDir === null ? 'stdout' : `\"${outputDir}\"`;\n throw new ConversionError(\n 'INPUT_UNREADABLE',\n cause.message,\n `${cause.hint ? `${cause.hint} ` : ''}What was written to ${where} before it failed is ` +\n `incomplete and should not be used.`,\n );\n }\n\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to ${outputDir === null ? 'stdout' : `\"${outputDir}\"`} failed: ${detail}`,\n writeHint(cause, outputDir === null),\n );\n }\n}\n\n/**\n * One record's samples in the long layout: `time_s,channel,value`, in time order.\n *\n * The groups are merged rather than written one after another. Every sample in a record\n * falls inside that record's span, so taking the earliest next sample across the groups\n * each time leaves the whole file sorted by `time_s` — which is the only thing that makes a\n * mixed-rate long table useful, since sorting 3 million rows afterwards is the reader's\n * problem and a large one.\n *\n * Ties go to the group with the higher rate, which is the order the groups are already in.\n * Within a sample time the channels come out in the order the file declares them.\n */\n/**\n * Whether two sample times are close enough that only rounding could separate them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude. Well below any\n * real sample interval — the finest a recording can declare is bounded by its record\n * duration and samples-per-record fields — and well above the one-ULP disagreement that two\n * exact divisions of the same instant produce.\n */\nfunction nearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/** The chosen instant as it will be written, for deciding which channels share it. */\nfunction formatAt(\n open: readonly OpenGroup[],\n cursors: Int32Array,\n recordStart: number,\n earliest: number,\n): string {\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n if (recordStart + sample / entry.group.rate === earliest) {\n return entry.formatTime(recordStart, sample);\n }\n }\n return '';\n}\n\nasync function writeLongRecord(\n file: EdfFile,\n open: readonly OpenGroup[],\n batch: RecordBatch,\n recordInBatch: number,\n recordStart: number,\n range: ConversionPlan['range'],\n): Promise<void> {\n const writer = open[0]?.writer;\n if (!writer) return;\n\n const cursors = new Int32Array(open.length);\n /* Reused across sample times so a three-million-row conversion allocates one of these. */\n const due: { entry: OpenGroup; groupIndex: number; sample: number; channel: number }[] = [];\n\n for (;;) {\n let earliest = Infinity;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time < earliest) earliest = time;\n }\n if (earliest === Infinity) return;\n\n /*\n Everything at this instant, in the order the file declares its channels.\n\n Groups are ordered by rate, largest first, because that is how the wide layout names\n its files. Emitting a tie group by group therefore ordered the channels by descending\n sampling rate — so a recording declaring `slow, medium, fast` wrote `fast, medium,\n slow`, while the documentation promised file order and channels.csv listed file order.\n Signal index is the file's own order, and the only one a reader can predict.\n\n \"At this instant\" is decided on the time as written, not on the double. `s / rate` for\n two channels at one moment need not give the same double: a 0.3 s record holding 12 and\n 4 samples makes 40 Hz and 13.333… Hz, and 9/40 is 0.22500000000000000555 while 3/13.333…\n is 0.22499999999999997780. Equality missed that, so those two rows fell out in numeric\n order — `slow` before `fast`, once, in the middle of a file that was otherwise right.\n\n Two rows are at one time exactly when they carry the same `time_s`, which is the only\n definition a reader of the CSV can apply. The numeric pre-filter keeps the common case\n to one comparison; formatting happens only for candidates already within a hair.\n */\n const earliestText = formatAt(open, cursors, recordStart, earliest);\n due.length = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time !== earliest) {\n // Far away in the ordinary case; only a near-tie is worth formatting.\n if (!nearlyEqual(time, earliest)) continue;\n if (entry.formatTime(recordStart, sample) !== earliestText) continue;\n }\n cursors[g] = sample + 1;\n // Same window rule as the wide layout, with the same per-rate slack.\n if (\n !sampleTimeIsInRange(earliest, range.startSeconds, range.endSeconds, toleranceFor(entry.group.rate))\n ) {\n continue;\n }\n for (let c = 0; c < entry.group.channels.length; c++) {\n due.push({ entry, groupIndex: g, sample, channel: c });\n }\n }\n if (due.length === 0) continue;\n if (due.length > 1) {\n due.sort(\n (a, b) =>\n (a.entry.group.channels[a.channel]?.signal.index ?? 0) -\n (b.entry.group.channels[b.channel]?.signal.index ?? 0),\n );\n }\n\n for (const item of due) {\n const channel = item.entry.group.channels[item.channel];\n const format = item.entry.formatters[item.channel];\n if (!channel || !format) continue;\n if (writer.hungUp) return;\n writer.pushLine(\n `${item.entry.formatTime(recordStart, item.sample)},${escapeCsvField(channel.column)},` +\n `${format(file.sampleAt(batch, recordInBatch, channel.signal, item.sample))}`,\n );\n item.entry.rows++;\n // Flushed inside the record for the same reason the wide layout is; see there.\n if (writer.full) await writer.flush();\n }\n }\n}\n\nasync function streamSignalRows(\n file: EdfFile,\n plan: ConversionPlan,\n open: OpenGroup[],\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n): Promise<WrittenFile[]> {\n const { startSeconds, endSeconds, startRecord, endRecord } = plan.range;\n const { recordDuration } = file.header;\n let recordsDone = 0;\n\n /*\n Every destination has hung up, so nothing formatted from here on could reach anyone.\n `--stdout | head -1` is the usual way to arrive here: the reader takes one line and\n closes the pipe while the conversion is still near the start of the recording.\n\n Stopping also makes the reported row count mean what it says — rows that reached the\n consumer, rather than rows the loop went on formatting into a discarded buffer.\n */\n const allHungUp = (): boolean => open.length > 0 && open.every((entry) => entry.writer.hungUp);\n\n for await (const batch of file.readRecords({ startRecord, endRecord })) {\n if (allHungUp()) break;\n for (let r = 0; r < batch.recordCount; r++) {\n if (allHungUp()) break;\n const index = batch.firstRecordIndex + r;\n const recordStart = recordStarts ? (recordStarts[index] ?? index * recordDuration) : index * recordDuration;\n\n if (plan.layout === 'long') {\n await writeLongRecord(file, open, batch, r, recordStart, plan.range);\n recordsDone++;\n continue;\n }\n\n for (const entry of open) {\n const { group, writer, formatters, formatTime } = entry;\n const { channels, rate } = group;\n // Slack that never reaches the next sample; see toleranceFor.\n const slack = toleranceFor(rate);\n\n for (let sample = 0; sample < group.samplesPerRecord; sample++) {\n const time = recordStart + sample / rate;\n if (!sampleTimeIsInRange(time, startSeconds, endSeconds, slack)) continue;\n\n let row = formatTime(recordStart, sample);\n for (let c = 0; c < channels.length; c++) {\n const channel = channels[c];\n const format = formatters[c];\n if (!channel || !format) continue;\n row += ',' + format(file.sampleAt(batch, r, channel.signal, sample));\n }\n if (writer.hungUp) break;\n writer.pushLine(row);\n entry.rows++;\n\n /*\n Flushed inside the record, not only at the end of one.\n\n The buffer was drained once per record, so the rows of a single record piled up\n with nothing emptying them — memory followed samples-per-record rather than the\n batch size the writer exists to hold to. One record of 16,000,000 samples died\n with a heap out of memory under a 256 MB cap, while the same 32 MB of samples\n split into 16,000 records converted to the same 283 MB CSV without trouble. The\n format allows either layout and says nothing about which to expect.\n\n `full` is a synchronous read of the pending size, so the twenty million rows that\n are not at a boundary cost a comparison rather than a microtask each.\n */\n if (writer.full) await writer.flush();\n }\n await writer.maybeFlush();\n }\n\n recordsDone++;\n }\n\n if (options.onProgress) {\n /*\n A caller's callback is the caller's, and its failures are not the destination's.\n\n This ran inside the same try that turns a stream failure into WRITE_FAILED, so a\n progress callback that threw came back as `Writing to \"out\" failed: caller bug`,\n advising the reader to check a destination that was working perfectly. It is the\n same misattribution the write hints had until 0.4.36, one layer up.\n\n The original is kept as `cause`, so the stack that actually matters survives, and\n the conversion still stops — the callback threw, and carrying on writing into a\n directory whose owner has just failed is not an improvement.\n */\n try {\n options.onProgress({\n recordsDone,\n recordsTotal: endRecord - startRecord,\n // Once per writer: the long layout's groups share one, so summing per group\n // reported a figure larger than the file being written.\n bytesWritten: [...new Set(open.map((entry) => entry.writer))].reduce(\n (sum, entry) => sum + entry.charsWritten,\n 0,\n ),\n });\n } catch (cause) {\n throw new ConversionError(\n 'CALLBACK_FAILED',\n `The onProgress callback threw: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'This is the caller\\'s callback, not the recording or the destination. Whatever was ' +\n 'written before it threw is incomplete and should not be used.',\n { cause },\n );\n }\n }\n }\n\n const closed = new Set<BufferedLineWriter>();\n for (const entry of open) {\n if (closed.has(entry.writer)) continue;\n closed.add(entry.writer);\n await entry.writer.end();\n // With --gzip the writer's stream is the compressor, whose end callback fires when the\n // compressor is done rather than when the file behind it is. Awaiting only that would\n // report success with the tail of the file still in flight.\n await entry.settled;\n }\n\n // A shared table is one file, and its row count is every group's rows, not the first's.\n if (plan.layout === 'long') {\n const first = open[0];\n if (!first) return [];\n return [{ name: first.group.fileName, rows: open.reduce((sum, e) => sum + e.rows, 0) }];\n }\n return open.map((entry) => ({ name: entry.group.fileName, rows: entry.rows }));\n}\n\n/**\n * The stream rows are written to, plus a promise for the data reaching its destination.\n *\n * Compression sits between the writer and the file as a transform. Failures below it — a\n * full disk, an unwritable path — surface on the file stream, where nothing is listening,\n * so they are forwarded onto the compressor: that is the stream the writer watches, and\n * routing them there keeps one error path rather than two.\n */\nfunction compressed(target: Writable, gzip: boolean): { stream: Writable; settled: Promise<void> } {\n if (!gzip) return { stream: target, settled: Promise.resolve() };\n const compressor = createGzip();\n target.on('error', (error: Error) => compressor.destroy(error));\n\n /*\n pipe() ends its destination when the source ends, and stdout must survive the\n conversion: the writer already refuses to close it, because a closed stdout breaks\n every later write in the process. Nothing waits on stdout either — it is not this\n tool's to finish.\n */\n const toStdout = target === process.stdout;\n compressor.pipe(target, { end: !toStdout });\n if (toStdout) return { stream: compressor, settled: Promise.resolve() };\n\n /*\n A failure under the compressor rejects both this promise and the writer's own end(),\n and end() is the one awaited first. Without a handler attached here that rejection\n belonged to nobody, and Node killed the process over it: `--gzip` into an unwritable\n path printed a raw EISDIR stack trace instead of the ordinary \"Writing to ... failed\"\n message and exit 1, which is what the same path does uncompressed.\n\n Attaching the handler is enough to make it handled. Awaiting `settled` downstream still\n reports the failure in the case where end() happened to succeed.\n */\n const settled = finished(target);\n settled.catch(() => {});\n return { stream: compressor, settled };\n}\n\n/**\n * Write one of the sidecar files, reporting a failure the way the signal writer does.\n *\n * These three used to call `writeFile` bare, so a failure escaped as whatever the\n * filesystem said — `EISDIR: illegal operation on a directory, open '...'` with no hint\n * and, more importantly, no mention that the signal files had already been written. The\n * conversion stopped half-done and the message gave no sign of it.\n */\nasync function writeOutputFile(\n outputDir: string,\n name: string,\n contents: string,\n gzip = false,\n bom = false,\n): Promise<void> {\n // metadata.json never gets one: JSON.parse rejects a leading U+FEFF, so a mark there\n // would break every reader of the file to help a spreadsheet that will not open it.\n if (bom) contents = UTF8_BOM + contents;\n try {\n // The sidecars are built in memory before being written, so compressing them in memory\n // costs nothing extra. Only the signal tables are large enough to need a stream.\n await writeFile(path.join(outputDir, name), gzip ? gzipSync(contents) : contents, gzip ? undefined : 'utf8');\n } catch (cause) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing \"${name}\" to \"${outputDir}\" failed: ${detail}`,\n writeHint(cause),\n );\n }\n}\n\nasync function writeChannelsCsv(\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const includedColumns = new Set(plan.groups.flatMap((g) => g.channels.map((c) => c.signal.index)));\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n const lines = [\n csvRow([\n 'column',\n 'signal_index',\n 'label',\n 'unit',\n 'sampling_rate_hz',\n 'samples_per_record',\n 'physical_min',\n 'physical_max',\n 'digital_min',\n 'digital_max',\n 'transducer',\n 'prefiltering',\n 'output_file',\n 'converted',\n ]),\n ];\n\n for (const signal of file.header.signals) {\n if (signal.isAnnotations) continue;\n const column = plan.columnNames.get(signal.index) ?? `signal_${signal.index}`;\n lines.push(\n csvRow([\n column,\n String(signal.index),\n signal.label,\n signal.physicalDimension,\n String(signal.samplingRate),\n String(signal.samplesPerRecord),\n String(signal.physicalMin),\n String(signal.physicalMax),\n String(signal.digitalMin),\n String(signal.digitalMax),\n signal.transducer,\n signal.prefiltering,\n fileFor.get(signal.index) ?? '',\n includedColumns.has(signal.index) ? 'yes' : 'no',\n ]),\n );\n }\n\n const name = gzip ? 'channels.csv.gz' : 'channels.csv';\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: lines.length - 1 };\n}\n\n/*\n The bounds an annotation is filtered against: what the caller actually asked for, not the\n window that survived being clamped to the recording.\n\n Filtering by the resolved window meant an unbounded request still lost events. `--end 999h`\n on a three-second file clamps to 3, and `--start 0` gets its end from the recording, so\n both filtered to [0, 3) and dropped the markers at 3.0 and 3.5 that a bare invocation\n keeps — asking for more of a recording returned less of it.\n\n An end the caller did not give is unbounded, not \"the end of the data\": EDF+ lets an\n annotation sit past the last sample, which is exactly where an end-of-recording marker is.\n\n `--duration` is measured from wherever the conversion actually starts. Anchoring it at 0\n instead read the same absent `--start` two ways in two adjacent lines, and on a recording\n that does not begin at zero the two windows did not even overlap: an EDF+D file whose first\n record sits at 30 s converted its samples from [30, 35) while filtering annotations against\n (-inf, 5), so every event inside the converted window was dropped and `annotations.csv` came\n back empty. `resolveRange` has always defaulted the same missing start to the earliest\n record, which is where this now takes it from.\n*/\nfunction requestedAnnotationWindow(\n options: ConvertOptions,\n recordingStart: number,\n): { from: number; to: number } {\n const from = options.start ?? -Infinity;\n const to =\n options.end !== undefined\n ? options.end\n : options.duration !== undefined\n ? (options.start ?? recordingStart) + options.duration\n : Infinity;\n return { from, to };\n}\n\nasync function writeAnnotationsCsv(\n outputDir: string,\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const inWindow = annotations\n .filter((a) => a.onset >= window.from && a.onset < window.to)\n .sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n\n const lines = [csvRow(['onset_s', 'duration_s', 'description', 'record_index'])];\n for (const annotation of inWindow) {\n lines.push(\n csvRow([\n String(annotation.onset),\n annotation.duration === null ? '' : String(annotation.duration),\n annotation.text,\n String(annotation.recordIndex),\n ]),\n );\n }\n\n const name = gzip ? 'annotations.csv.gz' : 'annotations.csv';\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: inWindow.length };\n}\n\n/**\n * What to try next, chosen from what actually went wrong.\n *\n * Every write failure carried the same advice — \"Free up space or choose another destination\n * with --out\" — which fits exactly one errno. A directory sitting where signals.csv belongs\n * came back telling the reader to free up disk space, and so did a read-only volume, a\n * permission denial and a path too long for the filesystem. Wrong advice is worse than none:\n * it sends someone to check `df` on a disk that is fine, and the thing that is actually\n * wrong stays unexamined.\n *\n * The errno is the one piece of the failure that names the cause, so it is what picks the\n * sentence. Anything unrecognised keeps the general form rather than guessing.\n */\nfunction writeHint(cause: unknown, toStdout = false): string {\n /*\n The stdout path writes no files, and --out is the flag its user chose not to pass.\n\n Both halves of this sentence were wrong there: \"the files written so far\" named files\n that do not exist, and \"choose another destination with --out\" is advice for a different\n command — the destination is whatever the shell redirected the stream to. Same class as\n the disk-space hint this function replaced, one flag over.\n */\n const preamble = toStdout\n ? 'What reached stdout before it failed is incomplete and should not be used. '\n : 'The files written so far are incomplete and should not be used. ';\n const elsewhere = toStdout ? 'redirect it somewhere else' : 'choose another with --out';\n const code = (cause as NodeJS.ErrnoException | null)?.code;\n switch (code) {\n case 'ENOSPC':\n return `${preamble}The destination is out of space; free some up or ${elsewhere}.`;\n case 'EDQUOT':\n return `${preamble}You are over your disk quota on this filesystem; ${elsewhere}.`;\n case 'EACCES':\n case 'EPERM':\n return `${preamble}You do not have permission to write there; ${elsewhere}.`;\n case 'EROFS':\n return `${preamble}That filesystem is mounted read-only; ${elsewhere}.`;\n case 'EISDIR':\n return `${preamble}A directory is sitting where that file belongs; remove or rename it, or ${elsewhere}.`;\n case 'ENOENT':\n return `${preamble}Part of that path no longer exists; make sure nothing is removing it while the conversion runs.`;\n case 'ENAMETOOLONG':\n return `${preamble}That path is longer than the filesystem allows; ${toStdout ? 'redirect it somewhere shorter' : 'choose a shorter destination with --out'}.`;\n case 'EMFILE':\n case 'ENFILE':\n return `${preamble}Too many files are open; a recording with many sampling rates opens one output file per rate, so --channels narrows it.`;\n case 'EPIPE':\n return `${preamble}Whatever was reading the output closed it before the conversion finished.`;\n default:\n return `${preamble}Check the destination and run the conversion again.`;\n }\n}\n\n/**\n * Confirms that everything handed to a file-backed stdout actually arrived.\n *\n * `edf2csv rec.edf --stdout > out.csv` onto a volume that the output very nearly fills lost\n * the tail in silence: 94,977 of 102,400 rows on disk, the file ending mid-row, stderr\n * announcing \"Wrote 102,400 rows to stdout.\" and the process exiting 0. The same recording\n * onto the same volume through `--out` fails correctly, which is what gives it away.\n *\n * POSIX `write` returns a short count rather than an error when the disk fills partway\n * through a single call, and only the NEXT write raises ENOSPC. `--out` always has a next\n * write — channels.csv and metadata.json come after — so it always finds out. `--stdout`\n * has nothing after it, and when fd 1 is a regular file Node's stdout is a SyncWriteStream\n * whose `_write` discards the byte count `writeSync` returns, so nothing is raised at all.\n * No error means no `#failure`, so checking that alone would not have caught this.\n *\n * What can be checked is the descriptor: how much it grew against how much it was given.\n * Only for a regular file — a pipe, a terminal or a socket has no size to compare, and on\n * those a short write cannot go unreported this way. Appending (`>>`) is fine, since the\n * starting size is taken before anything is written.\n */\nfunction auditStdout(): { count: (bytes: number) => void; verify: () => void } | null {\n let startSize: number;\n try {\n const info = fstatSync(1);\n if (!info.isFile()) return null;\n startSize = info.size;\n } catch {\n // No usable descriptor to audit; the conversion is not the place to complain about it.\n return null;\n }\n\n let expected = 0;\n return {\n count: (bytes: number): void => {\n expected += bytes;\n },\n verify: (): void => {\n let landed: number;\n try {\n landed = fstatSync(1).size - startSize;\n } catch {\n return;\n }\n if (landed >= expected) return;\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to stdout failed: ${expected - landed} of ${expected} bytes did not reach ` +\n `the destination, which stopped accepting them part way through.`,\n 'What is there ends mid-row and should not be used. The destination is almost ' +\n 'certainly out of space — a short write is how a filesystem reports filling up ' +\n 'mid-write, and nothing after it raised an error because there was nothing after it.',\n );\n },\n };\n}\n\n/** Data rows across every signal table, so \"nothing was written\" is one question. */\nfunction rowsIn(written: readonly WrittenFile[]): number {\n return written.reduce((total, file) => total + file.rows, 0);\n}\n\n\n/** Raised when the input moved while it was being read. See where it is pushed. */\nfunction inputChanged(hadChecksum: boolean): Diagnostic {\n return {\n code: 'INPUT_CHANGED',\n severity: 'warning',\n message:\n 'The input changed while it was being converted, so this output covers the file as ' +\n 'it was when the conversion started, not as it is now.',\n hint: hadChecksum\n ? 'No checksum was recorded: the bytes that were converted are no longer there to ' +\n 'hash. Convert again once the recording is finished.'\n : 'Convert again once the recording is finished to pick up the rest.',\n };\n}\n\nasync function writeMetadata(\n outputDir: string,\n inputPath: string,\n file: EdfFile,\n plan: ConversionPlan,\n written: readonly WrittenFile[],\n annotationCount: number,\n /** Hash of the bytes that were converted, or null when it could not be vouched for. */\n checksum: string | null,\n): Promise<void> {\n const { header } = file;\n\n /*\n The file as it was when it was opened, not as it is now.\n\n Both of these used to come from re-opening the path once the CSVs were written, which\n describes whatever answers to that name by then rather than what was converted. A\n recording still being written grew from 2,000 records to 3,000 mid-conversion and\n metadata.json recorded `data_records: 2000` — correct, the CSV holds 2,000 — beside the\n byte count and SHA-256 of the 3,000-record file. The two halves of one provenance record\n described two different files, and the checksum covered bytes nobody had converted.\n Replacing the file at that path did the same thing more thoroughly.\n\n `file.fileSize` is the number every record count and window in this output was derived\n from, and the hash is taken over exactly those bytes through the descriptor already open\n on them, so the record describes one file throughout.\n */\n const metadata = {\n tool: { name: 'edf2csv', version: TOOL_VERSION },\n source: {\n path: path.resolve(inputPath),\n bytes: file.fileSize,\n modified: new Date(file.modifiedAtOpenMs).toISOString(),\n sha256: checksum,\n },\n recording: {\n format: describeFormat(header),\n version: header.version,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n // Zone-less on purpose: EDF records local wall-clock digits and no timezone.\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n signal_count: header.signalCount,\n annotation_channels: file.annotationSignals.length,\n },\n conversion: {\n converted_at: new Date().toISOString(),\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n records_converted: [plan.range.startRecord, plan.range.endRecord],\n annotations_written: annotationCount,\n files: written.map((f) => ({ name: f.name, rows: f.rows })),\n rate_groups: plan.groups.map((g) => ({\n file: g.fileName,\n sampling_rate_hz: g.rate,\n channels: g.channels.map((c) => c.column),\n decimals: g.channels.map((c) => c.decimals),\n })),\n },\n notes: [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics].map((d) => ({\n code: d.code,\n severity: d.severity,\n message: d.message,\n })),\n };\n\n await writeOutputFile(outputDir, 'metadata.json', JSON.stringify(metadata, null, 2) + '\\n');\n}\n\n\n"]}
@@ -12,6 +12,29 @@
12
12
  * rather than reserving memory it will never use.
13
13
  */
14
14
  import type { EdfSignal } from '../edf/header.js';
15
+ /**
16
+ * How many cached sample slots a conversion has left to spend.
17
+ *
18
+ * MAX_CACHED_SPAN is a bound on one channel, and a bound on one channel is not a bound: a
19
+ * file may declare as many channels as it likes, and each was handed its own cache. A
20
+ * channel declaring the ordinary full 16-bit digital range takes the whole 512 KB, so a
21
+ * 256-channel montage reserved 134 MB of pointers before writing a row — a 7.9 MB
22
+ * recording that needed a 192 MB heap and died with a V8 out-of-memory fatal error under
23
+ * anything smaller. The caches were the live set; nothing else in the conversion came near
24
+ * them. It is the same shape of mistake the offset budget below was made to fix, one level
25
+ * over: there the unbounded count was rate groups, here it is channels.
26
+ *
27
+ * One budget for the whole conversion leaves the ordinary recording exactly as it was and
28
+ * puts a ceiling on the dense montage: the same 256-channel file now holds its caches to
29
+ * 16 MB and converts under a 48 MB heap. Channels ask in the order the groups are written,
30
+ * which is fastest rate first, so the cache goes to the channels with the most cells to
31
+ * format. The ones that miss out fall back to formatting directly, which produces
32
+ * identical text — the output is byte-for-byte what it was.
33
+ */
34
+ export interface SampleCacheBudget {
35
+ remaining: number;
36
+ }
37
+ export declare function newSampleCacheBudget(): SampleCacheBudget;
15
38
  /**
16
39
  * Format with a fixed number of decimals, normalising negative zero.
17
40
  *
@@ -21,7 +44,7 @@ import type { EdfSignal } from '../edf/header.js';
21
44
  export declare function fixed(value: number, decimals: number): string;
22
45
  /** Maps a raw digital sample to its formatted physical value. */
23
46
  export type SampleFormatter = (digital: number) => string;
24
- export declare function makeSampleFormatter(signal: EdfSignal, decimals: number): SampleFormatter;
47
+ export declare function makeSampleFormatter(signal: EdfSignal, decimals: number, budget?: SampleCacheBudget): SampleFormatter;
25
48
  export declare function timeDecimals(samplingRate: number): number;
26
49
  /** Human-readable byte size for warnings and summaries. */
27
50
  export declare function formatBytes(bytes: number): string;
@@ -14,6 +14,19 @@
14
14
  import { makeScaler } from '../edf/scale.js';
15
15
  /** Widest digital range worth caching: 16 bits of pointers is 512 KB per channel. */
16
16
  const MAX_CACHED_SPAN = 1 << 16;
17
+ /**
18
+ * Slots for the whole conversion: 16 MB of pointers if every one is claimed.
19
+ *
20
+ * Enough that a full 32-channel montage declaring the whole 16-bit range keeps every cache
21
+ * it had before, and 512 channels of an ordinary 12-bit ADC do too. Past that the extra
22
+ * channels format directly, which costs about a quarter of the conversion's time on a
23
+ * recording where almost none of them are cached — against a file that did not convert at
24
+ * all under a 128 MB heap.
25
+ */
26
+ const MAX_CACHED_SAMPLES = 1 << 21;
27
+ export function newSampleCacheBudget() {
28
+ return { remaining: MAX_CACHED_SAMPLES };
29
+ }
17
30
  /**
18
31
  * Format with a fixed number of decimals, normalising negative zero.
19
32
  *
@@ -51,14 +64,18 @@ export function fixed(value, decimals) {
51
64
  }
52
65
  return text.slice(1);
53
66
  }
54
- export function makeSampleFormatter(signal, decimals) {
67
+ export function makeSampleFormatter(signal, decimals, budget = newSampleCacheBudget()) {
55
68
  const scale = makeScaler(signal);
56
69
  const low = Math.min(signal.digitalMin, signal.digitalMax);
57
70
  const high = Math.max(signal.digitalMin, signal.digitalMax);
58
71
  const span = high - low + 1;
59
- if (!Number.isFinite(span) || span <= 0 || span > MAX_CACHED_SPAN) {
72
+ if (!Number.isFinite(span) ||
73
+ span <= 0 ||
74
+ span > MAX_CACHED_SPAN ||
75
+ span > budget.remaining) {
60
76
  return (digital) => fixed(scale(digital), decimals);
61
77
  }
78
+ budget.remaining -= span;
62
79
  /*
63
80
  The cache covers the channel's declared digital range, not the whole int16 domain.
64
81
 
@@ -66,7 +83,8 @@ export function makeSampleFormatter(signal, decimals) {
66
83
  dense montage cannot afford: a 400-channel recording needed over 200 MB of cache alone
67
84
  and died with a V8 out-of-memory fatal error before writing a row. Sizing to the
68
85
  declared span makes the ordinary 12-bit channel 32 KB instead — the same 400 channels
69
- now fit in about 13 MB.
86
+ now fit in about 13 MB. Channels that declare the full range still take the whole 512 KB,
87
+ which is what the budget above is for.
70
88
 
71
89
  Samples outside the declared range still occur in non-conforming files. They simply
72
90
  miss the cache and are formatted directly, which produces identical text.
@@ -1 +1 @@
1
- {"version":3,"file":"number.js","sourceRoot":"","sources":["../../src/format/number.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Number formatting for CSV cells.\n *\n * A one-hour, 23-channel, 256 Hz recording is about 21 million numeric cells, so\n * this is the hottest code in a conversion. Two things keep it cheap:\n *\n * - Every sample in a channel comes from a bounded set of integers (digitalMin to\n * digitalMax, typically 4096 distinct values for a 12-bit ADC). The formatted\n * text for a digital code never changes, so it is computed once and reused.\n * - The cache fills lazily. Real recordings visit only a fraction of the range,\n * and a channel with an implausibly wide range falls back to direct formatting\n * rather than reserving memory it will never use.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { makeScaler } from '../edf/scale.js';\n\n/** Widest digital range worth caching: 16 bits of pointers is 512 KB per channel. */\nconst MAX_CACHED_SPAN = 1 << 16;\n\n/**\n * Format with a fixed number of decimals, normalising negative zero.\n *\n * Without this, a sample that scales to a very small negative value prints as\n * \"-0.000\", which looks like a distinct measurement but is not.\n */\nexport function fixed(value: number, decimals: number): string {\n // An undefined value becomes an empty cell rather than the text \"NaN\" or \"Infinity\".\n // A channel whose header leaves the digital-to-physical mapping undefined scales to\n // NaN, and an empty field is the CSV convention for \"no value here\" — the same one\n // annotations.csv uses for an absent duration. Readers parse it back as NaN / NA\n // rather than as a measurement.\n if (!Number.isFinite(value)) return '';\n\n // toFixed switches to exponent notation at 1e21, which would put \"1e+21\" in a column\n // whose every other cell is plain fixed-decimal — and a reader parsing the column as\n // decimal text has no reason to expect it. Reachable because EDF's 8-character physical\n // range fields accept exponent form, so a header may legitimately say \"1e30\".\n //\n // Above 2^53 a double carries no fractional part anyway, so the integer expansion is\n // exact rather than an approximation of one.\n if (Math.abs(value) >= 1e21) {\n const whole = BigInt(Math.trunc(value)).toString();\n return decimals > 0 ? `${whole}.${'0'.repeat(decimals)}` : whole;\n }\n\n const text = value.toFixed(decimals);\n if (text.charCodeAt(0) !== 45 /* - */) return text;\n // Cheap check for \"-0\", \"-0.0\", \"-0.000\"...\n for (let i = 1; i < text.length; i++) {\n const c = text.charCodeAt(i);\n if (c === 46 /* . */ || c === 48 /* 0 */) continue;\n return text;\n }\n return text.slice(1);\n}\n\n/** Maps a raw digital sample to its formatted physical value. */\nexport type SampleFormatter = (digital: number) => string;\n\nexport function makeSampleFormatter(signal: EdfSignal, decimals: number): SampleFormatter {\n const scale = makeScaler(signal);\n const low = Math.min(signal.digitalMin, signal.digitalMax);\n const high = Math.max(signal.digitalMin, signal.digitalMax);\n const span = high - low + 1;\n\n if (!Number.isFinite(span) || span <= 0 || span > MAX_CACHED_SPAN) {\n return (digital: number): string => fixed(scale(digital), decimals);\n }\n\n /*\n The cache covers the channel's declared digital range, not the whole int16 domain.\n\n Allocating 65536 slots regardless of span cost 512 KB of pointers per channel, which a\n dense montage cannot afford: a 400-channel recording needed over 200 MB of cache alone\n and died with a V8 out-of-memory fatal error before writing a row. Sizing to the\n declared span makes the ordinary 12-bit channel 32 KB instead — the same 400 channels\n now fit in about 13 MB.\n\n Samples outside the declared range still occur in non-conforming files. They simply\n miss the cache and are formatted directly, which produces identical text.\n */\n const cache = new Array<string | undefined>(span);\n return (digital: number): string => {\n const slot = digital - low;\n if (slot >= 0 && slot < span) {\n const hit = cache[slot];\n if (hit !== undefined) return hit;\n const text = fixed(scale(digital), decimals);\n cache[slot] = text;\n return text;\n }\n return fixed(scale(digital), decimals);\n };\n}\n\n/**\n * Decimals for the time column.\n *\n * The interval between samples is 1/rate, which has a terminating decimal expansion of d\n * places exactly when 10^d divides evenly by the rate. Writing that many places makes sample\n * times exact rather than rounded, so `time_s * rate` comes back as a whole number instead\n * of 8191.999999.\n *\n * The search used to stop at nine places, and the comment here claimed \"every rate in common\n * use clears this — 256 Hz needs 8 places, 512 Hz needs 9\". The next two powers of two do\n * not: 1/1024 needs ten places and 1/2048 needs eleven, and those are the rates a BioSemi\n * ActiveTwo records at by default. Both fell through to the rounding fallback, so the two\n * most common high-rate EEG recordings got exactly the behaviour this function exists to\n * avoid — 0.0009766 for an interval of 0.0009765625.\n *\n * A rate of 2^a * 5^b terminates in max(a, b) places, so fifteen covers every power of two up\n * to 32768 Hz, far past anything that records biosignals. Rates with a repeating expansion\n * (3 Hz, say) still fall back to enough places to keep consecutive samples distinct, and that\n * fallback keeps its own cap.\n *\n * Fifteen and not more, because the test below has to stay exact: 10^16 is past 2^53, where a\n * double can no longer hold every integer, and `Number.isInteger(10 ** 17 / 3)` is true — so a\n * larger bound reports a terminating expansion for rates that have none, and 3 Hz would ask\n * for seventeen decimals of a number that repeats forever.\n *\n * The fallback is bounded by the same fifteen. It used to stop at nine, which defeated the\n * one thing it exists to do: at 3e10 Hz — non-terminating, so no exact expansion to find —\n * the interval is 3.3e-11 and nine places rounded every sample in a record to the same\n * timestamp. A column that cannot tell two samples apart is not keeping them distinct, which\n * is what this branch is for.\n */\nconst MAX_TIME_DECIMALS = 15;\n\nexport function timeDecimals(samplingRate: number): number {\n if (!(samplingRate > 0) || !Number.isFinite(samplingRate)) return 3;\n for (let d = 0; d <= MAX_TIME_DECIMALS; d++) {\n if (Number.isInteger(10 ** d / samplingRate)) return Math.max(3, d);\n }\n return Math.min(MAX_TIME_DECIMALS, Math.max(3, Math.ceil(Math.log10(samplingRate)) + 3));\n}\n\n/** Human-readable byte size for warnings and summaries. */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n const rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1));\n return `${rounded} ${units[unit]}`;\n}\n\n/** Human-readable duration: 1h 05m 12s. */\nexport function formatDuration(seconds: number): string {\n // A duration that is not a number cannot be broken into hours and minutes, and saying so\n // beats the alternative: the fallback below rendered these as \"NaNs\" and \"Infinitys\".\n if (!Number.isFinite(seconds)) return 'unknown';\n\n /*\n Past 2^53 the decomposition stops being arithmetic and starts being noise.\n\n `total - h * 3600 - m * 60` cannot be exact once `total` exceeds what a double can hold\n as a whole number, and the error lands in the seconds field, where it shows up as a\n value that cannot exist. A header declaring a record duration of 1e300 printed:\n\n Duration 8.333333333333333e+296h 48m -2880s\n\n Forty-eight minutes and minus forty-eight seconds, under an hours field in exponent\n notation. The seconds are the honest form for a figure this size — nobody reads\n 285 million years as hours — and the record count and duration are printed beside it\n anyway, so a corrupt header stays just as visible.\n */\n if (seconds < 0 || seconds >= Number.MAX_SAFE_INTEGER) return `${seconds}s`;\n\n // Round to the precision that will actually be printed BEFORE splitting into units.\n // Splitting first left the remainder to be rounded on its own, so 3599.9996 s decomposed\n // as 59 minutes and 59.9996 seconds and then printed as \"59m 60s\" — a duration that\n // cannot exist. Rounding first carries the extra second into the minute where it belongs.\n const total = Math.round(seconds * 1000) / 1000;\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = Math.round((total - h * 3600 - m * 60) * 1000) / 1000;\n const sText = Number.isInteger(s) ? String(s) : s.toFixed(3).replace(/\\.?0+$/u, '');\n if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m ${sText}s`;\n if (m > 0) return `${m}m ${sText}s`;\n return `${sText}s`;\n}\n\n/** A record can declare a great many samples; two arrays this size is the cost of caching. */\nconst MAX_CACHED_OFFSETS = 1 << 20;\n\n/**\n * How many cached offsets a conversion has left to spend.\n *\n * The cap used to be per rate group, and a file may hold as many rate groups as it has\n * channels. Twelve channels at twelve rates just under the cap — a 25 MB file — took\n * 1.66 GB and 36 seconds, where a 92 MB file at one rate takes 283 MB and finishes in a\n * fraction of that; twenty-four of them never finished at all. A per-group limit is not a\n * limit, since nothing bounds the number of groups.\n *\n * One budget for the whole conversion makes the single-group case identical to what it was\n * and the many-group case bounded. Groups ask in order of rate, fastest first, so the cache\n * goes to the tables with the most rows to write and the ones that miss out are the ones\n * that would have gained least.\n */\nexport interface OffsetBudget {\n remaining: number;\n}\n\nexport function newOffsetBudget(): OffsetBudget {\n return { remaining: MAX_CACHED_OFFSETS };\n}\n\n/**\n * Formats the time column, reusing the part of it that repeats.\n *\n * Every value cell is already cached — a channel has at most `digitalMax - digitalMin + 1`\n * distinct readings, so the same handful of strings serve millions of rows. The time column\n * had no such luck: it rises monotonically, so no two rows share a string and `toFixed` ran\n * once per row. On a ten-million-row conversion that was a third of the total time, more\n * than reading the file and writing the CSV put together.\n *\n * What repeats is the offset within a record. Sample `s` sits at `s / rate` from the start of\n * whichever record holds it, and there are only `samplesPerRecord` such offsets in the whole\n * recording. Splitting each into whole seconds and printed fraction turns the per-row work\n * into one integer addition and a concatenation:\n *\n * record starting at 42s, sample 7 of a 100 Hz record\n * -> 42 + 0 whole seconds, fraction \".070\" -> \"42.070\"\n *\n * The decomposition is only valid when the record starts on a whole, non-negative second,\n * which is what lets the fraction come entirely from the offset. A record starting at 0.5 s would mix the\n * two, so those fall back to formatting the sum directly. Continuous recordings start every\n * record at `index * recordDuration`, so this holds for all of them whose record duration is\n * a whole number of seconds, and for discontinuous files it holds per record depending on\n * where that record actually starts.\n */\nexport function makeTimeFormatter(\n samplesPerRecord: number,\n rate: number,\n decimals: number,\n budget: OffsetBudget = newOffsetBudget(),\n): (recordStart: number, sample: number) => string {\n const direct = (recordStart: number, sample: number): string =>\n fixed(recordStart + sample / rate, decimals);\n\n if (\n !(rate > 0) ||\n !Number.isFinite(rate) ||\n samplesPerRecord <= 0 ||\n samplesPerRecord > budget.remaining\n ) {\n return direct;\n }\n budget.remaining -= samplesPerRecord;\n\n // Whole seconds and printed fraction of each offset, taken from the formatted offset\n // itself so that an offset which rounds up to the next second (0.9996 at three places)\n // carries that second rather than losing it.\n const wholeOffset = new Float64Array(samplesPerRecord);\n const fractionText: string[] = new Array<string>(samplesPerRecord);\n for (let sample = 0; sample < samplesPerRecord; sample++) {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n wholeOffset[sample] = dot === -1 ? Number(text) : Number(text.slice(0, dot));\n fractionText[sample] = dot === -1 ? '' : text.slice(dot);\n }\n\n return (recordStart: number, sample: number): string => {\n /*\n Non-negative only. Appending a fraction to a negative whole part moves the time the\n wrong way: a record at -5 s and an offset of half a second is -4.5, but \"-5\" and\n \".500\" concatenate to -5.500. Recording times start at zero, so this is unreachable\n from a well-formed file — an EDF+ timekeeping TAL is free to carry a negative onset\n though, and that is enough reason for the fast path to decline it.\n */\n if (\n sample < 0 ||\n sample >= samplesPerRecord ||\n !Number.isInteger(recordStart) ||\n recordStart < 0\n ) {\n return direct(recordStart, sample);\n }\n const whole = recordStart + (wholeOffset[sample] as number);\n /*\n The same 1e21 cliff `fixed` guards against, arriving through the back door.\n\n `${whole}` is the implicit Number-to-String conversion, which switches to exponent\n notation at 1e21 exactly as `toFixed` does — and then the cached fraction is glued onto\n the end of it, so the cell reads \"1e+21.000\". That is not a number in any notation:\n pandas and R both parse it as NaN, and a column of ordinary decimals ends in a run of\n them. A header may legitimately say `1e21` in its 8-character record-duration field, so\n three records are enough to reach it.\n\n The slow path already expands these with BigInt. One comparison per row keeps that\n correct without giving up the cache for the other twenty million.\n */\n if (whole >= 1e21) return direct(recordStart, sample);\n return `${whole}${fractionText[sample] as string}`;\n };\n}\n"]}
1
+ {"version":3,"file":"number.js","sourceRoot":"","sources":["../../src/format/number.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Number formatting for CSV cells.\n *\n * A one-hour, 23-channel, 256 Hz recording is about 21 million numeric cells, so\n * this is the hottest code in a conversion. Two things keep it cheap:\n *\n * - Every sample in a channel comes from a bounded set of integers (digitalMin to\n * digitalMax, typically 4096 distinct values for a 12-bit ADC). The formatted\n * text for a digital code never changes, so it is computed once and reused.\n * - The cache fills lazily. Real recordings visit only a fraction of the range,\n * and a channel with an implausibly wide range falls back to direct formatting\n * rather than reserving memory it will never use.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { makeScaler } from '../edf/scale.js';\n\n/** Widest digital range worth caching: 16 bits of pointers is 512 KB per channel. */\nconst MAX_CACHED_SPAN = 1 << 16;\n\n/**\n * How many cached sample slots a conversion has left to spend.\n *\n * MAX_CACHED_SPAN is a bound on one channel, and a bound on one channel is not a bound: a\n * file may declare as many channels as it likes, and each was handed its own cache. A\n * channel declaring the ordinary full 16-bit digital range takes the whole 512 KB, so a\n * 256-channel montage reserved 134 MB of pointers before writing a row — a 7.9 MB\n * recording that needed a 192 MB heap and died with a V8 out-of-memory fatal error under\n * anything smaller. The caches were the live set; nothing else in the conversion came near\n * them. It is the same shape of mistake the offset budget below was made to fix, one level\n * over: there the unbounded count was rate groups, here it is channels.\n *\n * One budget for the whole conversion leaves the ordinary recording exactly as it was and\n * puts a ceiling on the dense montage: the same 256-channel file now holds its caches to\n * 16 MB and converts under a 48 MB heap. Channels ask in the order the groups are written,\n * which is fastest rate first, so the cache goes to the channels with the most cells to\n * format. The ones that miss out fall back to formatting directly, which produces\n * identical text — the output is byte-for-byte what it was.\n */\nexport interface SampleCacheBudget {\n remaining: number;\n}\n\n/**\n * Slots for the whole conversion: 16 MB of pointers if every one is claimed.\n *\n * Enough that a full 32-channel montage declaring the whole 16-bit range keeps every cache\n * it had before, and 512 channels of an ordinary 12-bit ADC do too. Past that the extra\n * channels format directly, which costs about a quarter of the conversion's time on a\n * recording where almost none of them are cached — against a file that did not convert at\n * all under a 128 MB heap.\n */\nconst MAX_CACHED_SAMPLES = 1 << 21;\n\nexport function newSampleCacheBudget(): SampleCacheBudget {\n return { remaining: MAX_CACHED_SAMPLES };\n}\n\n/**\n * Format with a fixed number of decimals, normalising negative zero.\n *\n * Without this, a sample that scales to a very small negative value prints as\n * \"-0.000\", which looks like a distinct measurement but is not.\n */\nexport function fixed(value: number, decimals: number): string {\n // An undefined value becomes an empty cell rather than the text \"NaN\" or \"Infinity\".\n // A channel whose header leaves the digital-to-physical mapping undefined scales to\n // NaN, and an empty field is the CSV convention for \"no value here\" — the same one\n // annotations.csv uses for an absent duration. Readers parse it back as NaN / NA\n // rather than as a measurement.\n if (!Number.isFinite(value)) return '';\n\n // toFixed switches to exponent notation at 1e21, which would put \"1e+21\" in a column\n // whose every other cell is plain fixed-decimal — and a reader parsing the column as\n // decimal text has no reason to expect it. Reachable because EDF's 8-character physical\n // range fields accept exponent form, so a header may legitimately say \"1e30\".\n //\n // Above 2^53 a double carries no fractional part anyway, so the integer expansion is\n // exact rather than an approximation of one.\n if (Math.abs(value) >= 1e21) {\n const whole = BigInt(Math.trunc(value)).toString();\n return decimals > 0 ? `${whole}.${'0'.repeat(decimals)}` : whole;\n }\n\n const text = value.toFixed(decimals);\n if (text.charCodeAt(0) !== 45 /* - */) return text;\n // Cheap check for \"-0\", \"-0.0\", \"-0.000\"...\n for (let i = 1; i < text.length; i++) {\n const c = text.charCodeAt(i);\n if (c === 46 /* . */ || c === 48 /* 0 */) continue;\n return text;\n }\n return text.slice(1);\n}\n\n/** Maps a raw digital sample to its formatted physical value. */\nexport type SampleFormatter = (digital: number) => string;\n\nexport function makeSampleFormatter(\n signal: EdfSignal,\n decimals: number,\n budget: SampleCacheBudget = newSampleCacheBudget(),\n): SampleFormatter {\n const scale = makeScaler(signal);\n const low = Math.min(signal.digitalMin, signal.digitalMax);\n const high = Math.max(signal.digitalMin, signal.digitalMax);\n const span = high - low + 1;\n\n if (\n !Number.isFinite(span) ||\n span <= 0 ||\n span > MAX_CACHED_SPAN ||\n span > budget.remaining\n ) {\n return (digital: number): string => fixed(scale(digital), decimals);\n }\n budget.remaining -= span;\n\n /*\n The cache covers the channel's declared digital range, not the whole int16 domain.\n\n Allocating 65536 slots regardless of span cost 512 KB of pointers per channel, which a\n dense montage cannot afford: a 400-channel recording needed over 200 MB of cache alone\n and died with a V8 out-of-memory fatal error before writing a row. Sizing to the\n declared span makes the ordinary 12-bit channel 32 KB instead — the same 400 channels\n now fit in about 13 MB. Channels that declare the full range still take the whole 512 KB,\n which is what the budget above is for.\n\n Samples outside the declared range still occur in non-conforming files. They simply\n miss the cache and are formatted directly, which produces identical text.\n */\n const cache = new Array<string | undefined>(span);\n return (digital: number): string => {\n const slot = digital - low;\n if (slot >= 0 && slot < span) {\n const hit = cache[slot];\n if (hit !== undefined) return hit;\n const text = fixed(scale(digital), decimals);\n cache[slot] = text;\n return text;\n }\n return fixed(scale(digital), decimals);\n };\n}\n\n/**\n * Decimals for the time column.\n *\n * The interval between samples is 1/rate, which has a terminating decimal expansion of d\n * places exactly when 10^d divides evenly by the rate. Writing that many places makes sample\n * times exact rather than rounded, so `time_s * rate` comes back as a whole number instead\n * of 8191.999999.\n *\n * The search used to stop at nine places, and the comment here claimed \"every rate in common\n * use clears this — 256 Hz needs 8 places, 512 Hz needs 9\". The next two powers of two do\n * not: 1/1024 needs ten places and 1/2048 needs eleven, and those are the rates a BioSemi\n * ActiveTwo records at by default. Both fell through to the rounding fallback, so the two\n * most common high-rate EEG recordings got exactly the behaviour this function exists to\n * avoid — 0.0009766 for an interval of 0.0009765625.\n *\n * A rate of 2^a * 5^b terminates in max(a, b) places, so fifteen covers every power of two up\n * to 32768 Hz, far past anything that records biosignals. Rates with a repeating expansion\n * (3 Hz, say) still fall back to enough places to keep consecutive samples distinct, and that\n * fallback keeps its own cap.\n *\n * Fifteen and not more, because the test below has to stay exact: 10^16 is past 2^53, where a\n * double can no longer hold every integer, and `Number.isInteger(10 ** 17 / 3)` is true — so a\n * larger bound reports a terminating expansion for rates that have none, and 3 Hz would ask\n * for seventeen decimals of a number that repeats forever.\n *\n * The fallback is bounded by the same fifteen. It used to stop at nine, which defeated the\n * one thing it exists to do: at 3e10 Hz — non-terminating, so no exact expansion to find —\n * the interval is 3.3e-11 and nine places rounded every sample in a record to the same\n * timestamp. A column that cannot tell two samples apart is not keeping them distinct, which\n * is what this branch is for.\n */\nconst MAX_TIME_DECIMALS = 15;\n\nexport function timeDecimals(samplingRate: number): number {\n if (!(samplingRate > 0) || !Number.isFinite(samplingRate)) return 3;\n for (let d = 0; d <= MAX_TIME_DECIMALS; d++) {\n if (Number.isInteger(10 ** d / samplingRate)) return Math.max(3, d);\n }\n return Math.min(MAX_TIME_DECIMALS, Math.max(3, Math.ceil(Math.log10(samplingRate)) + 3));\n}\n\n/** Human-readable byte size for warnings and summaries. */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n const rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1));\n return `${rounded} ${units[unit]}`;\n}\n\n/** Human-readable duration: 1h 05m 12s. */\nexport function formatDuration(seconds: number): string {\n // A duration that is not a number cannot be broken into hours and minutes, and saying so\n // beats the alternative: the fallback below rendered these as \"NaNs\" and \"Infinitys\".\n if (!Number.isFinite(seconds)) return 'unknown';\n\n /*\n Past 2^53 the decomposition stops being arithmetic and starts being noise.\n\n `total - h * 3600 - m * 60` cannot be exact once `total` exceeds what a double can hold\n as a whole number, and the error lands in the seconds field, where it shows up as a\n value that cannot exist. A header declaring a record duration of 1e300 printed:\n\n Duration 8.333333333333333e+296h 48m -2880s\n\n Forty-eight minutes and minus forty-eight seconds, under an hours field in exponent\n notation. The seconds are the honest form for a figure this size — nobody reads\n 285 million years as hours — and the record count and duration are printed beside it\n anyway, so a corrupt header stays just as visible.\n */\n if (seconds < 0 || seconds >= Number.MAX_SAFE_INTEGER) return `${seconds}s`;\n\n // Round to the precision that will actually be printed BEFORE splitting into units.\n // Splitting first left the remainder to be rounded on its own, so 3599.9996 s decomposed\n // as 59 minutes and 59.9996 seconds and then printed as \"59m 60s\" — a duration that\n // cannot exist. Rounding first carries the extra second into the minute where it belongs.\n const total = Math.round(seconds * 1000) / 1000;\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = Math.round((total - h * 3600 - m * 60) * 1000) / 1000;\n const sText = Number.isInteger(s) ? String(s) : s.toFixed(3).replace(/\\.?0+$/u, '');\n if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m ${sText}s`;\n if (m > 0) return `${m}m ${sText}s`;\n return `${sText}s`;\n}\n\n/** A record can declare a great many samples; two arrays this size is the cost of caching. */\nconst MAX_CACHED_OFFSETS = 1 << 20;\n\n/**\n * How many cached offsets a conversion has left to spend.\n *\n * The cap used to be per rate group, and a file may hold as many rate groups as it has\n * channels. Twelve channels at twelve rates just under the cap — a 25 MB file — took\n * 1.66 GB and 36 seconds, where a 92 MB file at one rate takes 283 MB and finishes in a\n * fraction of that; twenty-four of them never finished at all. A per-group limit is not a\n * limit, since nothing bounds the number of groups.\n *\n * One budget for the whole conversion makes the single-group case identical to what it was\n * and the many-group case bounded. Groups ask in order of rate, fastest first, so the cache\n * goes to the tables with the most rows to write and the ones that miss out are the ones\n * that would have gained least.\n */\nexport interface OffsetBudget {\n remaining: number;\n}\n\nexport function newOffsetBudget(): OffsetBudget {\n return { remaining: MAX_CACHED_OFFSETS };\n}\n\n/**\n * Formats the time column, reusing the part of it that repeats.\n *\n * Every value cell is already cached — a channel has at most `digitalMax - digitalMin + 1`\n * distinct readings, so the same handful of strings serve millions of rows. The time column\n * had no such luck: it rises monotonically, so no two rows share a string and `toFixed` ran\n * once per row. On a ten-million-row conversion that was a third of the total time, more\n * than reading the file and writing the CSV put together.\n *\n * What repeats is the offset within a record. Sample `s` sits at `s / rate` from the start of\n * whichever record holds it, and there are only `samplesPerRecord` such offsets in the whole\n * recording. Splitting each into whole seconds and printed fraction turns the per-row work\n * into one integer addition and a concatenation:\n *\n * record starting at 42s, sample 7 of a 100 Hz record\n * -> 42 + 0 whole seconds, fraction \".070\" -> \"42.070\"\n *\n * The decomposition is only valid when the record starts on a whole, non-negative second,\n * which is what lets the fraction come entirely from the offset. A record starting at 0.5 s would mix the\n * two, so those fall back to formatting the sum directly. Continuous recordings start every\n * record at `index * recordDuration`, so this holds for all of them whose record duration is\n * a whole number of seconds, and for discontinuous files it holds per record depending on\n * where that record actually starts.\n */\nexport function makeTimeFormatter(\n samplesPerRecord: number,\n rate: number,\n decimals: number,\n budget: OffsetBudget = newOffsetBudget(),\n): (recordStart: number, sample: number) => string {\n const direct = (recordStart: number, sample: number): string =>\n fixed(recordStart + sample / rate, decimals);\n\n if (\n !(rate > 0) ||\n !Number.isFinite(rate) ||\n samplesPerRecord <= 0 ||\n samplesPerRecord > budget.remaining\n ) {\n return direct;\n }\n budget.remaining -= samplesPerRecord;\n\n // Whole seconds and printed fraction of each offset, taken from the formatted offset\n // itself so that an offset which rounds up to the next second (0.9996 at three places)\n // carries that second rather than losing it.\n const wholeOffset = new Float64Array(samplesPerRecord);\n const fractionText: string[] = new Array<string>(samplesPerRecord);\n for (let sample = 0; sample < samplesPerRecord; sample++) {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n wholeOffset[sample] = dot === -1 ? Number(text) : Number(text.slice(0, dot));\n fractionText[sample] = dot === -1 ? '' : text.slice(dot);\n }\n\n return (recordStart: number, sample: number): string => {\n /*\n Non-negative only. Appending a fraction to a negative whole part moves the time the\n wrong way: a record at -5 s and an offset of half a second is -4.5, but \"-5\" and\n \".500\" concatenate to -5.500. Recording times start at zero, so this is unreachable\n from a well-formed file — an EDF+ timekeeping TAL is free to carry a negative onset\n though, and that is enough reason for the fast path to decline it.\n */\n if (\n sample < 0 ||\n sample >= samplesPerRecord ||\n !Number.isInteger(recordStart) ||\n recordStart < 0\n ) {\n return direct(recordStart, sample);\n }\n const whole = recordStart + (wholeOffset[sample] as number);\n /*\n The same 1e21 cliff `fixed` guards against, arriving through the back door.\n\n `${whole}` is the implicit Number-to-String conversion, which switches to exponent\n notation at 1e21 exactly as `toFixed` does — and then the cached fraction is glued onto\n the end of it, so the cell reads \"1e+21.000\". That is not a number in any notation:\n pandas and R both parse it as NaN, and a column of ordinary decimals ends in a run of\n them. A header may legitimately say `1e21` in its 8-character record-duration field, so\n three records are enough to reach it.\n\n The slow path already expands these with BigInt. One comparison per row keeps that\n correct without giving up the cache for the other twenty million.\n */\n if (whole >= 1e21) return direct(recordStart, sample);\n return `${whole}${fractionText[sample] as string}`;\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.5.51",
3
+ "version": "0.5.53",
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",