edf2csv 0.1.0

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/cli/report.d.ts +17 -0
  4. package/dist/cli/report.d.ts.map +1 -0
  5. package/dist/cli/report.js +107 -0
  6. package/dist/cli/report.js.map +1 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +315 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/convert/channels.d.ts +40 -0
  12. package/dist/convert/channels.d.ts.map +1 -0
  13. package/dist/convert/channels.js +134 -0
  14. package/dist/convert/channels.js.map +1 -0
  15. package/dist/convert/plan.d.ts +70 -0
  16. package/dist/convert/plan.d.ts.map +1 -0
  17. package/dist/convert/plan.js +127 -0
  18. package/dist/convert/plan.js.map +1 -0
  19. package/dist/convert/run.d.ts +48 -0
  20. package/dist/convert/run.d.ts.map +1 -0
  21. package/dist/convert/run.js +358 -0
  22. package/dist/convert/run.js.map +1 -0
  23. package/dist/convert/time-range.d.ts +63 -0
  24. package/dist/convert/time-range.d.ts.map +1 -0
  25. package/dist/convert/time-range.js +188 -0
  26. package/dist/convert/time-range.js.map +1 -0
  27. package/dist/convert/timing.d.ts +18 -0
  28. package/dist/convert/timing.d.ts.map +1 -0
  29. package/dist/convert/timing.js +69 -0
  30. package/dist/convert/timing.js.map +1 -0
  31. package/dist/edf/annotations.d.ts +43 -0
  32. package/dist/edf/annotations.d.ts.map +1 -0
  33. package/dist/edf/annotations.js +88 -0
  34. package/dist/edf/annotations.js.map +1 -0
  35. package/dist/edf/errors.d.ts +27 -0
  36. package/dist/edf/errors.d.ts.map +1 -0
  37. package/dist/edf/errors.js +23 -0
  38. package/dist/edf/errors.js.map +1 -0
  39. package/dist/edf/header.d.ts +115 -0
  40. package/dist/edf/header.d.ts.map +1 -0
  41. package/dist/edf/header.js +383 -0
  42. package/dist/edf/header.js.map +1 -0
  43. package/dist/edf/reader.d.ts +75 -0
  44. package/dist/edf/reader.d.ts.map +1 -0
  45. package/dist/edf/reader.js +224 -0
  46. package/dist/edf/reader.js.map +1 -0
  47. package/dist/edf/scale.d.ts +46 -0
  48. package/dist/edf/scale.d.ts.map +1 -0
  49. package/dist/edf/scale.js +76 -0
  50. package/dist/edf/scale.js.map +1 -0
  51. package/dist/format/csv.d.ts +34 -0
  52. package/dist/format/csv.d.ts.map +1 -0
  53. package/dist/format/csv.js +136 -0
  54. package/dist/format/csv.js.map +1 -0
  55. package/dist/format/number.d.ts +42 -0
  56. package/dist/format/number.d.ts.map +1 -0
  57. package/dist/format/number.js +108 -0
  58. package/dist/format/number.js.map +1 -0
  59. package/dist/index.d.ts +25 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +8 -0
  66. package/dist/version.js.map +1 -0
  67. package/package.json +73 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Chunked reader for EDF / EDF+ files.
3
+ *
4
+ * Data records are read in batches sized by a byte budget rather than all at once,
5
+ * so peak memory stays flat regardless of how long the recording is. A 4 GB file
6
+ * and a 4 MB file use the same working set.
7
+ */
8
+ import { open, stat } from 'node:fs/promises';
9
+ import { EdfError } from './errors.js';
10
+ import { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader } from './header.js';
11
+ import { decodeRecordAnnotations } from './annotations.js';
12
+ /** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */
13
+ export const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
14
+ export class EdfFile {
15
+ path;
16
+ fileSize;
17
+ header;
18
+ /** Records actually present in the file, which may differ from the header's claim. */
19
+ recordCount;
20
+ trailingBytes;
21
+ diagnostics;
22
+ #handle;
23
+ #closed = false;
24
+ constructor(init) {
25
+ this.path = init.path;
26
+ this.fileSize = init.fileSize;
27
+ this.header = init.header;
28
+ this.recordCount = init.recordCount;
29
+ this.trailingBytes = init.trailingBytes;
30
+ this.diagnostics = init.diagnostics;
31
+ this.#handle = init.handle;
32
+ }
33
+ static async open(path) {
34
+ const info = await stat(path).catch((cause) => {
35
+ throw new EdfError('UNREADABLE', `Cannot read "${path}": ${describe(cause)}`);
36
+ });
37
+ if (info.isDirectory()) {
38
+ throw new EdfError('UNREADABLE', `"${path}" is a directory, not an EDF file.`);
39
+ }
40
+ if (!info.isFile()) {
41
+ throw new EdfError('UNREADABLE', `"${path}" is not a regular file.`);
42
+ }
43
+ const handle = await open(path, 'r');
44
+ try {
45
+ const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));
46
+ if (fixed.length > 0) {
47
+ const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);
48
+ if (bytesRead < fixed.length)
49
+ throw changedWhileReading(0, fixed.length, bytesRead);
50
+ }
51
+ // The signal count decides how much more header there is to read.
52
+ let headerBuffer = fixed;
53
+ if (fixed.length === FIXED_HEADER_BYTES) {
54
+ // Some writers NUL-pad this field instead of space-padding it, and String.trim
55
+ // does not remove NULs — leaving the whole header unreadable for a valid file.
56
+ const ns = Number(fixed.subarray(252, 256).toString('latin1').replace(/[\0\s]/gu, ''));
57
+ if (Number.isInteger(ns) && ns > 0) {
58
+ const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;
59
+ if (total <= info.size) {
60
+ headerBuffer = Buffer.alloc(total);
61
+ const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);
62
+ if (bytesRead < total)
63
+ throw changedWhileReading(0, total, bytesRead);
64
+ }
65
+ }
66
+ }
67
+ const { header, recordCount, trailingBytes, diagnostics } = parseHeader(headerBuffer, info.size);
68
+ return new EdfFile({
69
+ path,
70
+ fileSize: info.size,
71
+ header,
72
+ recordCount,
73
+ trailingBytes,
74
+ diagnostics,
75
+ handle,
76
+ });
77
+ }
78
+ catch (error) {
79
+ await handle.close().catch(() => { });
80
+ throw error;
81
+ }
82
+ }
83
+ /** Signal channels, excluding the EDF+ annotations channel. */
84
+ get dataSignals() {
85
+ return this.header.signals.filter((s) => !s.isAnnotations);
86
+ }
87
+ get annotationSignals() {
88
+ return this.header.signals.filter((s) => s.isAnnotations);
89
+ }
90
+ /** Total recording duration in seconds, based on records actually present. */
91
+ get durationSeconds() {
92
+ return this.recordCount * this.header.recordDuration;
93
+ }
94
+ /** Read a half-open range of records in batches. */
95
+ async *readRecords(options = {}) {
96
+ this.#assertOpen();
97
+ const start = Math.max(0, options.startRecord ?? 0);
98
+ const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);
99
+ if (start >= end)
100
+ return;
101
+ const { recordBytes } = this.header;
102
+ const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;
103
+ const perChunk = Math.max(1, Math.floor(budget / recordBytes));
104
+ const buffer = Buffer.alloc(perChunk * recordBytes);
105
+ for (let record = start; record < end; record += perChunk) {
106
+ const count = Math.min(perChunk, end - record);
107
+ const bytes = count * recordBytes;
108
+ const position = this.header.headerBytes + record * recordBytes;
109
+ const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);
110
+ if (bytesRead < bytes) {
111
+ // The file is shorter than its own size said. Quietly stopping here would
112
+ // hand back a conversion missing its tail with nothing to show for it.
113
+ throw new EdfError('UNREADABLE', `Expected ${bytes} bytes of data at record ${record} but only ${bytesRead} were ` +
114
+ `available; the file appears to have changed size while it was being read.`, 'Make sure the recording is not still being written to, then try again.');
115
+ }
116
+ yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };
117
+ }
118
+ }
119
+ /** Read one sample as its raw digital value. */
120
+ sampleAt(batch, recordOffset, signal, sampleIndex) {
121
+ const position = recordOffset * this.header.recordBytes +
122
+ signal.byteOffsetInRecord +
123
+ sampleIndex * this.header.bytesPerSample;
124
+ if (this.header.bytesPerSample === 3) {
125
+ // BDF stores 24-bit little-endian two's complement. Loading the three bytes
126
+ // into the top of a 32-bit word and shifting back down sign-extends them.
127
+ const data = batch.data;
128
+ return ((data[position] << 8) |
129
+ (data[position + 1] << 16) |
130
+ (data[position + 2] << 24)) >> 8;
131
+ }
132
+ return batch.data.readInt16LE(position);
133
+ }
134
+ /** Byte offset of a signal's samples within a batch. */
135
+ offsetOf(batch, recordOffset, signal) {
136
+ return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;
137
+ }
138
+ /** The annotation channel's raw bytes for one record in a batch. */
139
+ annotationBytes(batch, recordOffset, signal) {
140
+ const start = this.offsetOf(batch, recordOffset, signal);
141
+ return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);
142
+ }
143
+ /**
144
+ * Read every EDF+ annotation in the file, plus the start time each record declares.
145
+ *
146
+ * Only the annotation channel is read, seeking straight to it inside each record
147
+ * rather than pulling whole records through memory. On a multi-gigabyte recording
148
+ * that is the difference between a few kilobytes of I/O and all of it.
149
+ *
150
+ * The whole file is always scanned, never just the records inside a requested
151
+ * window: writers are not obliged to store an annotation in the record its onset
152
+ * falls in, and some put every annotation in the first record. Reading only the
153
+ * window's records would drop those entirely.
154
+ */
155
+ async readAnnotations() {
156
+ this.#assertOpen();
157
+ const annotations = [];
158
+ const recordStarts = new Array(this.recordCount).fill(null);
159
+ let malformed = 0;
160
+ const channels = this.annotationSignals;
161
+ if (channels.length === 0)
162
+ return { annotations, recordStarts, malformed };
163
+ const { headerBytes, recordBytes, bytesPerSample } = this.header;
164
+ const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));
165
+ for (let record = 0; record < this.recordCount; record++) {
166
+ for (const [position, channel] of channels.entries()) {
167
+ const buffer = buffers[position];
168
+ if (!buffer || buffer.length === 0)
169
+ continue;
170
+ const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;
171
+ const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);
172
+ if (bytesRead < buffer.length) {
173
+ throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');
174
+ }
175
+ const decoded = decodeRecordAnnotations(buffer, record);
176
+ // Only the first annotation channel carries the record's timekeeping TAL.
177
+ if (position === 0)
178
+ recordStarts[record] = decoded.recordStart;
179
+ for (const annotation of decoded.annotations)
180
+ annotations.push(annotation);
181
+ malformed += decoded.malformed;
182
+ }
183
+ }
184
+ annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);
185
+ return { annotations, recordStarts, malformed };
186
+ }
187
+ async close() {
188
+ if (this.#closed)
189
+ return;
190
+ this.#closed = true;
191
+ await this.#handle.close();
192
+ }
193
+ #assertOpen() {
194
+ if (this.#closed)
195
+ throw new EdfError('UNREADABLE', 'This EDF file has already been closed.');
196
+ }
197
+ }
198
+ function describe(cause) {
199
+ if (cause instanceof Error) {
200
+ const code = cause.code;
201
+ if (code === 'ENOENT')
202
+ return 'no such file';
203
+ if (code === 'EACCES')
204
+ return 'permission denied';
205
+ return cause.message;
206
+ }
207
+ return String(cause);
208
+ }
209
+ /** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */
210
+ async function readFully(handle, buffer, offset, length, position) {
211
+ let total = 0;
212
+ while (total < length) {
213
+ const { bytesRead } = await handle.read(buffer, offset + total, length - total, position + total);
214
+ if (bytesRead === 0)
215
+ break;
216
+ total += bytesRead;
217
+ }
218
+ return total;
219
+ }
220
+ function changedWhileReading(record, expected, actual, subject = 'data') {
221
+ return new EdfError('UNREADABLE', `Expected ${expected} bytes of ${subject} at record ${record} but only ${actual} were ` +
222
+ `available; the file appears to have changed size while it was being read.`, 'Make sure the recording is not still being written to, then try again.');
223
+ }
224
+ //# sourceMappingURL=reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEnF,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAG3D,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAuBnD,MAAM,OAAO,OAAO;IACT,IAAI,CAAS;IACb,QAAQ,CAAS;IACjB,MAAM,CAAY;IAC3B,sFAAsF;IAC7E,WAAW,CAAS;IACpB,aAAa,CAAS;IACtB,WAAW,CAAe;IAEnC,OAAO,CAAa;IACpB,OAAO,GAAG,KAAK,CAAC;IAEhB,YAAoB,IAQnB;QACC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACxC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAY;QAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACrD,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,gBAAgB,IAAI,MAAM,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,IAAI,IAAI,oCAAoC,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,IAAI,IAAI,0BAA0B,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACpE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;gBACrE,IAAI,SAAS,GAAG,KAAK,CAAC,MAAM;oBAAE,MAAM,mBAAmB,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YACtF,CAAC;YAED,kEAAkE;YAClE,IAAI,YAAY,GAAG,KAAK,CAAC;YACzB,IAAI,KAAK,CAAC,MAAM,KAAK,kBAAkB,EAAE,CAAC;gBACxC,+EAA+E;gBAC/E,+EAA+E;gBAC/E,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;gBACvF,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;oBACnC,MAAM,KAAK,GAAG,kBAAkB,GAAG,EAAE,GAAG,mBAAmB,CAAC;oBAC5D,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;wBACvB,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;wBACnC,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;wBACrE,IAAI,SAAS,GAAG,KAAK;4BAAE,MAAM,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;oBACxE,CAAC;gBACH,CAAC;YACH,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,WAAW,CACrE,YAAY,EACZ,IAAI,CAAC,IAAI,CACV,CAAC;YAEF,OAAO,IAAI,OAAO,CAAC;gBACjB,IAAI;gBACJ,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,MAAM;gBACN,WAAW;gBACX,aAAa;gBACb,WAAW;gBACX,MAAM;aACP,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACrC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,iBAAiB;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC5D,CAAC;IAED,8EAA8E;IAC9E,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;IACvD,CAAC;IAED,oDAAoD;IACpD,KAAK,CAAC,CAAC,WAAW,CAAC,UAA8B,EAAE;QACjD,IAAI,CAAC,WAAW,EAAE,CAAC;QAEnB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9E,IAAI,KAAK,IAAI,GAAG;YAAE,OAAO;QAEzB,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACpC,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAC;QAEpD,KAAK,IAAI,MAAM,GAAG,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,MAAM,IAAI,QAAQ,EAAE,CAAC;YAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,GAAG,MAAM,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,KAAK,GAAG,WAAW,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;YAEhE,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC5E,IAAI,SAAS,GAAG,KAAK,EAAE,CAAC;gBACtB,0EAA0E;gBAC1E,uEAAuE;gBACvE,MAAM,IAAI,QAAQ,CAChB,YAAY,EACZ,YAAY,KAAK,4BAA4B,MAAM,aAAa,SAAS,QAAQ;oBAC/E,2EAA2E,EAC7E,wEAAwE,CACzE,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QAC1F,CAAC;IACH,CAAC;IAED,gDAAgD;IAChD,QAAQ,CAAC,KAAkB,EAAE,YAAoB,EAAE,MAAiB,EAAE,WAAmB;QACvF,MAAM,QAAQ,GACZ,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW;YACtC,MAAM,CAAC,kBAAkB;YACzB,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;QAE3C,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,CAAC,EAAE,CAAC;YACrC,4EAA4E;YAC5E,0EAA0E;YAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YACxB,OAAO,CACL,CAAE,IAAI,CAAC,QAAQ,CAAY,IAAI,CAAC,CAAC;gBACjC,CAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAY,IAAI,EAAE,CAAC;gBACtC,CAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAY,IAAI,EAAE,CAAC,CACvC,IAAI,CAAC,CAAC;QACT,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAED,wDAAwD;IACxD,QAAQ,CAAC,KAAkB,EAAE,YAAoB,EAAE,MAAiB;QAClE,OAAO,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,kBAAkB,CAAC;IAC5E,CAAC;IAED,oEAAoE;IACpE,eAAe,CAAC,KAAkB,EAAE,YAAoB,EAAE,MAAiB;QACzE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;QACzD,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAClG,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,eAAe;QAKnB,IAAI,CAAC,WAAW,EAAE,CAAC;QAEnB,MAAM,WAAW,GAAiB,EAAE,CAAC;QACrC,MAAM,YAAY,GAAsB,IAAI,KAAK,CAAgB,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9F,IAAI,SAAS,GAAG,CAAC,CAAC;QAElB,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;QAE3E,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACjE,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,GAAG,cAAc,CAAC,CAAC,CAAC;QAEvF,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC;YACzD,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;gBACrD,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACjC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;oBAAE,SAAS;gBAE7C,MAAM,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;gBAC/E,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAClF,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;oBAC9B,MAAM,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC;gBACjF,CAAC;gBAED,MAAM,OAAO,GAAG,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBACxD,0EAA0E;gBAC1E,IAAI,QAAQ,KAAK,CAAC;oBAAE,YAAY,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;gBAC/D,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,WAAW;oBAAE,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC3E,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC;YACjC,CAAC;QACH,CAAC;QAED,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;QAC/E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;IAED,WAAW;QACT,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,wCAAwC,CAAC,CAAC;IAC/F,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;QACnD,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,cAAc,CAAC;QAC7C,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,mBAAmB,CAAC;QAClD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,8FAA8F;AAC9F,KAAK,UAAU,SAAS,CACtB,MAAkB,EAClB,MAAc,EACd,MAAc,EACd,MAAc,EACd,QAAgB;IAEhB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,OAAO,KAAK,GAAG,MAAM,EAAE,CAAC;QACtB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CACrC,MAAM,EACN,MAAM,GAAG,KAAK,EACd,MAAM,GAAG,KAAK,EACd,QAAQ,GAAG,KAAK,CACjB,CAAC;QACF,IAAI,SAAS,KAAK,CAAC;YAAE,MAAM;QAC3B,KAAK,IAAI,SAAS,CAAC;IACrB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,mBAAmB,CAC1B,MAAc,EACd,QAAgB,EAChB,MAAc,EACd,OAAO,GAAG,MAAM;IAEhB,OAAO,IAAI,QAAQ,CACjB,YAAY,EACZ,YAAY,QAAQ,aAAa,OAAO,cAAc,MAAM,aAAa,MAAM,QAAQ;QACrF,2EAA2E,EAC7E,wEAAwE,CACzE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Digital-to-physical conversion.
3
+ *
4
+ * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)
5
+ * and (digitalMax -> physicalMax), which the specification writes as:
6
+ *
7
+ * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)
8
+ * physical = (digital - digitalMin) * gain + physicalMin
9
+ *
10
+ * That form is evaluated here in EDFlib's algebraically equivalent arrangement:
11
+ *
12
+ * offset = physicalMax / gain - digitalMax
13
+ * physical = gain * (offset + digital)
14
+ *
15
+ * The rearrangement is not cosmetic. Written the first way, a channel spanning
16
+ * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation
17
+ * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact
18
+ * value is 0.19536019536019536. EDFlib's form keeps the intermediate small
19
+ * (offset + digital = 0.5 here) and returns the correctly rounded result.
20
+ *
21
+ * Both properties matter. The values are as accurate as a double can express, and
22
+ * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,
23
+ * so the test suite can assert exact equality against a reference implementation
24
+ * rather than settling for a tolerance.
25
+ */
26
+ import type { EdfSignal } from './header.js';
27
+ export type Scaler = (digital: number) => number;
28
+ export declare function makeScaler(signal: EdfSignal): Scaler;
29
+ /**
30
+ * Smallest physical step this channel can express — one digital unit.
31
+ * Used to choose a decimal precision that preserves every distinct sample value.
32
+ */
33
+ export declare function quantizationStep(signal: EdfSignal): number;
34
+ /**
35
+ * Decimal places needed so that two adjacent digital codes never round to the same
36
+ * string. Two places past the quantization step keep rounding error far below the
37
+ * resolution the hardware actually recorded, without padding the file with digits
38
+ * that carry no information.
39
+ *
40
+ * The ceiling is 15 rather than a tidier number because a channel calibrated in
41
+ * volts rather than microvolts has a step near 1e-7, and one in tesla smaller
42
+ * still. A lower cap would round genuinely different samples to the same text. It
43
+ * costs nothing for ordinary channels, whose step lands them at three or four.
44
+ */
45
+ export declare function decimalsForSignal(signal: EdfSignal, max?: number): number;
46
+ //# sourceMappingURL=scale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAEjD,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAwBpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,SAAK,GAAG,MAAM,CAKrE"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Digital-to-physical conversion.
3
+ *
4
+ * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)
5
+ * and (digitalMax -> physicalMax), which the specification writes as:
6
+ *
7
+ * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)
8
+ * physical = (digital - digitalMin) * gain + physicalMin
9
+ *
10
+ * That form is evaluated here in EDFlib's algebraically equivalent arrangement:
11
+ *
12
+ * offset = physicalMax / gain - digitalMax
13
+ * physical = gain * (offset + digital)
14
+ *
15
+ * The rearrangement is not cosmetic. Written the first way, a channel spanning
16
+ * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation
17
+ * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact
18
+ * value is 0.19536019536019536. EDFlib's form keeps the intermediate small
19
+ * (offset + digital = 0.5 here) and returns the correctly rounded result.
20
+ *
21
+ * Both properties matter. The values are as accurate as a double can express, and
22
+ * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,
23
+ * so the test suite can assert exact equality against a reference implementation
24
+ * rather than settling for a tolerance.
25
+ */
26
+ export function makeScaler(signal) {
27
+ const { digitalMin, digitalMax, physicalMin, physicalMax } = signal;
28
+ // A zero digital span leaves the mapping undefined — the header contradicts itself.
29
+ // Reporting the physical minimum is the least misleading answer, and the header
30
+ // parser has already raised DEGENERATE_DIGITAL_RANGE so the user is not misled.
31
+ if (digitalMax === digitalMin)
32
+ return () => physicalMin;
33
+ const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);
34
+ // A flat physical range makes every sample the same value, and would divide by
35
+ // zero in the offset below.
36
+ if (gain === 0 || !Number.isFinite(gain))
37
+ return () => physicalMin;
38
+ // Deriving the offset divides by the gain. For every realistic calibration that is
39
+ // both safe and more accurate, but an absurd header (a huge physical range over a
40
+ // near-zero gain) could overflow it, so fall back to the specification's own
41
+ // arrangement rather than emitting Infinity.
42
+ const offset = physicalMax / gain - digitalMax;
43
+ if (!Number.isFinite(offset)) {
44
+ return (digital) => (digital - digitalMin) * gain + physicalMin;
45
+ }
46
+ return (digital) => gain * (offset + digital);
47
+ }
48
+ /**
49
+ * Smallest physical step this channel can express — one digital unit.
50
+ * Used to choose a decimal precision that preserves every distinct sample value.
51
+ */
52
+ export function quantizationStep(signal) {
53
+ const digitalSpan = signal.digitalMax - signal.digitalMin;
54
+ if (digitalSpan === 0)
55
+ return 0;
56
+ return Math.abs((signal.physicalMax - signal.physicalMin) / digitalSpan);
57
+ }
58
+ /**
59
+ * Decimal places needed so that two adjacent digital codes never round to the same
60
+ * string. Two places past the quantization step keep rounding error far below the
61
+ * resolution the hardware actually recorded, without padding the file with digits
62
+ * that carry no information.
63
+ *
64
+ * The ceiling is 15 rather than a tidier number because a channel calibrated in
65
+ * volts rather than microvolts has a step near 1e-7, and one in tesla smaller
66
+ * still. A lower cap would round genuinely different samples to the same text. It
67
+ * costs nothing for ordinary channels, whose step lands them at three or four.
68
+ */
69
+ export function decimalsForSignal(signal, max = 15) {
70
+ const step = quantizationStep(signal);
71
+ if (!(step > 0) || !Number.isFinite(step))
72
+ return 3;
73
+ const needed = Math.ceil(-Math.log10(step)) + 2;
74
+ return Math.min(max, Math.max(0, needed));
75
+ }
76
+ //# sourceMappingURL=scale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale.js","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAMH,MAAM,UAAU,UAAU,CAAC,MAAiB;IAC1C,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;IAEpE,oFAAoF;IACpF,gFAAgF;IAChF,gFAAgF;IAChF,IAAI,UAAU,KAAK,UAAU;QAAE,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC;IAExD,MAAM,IAAI,GAAG,CAAC,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC,CAAC;IAErE,+EAA+E;IAC/E,4BAA4B;IAC5B,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC;IAEnE,mFAAmF;IACnF,kFAAkF;IAClF,6EAA6E;IAC7E,6CAA6C;IAC7C,MAAM,MAAM,GAAG,WAAW,GAAG,IAAI,GAAG,UAAU,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,OAAe,EAAU,EAAE,CAAC,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,IAAI,GAAG,WAAW,CAAC;IAClF,CAAC;IAED,OAAO,CAAC,OAAe,EAAU,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IAC1D,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,GAAG,GAAG,EAAE;IAC3D,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAChD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CSV escaping and a buffered, backpressure-aware line writer.
3
+ *
4
+ * Rows are accumulated in memory and flushed in large blocks. Writing row by row
5
+ * would issue millions of small stream writes, and ignoring the return value of
6
+ * `write()` would let Node buffer the entire output in RAM — which for an hour of
7
+ * 23-channel EEG is several hundred megabytes.
8
+ */
9
+ import type { Writable } from 'node:stream';
10
+ /** Quote a field only when CSV requires it, doubling any embedded quotes. */
11
+ export declare function escapeCsvField(value: string): string;
12
+ export declare function csvRow(cells: readonly string[]): string;
13
+ /**
14
+ * Accumulates text and flushes it to a stream, respecting backpressure.
15
+ *
16
+ * Call `push()` freely, then `await maybeFlush()` at a row boundary. Memory stays
17
+ * bounded by the flush threshold plus whatever the stream has not yet drained.
18
+ */
19
+ export declare class BufferedLineWriter {
20
+ #private;
21
+ constructor(stream: Writable, threshold?: number);
22
+ /** Characters written to the stream so far, before any encoding expansion. */
23
+ get charsWritten(): number;
24
+ push(text: string): void;
25
+ pushLine(text: string): void;
26
+ /** Flush if enough has accumulated. Await this at row boundaries. */
27
+ maybeFlush(): Promise<void>;
28
+ flush(): Promise<void>;
29
+ /** Flush anything left and close the stream. */
30
+ end(): Promise<void>;
31
+ /** Close the stream without caring whether the data made it out. */
32
+ destroy(): void;
33
+ }
34
+ //# sourceMappingURL=csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ5C,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,qBAAa,kBAAkB;;gBASjB,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAE,MAAgC;IAWzE,8EAA8E;IAC9E,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK5B,qEAAqE;IAC/D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C5B,gDAAgD;IAC1C,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAe1B,oEAAoE;IACpE,OAAO,IAAI,IAAI;CAOhB"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * CSV escaping and a buffered, backpressure-aware line writer.
3
+ *
4
+ * Rows are accumulated in memory and flushed in large blocks. Writing row by row
5
+ * would issue millions of small stream writes, and ignoring the return value of
6
+ * `write()` would let Node buffer the entire output in RAM — which for an hour of
7
+ * 23-channel EEG is several hundred megabytes.
8
+ */
9
+ import { once } from 'node:events';
10
+ /** Flush once this many characters have accumulated. */
11
+ const DEFAULT_FLUSH_THRESHOLD = 1 << 20; // 1 MiB
12
+ const NEEDS_QUOTING = /[",\r\n]/u;
13
+ /** Quote a field only when CSV requires it, doubling any embedded quotes. */
14
+ export function escapeCsvField(value) {
15
+ if (value === '')
16
+ return '';
17
+ if (!NEEDS_QUOTING.test(value))
18
+ return value;
19
+ return `"${value.replaceAll('"', '""')}"`;
20
+ }
21
+ export function csvRow(cells) {
22
+ return cells.map(escapeCsvField).join(',');
23
+ }
24
+ /**
25
+ * Accumulates text and flushes it to a stream, respecting backpressure.
26
+ *
27
+ * Call `push()` freely, then `await maybeFlush()` at a row boundary. Memory stays
28
+ * bounded by the flush threshold plus whatever the stream has not yet drained.
29
+ */
30
+ export class BufferedLineWriter {
31
+ #stream;
32
+ #parts = [];
33
+ #pending = 0;
34
+ #threshold;
35
+ #bytesWritten = 0;
36
+ #ended = false;
37
+ #failure = null;
38
+ constructor(stream, threshold = DEFAULT_FLUSH_THRESHOLD) {
39
+ this.#stream = stream;
40
+ this.#threshold = threshold;
41
+ // A stream with no 'error' listener throws asynchronously and takes the whole
42
+ // process down with a raw stack trace. Capturing the error here lets the next
43
+ // flush surface it as a normal failure with a usable message.
44
+ this.#stream.on('error', (error) => {
45
+ this.#failure ??= error;
46
+ });
47
+ }
48
+ /** Characters written to the stream so far, before any encoding expansion. */
49
+ get charsWritten() {
50
+ return this.#bytesWritten;
51
+ }
52
+ push(text) {
53
+ this.#parts.push(text);
54
+ this.#pending += text.length;
55
+ }
56
+ pushLine(text) {
57
+ this.push(text);
58
+ this.push('\n');
59
+ }
60
+ /** Flush if enough has accumulated. Await this at row boundaries. */
61
+ async maybeFlush() {
62
+ if (this.#pending >= this.#threshold)
63
+ await this.flush();
64
+ }
65
+ async flush() {
66
+ if (this.#failure)
67
+ throw this.#failure;
68
+ if (this.#parts.length === 0)
69
+ return;
70
+ const chunk = this.#parts.join('');
71
+ this.#parts = [];
72
+ this.#pending = 0;
73
+ this.#bytesWritten += chunk.length;
74
+ if (!this.#stream.write(chunk)) {
75
+ // The consumer is behind; wait rather than letting Node buffer without bound.
76
+ // Waiting only on 'drain' would hang forever if the stream fails instead.
77
+ await this.#drain();
78
+ }
79
+ }
80
+ /** Resolve on 'drain', reject if the stream fails first. */
81
+ async #drain() {
82
+ if (this.#failure)
83
+ throw this.#failure;
84
+ await new Promise((resolve, reject) => {
85
+ const cleanup = () => {
86
+ this.#stream.off('drain', onDrain);
87
+ this.#stream.off('error', onError);
88
+ this.#stream.off('close', onClose);
89
+ };
90
+ const onDrain = () => {
91
+ cleanup();
92
+ resolve();
93
+ };
94
+ const onError = (error) => {
95
+ cleanup();
96
+ reject(error);
97
+ };
98
+ const onClose = () => {
99
+ cleanup();
100
+ reject(this.#failure ?? new Error('The output stream closed before the data was written.'));
101
+ };
102
+ this.#stream.once('drain', onDrain);
103
+ this.#stream.once('error', onError);
104
+ this.#stream.once('close', onClose);
105
+ });
106
+ }
107
+ /** Flush anything left and close the stream. */
108
+ async end() {
109
+ if (this.#ended)
110
+ return;
111
+ this.#ended = true;
112
+ await this.flush();
113
+ // stdout must not be closed; ending it would break piping for the rest of the process.
114
+ if (this.#stream === process.stdout || this.#stream === process.stderr)
115
+ return;
116
+ await new Promise((resolve, reject) => {
117
+ this.#stream.end((error) => {
118
+ const failure = error ?? this.#failure;
119
+ if (failure)
120
+ reject(failure);
121
+ else
122
+ resolve();
123
+ });
124
+ });
125
+ }
126
+ /** Close the stream without caring whether the data made it out. */
127
+ destroy() {
128
+ if (this.#ended)
129
+ return;
130
+ this.#ended = true;
131
+ this.#parts = [];
132
+ this.#pending = 0;
133
+ this.#stream.destroy();
134
+ }
135
+ }
136
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/format/csv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Number formatting for CSV cells.
3
+ *
4
+ * A one-hour, 23-channel, 256 Hz recording is about 21 million numeric cells, so
5
+ * this is the hottest code in a conversion. Two things keep it cheap:
6
+ *
7
+ * - Every sample in a channel comes from a bounded set of integers (digitalMin to
8
+ * digitalMax, typically 4096 distinct values for a 12-bit ADC). The formatted
9
+ * text for a digital code never changes, so it is computed once and reused.
10
+ * - The cache fills lazily. Real recordings visit only a fraction of the range,
11
+ * and a channel with an implausibly wide range falls back to direct formatting
12
+ * rather than reserving memory it will never use.
13
+ */
14
+ import type { EdfSignal } from '../edf/header.js';
15
+ /**
16
+ * Format with a fixed number of decimals, normalising negative zero.
17
+ *
18
+ * Without this, a sample that scales to a very small negative value prints as
19
+ * "-0.000", which looks like a distinct measurement but is not.
20
+ */
21
+ export declare function fixed(value: number, decimals: number): string;
22
+ /** Maps a raw digital sample to its formatted physical value. */
23
+ export type SampleFormatter = (digital: number) => string;
24
+ export declare function makeSampleFormatter(signal: EdfSignal, decimals: number): SampleFormatter;
25
+ /**
26
+ * Decimals for the time column.
27
+ *
28
+ * The interval between samples is 1/rate, which has a terminating decimal
29
+ * expansion of d places exactly when 10^d divides evenly by the rate. Every rate
30
+ * in common use clears this — 256 Hz needs 8 places, 512 Hz needs 9, 250 Hz and
31
+ * 1000 Hz need 3 — so sample times are written exactly rather than rounded, and
32
+ * `time_s * rate` comes back as a whole number instead of 8191.999999.
33
+ *
34
+ * Rates with a repeating expansion (3 Hz, say) fall back to enough places to keep
35
+ * consecutive samples distinct.
36
+ */
37
+ export declare function timeDecimals(samplingRate: number): number;
38
+ /** Human-readable byte size for warnings and summaries. */
39
+ export declare function formatBytes(bytes: number): string;
40
+ /** Human-readable duration: 1h 05m 12s. */
41
+ export declare function formatDuration(seconds: number): string;
42
+ //# sourceMappingURL=number.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number.d.ts","sourceRoot":"","sources":["../../src/format/number.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAMlD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU7D;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAE1D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAyBxF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUjD;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAStD"}