@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 +21 -0
- package/README.md +174 -0
- package/dist/audio.d.ts +343 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +821 -0
- package/dist/audio.js.map +1 -0
- package/dist/fft.d.ts +51 -0
- package/dist/fft.d.ts.map +1 -0
- package/dist/fft.js +246 -0
- package/dist/fft.js.map +1 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/midi.d.ts +333 -0
- package/dist/midi.d.ts.map +1 -0
- package/dist/midi.js +651 -0
- package/dist/midi.js.map +1 -0
- package/dist/movement.d.ts +431 -0
- package/dist/movement.d.ts.map +1 -0
- package/dist/movement.js +701 -0
- package/dist/movement.js.map +1 -0
- package/package.json +70 -0
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.
|
package/dist/audio.d.ts
ADDED
|
@@ -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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"}
|