edfcore 0.2.58 → 0.2.61
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 +37 -0
- package/README.md +11 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/sample-locate.d.ts +49 -0
- package/dist/sample-locate.d.ts.map +1 -0
- package/dist/sample-locate.js +172 -0
- package/dist/sample-locate.js.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/index.ts +5 -0
- package/src/sample-locate.ts +212 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ alone does not tell you whether you were affected.
|
|
|
6
6
|
edfcore is pre-1.0. Patch releases have carried behaviour changes where the old behaviour was a
|
|
7
7
|
defect; those are called out below.
|
|
8
8
|
|
|
9
|
+
## 0.2.60
|
|
10
|
+
|
|
11
|
+
- **Added** `sampleAt`, `sampleStartTicksOf` and `sampleStartSecondsOf` — the recording-aware
|
|
12
|
+
counterpart to the sample-grid family, and the groundwork for 0.3.0.
|
|
13
|
+
- 0.2.32 documented why the existing three cannot be fixed in place: they take
|
|
14
|
+
`(signal, value, recordDurationTicks)`, so a gap is not in their arguments and no arithmetic
|
|
15
|
+
inside them could find one. These take the RECORDING. On a contiguous file they agree with the
|
|
16
|
+
grid functions exactly — asserted sample by sample — and on a discontinuous one they differ by
|
|
17
|
+
the gaps.
|
|
18
|
+
- `sampleAt` can return **`undefined`**, which is the answer the grid form structurally cannot
|
|
19
|
+
give: no sample exists at that instant, because it falls in a gap, before the recording, or after
|
|
20
|
+
it. `sampleIndexAt(signal, 5, d)` on a six-record file with a hole at 5 s names record 5; there
|
|
21
|
+
is no record 5 at that time.
|
|
22
|
+
- Both refuse a probed index on a file with gaps rather than guessing, the same rule `segmentAt`
|
|
23
|
+
follows. A round-trip test pins the pair together across the gap: the sample at a sample's start
|
|
24
|
+
is that sample, for every sample in the file.
|
|
25
|
+
|
|
26
|
+
## 0.2.59
|
|
27
|
+
|
|
28
|
+
- **Added** `calib.rec` from edfplus.info — the last corpus the README named. It was written by
|
|
29
|
+
**Bob Kemp, who wrote the EDF specification**, expressly to check that a reader gets amplitude
|
|
30
|
+
and POLARITY right, which makes it the closest thing this format has to a conformance test.
|
|
31
|
+
- It is the only fixture in the suite whose expected values come from neither edfcore nor another
|
|
32
|
+
library, but from the file's own design. Its declared range is ±100 µV over ±4096 digital units,
|
|
33
|
+
so the gain is 25/1024 — a small integer over a power of two, exactly representable in float64 —
|
|
34
|
+
and the offset is exactly zero. Digital `-2048` is therefore exactly `-50 µV`, checkable from the
|
|
35
|
+
header by hand, and every level in the file is asserted with no tolerance.
|
|
36
|
+
- The polarity assertions are the point. A reader that swapped the physical bounds returns the
|
|
37
|
+
right magnitudes with the wrong sign — plausible microvolts that invert the clinical reading of
|
|
38
|
+
the trace — so the extremes are asserted as signed values tied to the digital codes that produced
|
|
39
|
+
them, not as an amplitude.
|
|
40
|
+
- I first wrote the gain up as "a power of two". It is not; log2 of it is -5.356. The corrected
|
|
41
|
+
claim and a test pinning the distinction are both in the file, so nobody simplifies it back.
|
|
42
|
+
- The coverage guard added in 0.2.50 earned its place on the way: it failed this release because
|
|
43
|
+
the new corpus file had no parity golden yet. That is exactly the drift it exists to catch, and
|
|
44
|
+
it caught it before the release rather than after.
|
|
45
|
+
|
|
9
46
|
## 0.2.58
|
|
10
47
|
|
|
11
48
|
- **Added** CHB-MIT to the corpus, closing a gap the README has named since 0.1. It is chosen to be
|
package/README.md
CHANGED
|
@@ -341,8 +341,17 @@ also supplies something no synthetic fixture here had — a montage that names o
|
|
|
341
341
|
which is what `EdfAmbiguousChannelError` exists for and had until now only ever been raised against
|
|
342
342
|
a fixture written to raise it.
|
|
343
343
|
|
|
344
|
-
**
|
|
345
|
-
|
|
344
|
+
**The format author's own calibration file, as of 0.2.59.** `calib.rec` was written by Bob Kemp —
|
|
345
|
+
who wrote the EDF specification — expressly to check that a reader gets amplitude *and polarity*
|
|
346
|
+
right. It is the only fixture here whose expected values come from neither edfcore nor another
|
|
347
|
+
library but from the file's own design: its gain is 25/1024, exactly representable in binary, so
|
|
348
|
+
digital `-2048` is exactly `-50 µV` and a human can check it from the header by hand. edfcore
|
|
349
|
+
returns the exact levels, and the negative extreme comes from the negative code — which is the
|
|
350
|
+
whole point, since a reader that swapped the bounds would return plausible microvolts of the wrong
|
|
351
|
+
sign and invert the clinical reading of the trace.
|
|
352
|
+
|
|
353
|
+
**Still open before 1.0.** Nothing named. The remaining work is API, not verification — see the
|
|
354
|
+
0.3 note in [CHANGELOG.md](https://github.com/tayal-sarthak/edfcore/blob/main/CHANGELOG.md).
|
|
346
355
|
|
|
347
356
|
**Shipped since 0.1.6.** Min/max envelope decimation (`readEnvelope`,
|
|
348
357
|
`readEnvelopeAtResolution`). BioSemi Status-byte helpers (`readTriggers`). Streaming iteration
|
package/dist/constants.d.ts
CHANGED
|
@@ -109,5 +109,5 @@ export declare const SIGNAL_FIELD_BLOCK_OFFSETS: {
|
|
|
109
109
|
readonly reserved: 224;
|
|
110
110
|
};
|
|
111
111
|
/** Published package version. Kept in sync with package.json by a test. */
|
|
112
|
-
export declare const VERSION = "0.2.
|
|
112
|
+
export declare const VERSION = "0.2.61";
|
|
113
113
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -51,5 +51,6 @@ export { formatHeader } from './format-header.js';
|
|
|
51
51
|
export { inspectEdf } from './inspect.js';
|
|
52
52
|
export { openEdf, readAnnotations, readRecords, readWindow } from './recording.js';
|
|
53
53
|
export { sampleIndexAt, sampleStartSeconds, sampleStartTicks, } from './sample-grid.js';
|
|
54
|
+
export { sampleAt, sampleStartSecondsOf, sampleStartTicksOf, } from './sample-locate.js';
|
|
54
55
|
export { streamRecords } from './stream.js';
|
|
55
56
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -63,5 +63,6 @@ export { formatHeader } from './format-header.js';
|
|
|
63
63
|
export { inspectEdf } from './inspect.js';
|
|
64
64
|
export { openEdf, readAnnotations, readRecords, readWindow } from './recording.js';
|
|
65
65
|
export { sampleIndexAt, sampleStartSeconds, sampleStartTicks, } from './sample-grid.js';
|
|
66
|
+
export { sampleAt, sampleStartSecondsOf, sampleStartTicksOf, } from './sample-locate.js';
|
|
66
67
|
export { streamRecords } from './stream.js';
|
|
67
68
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2EH,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,8EAA8E;AAC9E,2EAA2E;AAC3E,2CAA2C;AAC3C,8EAA8E;AAE9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,KAAK,EACL,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2EH,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,8EAA8E;AAC9E,2EAA2E;AAC3E,2CAA2C;AAC3C,8EAA8E;AAE9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,KAAK,EACL,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time and sample index, on the recording's own axis.
|
|
3
|
+
*
|
|
4
|
+
* Layer 7. The recording-aware counterpart to `sample-grid.ts`, and the reason it exists is stated
|
|
5
|
+
* plainly there: `sampleIndexAt`, `sampleStartTicks` and `sampleStartSeconds` take
|
|
6
|
+
* `(signal, value, recordDurationTicks)` — no index, no timeline — so a gap is not in their
|
|
7
|
+
* arguments and no arithmetic inside them could find one. They measure the signal's own SAMPLE
|
|
8
|
+
* GRID, which equals elapsed recording time only when the recording is contiguous.
|
|
9
|
+
*
|
|
10
|
+
* These two take the recording, so they can answer the question people actually mean. On a
|
|
11
|
+
* contiguous file they agree with the grid functions exactly. On an EDF+D file they differ by the
|
|
12
|
+
* gaps, and `sampleAt` can answer something the grid functions structurally cannot: that an
|
|
13
|
+
* instant has NO sample at all, because it falls in a hole.
|
|
14
|
+
*
|
|
15
|
+
* Both refuse a probed index on a file whose records do not cover its span, for the reason
|
|
16
|
+
* `segmentAt` does: `undefined` from `sampleAt` means "no sample exists here", and an index that
|
|
17
|
+
* has read record 0 and the last record cannot say that about anything in between. Merging "there
|
|
18
|
+
* is a gap here" with "nobody looked" is the confusion this whole area of the API avoids.
|
|
19
|
+
*/
|
|
20
|
+
import type { EdfRecording, EdfSampleLocation } from './types.js';
|
|
21
|
+
/**
|
|
22
|
+
* The sample covering `seconds`, or `undefined` when no sample does.
|
|
23
|
+
*
|
|
24
|
+
* `undefined` is a real answer rather than a failure: on an EDF+D file an instant inside a gap has
|
|
25
|
+
* no sample, and so does any time before the recording starts or after it ends. That is the case
|
|
26
|
+
* `sampleIndexAt` cannot express — given only a signal and a record duration it always returns an
|
|
27
|
+
* index, even one past the end of the file.
|
|
28
|
+
*
|
|
29
|
+
* Floor, not round, and in exact integer arithmetic on ticks: a sample covers the half-open
|
|
30
|
+
* interval from its own start to the next one's, so the sample "at" a time is the one already
|
|
31
|
+
* running when that time arrives.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sampleAt(recording: EdfRecording, signalIndex: number, seconds: number): EdfSampleLocation | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* When a sample starts, in exact ticks on the recording's axis.
|
|
36
|
+
*
|
|
37
|
+
* The inverse of `sampleAt`, and the recording-aware form of `sampleStartTicks`. On a contiguous
|
|
38
|
+
* file the two agree exactly; on an EDF+D file this one includes the gaps that precede the sample
|
|
39
|
+
* and `sampleStartTicks` does not.
|
|
40
|
+
*
|
|
41
|
+
* Rounds UP to a whole tick, as `sampleStartTicks` does: a sample boundary need not fall on one —
|
|
42
|
+
* 128 samples over 0.3 s puts sample 1 at 23,437.5 ticks — and truncating would return a tick
|
|
43
|
+
* lying inside the previous sample, which `sampleAt` would then map straight back to that
|
|
44
|
+
* previous sample.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sampleStartTicksOf(recording: EdfRecording, signalIndex: number, sampleIndex: number): bigint;
|
|
47
|
+
/** `sampleStartTicksOf` as float64 seconds. Compare with the ticks, never with this. */
|
|
48
|
+
export declare function sampleStartSecondsOf(recording: EdfRecording, signalIndex: number, sampleIndex: number): number;
|
|
49
|
+
//# sourceMappingURL=sample-locate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-locate.d.ts","sourceRoot":"","sources":["../src/sample-locate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAyB,MAAM,YAAY,CAAC;AAkFzF;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,iBAAiB,GAAG,SAAS,CAqC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CA0BR;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time and sample index, on the recording's own axis.
|
|
3
|
+
*
|
|
4
|
+
* Layer 7. The recording-aware counterpart to `sample-grid.ts`, and the reason it exists is stated
|
|
5
|
+
* plainly there: `sampleIndexAt`, `sampleStartTicks` and `sampleStartSeconds` take
|
|
6
|
+
* `(signal, value, recordDurationTicks)` — no index, no timeline — so a gap is not in their
|
|
7
|
+
* arguments and no arithmetic inside them could find one. They measure the signal's own SAMPLE
|
|
8
|
+
* GRID, which equals elapsed recording time only when the recording is contiguous.
|
|
9
|
+
*
|
|
10
|
+
* These two take the recording, so they can answer the question people actually mean. On a
|
|
11
|
+
* contiguous file they agree with the grid functions exactly. On an EDF+D file they differ by the
|
|
12
|
+
* gaps, and `sampleAt` can answer something the grid functions structurally cannot: that an
|
|
13
|
+
* instant has NO sample at all, because it falls in a hole.
|
|
14
|
+
*
|
|
15
|
+
* Both refuse a probed index on a file whose records do not cover its span, for the reason
|
|
16
|
+
* `segmentAt` does: `undefined` from `sampleAt` means "no sample exists here", and an index that
|
|
17
|
+
* has read record 0 and the last record cannot say that about anything in between. Merging "there
|
|
18
|
+
* is a gap here" with "nobody looked" is the confusion this whole area of the API avoids.
|
|
19
|
+
*/
|
|
20
|
+
import { EdfChannelNotFoundError } from './errors.js';
|
|
21
|
+
import { segmentAt } from './record-index.js';
|
|
22
|
+
import { secondsToTicks, ticksToSeconds } from './tal/ticks.js';
|
|
23
|
+
/** `b` must be positive. Bigint `/` truncates toward zero, so negatives need the correction. */
|
|
24
|
+
function floorDiv(a, b) {
|
|
25
|
+
const quotient = a / b;
|
|
26
|
+
return a % b === 0n || a > 0n ? quotient : quotient - 1n;
|
|
27
|
+
}
|
|
28
|
+
function resolveSignal(recording, signalIndex, caller) {
|
|
29
|
+
const signal = recording.header.signals[signalIndex];
|
|
30
|
+
if (signal === undefined) {
|
|
31
|
+
throw new EdfChannelNotFoundError(`${caller}(): signalIndex ${signalIndex} is outside the ` +
|
|
32
|
+
`${recording.header.signals.length} signals this file declares. Next: pass an index from ` +
|
|
33
|
+
'header.dataSignalIndices, or resolve one with getSignal(header, label).', {
|
|
34
|
+
selector: signalIndex,
|
|
35
|
+
availableLabels: recording.header.signals.map((s) => s.label),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (signal.kind === 'annotations') {
|
|
39
|
+
throw new RangeError(`${caller}(): signal ${signalIndex} (${JSON.stringify(signal.label)}) is an annotations ` +
|
|
40
|
+
'channel, whose region holds TAL text rather than samples, so it has no sample grid. ' +
|
|
41
|
+
'Next: use onsetTicksFromFirstRecord on the annotations themselves.');
|
|
42
|
+
}
|
|
43
|
+
if (signal.samplesPerRecord <= 0) {
|
|
44
|
+
throw new RangeError(`${caller}(): signal ${signalIndex} (${JSON.stringify(signal.label)}) declares ` +
|
|
45
|
+
`${signal.samplesPerRecord} samples per record, so it has no sample grid to index. ` +
|
|
46
|
+
'Next: check header.diagnostics for ZERO_SAMPLES_PER_RECORD.');
|
|
47
|
+
}
|
|
48
|
+
if (recording.header.recordDurationTicks <= 0n) {
|
|
49
|
+
throw new RangeError(`${caller}(): this file declares a record duration of zero, so records do not advance in ` +
|
|
50
|
+
'time and no elapsed time maps to a sample. This is legal EDF and a scoring file relies ' +
|
|
51
|
+
'on it. Next: index by record with readRecords().');
|
|
52
|
+
}
|
|
53
|
+
return signal;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The record a segment places at a given position, and that record's true start in ticks.
|
|
57
|
+
*
|
|
58
|
+
* `segment.startTicks` is already on the recording's axis, and records inside one segment are
|
|
59
|
+
* contiguous by construction, so this is exact.
|
|
60
|
+
*/
|
|
61
|
+
function recordStartTicks(recording, segment, recordIndex) {
|
|
62
|
+
const duration = recording.header.recordDurationTicks;
|
|
63
|
+
if (segment === undefined)
|
|
64
|
+
return BigInt(recordIndex) * duration;
|
|
65
|
+
return segment.startTicks + BigInt(recordIndex - segment.records.start) * duration;
|
|
66
|
+
}
|
|
67
|
+
/** The segment holding a record, on a scanned index; `undefined` when the file is contiguous. */
|
|
68
|
+
function segmentOfRecord(recording, recordIndex) {
|
|
69
|
+
const segments = recording.index.segments;
|
|
70
|
+
if (segments === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
for (const segment of segments) {
|
|
73
|
+
const first = segment.records.start;
|
|
74
|
+
if (recordIndex >= first && recordIndex < first + segment.records.count)
|
|
75
|
+
return segment;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether this recording needs a scanned index before a time can be located at all.
|
|
81
|
+
*
|
|
82
|
+
* A file whose records cover exactly its span is contiguous, and the nominal grid IS the true one
|
|
83
|
+
* — so a probed index is enough and no scan is demanded of the caller for nothing.
|
|
84
|
+
*/
|
|
85
|
+
function requiresScan(recording) {
|
|
86
|
+
return recording.timeline.spanSeconds !== recording.timeline.coveredSeconds;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The sample covering `seconds`, or `undefined` when no sample does.
|
|
90
|
+
*
|
|
91
|
+
* `undefined` is a real answer rather than a failure: on an EDF+D file an instant inside a gap has
|
|
92
|
+
* no sample, and so does any time before the recording starts or after it ends. That is the case
|
|
93
|
+
* `sampleIndexAt` cannot express — given only a signal and a record duration it always returns an
|
|
94
|
+
* index, even one past the end of the file.
|
|
95
|
+
*
|
|
96
|
+
* Floor, not round, and in exact integer arithmetic on ticks: a sample covers the half-open
|
|
97
|
+
* interval from its own start to the next one's, so the sample "at" a time is the one already
|
|
98
|
+
* running when that time arrives.
|
|
99
|
+
*/
|
|
100
|
+
export function sampleAt(recording, signalIndex, seconds) {
|
|
101
|
+
const signal = resolveSignal(recording, signalIndex, 'sampleAt');
|
|
102
|
+
if (!Number.isFinite(seconds)) {
|
|
103
|
+
throw new RangeError(`sampleAt(): seconds must be a finite number, received ${seconds}.`);
|
|
104
|
+
}
|
|
105
|
+
const duration = recording.header.recordDurationTicks;
|
|
106
|
+
const perRecord = BigInt(signal.samplesPerRecord);
|
|
107
|
+
const ticks = secondsToTicks(seconds);
|
|
108
|
+
if (!requiresScan(recording)) {
|
|
109
|
+
// Contiguous: the nominal grid is the true one, but the answer is still bounded by the file.
|
|
110
|
+
const recordIndex = Number(floorDiv(ticks, duration));
|
|
111
|
+
if (recordIndex < 0 || recordIndex >= recording.header.recordCount)
|
|
112
|
+
return undefined;
|
|
113
|
+
const withinTicks = ticks - BigInt(recordIndex) * duration;
|
|
114
|
+
const sampleWithinRecord = Number(floorDiv(withinTicks * perRecord, duration));
|
|
115
|
+
return {
|
|
116
|
+
sampleIndex: recordIndex * signal.samplesPerRecord + sampleWithinRecord,
|
|
117
|
+
recordIndex,
|
|
118
|
+
sampleWithinRecord,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Discontinuous: `segmentAt` owns the "is there data here at all" question, and throws for a
|
|
122
|
+
// probed index rather than guessing.
|
|
123
|
+
const segment = segmentAt(recording.index, seconds);
|
|
124
|
+
if (segment === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
const offsetTicks = ticks - segment.startTicks;
|
|
127
|
+
const recordIndex = segment.records.start + Number(floorDiv(offsetTicks, duration));
|
|
128
|
+
const withinTicks = offsetTicks - floorDiv(offsetTicks, duration) * duration;
|
|
129
|
+
const sampleWithinRecord = Number(floorDiv(withinTicks * perRecord, duration));
|
|
130
|
+
return {
|
|
131
|
+
sampleIndex: recordIndex * signal.samplesPerRecord + sampleWithinRecord,
|
|
132
|
+
recordIndex,
|
|
133
|
+
sampleWithinRecord,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* When a sample starts, in exact ticks on the recording's axis.
|
|
138
|
+
*
|
|
139
|
+
* The inverse of `sampleAt`, and the recording-aware form of `sampleStartTicks`. On a contiguous
|
|
140
|
+
* file the two agree exactly; on an EDF+D file this one includes the gaps that precede the sample
|
|
141
|
+
* and `sampleStartTicks` does not.
|
|
142
|
+
*
|
|
143
|
+
* Rounds UP to a whole tick, as `sampleStartTicks` does: a sample boundary need not fall on one —
|
|
144
|
+
* 128 samples over 0.3 s puts sample 1 at 23,437.5 ticks — and truncating would return a tick
|
|
145
|
+
* lying inside the previous sample, which `sampleAt` would then map straight back to that
|
|
146
|
+
* previous sample.
|
|
147
|
+
*/
|
|
148
|
+
export function sampleStartTicksOf(recording, signalIndex, sampleIndex) {
|
|
149
|
+
const signal = resolveSignal(recording, signalIndex, 'sampleStartTicksOf');
|
|
150
|
+
if (!Number.isSafeInteger(sampleIndex)) {
|
|
151
|
+
throw new RangeError(`sampleStartTicksOf(): sampleIndex must be a whole number, received ${sampleIndex}.`);
|
|
152
|
+
}
|
|
153
|
+
const perRecord = signal.samplesPerRecord;
|
|
154
|
+
const duration = recording.header.recordDurationTicks;
|
|
155
|
+
const recordIndex = Math.floor(sampleIndex / perRecord);
|
|
156
|
+
const within = BigInt(sampleIndex - recordIndex * perRecord);
|
|
157
|
+
if (requiresScan(recording) && recording.index.segments === undefined) {
|
|
158
|
+
throw new RangeError('sampleStartTicksOf(): this file has gaps and its index has not been scanned, so the true ' +
|
|
159
|
+
'start of a record after a gap is not known. Next: await buildRecordIndex(recording) and ' +
|
|
160
|
+
'read the result into the recording.');
|
|
161
|
+
}
|
|
162
|
+
const start = recordStartTicks(recording, segmentOfRecord(recording, recordIndex), recordIndex);
|
|
163
|
+
const numerator = within * duration;
|
|
164
|
+
const offset = numerator / BigInt(perRecord);
|
|
165
|
+
// Ceil, matching `sampleStartTicks`.
|
|
166
|
+
return start + (numerator % BigInt(perRecord) === 0n ? offset : offset + 1n);
|
|
167
|
+
}
|
|
168
|
+
/** `sampleStartTicksOf` as float64 seconds. Compare with the ticks, never with this. */
|
|
169
|
+
export function sampleStartSecondsOf(recording, signalIndex, sampleIndex) {
|
|
170
|
+
return ticksToSeconds(sampleStartTicksOf(recording, signalIndex, sampleIndex));
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=sample-locate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample-locate.js","sourceRoot":"","sources":["../src/sample-locate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time and sample index, on the recording's own axis.
|
|
3
|
+
*
|
|
4
|
+
* Layer 7. The recording-aware counterpart to `sample-grid.ts`, and the reason it exists is stated
|
|
5
|
+
* plainly there: `sampleIndexAt`, `sampleStartTicks` and `sampleStartSeconds` take
|
|
6
|
+
* `(signal, value, recordDurationTicks)` — no index, no timeline — so a gap is not in their
|
|
7
|
+
* arguments and no arithmetic inside them could find one. They measure the signal's own SAMPLE
|
|
8
|
+
* GRID, which equals elapsed recording time only when the recording is contiguous.
|
|
9
|
+
*
|
|
10
|
+
* These two take the recording, so they can answer the question people actually mean. On a
|
|
11
|
+
* contiguous file they agree with the grid functions exactly. On an EDF+D file they differ by the
|
|
12
|
+
* gaps, and `sampleAt` can answer something the grid functions structurally cannot: that an
|
|
13
|
+
* instant has NO sample at all, because it falls in a hole.
|
|
14
|
+
*
|
|
15
|
+
* Both refuse a probed index on a file whose records do not cover its span, for the reason
|
|
16
|
+
* `segmentAt` does: `undefined` from `sampleAt` means "no sample exists here", and an index that
|
|
17
|
+
* has read record 0 and the last record cannot say that about anything in between. Merging "there
|
|
18
|
+
* is a gap here" with "nobody looked" is the confusion this whole area of the API avoids.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { EdfChannelNotFoundError } from './errors.js';
|
|
22
|
+
import { segmentAt } from './record-index.js';
|
|
23
|
+
import { secondsToTicks, ticksToSeconds } from './tal/ticks.js';
|
|
24
|
+
import type { EdfRecording, EdfSampleLocation, EdfSegment, EdfSignal } from './types.js';
|
|
25
|
+
|
|
26
|
+
/** `b` must be positive. Bigint `/` truncates toward zero, so negatives need the correction. */
|
|
27
|
+
function floorDiv(a: bigint, b: bigint): bigint {
|
|
28
|
+
const quotient = a / b;
|
|
29
|
+
return a % b === 0n || a > 0n ? quotient : quotient - 1n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveSignal(recording: EdfRecording, signalIndex: number, caller: string): EdfSignal {
|
|
33
|
+
const signal = recording.header.signals[signalIndex];
|
|
34
|
+
if (signal === undefined) {
|
|
35
|
+
throw new EdfChannelNotFoundError(
|
|
36
|
+
`${caller}(): signalIndex ${signalIndex} is outside the ` +
|
|
37
|
+
`${recording.header.signals.length} signals this file declares. Next: pass an index from ` +
|
|
38
|
+
'header.dataSignalIndices, or resolve one with getSignal(header, label).',
|
|
39
|
+
{
|
|
40
|
+
selector: signalIndex,
|
|
41
|
+
availableLabels: recording.header.signals.map((s) => s.label),
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (signal.kind === 'annotations') {
|
|
46
|
+
throw new RangeError(
|
|
47
|
+
`${caller}(): signal ${signalIndex} (${JSON.stringify(signal.label)}) is an annotations ` +
|
|
48
|
+
'channel, whose region holds TAL text rather than samples, so it has no sample grid. ' +
|
|
49
|
+
'Next: use onsetTicksFromFirstRecord on the annotations themselves.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (signal.samplesPerRecord <= 0) {
|
|
53
|
+
throw new RangeError(
|
|
54
|
+
`${caller}(): signal ${signalIndex} (${JSON.stringify(signal.label)}) declares ` +
|
|
55
|
+
`${signal.samplesPerRecord} samples per record, so it has no sample grid to index. ` +
|
|
56
|
+
'Next: check header.diagnostics for ZERO_SAMPLES_PER_RECORD.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (recording.header.recordDurationTicks <= 0n) {
|
|
60
|
+
throw new RangeError(
|
|
61
|
+
`${caller}(): this file declares a record duration of zero, so records do not advance in ` +
|
|
62
|
+
'time and no elapsed time maps to a sample. This is legal EDF and a scoring file relies ' +
|
|
63
|
+
'on it. Next: index by record with readRecords().',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return signal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The record a segment places at a given position, and that record's true start in ticks.
|
|
71
|
+
*
|
|
72
|
+
* `segment.startTicks` is already on the recording's axis, and records inside one segment are
|
|
73
|
+
* contiguous by construction, so this is exact.
|
|
74
|
+
*/
|
|
75
|
+
function recordStartTicks(
|
|
76
|
+
recording: EdfRecording,
|
|
77
|
+
segment: EdfSegment | undefined,
|
|
78
|
+
recordIndex: number,
|
|
79
|
+
): bigint {
|
|
80
|
+
const duration = recording.header.recordDurationTicks;
|
|
81
|
+
if (segment === undefined) return BigInt(recordIndex) * duration;
|
|
82
|
+
return segment.startTicks + BigInt(recordIndex - segment.records.start) * duration;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The segment holding a record, on a scanned index; `undefined` when the file is contiguous. */
|
|
86
|
+
function segmentOfRecord(recording: EdfRecording, recordIndex: number): EdfSegment | undefined {
|
|
87
|
+
const segments = recording.index.segments;
|
|
88
|
+
if (segments === undefined) return undefined;
|
|
89
|
+
for (const segment of segments) {
|
|
90
|
+
const first = segment.records.start;
|
|
91
|
+
if (recordIndex >= first && recordIndex < first + segment.records.count) return segment;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether this recording needs a scanned index before a time can be located at all.
|
|
98
|
+
*
|
|
99
|
+
* A file whose records cover exactly its span is contiguous, and the nominal grid IS the true one
|
|
100
|
+
* — so a probed index is enough and no scan is demanded of the caller for nothing.
|
|
101
|
+
*/
|
|
102
|
+
function requiresScan(recording: EdfRecording): boolean {
|
|
103
|
+
return recording.timeline.spanSeconds !== recording.timeline.coveredSeconds;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The sample covering `seconds`, or `undefined` when no sample does.
|
|
108
|
+
*
|
|
109
|
+
* `undefined` is a real answer rather than a failure: on an EDF+D file an instant inside a gap has
|
|
110
|
+
* no sample, and so does any time before the recording starts or after it ends. That is the case
|
|
111
|
+
* `sampleIndexAt` cannot express — given only a signal and a record duration it always returns an
|
|
112
|
+
* index, even one past the end of the file.
|
|
113
|
+
*
|
|
114
|
+
* Floor, not round, and in exact integer arithmetic on ticks: a sample covers the half-open
|
|
115
|
+
* interval from its own start to the next one's, so the sample "at" a time is the one already
|
|
116
|
+
* running when that time arrives.
|
|
117
|
+
*/
|
|
118
|
+
export function sampleAt(
|
|
119
|
+
recording: EdfRecording,
|
|
120
|
+
signalIndex: number,
|
|
121
|
+
seconds: number,
|
|
122
|
+
): EdfSampleLocation | undefined {
|
|
123
|
+
const signal = resolveSignal(recording, signalIndex, 'sampleAt');
|
|
124
|
+
if (!Number.isFinite(seconds)) {
|
|
125
|
+
throw new RangeError(`sampleAt(): seconds must be a finite number, received ${seconds}.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const duration = recording.header.recordDurationTicks;
|
|
129
|
+
const perRecord = BigInt(signal.samplesPerRecord);
|
|
130
|
+
const ticks = secondsToTicks(seconds);
|
|
131
|
+
|
|
132
|
+
if (!requiresScan(recording)) {
|
|
133
|
+
// Contiguous: the nominal grid is the true one, but the answer is still bounded by the file.
|
|
134
|
+
const recordIndex = Number(floorDiv(ticks, duration));
|
|
135
|
+
if (recordIndex < 0 || recordIndex >= recording.header.recordCount) return undefined;
|
|
136
|
+
const withinTicks = ticks - BigInt(recordIndex) * duration;
|
|
137
|
+
const sampleWithinRecord = Number(floorDiv(withinTicks * perRecord, duration));
|
|
138
|
+
return {
|
|
139
|
+
sampleIndex: recordIndex * signal.samplesPerRecord + sampleWithinRecord,
|
|
140
|
+
recordIndex,
|
|
141
|
+
sampleWithinRecord,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Discontinuous: `segmentAt` owns the "is there data here at all" question, and throws for a
|
|
146
|
+
// probed index rather than guessing.
|
|
147
|
+
const segment = segmentAt(recording.index, seconds);
|
|
148
|
+
if (segment === undefined) return undefined;
|
|
149
|
+
|
|
150
|
+
const offsetTicks = ticks - segment.startTicks;
|
|
151
|
+
const recordIndex = segment.records.start + Number(floorDiv(offsetTicks, duration));
|
|
152
|
+
const withinTicks = offsetTicks - floorDiv(offsetTicks, duration) * duration;
|
|
153
|
+
const sampleWithinRecord = Number(floorDiv(withinTicks * perRecord, duration));
|
|
154
|
+
return {
|
|
155
|
+
sampleIndex: recordIndex * signal.samplesPerRecord + sampleWithinRecord,
|
|
156
|
+
recordIndex,
|
|
157
|
+
sampleWithinRecord,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* When a sample starts, in exact ticks on the recording's axis.
|
|
163
|
+
*
|
|
164
|
+
* The inverse of `sampleAt`, and the recording-aware form of `sampleStartTicks`. On a contiguous
|
|
165
|
+
* file the two agree exactly; on an EDF+D file this one includes the gaps that precede the sample
|
|
166
|
+
* and `sampleStartTicks` does not.
|
|
167
|
+
*
|
|
168
|
+
* Rounds UP to a whole tick, as `sampleStartTicks` does: a sample boundary need not fall on one —
|
|
169
|
+
* 128 samples over 0.3 s puts sample 1 at 23,437.5 ticks — and truncating would return a tick
|
|
170
|
+
* lying inside the previous sample, which `sampleAt` would then map straight back to that
|
|
171
|
+
* previous sample.
|
|
172
|
+
*/
|
|
173
|
+
export function sampleStartTicksOf(
|
|
174
|
+
recording: EdfRecording,
|
|
175
|
+
signalIndex: number,
|
|
176
|
+
sampleIndex: number,
|
|
177
|
+
): bigint {
|
|
178
|
+
const signal = resolveSignal(recording, signalIndex, 'sampleStartTicksOf');
|
|
179
|
+
if (!Number.isSafeInteger(sampleIndex)) {
|
|
180
|
+
throw new RangeError(
|
|
181
|
+
`sampleStartTicksOf(): sampleIndex must be a whole number, received ${sampleIndex}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const perRecord = signal.samplesPerRecord;
|
|
186
|
+
const duration = recording.header.recordDurationTicks;
|
|
187
|
+
const recordIndex = Math.floor(sampleIndex / perRecord);
|
|
188
|
+
const within = BigInt(sampleIndex - recordIndex * perRecord);
|
|
189
|
+
|
|
190
|
+
if (requiresScan(recording) && recording.index.segments === undefined) {
|
|
191
|
+
throw new RangeError(
|
|
192
|
+
'sampleStartTicksOf(): this file has gaps and its index has not been scanned, so the true ' +
|
|
193
|
+
'start of a record after a gap is not known. Next: await buildRecordIndex(recording) and ' +
|
|
194
|
+
'read the result into the recording.',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const start = recordStartTicks(recording, segmentOfRecord(recording, recordIndex), recordIndex);
|
|
199
|
+
const numerator = within * duration;
|
|
200
|
+
const offset = numerator / BigInt(perRecord);
|
|
201
|
+
// Ceil, matching `sampleStartTicks`.
|
|
202
|
+
return start + (numerator % BigInt(perRecord) === 0n ? offset : offset + 1n);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `sampleStartTicksOf` as float64 seconds. Compare with the ticks, never with this. */
|
|
206
|
+
export function sampleStartSecondsOf(
|
|
207
|
+
recording: EdfRecording,
|
|
208
|
+
signalIndex: number,
|
|
209
|
+
sampleIndex: number,
|
|
210
|
+
): number {
|
|
211
|
+
return ticksToSeconds(sampleStartTicksOf(recording, signalIndex, sampleIndex));
|
|
212
|
+
}
|