@miadi/ava8-measure
v0.1.0
Published
Read a rendered artefact and hold it to a claim. Standard MIDI decoding with register, mode, drum-position and note-for-note measures; spectral share and f0 over PCM; and OSC movement analysis whose first act is always to drop held values. The family's ot
Readme
@miadi/ava8-measure
Read a rendered artefact and hold it to a claim.
Every other package in the ava8 family writes. @miadi/ava8-core writes MIDI;
@miadi/movement-conductor records and summarises a take; @miadi/capture stores one.
Nothing read a rendered artefact back and asked whether it is what it said it was.
That is this package.
ESM only. Zero runtime dependencies beyond @miadi/ava8-atelier. No DOM, no filesystem —
bytes in, numbers out — so it runs in a browser, in a test, and on a capture device.
npm install @miadi/ava8-measureThe rule that defines it
Read the rendered artefact, never the source. A generator's header states an intention.
Three tools sit between that intention and the file, and each has silently changed something
at least once: an accidental contaminated its bar and moved a note a semitone; a bare Q:
line was ignored so a tempo change existed only in a comment; a container refused a codec and
wrote zero bytes while reporting success.
A count that comes out right is not proof. The right number of drum hits can land off the beat. Read placement, not totals.
Three surfaces
| subpath | reads | answers |
|---|---|---|
| ./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 |
| ./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 |
| ./movement | OSC capture text | held-value ratio and the real new-value rate, per-second magnitudes, onsets, unwrapped heading, compass sextants, stillness spans |
The first act on a movement capture is always dedupe
A capture requested at 100 Hz delivered 1627 packets in 16.6 s — 98 Hz of packets, of which 1232 were repeats of their predecessor. 395 new values: 23.8 Hz, median gap 41 ms.
Onsets computed on the raw stream returned 28 attacks spaced 120–133 ms: regular, credible, and entirely false. That was the staircase of the held values, not a body. On the deduped stream: 15 attacks, spaced 153 to 1481 ms.
So dedupe is not an option and it is not a filter you may skip. onsets deduplicates by
default and reports whether it did.
What the numbers mean, and what they do not
stridence is comparable to itself. It does not reproduce two figures published by the
session that invented the method — it reads 15–25 % low against them — because the spectral
code was never committed and the candidate renders are gone. Re-measure every candidate
rather than quoting a figure from that day. The doc comment says so at the call site, where
it is harder to ignore.
onsets reproduces the count and the shape of a published list; 11 of 15 timestamps land
within 30 ms and four do not, because that session's peak-picking was never written down.
Movement captures carry no declared units. The capture's own field ledger says the channel semantic map is absent. Numbers are reported without units, and the music is built from ratios, so nothing is lost — but a unit invented here would be a claim nobody can support.
The conformance harness
This package is a port of three Python programs (atelier_midi.py, atelier_audio.py,
atelier_movement.py). The port was checked against them — and the checking happened in a
scratch directory that is gone, leaving a paragraph in a commit message. A claim nobody can
re-run is a claim, not a proof, which is the exact failure this package exists to name, so
the check now lives in the repository:
| | |
|---|---|
| test/fixtures/ | 19 committed fixtures, 195 KB — 8 MIDI, 6 WAV, 5 OSC captures |
| test/fixtures/expected-*.json | 356 measurements (276 structural, 80 numeric), all written by the Python |
| test/conformance.test.mjs | mirrors every one of them in TypeScript and compares |
| scripts/regenerate-fixtures.mjs | rebuilds the corpus and the expected values, end to end |
Those 356 measurements expand to 1741 structural values and 454 numeric ones, because a measurement is often a list: every note of a rendered file as a six-tuple, every voiced frame of an f0 track, every sextant of a walked circle.
Synthetic fixtures only. Every byte of the corpus comes out of a counter or a table in
regenerate-fixtures.mjs. No recording, no movement capture, and nothing out of a composition
folder is committed, read, or compared against here — consent forbids a person's voice or
recordings leaving the device they were made on, and "just to compare" is not an exception.
The harness prints synthetic fixtures only on every run.
node --test test/conformance.test.mjs needs no Python and no numpy. It reads the
committed JSON. Only regeneration needs the oracle, which is what makes the suite runnable on
a host where numpy cannot be imported at all.
Two tiers, and they are not the same claim
Structural results are compared for exact equality: note counts, per-track registers and their overlaps, band occupancy, drum slots and their bar numbers, dedupe counts, onset counts and their rounded times, sextant sequences, heading wraps, stillness spans, held-note boundaries, and the formatted verdict strings. Both programs reach these by integer arithmetic or by a single division of two counts, so a difference is a defect and never a rounding.
Spectral and other accumulated results are compared within a published tolerance — a mean power spectrum, an autocorrelation, a compensated mean, anything summed over many floating-point operations, where the two languages accumulate in a different order:
| | | |---|---| | relative tolerance | 1e-9 | | absolute floor, for values near zero where a ratio says nothing | 1e-12 |
Both numbers live in test/fixtures/tolerances.json and the suite asserts that this table
still states them, so the two cannot drift apart.
How they were chosen. The worst relative divergence this corpus actually produces is 1.6e-12 (a spectrum bin), and the worst absolute divergence is 1.4e-14 (an f0 frame in MIDI floats). 1e-9 leaves roughly three orders of magnitude of headroom over that, which absorbs a different libm on a different host without absorbing anything that could change a musical verdict: the stridence thresholds are 0.03 apart, seven orders of magnitude coarser. The run prints the worst divergence it observed, so the headroom is visible rather than asserted.
What the harness deliberately does not encode
The two figures named above under What the numbers mean are absent from the corpus on
purpose, and test/conformance.test.mjs records them under NOT_REPRODUCED with the reason
rather than leaving a silent hole that would read as coverage:
- stridence against the session's published 7.60 % and 11.30 % — the spectral code that
produced them was never committed and the renders are gone, so a fixture asserting 7.60 %
would assert a number no program on this machine can produce. The corpus pins stridence
against the Python oracle only: that proves the port agrees with
atelier_audio.py, and proves nothing about that day's figures. - onsets against the session's fifteen timestamps — its peak-picking was never written
down, and the take is a person's movement capture, which may not leave the device it was
made on. The corpus pins
onsetson a synthetic staircase instead, where the raw stream's three ghost attacks and the deduped stream's three real ones are both committed.
Regenerating
ATELIER_SCRIPTS=/path/to/atelier-jerry/scripts node scripts/regenerate-fixtures.mjs
node --test test/conformance.test.mjsThe first command writes the corpus, then hands it to scripts/oracle.py, which imports the
three Python modules and writes the expected values. The TypeScript is never consulted during
regeneration — a harness that generated its own expectations would be comparing the port
against itself and would prove nothing.
The FFT is hand-written on purpose
About 120 lines of iterative radix-2 Cooley–Tukey over Float64Array. Every call site uses a
power-of-two length, so radix-2 covers all of it.
The house builds with tsc and no bundler. A hand-written transform is smaller than the
argument about which npm FFT package to trust, and it can be audited in one sitting.
Two details that would silently change every number if they were wrong, and are asserted in
the tests: the Hann window is the symmetric one (n-1 in the denominator, as numpy
writes it, not the periodic form every tutorial gives), and a band share sums magnitude,
not power — power puts every piece under one percent and makes a 13.12 % threshold
unreachable by any sound.
What it does not own
| | |
|---|---|
| the thresholds, the register plan, provenance, consent | @miadi/ava8-atelier |
| writing ABC or MIDI | @miadi/ava8-core |
| rendering or playing a score | @miadi/ava8-abcjs, @miadi/ava8-react |
| recording a take, conducting movement into frames | @miadi/capture, @miadi/movement-conductor |
| turning a MIDI into audio | @miadi/jeremyai |
This package measures. It does not decide what the measurement means — that judgement lives
in @miadi/ava8-atelier, and the decision itself belongs to a person.
