Everything below comes out of one offline script that anyone can re-run. Where a layer has not been measured yet, it says so.
The engine reads 40 ms frames with a 20 ms hop. Below are the aggregates it returns for each recording — no smoothing, no rounding up.
Self-produced English speech, 16 kHz mono. The baseline clip.
Identical audio with trailing silence appended — a control for what padding does and does not change.
A full captured session segment — the kind the engine actually sees in production.
Pipeline frame-autocorr-unbiased/v2 · provider numpy-autocorr · jitter and shimmer are frame-level proxies and must not be compared numerically against Praat period-based figures. The two aggregates that move under padding are the ones that divide by total duration.
We take the measured pitch track, then rebuild it from K straight segments and measure the residual. The optimum is found by dynamic programming, so the error can only fall as K rises.
Two recordings, four budgets each. Vertical axis is error in semitones; horizontal axis is how many segments the contour was compressed to.
| Recording | K = 4 | K = 8 | K = 16 | K = 32 |
|---|---|---|---|---|
| TTS short sentence | 11.58 | 6.67 | 3.38 | 1.08 |
| End-to-end session capture | 41.77 | 38.73 | 32.77 | 22.25 |
Unit is the semitone. The session capture needs far more budget because it contains many more distinct pitch targets — that is a property of the recording, not a defect of the estimator. Frame-level pitch tracks are used inside the measurement script only and are never stored or published.
We built five carriers whose pitch level and pitch variability were fixed by design, then let the engine measure them blind. The decision rule was written down before the run.
For each carrier we know the pitch level and pitch spread we asked for. Here is what the engine reported back, and the gap.
| Intended expression | Designed f0 | Measured f0 | Error | Designed spread | Measured spread | Error |
|---|---|---|---|---|---|---|
| Understated | 162.0 | 161.93 | −0.04% | 0.090 | 0.092 | +2.3% |
| Calm, unhurried | 135.0 | 135.07 | +0.05% | 0.100 | 0.107 | +6.5% |
| Low and heavy | 112.0 | 110.96 | −0.93% | 0.120 | 0.155 | +29.2% |
| Tense, quick | 205.0 | 203.03 | −0.96% | 0.260 | 0.279 | +7.1% |
| Flat | 160.0 | 160.22 | +0.14% | 0.035 | 0.036 | +1.4% |
The worst case is the lowest carrier, and the reason is arithmetic: at 112 Hz a 40 ms frame holds only about 4.5 periods, so the autocorrelation peak is positioned with more variance than at 205 Hz, where the same frame holds about 8. The mean level is recovered far better than the spread, and we report the spread error rather than hide it behind an average.
A results page is only worth reading if it lists its own gaps. These are ours, as of this build.
| Layer | State | Basis |
|---|---|---|
| Pitch estimation on synthetic tones | 39 / 39 | 140 Hz–300 Hz sweep, 13 assertions, all passing |
| Acoustic tracing on real speech | measured | 3 recordings, aggregates only |
| Pitch-contour reconstruction | measured | DP-optimal piecewise-linear fit |
| Expression separation | measured | 5 synthetic carriers, pre-registered rule |
| Human-labelled audio agreement | 0 | no consented audio, no annotators yet |
| Compressed-audio decoding | off locally | PyAV not installed on the measurement host |
Every figure on this page is written to a single JSON file by a single offline script. Nothing on this page carries its own data.
Python 3, NumPy only. Deterministic — the same seed always produces the same JSON.
Aggregates, reconstruction error, confusion counts, inversion gaps — and the list of things it did not measure.