@miadi/ava8-measure 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Miadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # `@miadi/ava8-measure`
2
+
3
+ **Read a rendered artefact and hold it to a claim.**
4
+
5
+ Every other package in the ava8 family writes. `@miadi/ava8-core` writes MIDI;
6
+ `@miadi/movement-conductor` records and summarises a take; `@miadi/capture` stores one.
7
+ Nothing read a rendered artefact back and asked whether it is what it said it was.
8
+
9
+ That is this package.
10
+
11
+ ESM only. Zero runtime dependencies beyond `@miadi/ava8-atelier`. No DOM, no filesystem —
12
+ bytes in, numbers out — so it runs in a browser, in a test, and on a capture device.
13
+
14
+ ```bash
15
+ npm install @miadi/ava8-measure
16
+ ```
17
+
18
+ ## The rule that defines it
19
+
20
+ **Read the rendered artefact, never the source.** A generator's header states an intention.
21
+ Three tools sit between that intention and the file, and each has silently changed something
22
+ at least once: an accidental contaminated its bar and moved a note a semitone; a bare `Q:`
23
+ line was ignored so a tempo change existed only in a comment; a container refused a codec and
24
+ wrote zero bytes while reporting success.
25
+
26
+ **A count that comes out right is not proof.** The right number of drum hits can land off the
27
+ beat. Read placement, not totals.
28
+
29
+ ## Three surfaces
30
+
31
+ | subpath | reads | answers |
32
+ |---|---|---|
33
+ | `./midi` | Standard MIDI bytes | registers per track and their overlap, band occupancy, duration-weighted pitch classes, mode purity, drum position within the bar, note-for-note and multiset identity between two files |
34
+ | `./audio` | PCM WAV bytes | spectral share in a named band, stridence, f0 by autocorrelation with octave folding, held notes, motif versus drone, recurring interval cells, crossfade seams |
35
+ | `./movement` | OSC capture text | held-value ratio and the real new-value rate, per-second magnitudes, onsets, unwrapped heading, compass sextants, stillness spans |
36
+
37
+ ## The first act on a movement capture is always dedupe
38
+
39
+ A capture requested at 100 Hz delivered 1627 packets in 16.6 s — 98 Hz of packets, of which
40
+ **1232 were repeats of their predecessor**. 395 new values: 23.8 Hz, median gap 41 ms.
41
+
42
+ Onsets computed on the raw stream returned 28 attacks spaced 120–133 ms: regular, credible,
43
+ and entirely false. That was the staircase of the held values, not a body. On the deduped
44
+ stream: 15 attacks, spaced 153 to 1481 ms.
45
+
46
+ So `dedupe` is not an option and it is not a filter you may skip. `onsets` deduplicates by
47
+ default and reports whether it did.
48
+
49
+ ## What the numbers mean, and what they do not
50
+
51
+ `stridence` is **comparable to itself**. It does not reproduce two figures published by the
52
+ session that invented the method — it reads 15–25 % low against them — because the spectral
53
+ code was never committed and the candidate renders are gone. Re-measure every candidate
54
+ rather than quoting a figure from that day. The doc comment says so at the call site, where
55
+ it is harder to ignore.
56
+
57
+ `onsets` reproduces the count and the shape of a published list; 11 of 15 timestamps land
58
+ within 30 ms and four do not, because that session's peak-picking was never written down.
59
+
60
+ Movement captures carry **no declared units**. The capture's own field ledger says the
61
+ channel semantic map is absent. Numbers are reported without units, and the music is built
62
+ from ratios, so nothing is lost — but a unit invented here would be a claim nobody can
63
+ support.
64
+
65
+ ## The conformance harness
66
+
67
+ This package is a port of three Python programs (`atelier_midi.py`, `atelier_audio.py`,
68
+ `atelier_movement.py`). The port was checked against them — and the checking happened in a
69
+ scratch directory that is gone, leaving a paragraph in a commit message. **A claim nobody can
70
+ re-run is a claim, not a proof**, which is the exact failure this package exists to name, so
71
+ the check now lives in the repository:
72
+
73
+ | | |
74
+ |---|---|
75
+ | `test/fixtures/` | 19 committed fixtures, 195 KB — 8 MIDI, 6 WAV, 5 OSC captures |
76
+ | `test/fixtures/expected-*.json` | 356 measurements (276 structural, 80 numeric), all written by the Python |
77
+ | `test/conformance.test.mjs` | mirrors every one of them in TypeScript and compares |
78
+ | `scripts/regenerate-fixtures.mjs` | rebuilds the corpus **and** the expected values, end to end |
79
+
80
+ Those 356 measurements expand to 1741 structural values and 454 numeric ones, because a
81
+ measurement is often a list: every note of a rendered file as a six-tuple, every voiced frame
82
+ of an f0 track, every sextant of a walked circle.
83
+
84
+ **Synthetic fixtures only.** Every byte of the corpus comes out of a counter or a table in
85
+ `regenerate-fixtures.mjs`. No recording, no movement capture, and nothing out of a composition
86
+ folder is committed, read, or compared against here — consent forbids a person's voice or
87
+ recordings leaving the device they were made on, and "just to compare" is not an exception.
88
+ The harness prints `synthetic fixtures only` on every run.
89
+
90
+ `node --test test/conformance.test.mjs` needs **no Python and no numpy**. It reads the
91
+ committed JSON. Only regeneration needs the oracle, which is what makes the suite runnable on
92
+ a host where numpy cannot be imported at all.
93
+
94
+ ### Two tiers, and they are not the same claim
95
+
96
+ **Structural results are compared for exact equality**: note counts, per-track registers and
97
+ their overlaps, band occupancy, drum slots and their bar numbers, dedupe counts, onset counts
98
+ and their rounded times, sextant sequences, heading wraps, stillness spans, held-note
99
+ boundaries, and the formatted verdict strings. Both programs reach these by integer arithmetic
100
+ or by a single division of two counts, so a difference is a defect and never a rounding.
101
+
102
+ **Spectral and other accumulated results are compared within a published tolerance** — a mean
103
+ power spectrum, an autocorrelation, a compensated mean, anything summed over many
104
+ floating-point operations, where the two languages accumulate in a different order:
105
+
106
+ | | |
107
+ |---|---|
108
+ | relative tolerance | **1e-9** |
109
+ | absolute floor, for values near zero where a ratio says nothing | **1e-12** |
110
+
111
+ Both numbers live in `test/fixtures/tolerances.json` and the suite asserts that this table
112
+ still states them, so the two cannot drift apart.
113
+
114
+ **How they were chosen.** The worst relative divergence this corpus actually produces is
115
+ 1.6e-12 (a spectrum bin), and the worst absolute divergence is 1.4e-14 (an f0 frame in MIDI
116
+ floats). 1e-9 leaves roughly three orders of magnitude of headroom over that, which absorbs a
117
+ different libm on a different host without absorbing anything that could change a musical
118
+ verdict: the stridence thresholds are 0.03 apart, seven orders of magnitude coarser. The run
119
+ prints the worst divergence it observed, so the headroom is visible rather than asserted.
120
+
121
+ ### What the harness deliberately does not encode
122
+
123
+ The two figures named above under *What the numbers mean* are **absent from the corpus on
124
+ purpose**, and `test/conformance.test.mjs` records them under `NOT_REPRODUCED` with the reason
125
+ rather than leaving a silent hole that would read as coverage:
126
+
127
+ - **stridence** against the session's published 7.60 % and 11.30 % — the spectral code that
128
+ produced them was never committed and the renders are gone, so a fixture asserting 7.60 %
129
+ would assert a number no program on this machine can produce. The corpus pins stridence
130
+ against the Python oracle only: that proves the port agrees with `atelier_audio.py`, and
131
+ proves nothing about that day's figures.
132
+ - **onsets** against the session's fifteen timestamps — its peak-picking was never written
133
+ down, and the take is a person's movement capture, which may not leave the device it was
134
+ made on. The corpus pins `onsets` on a synthetic staircase instead, where the raw stream's
135
+ three ghost attacks and the deduped stream's three real ones are both committed.
136
+
137
+ ### Regenerating
138
+
139
+ ```bash
140
+ ATELIER_SCRIPTS=/path/to/atelier-jerry/scripts node scripts/regenerate-fixtures.mjs
141
+ node --test test/conformance.test.mjs
142
+ ```
143
+
144
+ The first command writes the corpus, then hands it to `scripts/oracle.py`, which imports the
145
+ three Python modules and writes the expected values. The TypeScript is never consulted during
146
+ regeneration — a harness that generated its own expectations would be comparing the port
147
+ against itself and would prove nothing.
148
+
149
+ ## The FFT is hand-written on purpose
150
+
151
+ About 120 lines of iterative radix-2 Cooley–Tukey over `Float64Array`. Every call site uses a
152
+ power-of-two length, so radix-2 covers all of it.
153
+
154
+ The house builds with `tsc` and no bundler. A hand-written transform is smaller than the
155
+ argument about which npm FFT package to trust, and it can be audited in one sitting.
156
+
157
+ Two details that would silently change every number if they were wrong, and are asserted in
158
+ the tests: the Hann window is the **symmetric** one (`n-1` in the denominator, as numpy
159
+ writes it, not the periodic form every tutorial gives), and a band share sums **magnitude**,
160
+ not power — power puts every piece under one percent and makes a 13.12 % threshold
161
+ unreachable by any sound.
162
+
163
+ ## What it does not own
164
+
165
+ | | |
166
+ |---|---|
167
+ | the thresholds, the register plan, provenance, consent | `@miadi/ava8-atelier` |
168
+ | writing ABC or MIDI | `@miadi/ava8-core` |
169
+ | rendering or playing a score | `@miadi/ava8-abcjs`, `@miadi/ava8-react` |
170
+ | recording a take, conducting movement into frames | `@miadi/capture`, `@miadi/movement-conductor` |
171
+ | turning a MIDI into audio | `@miadi/jeremyai` |
172
+
173
+ This package measures. It does not decide what the measurement means — that judgement lives
174
+ in `@miadi/ava8-atelier`, and the decision itself belongs to a person.
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Spectral share and pitch, read back off a rendered artefact.
3
+ *
4
+ * A port of the atelier's `atelier_audio.py` (numpy + the standard library
5
+ * `wave` module) onto typed arrays, so the same measurement can run where
6
+ * numpy cannot. The numbers are the point: this file agrees with that program
7
+ * on synthetic signals to better than 1e-6 relative, and four details are the
8
+ * reason it does. Each one silently changes every number if it is written the
9
+ * way a DSP tutorial writes it.
10
+ *
11
+ * 1. `hann(n)` is `np.hanning(n)` — the SYMMETRIC window, denominator `n - 1`,
12
+ * not the periodic `n`. The difference is a fraction of a percent of a band
13
+ * share, measured against a reject line that sits at 13.12 %.
14
+ * 2. `bandShare` sums MAGNITUDE by default, not power. Summing |X|² instead
15
+ * puts every real piece under 1 % and makes the reject line unreachable by
16
+ * any sound, which cannot be what the threshold was calibrated against.
17
+ * 3. A median over an even-length window is the mean of the two middle values,
18
+ * and `medianSmooth` drops NaNs BEFORE taking the median.
19
+ * 4. The band mask is half-open — `freqs >= lo && freqs < hi`. The `<` decides
20
+ * a whole bin at a band edge. (`octaveErrorCheck` uses a CLOSED interval
21
+ * around each partial; that difference is in the original and is kept.)
22
+ *
23
+ * ## NaN is load-bearing
24
+ *
25
+ * `f0Track` returns a `Float64Array` carrying `NaN` for every unvoiced frame,
26
+ * and `isNaN` guards nearly every loop downstream. Silence with a pitch
27
+ * attached is an invention, and an unvoiced frame that reads as a number is
28
+ * how one gets invented. Do not reach for `number | null` here.
29
+ *
30
+ * ## Bytes in, never a path
31
+ *
32
+ * `decodeWav` takes a `Uint8Array`. This module opens no file, imports no
33
+ * `node:fs`, and touches no `Buffer` — a measurement library that reads the
34
+ * disk decides where a recording lives, and that decision belongs to whoever
35
+ * consented to the recording.
36
+ */
37
+ import { STRIDENCE, stridenceVerdict } from "@miadi/ava8-atelier";
38
+ import type { StridenceVerdict } from "@miadi/ava8-atelier";
39
+ export { STRIDENCE, stridenceVerdict };
40
+ export type { StridenceVerdict };
41
+ /** Mono float samples in [-1, 1], and the sample rate that produced them. */
42
+ export interface Audio {
43
+ readonly samples: Float64Array;
44
+ readonly rate: number;
45
+ /** Channel count of the source, before the mixdown. */
46
+ readonly channels: number;
47
+ }
48
+ /** Thrown for a WAV this module will not guess at. */
49
+ export declare class UnsupportedWav extends Error {
50
+ constructor(message: string);
51
+ }
52
+ /**
53
+ * Decode a PCM WAV from bytes and mix it down to mono.
54
+ *
55
+ * Mono on purpose: every measure here is about spectral content and pitch, and
56
+ * a stereo pair measured separately would answer twice without deciding
57
+ * anything. Refuses a compressed or float WAV rather than guessing — a silent
58
+ * wrong answer is worse than a stop, and it is the same refusal the numpy
59
+ * original inherits from Python's `wave` module.
60
+ *
61
+ * 8-bit is unsigned, 16/24/32-bit are signed little-endian, matching the
62
+ * original byte for byte — including its `float32` intermediate, which is why
63
+ * a 32-bit source rounds identically here and there.
64
+ */
65
+ export declare function decodeWav(bytes: Uint8Array): Audio;
66
+ /** Default analysis frame, so the same file measured on two hosts agrees. */
67
+ export declare const N_FFT = 2048;
68
+ /** Default hop: 50 % overlap. */
69
+ export declare const HOP = 1024;
70
+ /**
71
+ * `np.hanning(n)` — the SYMMETRIC Hann window, denominator `n - 1`.
72
+ *
73
+ * Not the periodic window (`denominator n`) that most DSP writing means by
74
+ * "Hann". The two differ by about 1e-1 at the shoulders of a short window and
75
+ * shift a band share by a fraction of a percent, which is the whole margin
76
+ * between one verdict and another.
77
+ *
78
+ * Written as numpy writes it — `0.5 + 0.5·cos(π·n/(M-1))` over
79
+ * `n = 1-M, 3-M, … M-1` — rather than the algebraically equal
80
+ * `0.5 - 0.5·cos(2πi/(M-1))`, because the two forms differ in the last bit and
81
+ * only this one reproduces `np.hanning` exactly.
82
+ */
83
+ export declare function hann(n: number): Float64Array;
84
+ export interface SpectrumOptions {
85
+ /** Analysis frame length; must be a power of two. */
86
+ nFft?: number;
87
+ /** Frame advance in samples. */
88
+ hop?: number;
89
+ }
90
+ export interface MeanSpectrum {
91
+ /** Bin centre frequencies in Hz. */
92
+ freqs: Float64Array;
93
+ /** Mean |X|² per bin over every whole frame. */
94
+ power: Float64Array;
95
+ }
96
+ /**
97
+ * Mean power spectrum over the whole file, Hann-windowed.
98
+ *
99
+ * Fixed 2048-point frames with 50 % overlap, so the same file measured twice
100
+ * returns the same number. A band edge is given in Hz, never in bins.
101
+ */
102
+ export declare function spectrum(audio: Audio, opts?: SpectrumOptions): MeanSpectrum;
103
+ export interface BandShareOptions extends SpectrumOptions {
104
+ /**
105
+ * `"magnitude"` sums |X| and is the default. `"power"` sums |X|² and is a
106
+ * DIFFERENT number — never compare one against the other.
107
+ */
108
+ weight?: "magnitude" | "power";
109
+ }
110
+ /**
111
+ * Share of total spectral weight inside an arbitrary Hz band, whole file.
112
+ *
113
+ * Decides which instrument a piece gets, and whether an arrangement will mask
114
+ * a singer. Two bands are read for every candidate timbre: 2-5 kHz (does it
115
+ * scratch) and the singer's own band in Hz (does it sit on top of them). The
116
+ * winner is the best of the two numbers, not the prettiest sound.
117
+ *
118
+ * `weight: "magnitude"` sums |X| — the default, and the only scale on which
119
+ * the thresholds in `@miadi/ava8-atelier` mean anything. Summing |X|² instead
120
+ * puts every real piece under 1 % and makes a 13.12 % reject line unreachable
121
+ * by any sound, which cannot be what those thresholds were calibrated against.
122
+ *
123
+ * The mask is half-open: `freqs >= lo && freqs < hi`.
124
+ */
125
+ export declare function bandShare(audio: Audio, loHz: number, hiHz: number, opts?: BandShareOptions): number;
126
+ /**
127
+ * Share of spectral magnitude in 2000-5000 Hz over the whole file.
128
+ *
129
+ * THE timbre decision. See `STRIDENCE` and `stridenceVerdict` in
130
+ * `@miadi/ava8-atelier` for the thresholds; they belong to a musician and are
131
+ * not redefined here. Whole file on purpose: a bright section measured alone
132
+ * answers a different question than "does the piece scratch".
133
+ *
134
+ * UNVERIFIED against the session's own figures. The atelier's stridence code
135
+ * was never written down in a generator, and the candidate renders it read
136
+ * are gone, so this implementation could not be checked number for number.
137
+ * Measured here on the surviving renders of 2026-08-16: op019 2.09 %,
138
+ * op018 4.42 %, ava2v2 6.50 %, op023 8.44 % — the same ordering and the same
139
+ * 4-8 % range the session reported for that day, running 15-25 % low against
140
+ * the two figures it named (ava2v2 7.60 %, op023 11.30 %). Treat a number
141
+ * from this function as comparable to another number from this function, and
142
+ * re-measure every candidate rather than quoting a figure from the day.
143
+ */
144
+ export declare function stridence(audio: Audio, opts?: BandShareOptions): number;
145
+ export interface F0Track {
146
+ /** Frame centres, seconds. */
147
+ times: Float64Array;
148
+ /** MIDI floats, NaN where unvoiced. */
149
+ midi: Float64Array;
150
+ /** Hz, NaN where unvoiced. Recomputed from `midi` after smoothing. */
151
+ hz: Float64Array;
152
+ /** Per-frame RMS. */
153
+ rms: Float64Array;
154
+ /** Normalised autocorrelation peak, 0-1. */
155
+ confidence: Float64Array;
156
+ rate: number;
157
+ windowS: number;
158
+ hopS: number;
159
+ }
160
+ export interface F0Options {
161
+ windowS?: number;
162
+ hopS?: number;
163
+ fmin?: number;
164
+ fmax?: number;
165
+ confFloor?: number;
166
+ rmsFloor?: number;
167
+ /** Frames in the median smoother; 0 or 1 disables it. */
168
+ smooth?: number;
169
+ }
170
+ /**
171
+ * Autocorrelation f0 per 40 ms window, 5-frame median smoothing.
172
+ *
173
+ * Decides what is actually sung, as opposed to what a listener reports. Two
174
+ * guards, both earned:
175
+ * · frames below `confFloor` or `rmsFloor` are UNVOICED (NaN), not forced to
176
+ * a pitch — silence with a pitch attached is an invention;
177
+ * · the median smoother removes single-frame octave flips, which are the
178
+ * cheap half of the octave problem. The expensive half needs
179
+ * `octaveErrorCheck`, and skipping it once invented a note nobody sang.
180
+ */
181
+ export declare function f0Track(audio: Audio, opts?: F0Options): F0Track;
182
+ /**
183
+ * Median over `k` frames, NaN-aware. A single wild frame must not become a note.
184
+ *
185
+ * NaNs are dropped BEFORE the median, not treated as values and not
186
+ * propagated: a window that is half unvoiced still has a real median, and a
187
+ * window that is entirely unvoiced stays NaN.
188
+ */
189
+ export declare function medianSmooth(series: ArrayLike<number>, k: number): Float64Array;
190
+ /**
191
+ * Fold every MIDI value down by octaves until it sits at or under `ceiling`.
192
+ *
193
+ * Decides where a voice really is. A tracker that reports B4 for a man droning
194
+ * B2 is reporting its own error, and folding is the first half of the fix —
195
+ * the second half is `octaveErrorCheck`, which says whether the fold was
196
+ * warranted or whether the note up there was genuinely sung.
197
+ */
198
+ export declare function foldOctaves(midiSeries: ArrayLike<number>, ceiling: number, floor?: number): Float64Array;
199
+ export interface OctaveCheckOptions {
200
+ /** Which sub-partial to test against; 4 asks about f/4. */
201
+ ratioHz?: number;
202
+ /** Half-width of the measurement band, as a fraction of the centre. */
203
+ bandFrac?: number;
204
+ /** Below this, f/ratio is too low to measure and the frame is skipped. */
205
+ minHz?: number;
206
+ /** The analysis window is this many times the tracker's. */
207
+ longWindowFactor?: number;
208
+ }
209
+ export interface OctaveCheck {
210
+ frames: number;
211
+ medianRatio: number | null;
212
+ meanRatio: number | null;
213
+ shareBelow: number | null;
214
+ verdict: string;
215
+ }
216
+ /**
217
+ * Energy at f/4 against energy at f, frame by frame. This caught an invented note.
218
+ *
219
+ * Decides whether a reported high pitch is real. When `shareBelow` is high and
220
+ * `medianRatio` > 1, the tracker is octave-doubling and the series must be
221
+ * folded before a note is claimed.
222
+ *
223
+ * The analysis window here is longer than the tracker's (4× by default): f/4
224
+ * of 494 Hz is 123 Hz, which a 40 ms window cannot resolve. Measuring the
225
+ * correction at the resolution that produced the error would reproduce it.
226
+ *
227
+ * The ratio is a POWER ratio, so a component at four times the amplitude reads
228
+ * 16 and not 4. Verified against a synthesised case: f/4 built at 5× amplitude
229
+ * reads 24.82. Say which ratio you mean when you quote it.
230
+ *
231
+ * Note the band here is CLOSED — `>= lo && <= hi` — unlike the half-open mask
232
+ * in `bandShare`. That asymmetry is in the original and is preserved.
233
+ */
234
+ export declare function octaveErrorCheck(audio: Audio, f0: F0Track, opts?: OctaveCheckOptions): OctaveCheck;
235
+ export interface HeldNote {
236
+ start: number;
237
+ end: number;
238
+ midi: number;
239
+ rms: number;
240
+ duration: number;
241
+ }
242
+ export interface HeldNoteOptions {
243
+ minMs?: number;
244
+ tolSemitones?: number;
245
+ }
246
+ /** What `heldNotes` needs of a track, so a folded MIDI series can be swapped in. */
247
+ export interface F0Like {
248
+ times: ArrayLike<number>;
249
+ midi: ArrayLike<number>;
250
+ rms: ArrayLike<number>;
251
+ hopS: number;
252
+ }
253
+ /**
254
+ * Frames that stay within ±1 semitone for at least 200 ms become one note.
255
+ *
256
+ * Decides what is singing and what is speech. Speech never holds a pitch for
257
+ * 200 ms; a drone holds it for seconds. Everything downstream — the
258
+ * pitch-class profile, the motif database, the interval cells — is built on
259
+ * these notes and not on frames, because a frame count is a property of the
260
+ * tracker's hop and a held note is a property of the singer.
261
+ */
262
+ export declare function heldNotes(f0: F0Like, opts?: HeldNoteOptions): HeldNote[];
263
+ export declare const MOTIF_MIN_NOTES = 3;
264
+ export declare const MOTIF_MIN_DISTINCT = 3;
265
+ export declare const MOTIF_MIN_SPAN = 3;
266
+ export interface Gesture {
267
+ kind: "motif" | "drone";
268
+ start: number;
269
+ end: number;
270
+ duration: number;
271
+ nNotes: number;
272
+ distinct: number;
273
+ span: number;
274
+ pitches: number[];
275
+ intervals: number[];
276
+ notes: HeldNote[];
277
+ }
278
+ /**
279
+ * Split held notes into gestures at every silence, then classify each one.
280
+ *
281
+ * The rule, and it is a rule and not an impression: a gesture is a MOTIF when
282
+ * it has ≥3 notes, ≥3 distinct pitches and a span of ≥3 semitones. Anything
283
+ * else is a DRONE. Without it, someone who drones reads as a melodist — which
284
+ * is exactly the mistake that would have had them handed a tune to sing
285
+ * instead of a world to stand inside.
286
+ */
287
+ export declare function motifs(notes: readonly HeldNote[], opts?: {
288
+ gapS?: number;
289
+ }): Gesture[];
290
+ export interface IntervalCell {
291
+ cell: number[];
292
+ length: number;
293
+ count: number;
294
+ times: number[];
295
+ gestures: number[];
296
+ }
297
+ /**
298
+ * Recurring interval sequences across gestures, with the time of every hit.
299
+ *
300
+ * Intervals and not pitches on purpose: a cell sung twice at different heights
301
+ * is the same cell, and counting pitches would miss it.
302
+ *
303
+ * Returns cells sorted by count then length, each with its timestamps. A cell
304
+ * that appears once is still returned; the timestamps are what let a human
305
+ * listen and disagree.
306
+ */
307
+ export declare function intervalCells(gestures: readonly Gesture[], opts?: {
308
+ lengths?: readonly number[];
309
+ motifsOnly?: boolean;
310
+ }): IntervalCell[];
311
+ export interface Seam {
312
+ at: number;
313
+ windowS: number;
314
+ rmsBefore: number;
315
+ rmsDuring: number;
316
+ rmsAfter: number;
317
+ dbBefore: number;
318
+ dbDuring: number;
319
+ dbAfter: number;
320
+ /** `null` when either side is silence — a ratio would excuse the hole. */
321
+ jumpDb: number | null;
322
+ silent: boolean;
323
+ dip: boolean;
324
+ }
325
+ /**
326
+ * RMS before, during and after every crossfade point.
327
+ *
328
+ * Decides whether an assembled piece is listenable. A seam that dips to
329
+ * silence is a hole; a seam that jumps is a click. Both are audible and both
330
+ * are invisible in the source score, because the score does not know the
331
+ * pieces were glued. Reported as levels and as a difference: dB differences
332
+ * are what an ear notices, and `jumpDb` is the number to argue about.
333
+ */
334
+ export declare function seams(audio: Audio, times: readonly number[], opts?: {
335
+ windowS?: number;
336
+ }): Seam[];
337
+ /**
338
+ * Python's `round(x, d)` — half to EVEN, where JavaScript rounds half away
339
+ * from zero. `round(2.5)` is 2 in Python and 3 in JavaScript, and the pitches
340
+ * a motif is classified on are rounded MIDI floats.
341
+ */
342
+ export declare function pyRound(x: number, digits: number): number;
343
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}