From fa558641b8986f374796c0580b0d10a3bd856379 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 04:00:25 +0000 Subject: [PATCH 1/5] docs: record decision to decline audio-analyzer-rs incorporation ASA is server-side Python on native Essentia and already computes a superset of the crate, so a same-class cross-check oracle would be decorative (cross-impl agreement != correctness; the crate's R128 is from-scratch, no ebur128 dep). tonnetz, the only gap, has no consumer and is dropped -- Harmonia can derive it from the chroma ASA already exports. Python-native validation is tracked separately, trigger-gated. Supersedes Plan 4's "cheap and high-value" oracle recommendation in incorporations/forking-plans-2026-05-14.md. https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8 --- .../audio-analyzer-rs-decision-2026-05-20.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/history/audio-analyzer-rs-decision-2026-05-20.md diff --git a/docs/history/audio-analyzer-rs-decision-2026-05-20.md b/docs/history/audio-analyzer-rs-decision-2026-05-20.md new file mode 100644 index 00000000..c2025512 --- /dev/null +++ b/docs/history/audio-analyzer-rs-decision-2026-05-20.md @@ -0,0 +1,102 @@ +# audio-analyzer-rs incorporation — decision: declined — 2026-05-20 + +> **Decided 2026-05-20.** ASA will **not** incorporate +> [`JuzzyDee/audio-analyzer-rs`](https://github.com/JuzzyDee/audio-analyzer-rs). +> tonnetz (the one feature ASA lacked) is **dropped**, not added. The crate's +> WASM phase stays pivot-gated. This **supersedes** the "cross-check oracle first — +> cheap and high-value" recommendation in +> [`incorporations/forking-plans-2026-05-14.md`](../../incorporations/forking-plans-2026-05-14.md) +> (Plan 4). Past-tense paper trail; not a living doc. + +## Scope + +The discovery framing proposed adopting `audio-analyzer-rs` as ASA's core engine — +compiling it to WASM and retiring Essentia.js — or, failing that, running it as a +cross-check oracle (Plan 4). This note records the decision after grounding that framing +against the actual codebase. + +## Decision + +1. **Do not incorporate the crate** — not as core engine (a), not as a selective vendor + (b), not as a cross-check oracle (c). +2. **Drop tonnetz.** It is the only crate feature ASA lacks; it has no consumer; the + sibling project that would use it can compute it itself. +3. **WASM stays pivot-gated.** A Rust→WASM build only matters if ASA becomes a + client-side product — not on today's roadmap (Plan 4, phase 2). + +## Why + +**The premise was inverted.** ASA is server-side Python (native Essentia via +`analyze_core.py`), not an in-browser Essentia.js library. There is no Essentia.js to +retire. + +**ASA already computes a superset of the crate**, usually via the more-validated, +EBU-verified Essentia, plus chords/genre/detectors the crate has none of: + +| Crate output | ASA equivalent (already shipped) | +| --- | --- | +| centroid / bandwidth / rolloff / flatness, 7 bands, contrast, 13 MFCC, chroma | `spectralDetail.*`, `spectralBalance` · Essentia | +| key (Krumhansl-Schmuckler) | `key` · Essentia `KeyExtractor` (EDMA profile) | +| LUFS / LRA / true-peak / crest | `lufsIntegrated` / `lufsRange` / `truePeak` / `crestFactor` · Essentia (EBU Tech 3341/3342-verified in `apps/backend/tests/test_loudness_r128.py`) | +| stereo width / correlation / mono-compat | `stereoDetail.*` (richer: per-band correlations, correlation curve) | +| BPM / beats / onsets, sections | `bpm` + `rhythmDetail.*`, `structure.*` | +| HPSS (median-filter split) | Demucs learned stems (`stemAnalysis.*`) — a different technique, not "better HPSS"; `librosa.effects.hpss` covers a literal split if ever needed | +| **tonnetz** | **none → dropped** | +| chords / melody / 11 detectors | crate has none; ASA has `chordDetail`, transcription, acid/reverb/vocal/supersaw/bass/kick/snare/hihat/sidechain/genre/saturation | + +**The oracle (c) is decorative.** An oracle that is *less validated than the thing it +checks* yields ambiguous signal: on disagreement we trust Essentia by default, so the +disagreement is logged and ignored. Cross-implementation agreement ≠ correctness. And: + +- The crate's `Cargo.toml` lists **no `ebur128` dependency** — its EBU R128 is a + from-scratch implementation, i.e. weaker as a loudness reference than Essentia (already + verified here) or `pyloudnorm` / `ffmpeg -af ebur128`. +- ASA already runs **independent cross-checks in-product**: BPM via Essentia + `RhythmExtractor2013` vs `PercivalBpmEstimator` (`bpmAgreement`), and chords via Viterbi + vs Essentia `ChordExtractor` (`chordDetail.chordTimelineAgreement`). + +This contradicts Plan 4's "cheap and high-value" call: cheap, yes; valuable, no. The +correct way to serve PURPOSE.md invariants #1 (measurement authority) and #4 (honest +uncertainty) is Python-native validation (reference libraries + labeled datasets), not a +same-class Rust oracle — see Follow-up. + +**tonnetz has no consumer.** Repo-wide, "tonnetz" appears only inside +`incorporations/forking-plans-2026-05-14.md`. The Phase 2 prompt's harmonic inputs are +`key`, `keyConfidence`, `chordDetail.*`, and `chroma` — no tonnetz. The sibling +**Harmonia** project (React + Tonal.js) can derive tonnetz from the chroma ASA already +exports (`spectralDetail.chroma`). Emitting it now would be speculative schema surface. + +## Verified facts (provenance) + +- **Crate** (via its GitHub `Cargo.toml` / README): Rust edition 2024, MIT, v1.0.0, ~21★; + **no `[lib]` target** (two bins: `cli` = `src/main.rs`, `mcp-server` = `src/mcp_server.rs`); + deps `symphonia 0.5`, `rustfft 6.2`, `rmcp 1.1`, `tokio 1` (full), `serde`, `serde_json`, + `schemars`, `tracing` — **no `ebur128`**. Reads a file path; analysis core (`src/lib.rs` + `pub mod analysis`) is separable from the MCP transport (`rmcp`/`tokio`, confined to the + `mcp-server` bin) — relevant only to a future WASM build. +- **ASA**: 56-key Phase 1 schema; key = Essentia EDMA (not K-S); chroma = HPCP; loudness = + `LoudnessEBUR128` (`analyze_core.py`). + +## Follow-up (tracked separately — not in this change) + +A **Python-native Phase 1 validation harness** is greenlit as its own plan + PR, gated on +a stated trigger (e.g. an upcoming `analyze.py`/Essentia change, distrusted key/tempo +accuracy, observed regressions, or populating the stalled `tests/ground_truth/` corpus). +The trigger should drive the gate design. Recorded design intent: + +- [ ] **Loudness → reference gate** (`pyloudnorm`; BS.1770 is a spec, so it is a true + reference). Builds on `apps/backend/tests/test_loudness_r128.py`. +- [ ] **chroma / MFCC / spectral-centroid → regression (golden-snapshot) gate, not + cross-impl.** Essentia HPCP vs librosa chroma (and MFCC mel/DCT/liftering) are + different algorithms; tolerance-tuning to force agreement is a calibration tar pit. +- [ ] **key / tempo accuracy → labeled datasets** (e.g. GiantSteps), MIREX-style weighted + key scoring + ±4% tempo (×2/÷2 octave credit) inline. Audio fetched to a gitignored + cache, never committed. (The real payoff is actual accuracy numbers, which may be + network-gated in CI — don't count scaffolding-without-a-baseline as done.) +- [ ] Pin `librosa`/`pyloudnorm`/`numpy`; make "the gate bites" a committed meta-test. + +## Out of scope + +- Vendoring the crate, adding a Rust toolchain to repo/CI. +- Any `analyze.py` output / schema change (tonnetz not added; contract unchanged). +- The crate's WASM phase (revisit only under a client-side-product decision). From b7a729a2628e3b5b1fcf97c90f1ab7018f4cd915 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 01:59:12 +0000 Subject: [PATCH 2/5] test(backend): add Phase 1 golden-snapshot regression gate Snapshot default-mode analyze.py output against a committed golden so any unintended drift in a measured field fails CI -- a safety net for upcoming analyze.py/Essentia changes. - tests/test_phase1_golden.py runs analyze.py on a deterministic procedural fixture and diffs the JSON against the golden with a recursive tolerant comparator (NaN/Inf-aware, bool-before-int, key-set + list-length drift, capped report). Output was byte-identical across 5 runs; the tolerance is cross-environment float-jitter insurance, not run-to-run noise. - Committed gate-bites meta-tests prove the comparator catches drift and does not over-bite (incl. in-memory NaN/Inf/bool and cap cases). - melodyDetail.midiFile (a transient per-run artifact path) is the sole excluded field, stored in the golden as a placeholder. - Regenerate after an intentional change with UPDATE_PHASE1_GOLDEN=1. No analyze.py / schema / requirements.txt change. https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8 --- .../tests/fixtures/golden/phase1_default.json | 2913 +++++++++++++++++ apps/backend/tests/test_phase1_golden.py | 367 +++ 2 files changed, 3280 insertions(+) create mode 100644 apps/backend/tests/fixtures/golden/phase1_default.json create mode 100644 apps/backend/tests/test_phase1_golden.py diff --git a/apps/backend/tests/fixtures/golden/phase1_default.json b/apps/backend/tests/fixtures/golden/phase1_default.json new file mode 100644 index 00000000..a8b98289 --- /dev/null +++ b/apps/backend/tests/fixtures/golden/phase1_default.json @@ -0,0 +1,2913 @@ +{ + "acidDetail": { + "bassRhythmDensity": 0.2, + "centroidOscillationHz": 4, + "confidence": 0.09, + "isAcid": false, + "resonanceLevel": 0.17 + }, + "arrangementDetail": { + "noveltyCurve": [ + 0.0, + 0.0002, + 0.0012, + 0.0022, + 0.0042, + 0.005, + 0.0043, + 0.0052, + 0.0033, + 0.0025, + 0.0026, + 0.0005, + 0.0004, + 0.0003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0002, + 0.0002, + 0.0003, + 0.0004, + 0.0002, + 0.0004, + 0.0005, + 0.0004, + 0.001, + 0.0008, + 0.0007, + 0.0008, + 0.0002, + 0.0002, + 0.0001, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "noveltyMean": 0.0059, + "noveltyPeaks": [], + "noveltyStdDev": 0.0692 + }, + "bassDetail": { + "averageDecayMs": 1000, + "fundamentalHz": 120, + "grooveType": "straight", + "swingPercent": 0, + "transientCount": 0, + "transientRatio": 0.2, + "type": "sustained" + }, + "beatsLoudness": { + "accentPattern": [ + 0.9723, + 1.0, + 0.958, + 0.9187 + ], + "beatCount": 13, + "beatLoudnessVariation": 0.1219, + "highBandAccentPattern": [ + 0.9344, + 0.3884, + 1.0, + 0.2675 + ], + "highDominantRatio": 0.0, + "kickDominantRatio": 0.0807, + "lowBandAccentPattern": [ + 0.9687, + 1.0, + 0.9693, + 0.9393 + ], + "meanBeatLoudness": 0.6178, + "midBandAccentPattern": [ + 0.9686, + 1.0, + 0.969, + 0.9392 + ], + "midDominantRatio": 0.0797, + "overallAccentPattern": [ + 0.9723, + 1.0, + 0.958, + 0.9187 + ], + "patternBeatsPerBar": 4 + }, + "bpm": 135.4, + "bpmAgreement": false, + "bpmConfidence": 1.08, + "bpmDoubletime": false, + "bpmPercival": 94.0, + "bpmRawOriginal": 135.4, + "bpmSource": "rhythm_extractor", + "chordDetail": { + "chordChangeCount": 0, + "chordSequence": [ + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "Am", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m", + "F#m" + ], + "chordStrength": 0.7792, + "chordTimeline": [ + { + "confidence": 0.9284, + "endSec": 6.037, + "label": "Am", + "labelLong": "A minor", + "startSec": 0.0 + } + ], + "chordTimelineAgreement": true, + "chordTimelineSource": "librosa_viterbi", + "dominantChords": [ + "Am", + "F#m" + ], + "progression": [ + "Am", + "F#m" + ] + }, + "crestFactor": 7.7, + "danceability": { + "danceability": 0.7086, + "dfa": 1.2499 + }, + "durationSeconds": 6.0, + "dynamicCharacter": { + "attackTimeStdDev": 0.0759, + "dynamicComplexity": 0.4968, + "logAttackTime": -2.9095, + "loudnessDb": -9.8382, + "loudnessVariation": -9.8382, + "spectralFlatness": 0.0059 + }, + "dynamicSpread": 380857.6, + "effectsDetail": { + "gatingDetected": false, + "gatingEventCount": 1, + "gatingRate": null, + "gatingRegularity": 0.0 + }, + "essentiaFeatures": { + "dissonance": 0.0015, + "hfc": 135.3434, + "spectralComplexity": 3.4308, + "zeroCrossingRate": 0.0123 + }, + "genreDetail": { + "confidence": 0.4, + "genre": "ambient-drone", + "genreFamily": "ambient", + "secondaryGenre": "acoustic", + "topScores": [ + { + "genre": "ambient-drone", + "score": 0.6429 + }, + { + "genre": "acoustic", + "score": 0.6378 + }, + { + "genre": "ambient", + "score": 0.524 + }, + { + "genre": "dub-techno", + "score": 0.5079 + }, + { + "genre": "rock", + "score": 0.4341 + } + ] + }, + "grooveDetail": { + "hihatAccent": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "hihatSwing": 0.0, + "kickAccent": [ + 0.059, + 0.0577, + 0.0562, + 0.0547, + 0.0532, + 0.0516, + 0.05, + 0.0483, + 0.0466, + 0.0451, + 0.0435, + 0.042, + 0.0407 + ], + "kickSwing": 0.0067, + "perDrumSwing": { + "hihat": 0.0, + "kick": 0.0067, + "snare": 0.0067 + } + }, + "hihatDetail": null, + "key": "A Minor", + "keyConfidence": 0.89, + "keyProfile": "edma", + "kickDetail": { + "fundamentalHz": 50.0, + "harmonicRatio": 0.0, + "isDistorted": false, + "kickCount": 0, + "thd": 0.0 + }, + "lufsCurve": { + "momentary": [ + { + "lufs": -4.8, + "t": 0.0 + }, + { + "lufs": -4.8, + "t": 0.1 + }, + { + "lufs": -4.9, + "t": 0.2 + }, + { + "lufs": -4.8, + "t": 0.3 + }, + { + "lufs": -4.8, + "t": 0.4 + }, + { + "lufs": -4.9, + "t": 0.5 + }, + { + "lufs": -4.9, + "t": 0.6 + }, + { + "lufs": -5.0, + "t": 0.7 + }, + { + "lufs": -5.0, + "t": 0.8 + }, + { + "lufs": -5.0, + "t": 0.9 + }, + { + "lufs": -5.0, + "t": 1.0 + }, + { + "lufs": -5.1, + "t": 1.1 + }, + { + "lufs": -5.1, + "t": 1.2 + }, + { + "lufs": -5.2, + "t": 1.3 + }, + { + "lufs": -5.2, + "t": 1.4 + }, + { + "lufs": -5.2, + "t": 1.5 + }, + { + "lufs": -5.2, + "t": 1.6 + }, + { + "lufs": -5.2, + "t": 1.7 + }, + { + "lufs": -5.3, + "t": 1.8 + }, + { + "lufs": -5.4, + "t": 1.9 + }, + { + "lufs": -5.4, + "t": 2.0 + }, + { + "lufs": -5.4, + "t": 2.1 + }, + { + "lufs": -5.4, + "t": 2.2 + }, + { + "lufs": -5.4, + "t": 2.3 + }, + { + "lufs": -5.5, + "t": 2.4 + }, + { + "lufs": -5.5, + "t": 2.5 + }, + { + "lufs": -5.6, + "t": 2.6 + }, + { + "lufs": -5.6, + "t": 2.7 + }, + { + "lufs": -5.6, + "t": 2.8 + }, + { + "lufs": -5.6, + "t": 2.9 + }, + { + "lufs": -5.6, + "t": 3.0 + }, + { + "lufs": -5.7, + "t": 3.1 + }, + { + "lufs": -5.8, + "t": 3.2 + }, + { + "lufs": -5.8, + "t": 3.3 + }, + { + "lufs": -5.8, + "t": 3.4 + }, + { + "lufs": -5.8, + "t": 3.5 + }, + { + "lufs": -5.8, + "t": 3.6 + }, + { + "lufs": -5.9, + "t": 3.7 + }, + { + "lufs": -6.0, + "t": 3.8 + }, + { + "lufs": -6.0, + "t": 3.9 + }, + { + "lufs": -6.0, + "t": 4.0 + }, + { + "lufs": -6.0, + "t": 4.1 + }, + { + "lufs": -6.0, + "t": 4.2 + }, + { + "lufs": -6.1, + "t": 4.3 + }, + { + "lufs": -6.2, + "t": 4.4 + }, + { + "lufs": -6.2, + "t": 4.5 + }, + { + "lufs": -6.2, + "t": 4.6 + }, + { + "lufs": -6.2, + "t": 4.7 + }, + { + "lufs": -6.3, + "t": 4.8 + }, + { + "lufs": -6.3, + "t": 4.9 + }, + { + "lufs": -6.4, + "t": 5.0 + }, + { + "lufs": -6.4, + "t": 5.1 + }, + { + "lufs": -6.4, + "t": 5.2 + }, + { + "lufs": -6.4, + "t": 5.3 + }, + { + "lufs": -6.5, + "t": 5.4 + }, + { + "lufs": -6.5, + "t": 5.5 + }, + { + "lufs": -6.6, + "t": 5.6 + } + ], + "shortTerm": [ + { + "lufs": -5.2, + "t": 0.0 + }, + { + "lufs": -5.2, + "t": 0.1 + }, + { + "lufs": -5.2, + "t": 0.2 + }, + { + "lufs": -5.2, + "t": 0.3 + }, + { + "lufs": -5.3, + "t": 0.4 + }, + { + "lufs": -5.3, + "t": 0.5 + }, + { + "lufs": -5.3, + "t": 0.6 + }, + { + "lufs": -5.4, + "t": 0.7 + }, + { + "lufs": -5.4, + "t": 0.8 + }, + { + "lufs": -5.4, + "t": 0.9 + }, + { + "lufs": -5.4, + "t": 1.0 + }, + { + "lufs": -5.5, + "t": 1.1 + }, + { + "lufs": -5.5, + "t": 1.2 + }, + { + "lufs": -5.5, + "t": 1.3 + }, + { + "lufs": -5.6, + "t": 1.4 + }, + { + "lufs": -5.6, + "t": 1.5 + }, + { + "lufs": -5.6, + "t": 1.6 + }, + { + "lufs": -5.7, + "t": 1.7 + }, + { + "lufs": -5.7, + "t": 1.8 + }, + { + "lufs": -5.7, + "t": 1.9 + }, + { + "lufs": -5.8, + "t": 2.0 + }, + { + "lufs": -5.8, + "t": 2.1 + }, + { + "lufs": -5.8, + "t": 2.2 + }, + { + "lufs": -5.9, + "t": 2.3 + }, + { + "lufs": -5.9, + "t": 2.4 + }, + { + "lufs": -5.9, + "t": 2.5 + }, + { + "lufs": -6.0, + "t": 2.6 + }, + { + "lufs": -6.0, + "t": 2.7 + }, + { + "lufs": -6.0, + "t": 2.8 + }, + { + "lufs": -6.1, + "t": 2.9 + }, + { + "lufs": -6.1, + "t": 3.0 + } + ] + }, + "lufsIntegrated": -5.6, + "lufsMomentaryMax": -4.8, + "lufsRange": 0.8, + "lufsShortTermMax": -5.2, + "melodyDetail": { + "dominantNotes": [ + 45 + ], + "midiFile": "", + "noteCount": 1, + "notes": [ + { + "duration": 5.962, + "midi": 45, + "onset": 0.015 + } + ], + "pitchConfidence": 0.2241, + "pitchRange": { + "max": 45, + "min": 45 + }, + "sourceSeparated": false, + "vibratoConfidence": 0.0, + "vibratoExtent": 0.0, + "vibratoPresent": false, + "vibratoRate": 0.0 + }, + "monoCompatible": true, + "perceptual": { + "roughness": 0.0015, + "sharpness": 0.0014 + }, + "pitchDetail": null, + "plr": 6.6, + "reverbDetail": { + "isWet": false, + "measured": false, + "perBandRt60": null, + "preDelayMs": null, + "rt60": null, + "tailEnergyRatio": null + }, + "rhythmDetail": { + "beatGrid": [ + 0.406, + 0.813, + 1.219, + 1.625, + 2.032, + 2.45, + 2.868, + 3.32, + 3.762, + 4.203, + 4.644, + 5.085, + 5.526 + ], + "beatPositions": [ + 1, + 2, + 3, + 4, + 1, + 2, + 3, + 4, + 1, + 2, + 3, + 4, + 1 + ], + "downbeats": [ + 0.406, + 2.032, + 3.762, + 5.526 + ], + "grooveAmount": 0.0402, + "onsetRate": 0.33, + "phraseGrid": { + "phrases16Bar": [ + 0.406 + ], + "phrases4Bar": [ + 0.406 + ], + "phrases8Bar": [ + 0.406 + ], + "totalBars": 4, + "totalPhrases8Bar": 1 + }, + "tempoCurve": [ + { + "bpm": 147.7, + "t": 0.61 + }, + { + "bpm": 147.7, + "t": 1.02 + }, + { + "bpm": 147.7, + "t": 1.42 + }, + { + "bpm": 147.7, + "t": 1.83 + }, + { + "bpm": 143.6, + "t": 2.24 + }, + { + "bpm": 143.6, + "t": 2.66 + }, + { + "bpm": 136.0, + "t": 3.09 + }, + { + "bpm": 136.0, + "t": 3.54 + }, + { + "bpm": 136.0, + "t": 3.98 + }, + { + "bpm": 136.0, + "t": 4.42 + }, + { + "bpm": 136.0, + "t": 4.86 + }, + { + "bpm": 136.0, + "t": 5.31 + } + ], + "tempoStability": 0.9598 + }, + "rhythmTimeline": { + "availableBars": 3, + "beatsPerBar": 4, + "selectionMethod": "representative_dsp_window", + "stepsPerBeat": 4, + "windows": [ + { + "bars": 3, + "endBar": 3, + "highBandSteps": [ + 0.2572, + 0.0104, + 0.0008, + 0.001, + 0.0266, + 0.2389, + 0.372, + 0.8732, + 0.8013, + 1.0, + 0.5644, + 0.4728, + 0.0968, + 0.0754, + 0.0036, + 0.0042, + 0.0082, + 0.0079, + 0.008, + 0.0342, + 0.1939, + 0.2629, + 0.1855, + 0.2141, + 0.0343, + 0.0128, + 0.0124, + 0.0122, + 0.0117, + 0.0116, + 0.0113, + 0.0116, + 0.0116, + 0.0112, + 0.0303, + 0.0303, + 0.032, + 0.0329, + 0.0349, + 0.0349, + 0.0338, + 0.0323, + 0.0304, + 0.0333, + 0.0315, + 0.0317, + 0.0321, + 0.0342 + ], + "lowBandSteps": [ + 0.8835, + 0.8046, + 0.7568, + 0.7931, + 0.8411, + 0.7872, + 0.8201, + 0.8517, + 0.7307, + 1.0, + 0.8574, + 0.6914, + 0.7552, + 0.7905, + 0.7351, + 0.684, + 0.0007, + 0.0003, + 0.0013, + 0.0004, + 0.0037, + 0.0086, + 0.0057, + 0.0057, + 0.0274, + 0.0306, + 0.0226, + 0.0206, + 0.6697, + 0.6955, + 0.6688, + 0.6594, + 0.6723, + 0.6349, + 0.6641, + 0.635, + 0.6406, + 0.648, + 0.617, + 0.6436, + 0.6087, + 0.617, + 0.6109, + 0.585, + 0.6098, + 0.575, + 0.5924, + 0.5836 + ], + "midBandSteps": [ + 0.9573, + 0.9521, + 0.9451, + 0.9389, + 0.9329, + 0.9238, + 0.9144, + 0.902, + 0.8984, + 0.8883, + 0.8869, + 0.8816, + 0.8802, + 0.8751, + 0.8687, + 0.8619, + 0.9059, + 0.8996, + 0.8918, + 0.8857, + 0.8779, + 0.8691, + 0.864, + 0.8575, + 1.0, + 0.9915, + 0.9836, + 0.9754, + 0.9175, + 0.9106, + 0.9026, + 0.8951, + 0.8881, + 0.8798, + 0.8732, + 0.8652, + 0.8581, + 0.851, + 0.843, + 0.8365, + 0.8286, + 0.8217, + 0.8147, + 0.807, + 0.8007, + 0.7928, + 0.7863, + 0.7791 + ], + "overallSteps": [ + 0.9576, + 0.9523, + 0.9453, + 0.9392, + 0.9331, + 0.924, + 0.9147, + 0.9023, + 0.8986, + 0.8886, + 0.8872, + 0.8818, + 0.8804, + 0.8754, + 0.8689, + 0.8621, + 0.9059, + 0.8995, + 0.8918, + 0.8857, + 0.8779, + 0.8691, + 0.864, + 0.8575, + 1.0, + 0.9915, + 0.9836, + 0.9754, + 0.9177, + 0.9108, + 0.9028, + 0.8953, + 0.8883, + 0.88, + 0.8734, + 0.8654, + 0.8583, + 0.8511, + 0.8432, + 0.8367, + 0.8288, + 0.8219, + 0.8148, + 0.8072, + 0.8008, + 0.793, + 0.7865, + 0.7793 + ], + "startBar": 1 + } + ] + }, + "sampleRate": 44100, + "saturationDetail": { + "clippedSampleCount": 1367, + "clippedSamplePercent": 0.5166, + "nearClippedSampleCount": 2952, + "nearClippedSamplePercent": 1.116, + "peakRatio95to50": 2.681, + "rmsToPeakRatioDb": 7.74, + "saturationLikely": true + }, + "segmentKey": [ + { + "key": "A Minor", + "keyConfidence": 0.89, + "segmentIndex": 0 + } + ], + "segmentLoudness": [ + { + "end": 6.0, + "lra": 0.8, + "lufs": -5.6, + "segmentIndex": 0, + "start": 0.0 + } + ], + "segmentSpectral": [ + { + "barkBands": [ + -45.1593, + -42.6317, + -42.3887, + -36.5489, + -4.5237, + -9.6341, + -14.0747, + -47.1577, + -50.8125, + -51.0924, + -51.6736, + -53.7595, + -53.7207, + -53.8242, + -58.3335, + -56.6625, + -60.8894, + -61.7893, + -63.6887, + -64.6266, + -65.5867, + -66.9544, + -68.0964, + -68.5598 + ], + "segmentIndex": 0, + "spectralCentroid": 279.8, + "spectralRolloff": 336.6, + "stereoCorrelation": 1.0, + "stereoWidth": 0.0 + } + ], + "segmentStereo": [ + { + "segmentIndex": 0, + "stereoCorrelation": 1.0, + "stereoWidth": 0.0 + } + ], + "sidechainDetail": { + "envelopeShape": [ + 1.0, + 0.979, + 0.945, + 0.973, + 0.993, + 0.934, + 0.96, + 0.957, + 0.955, + 0.949, + 0.969, + 0.953, + 0.911, + 0.949, + 0.949, + 0.915 + ], + "envelopeShape32": [ + 0.939, + 1.0, + 0.96, + 0.979, + 0.945, + 0.936, + 0.937, + 0.973, + 0.993, + 0.92, + 0.934, + 0.921, + 0.96, + 0.936, + 0.957, + 0.939, + 0.883, + 0.955, + 0.949, + 0.909, + 0.969, + 0.903, + 0.91, + 0.953, + 0.903, + 0.911, + 0.949, + 0.899, + 0.894, + 0.949, + 0.915, + 0.866 + ], + "pumpingConfidence": 0.0854, + "pumpingRate": null, + "pumpingRegularity": 0.0, + "pumpingStrength": 0.059 + }, + "snareDetail": { + "bandHz": [ + 120.0, + 2000.0 + ], + "hitCount": 26, + "hitsPerSecond": 4.33, + "meanAttackSharpness": 0.0083, + "meanBodyEnergyRatio": 1.0, + "meanCentroidHz": 295.6, + "meanDecayFrames": 57.8, + "meanDecaySeconds": 0.336, + "meanSnapEnergyRatio": 0.0 + }, + "spectralBalance": { + "brilliance": -64.7, + "highs": -62.2, + "lowBass": -4.9, + "lowMids": -6.1, + "mids": -41.0, + "subBass": -41.6, + "upperMids": -53.4 + }, + "spectralBalanceTimeSeries": [ + { + "brilliance": -63.7, + "highs": -56.5, + "lowBass": -4.5, + "lowMids": -6.7, + "mids": -24.4, + "subBass": -21.1, + "t": 0.01, + "upperMids": -43.9 + }, + { + "brilliance": -66.8, + "highs": -58.0, + "lowBass": -3.4, + "lowMids": -5.2, + "mids": -32.3, + "subBass": -45.2, + "t": 0.06, + "upperMids": -44.6 + }, + { + "brilliance": -68.2, + "highs": -59.8, + "lowBass": -3.6, + "lowMids": -5.2, + "mids": -35.0, + "subBass": -48.0, + "t": 0.1, + "upperMids": -45.8 + }, + { + "brilliance": -71.0, + "highs": -60.7, + "lowBass": -3.9, + "lowMids": -5.2, + "mids": -36.7, + "subBass": -48.1, + "t": 0.15, + "upperMids": -47.1 + }, + { + "brilliance": -69.2, + "highs": -61.6, + "lowBass": -4.2, + "lowMids": -5.3, + "mids": -35.6, + "subBass": -49.7, + "t": 0.2, + "upperMids": -47.1 + }, + { + "brilliance": -69.6, + "highs": -61.6, + "lowBass": -4.6, + "lowMids": -5.4, + "mids": -37.6, + "subBass": -50.0, + "t": 0.24, + "upperMids": -47.2 + }, + { + "brilliance": -72.3, + "highs": -63.6, + "lowBass": -4.8, + "lowMids": -5.4, + "mids": -41.0, + "subBass": -52.4, + "t": 0.29, + "upperMids": -49.7 + }, + { + "brilliance": -74.4, + "highs": -67.2, + "lowBass": -5.0, + "lowMids": -5.5, + "mids": -41.7, + "subBass": -54.2, + "t": 0.34, + "upperMids": -51.7 + }, + { + "brilliance": -77.4, + "highs": -67.6, + "lowBass": -4.9, + "lowMids": -5.4, + "mids": -45.0, + "subBass": -59.3, + "t": 0.38, + "upperMids": -52.9 + }, + { + "brilliance": -76.5, + "highs": -68.1, + "lowBass": -4.7, + "lowMids": -5.4, + "mids": -44.1, + "subBass": -56.6, + "t": 0.43, + "upperMids": -54.3 + }, + { + "brilliance": -78.3, + "highs": -70.0, + "lowBass": -4.4, + "lowMids": -5.4, + "mids": -46.9, + "subBass": -64.4, + "t": 0.48, + "upperMids": -57.0 + }, + { + "brilliance": -84.3, + "highs": -74.5, + "lowBass": -4.1, + "lowMids": -5.3, + "mids": -47.6, + "subBass": -66.8, + "t": 0.52, + "upperMids": -64.8 + }, + { + "brilliance": -91.4, + "highs": -95.9, + "lowBass": -3.8, + "lowMids": -5.2, + "mids": -47.8, + "subBass": -64.6, + "t": 0.57, + "upperMids": -99.2 + }, + { + "brilliance": -91.4, + "highs": -95.2, + "lowBass": -3.5, + "lowMids": -5.2, + "mids": -47.5, + "subBass": -65.0, + "t": 0.62, + "upperMids": -97.8 + }, + { + "brilliance": -91.1, + "highs": -95.4, + "lowBass": -3.4, + "lowMids": -5.2, + "mids": -47.4, + "subBass": -64.7, + "t": 0.66, + "upperMids": -96.4 + }, + { + "brilliance": -91.2, + "highs": -95.5, + "lowBass": -3.4, + "lowMids": -5.2, + "mids": -47.7, + "subBass": -63.8, + "t": 0.71, + "upperMids": -96.6 + }, + { + "brilliance": -89.7, + "highs": -95.4, + "lowBass": -3.5, + "lowMids": -5.2, + "mids": -47.9, + "subBass": -66.0, + "t": 0.75, + "upperMids": -96.3 + }, + { + "brilliance": -89.8, + "highs": -95.1, + "lowBass": -3.7, + "lowMids": -5.3, + "mids": -47.7, + "subBass": -64.0, + "t": 0.8, + "upperMids": -96.2 + }, + { + "brilliance": -82.8, + "highs": -73.4, + "lowBass": -4.0, + "lowMids": -5.4, + "mids": -47.5, + "subBass": -65.9, + "t": 0.85, + "upperMids": -64.5 + }, + { + "brilliance": -79.9, + "highs": -72.0, + "lowBass": -4.4, + "lowMids": -5.5, + "mids": -46.5, + "subBass": -61.0, + "t": 0.89, + "upperMids": -57.1 + }, + { + "brilliance": -79.6, + "highs": -73.5, + "lowBass": -4.7, + "lowMids": -5.5, + "mids": -46.9, + "subBass": -63.2, + "t": 0.94, + "upperMids": -57.8 + }, + { + "brilliance": -75.6, + "highs": -68.6, + "lowBass": -5.0, + "lowMids": -5.6, + "mids": -45.9, + "subBass": -66.6, + "t": 0.99, + "upperMids": -53.7 + }, + { + "brilliance": -75.2, + "highs": -67.3, + "lowBass": -5.2, + "lowMids": -5.6, + "mids": -43.7, + "subBass": -55.9, + "t": 1.03, + "upperMids": -53.3 + }, + { + "brilliance": -75.1, + "highs": -66.3, + "lowBass": -5.2, + "lowMids": -5.7, + "mids": -43.5, + "subBass": -56.8, + "t": 1.08, + "upperMids": -51.5 + }, + { + "brilliance": -73.0, + "highs": -63.9, + "lowBass": -5.0, + "lowMids": -5.7, + "mids": -41.1, + "subBass": -53.1, + "t": 1.13, + "upperMids": -49.4 + }, + { + "brilliance": -72.2, + "highs": -63.5, + "lowBass": -4.7, + "lowMids": -5.6, + "mids": -40.2, + "subBass": -55.6, + "t": 1.17, + "upperMids": -48.7 + }, + { + "brilliance": -74.1, + "highs": -63.6, + "lowBass": -4.3, + "lowMids": -5.6, + "mids": -41.0, + "subBass": -51.8, + "t": 1.22, + "upperMids": -49.5 + }, + { + "brilliance": -70.1, + "highs": -62.2, + "lowBass": -4.0, + "lowMids": -5.5, + "mids": -38.3, + "subBass": -54.4, + "t": 1.27, + "upperMids": -46.9 + }, + { + "brilliance": -70.2, + "highs": -61.4, + "lowBass": -3.8, + "lowMids": -5.5, + "mids": -36.8, + "subBass": -48.7, + "t": 1.31, + "upperMids": -46.0 + }, + { + "brilliance": -72.2, + "highs": -62.3, + "lowBass": -3.6, + "lowMids": -5.5, + "mids": -38.4, + "subBass": -53.4, + "t": 1.36, + "upperMids": -47.4 + }, + { + "brilliance": -73.0, + "highs": -63.4, + "lowBass": -3.6, + "lowMids": -5.4, + "mids": -39.8, + "subBass": -51.0, + "t": 1.4, + "upperMids": -48.9 + }, + { + "brilliance": -68.9, + "highs": -60.8, + "lowBass": -3.7, + "lowMids": -5.5, + "mids": -38.1, + "subBass": -53.2, + "t": 1.45, + "upperMids": -46.3 + }, + { + "brilliance": -70.5, + "highs": -61.4, + "lowBass": -3.9, + "lowMids": -5.6, + "mids": -39.2, + "subBass": -52.0, + "t": 1.5, + "upperMids": -47.1 + }, + { + "brilliance": -74.7, + "highs": -64.5, + "lowBass": -4.2, + "lowMids": -5.6, + "mids": -43.0, + "subBass": -55.9, + "t": 1.54, + "upperMids": -51.2 + }, + { + "brilliance": -78.2, + "highs": -68.2, + "lowBass": -4.6, + "lowMids": -5.7, + "mids": -44.2, + "subBass": -55.6, + "t": 1.59, + "upperMids": -54.0 + }, + { + "brilliance": -75.5, + "highs": -67.7, + "lowBass": -5.0, + "lowMids": -5.8, + "mids": -45.5, + "subBass": -62.5, + "t": 1.64, + "upperMids": -52.0 + }, + { + "brilliance": -79.5, + "highs": -72.1, + "lowBass": -5.2, + "lowMids": -5.8, + "mids": -47.3, + "subBass": -64.6, + "t": 1.68, + "upperMids": -58.0 + }, + { + "brilliance": -83.5, + "highs": -77.7, + "lowBass": -5.4, + "lowMids": -5.8, + "mids": -47.8, + "subBass": -68.6, + "t": 1.73, + "upperMids": -63.7 + }, + { + "brilliance": -84.3, + "highs": -75.2, + "lowBass": -5.3, + "lowMids": -5.8, + "mids": -47.9, + "subBass": -67.5, + "t": 1.78, + "upperMids": -68.0 + }, + { + "brilliance": -87.2, + "highs": -85.7, + "lowBass": -5.1, + "lowMids": -5.8, + "mids": -48.1, + "subBass": -69.3, + "t": 1.82, + "upperMids": -94.8 + }, + { + "brilliance": -87.2, + "highs": -85.7, + "lowBass": -4.8, + "lowMids": -5.8, + "mids": -48.2, + "subBass": -65.9, + "t": 1.87, + "upperMids": -94.7 + }, + { + "brilliance": -87.1, + "highs": -85.5, + "lowBass": -4.5, + "lowMids": -5.7, + "mids": -48.1, + "subBass": -67.3, + "t": 1.92, + "upperMids": -94.7 + }, + { + "brilliance": -87.1, + "highs": -85.4, + "lowBass": -4.2, + "lowMids": -5.7, + "mids": -48.0, + "subBass": -64.2, + "t": 1.96, + "upperMids": -94.5 + }, + { + "brilliance": -84.8, + "highs": -83.8, + "lowBass": -3.9, + "lowMids": -5.6, + "mids": -48.2, + "subBass": -65.9, + "t": 2.01, + "upperMids": -85.4 + }, + { + "brilliance": -84.2, + "highs": -83.1, + "lowBass": -3.8, + "lowMids": -5.6, + "mids": -48.3, + "subBass": -65.1, + "t": 2.05, + "upperMids": -83.5 + }, + { + "brilliance": -84.3, + "highs": -83.2, + "lowBass": -3.8, + "lowMids": -5.6, + "mids": -48.0, + "subBass": -64.3, + "t": 2.1, + "upperMids": -83.5 + }, + { + "brilliance": -84.2, + "highs": -83.2, + "lowBass": -3.9, + "lowMids": -5.7, + "mids": -47.9, + "subBass": -65.6, + "t": 2.15, + "upperMids": -83.9 + }, + { + "brilliance": -84.4, + "highs": -83.3, + "lowBass": -4.1, + "lowMids": -5.7, + "mids": -48.2, + "subBass": -65.1, + "t": 2.19, + "upperMids": -83.7 + }, + { + "brilliance": -84.5, + "highs": -83.4, + "lowBass": -4.4, + "lowMids": -5.8, + "mids": -48.4, + "subBass": -66.4, + "t": 2.24, + "upperMids": -83.8 + }, + { + "brilliance": -84.7, + "highs": -83.5, + "lowBass": -4.8, + "lowMids": -5.9, + "mids": -48.2, + "subBass": -67.3, + "t": 2.29, + "upperMids": -83.9 + }, + { + "brilliance": -82.8, + "highs": -75.4, + "lowBass": -5.2, + "lowMids": -6.0, + "mids": -48.1, + "subBass": -66.1, + "t": 2.33, + "upperMids": -67.7 + }, + { + "brilliance": -84.5, + "highs": -80.9, + "lowBass": -5.5, + "lowMids": -6.0, + "mids": -48.2, + "subBass": -68.3, + "t": 2.38, + "upperMids": -75.1 + }, + { + "brilliance": -81.1, + "highs": -71.3, + "lowBass": -5.6, + "lowMids": -6.1, + "mids": -48.2, + "subBass": -68.5, + "t": 2.43, + "upperMids": -64.6 + }, + { + "brilliance": -80.0, + "highs": -74.1, + "lowBass": -5.6, + "lowMids": -6.1, + "mids": -47.6, + "subBass": -65.5, + "t": 2.47, + "upperMids": -58.8 + }, + { + "brilliance": -78.4, + "highs": -71.7, + "lowBass": -5.4, + "lowMids": -6.0, + "mids": -47.4, + "subBass": -63.2, + "t": 2.52, + "upperMids": -56.7 + }, + { + "brilliance": -75.1, + "highs": -68.5, + "lowBass": -5.1, + "lowMids": -6.0, + "mids": -45.9, + "subBass": -61.0, + "t": 2.57, + "upperMids": -53.7 + }, + { + "brilliance": -76.1, + "highs": -68.1, + "lowBass": -4.7, + "lowMids": -6.0, + "mids": -46.0, + "subBass": -61.4, + "t": 2.61, + "upperMids": -52.9 + }, + { + "brilliance": -77.3, + "highs": -69.1, + "lowBass": -4.4, + "lowMids": -5.9, + "mids": -47.7, + "subBass": -62.9, + "t": 2.66, + "upperMids": -56.6 + }, + { + "brilliance": -75.7, + "highs": -67.0, + "lowBass": -4.2, + "lowMids": -5.9, + "mids": -45.3, + "subBass": -61.5, + "t": 2.71, + "upperMids": -51.9 + }, + { + "brilliance": -72.7, + "highs": -65.9, + "lowBass": -4.0, + "lowMids": -5.9, + "mids": -44.6, + "subBass": -57.7, + "t": 2.75, + "upperMids": -51.3 + }, + { + "brilliance": -76.7, + "highs": -71.1, + "lowBass": -4.0, + "lowMids": -5.8, + "mids": -46.5, + "subBass": -61.5, + "t": 2.8, + "upperMids": -55.2 + }, + { + "brilliance": -78.5, + "highs": -72.3, + "lowBass": -4.1, + "lowMids": -5.9, + "mids": -47.7, + "subBass": -63.3, + "t": 2.84, + "upperMids": -57.9 + }, + { + "brilliance": -77.3, + "highs": -68.6, + "lowBass": -4.4, + "lowMids": -5.9, + "mids": -48.2, + "subBass": -67.2, + "t": 2.89, + "upperMids": -59.8 + }, + { + "brilliance": -79.9, + "highs": -72.7, + "lowBass": -4.7, + "lowMids": -6.0, + "mids": -48.3, + "subBass": -64.8, + "t": 2.94, + "upperMids": -66.0 + }, + { + "brilliance": -82.3, + "highs": -80.8, + "lowBass": -5.0, + "lowMids": -6.1, + "mids": -48.3, + "subBass": -66.8, + "t": 2.98, + "upperMids": -77.1 + }, + { + "brilliance": -82.7, + "highs": -82.4, + "lowBass": -5.4, + "lowMids": -6.2, + "mids": -48.5, + "subBass": -67.4, + "t": 3.03, + "upperMids": -81.4 + }, + { + "brilliance": -82.6, + "highs": -82.4, + "lowBass": -5.7, + "lowMids": -6.2, + "mids": -48.6, + "subBass": -69.1, + "t": 3.08, + "upperMids": -81.3 + }, + { + "brilliance": -82.7, + "highs": -82.4, + "lowBass": -5.8, + "lowMids": -6.3, + "mids": -48.5, + "subBass": -69.4, + "t": 3.12, + "upperMids": -81.6 + }, + { + "brilliance": -82.9, + "highs": -82.4, + "lowBass": -5.8, + "lowMids": -6.3, + "mids": -48.4, + "subBass": -68.8, + "t": 3.17, + "upperMids": -81.5 + }, + { + "brilliance": -83.0, + "highs": -82.4, + "lowBass": -5.6, + "lowMids": -6.3, + "mids": -48.3, + "subBass": -68.3, + "t": 3.22, + "upperMids": -81.4 + }, + { + "brilliance": -82.7, + "highs": -82.5, + "lowBass": -5.3, + "lowMids": -6.2, + "mids": -48.3, + "subBass": -66.8, + "t": 3.26, + "upperMids": -81.3 + }, + { + "brilliance": -82.6, + "highs": -82.0, + "lowBass": -4.9, + "lowMids": -6.2, + "mids": -48.6, + "subBass": -67.9, + "t": 3.31, + "upperMids": -81.5 + }, + { + "brilliance": -82.6, + "highs": -82.1, + "lowBass": -4.6, + "lowMids": -6.1, + "mids": -48.7, + "subBass": -65.6, + "t": 3.36, + "upperMids": -81.3 + }, + { + "brilliance": -82.7, + "highs": -82.1, + "lowBass": -4.4, + "lowMids": -6.1, + "mids": -48.5, + "subBass": -65.4, + "t": 3.4, + "upperMids": -81.3 + }, + { + "brilliance": -82.6, + "highs": -82.1, + "lowBass": -4.2, + "lowMids": -6.0, + "mids": -48.5, + "subBass": -65.2, + "t": 3.45, + "upperMids": -81.4 + }, + { + "brilliance": -82.6, + "highs": -82.0, + "lowBass": -4.2, + "lowMids": -6.1, + "mids": -48.7, + "subBass": -64.5, + "t": 3.49, + "upperMids": -81.3 + }, + { + "brilliance": -82.8, + "highs": -82.3, + "lowBass": -4.3, + "lowMids": -6.1, + "mids": -48.7, + "subBass": -66.9, + "t": 3.54, + "upperMids": -81.2 + }, + { + "brilliance": -82.9, + "highs": -82.4, + "lowBass": -4.6, + "lowMids": -6.2, + "mids": -48.4, + "subBass": -65.0, + "t": 3.59, + "upperMids": -81.5 + }, + { + "brilliance": -82.5, + "highs": -82.5, + "lowBass": -4.9, + "lowMids": -6.2, + "mids": -48.4, + "subBass": -66.8, + "t": 3.63, + "upperMids": -81.4 + }, + { + "brilliance": -82.7, + "highs": -82.5, + "lowBass": -5.3, + "lowMids": -6.3, + "mids": -48.6, + "subBass": -67.3, + "t": 3.68, + "upperMids": -81.6 + }, + { + "brilliance": -82.8, + "highs": -82.7, + "lowBass": -5.6, + "lowMids": -6.4, + "mids": -48.8, + "subBass": -68.1, + "t": 3.73, + "upperMids": -81.6 + }, + { + "brilliance": -82.7, + "highs": -82.8, + "lowBass": -5.9, + "lowMids": -6.5, + "mids": -48.8, + "subBass": -70.1, + "t": 3.77, + "upperMids": -81.5 + }, + { + "brilliance": -82.8, + "highs": -82.6, + "lowBass": -6.0, + "lowMids": -6.5, + "mids": -48.7, + "subBass": -68.0, + "t": 3.82, + "upperMids": -81.7 + }, + { + "brilliance": -82.9, + "highs": -82.7, + "lowBass": -6.0, + "lowMids": -6.5, + "mids": -48.7, + "subBass": -69.7, + "t": 3.87, + "upperMids": -81.6 + }, + { + "brilliance": -83.0, + "highs": -82.8, + "lowBass": -5.8, + "lowMids": -6.5, + "mids": -48.6, + "subBass": -67.3, + "t": 3.91, + "upperMids": -81.7 + }, + { + "brilliance": -82.8, + "highs": -82.6, + "lowBass": -5.5, + "lowMids": -6.4, + "mids": -48.7, + "subBass": -69.1, + "t": 3.96, + "upperMids": -81.3 + }, + { + "brilliance": -77.5, + "highs": -82.4, + "lowBass": -5.2, + "lowMids": -6.4, + "mids": -48.8, + "subBass": -65.7, + "t": 4.01, + "upperMids": -82.0 + }, + { + "brilliance": -75.6, + "highs": -82.7, + "lowBass": -4.8, + "lowMids": -6.3, + "mids": -48.6, + "subBass": -67.2, + "t": 4.05, + "upperMids": -82.8 + }, + { + "brilliance": -75.5, + "highs": -82.1, + "lowBass": -4.6, + "lowMids": -6.3, + "mids": -48.6, + "subBass": -64.8, + "t": 4.1, + "upperMids": -82.8 + }, + { + "brilliance": -75.7, + "highs": -82.1, + "lowBass": -4.5, + "lowMids": -6.3, + "mids": -49.0, + "subBass": -66.0, + "t": 4.14, + "upperMids": -83.0 + }, + { + "brilliance": -75.7, + "highs": -82.7, + "lowBass": -4.5, + "lowMids": -6.3, + "mids": -49.1, + "subBass": -66.5, + "t": 4.19, + "upperMids": -82.7 + }, + { + "brilliance": -75.6, + "highs": -81.9, + "lowBass": -4.6, + "lowMids": -6.3, + "mids": -48.8, + "subBass": -65.1, + "t": 4.24, + "upperMids": -83.2 + }, + { + "brilliance": -75.9, + "highs": -82.3, + "lowBass": -4.8, + "lowMids": -6.4, + "mids": -48.6, + "subBass": -66.6, + "t": 4.28, + "upperMids": -82.7 + }, + { + "brilliance": -75.9, + "highs": -82.8, + "lowBass": -5.1, + "lowMids": -6.5, + "mids": -48.8, + "subBass": -66.2, + "t": 4.33, + "upperMids": -83.0 + }, + { + "brilliance": -76.1, + "highs": -82.5, + "lowBass": -5.5, + "lowMids": -6.6, + "mids": -48.9, + "subBass": -67.5, + "t": 4.38, + "upperMids": -83.3 + }, + { + "brilliance": -76.0, + "highs": -82.9, + "lowBass": -5.9, + "lowMids": -6.6, + "mids": -48.8, + "subBass": -68.8, + "t": 4.42, + "upperMids": -82.8 + }, + { + "brilliance": -76.1, + "highs": -82.9, + "lowBass": -6.1, + "lowMids": -6.7, + "mids": -48.9, + "subBass": -68.0, + "t": 4.47, + "upperMids": -83.3 + }, + { + "brilliance": -76.4, + "highs": -82.7, + "lowBass": -6.3, + "lowMids": -6.7, + "mids": -49.0, + "subBass": -69.3, + "t": 4.52, + "upperMids": -83.1 + }, + { + "brilliance": -76.3, + "highs": -83.1, + "lowBass": -6.2, + "lowMids": -6.7, + "mids": -49.0, + "subBass": -69.0, + "t": 4.56, + "upperMids": -83.3 + }, + { + "brilliance": -76.3, + "highs": -82.6, + "lowBass": -6.0, + "lowMids": -6.7, + "mids": -49.0, + "subBass": -69.4, + "t": 4.61, + "upperMids": -83.3 + }, + { + "brilliance": -76.2, + "highs": -82.9, + "lowBass": -5.7, + "lowMids": -6.7, + "mids": -49.0, + "subBass": -67.6, + "t": 4.66, + "upperMids": -83.4 + }, + { + "brilliance": -76.1, + "highs": -82.8, + "lowBass": -5.4, + "lowMids": -6.6, + "mids": -48.8, + "subBass": -67.6, + "t": 4.7, + "upperMids": -83.2 + }, + { + "brilliance": -76.0, + "highs": -82.3, + "lowBass": -5.1, + "lowMids": -6.6, + "mids": -48.8, + "subBass": -66.0, + "t": 4.75, + "upperMids": -83.2 + }, + { + "brilliance": -75.8, + "highs": -82.7, + "lowBass": -4.8, + "lowMids": -6.5, + "mids": -49.1, + "subBass": -66.3, + "t": 4.79, + "upperMids": -82.7 + }, + { + "brilliance": -75.9, + "highs": -82.8, + "lowBass": -4.7, + "lowMids": -6.5, + "mids": -49.3, + "subBass": -66.7, + "t": 4.84, + "upperMids": -82.9 + }, + { + "brilliance": -75.8, + "highs": -82.2, + "lowBass": -4.7, + "lowMids": -6.5, + "mids": -49.0, + "subBass": -64.4, + "t": 4.89, + "upperMids": -82.9 + }, + { + "brilliance": -75.9, + "highs": -82.9, + "lowBass": -4.8, + "lowMids": -6.6, + "mids": -48.9, + "subBass": -67.1, + "t": 4.93, + "upperMids": -82.7 + }, + { + "brilliance": -75.5, + "highs": -82.5, + "lowBass": -5.1, + "lowMids": -6.6, + "mids": -49.2, + "subBass": -65.0, + "t": 4.98, + "upperMids": -83.1 + }, + { + "brilliance": -75.4, + "highs": -82.2, + "lowBass": -5.4, + "lowMids": -6.7, + "mids": -49.2, + "subBass": -68.2, + "t": 5.03, + "upperMids": -83.1 + }, + { + "brilliance": -75.4, + "highs": -82.9, + "lowBass": -5.7, + "lowMids": -6.8, + "mids": -49.0, + "subBass": -67.4, + "t": 5.07, + "upperMids": -82.9 + }, + { + "brilliance": -75.6, + "highs": -82.3, + "lowBass": -6.1, + "lowMids": -6.9, + "mids": -48.9, + "subBass": -68.6, + "t": 5.12, + "upperMids": -83.4 + }, + { + "brilliance": -75.7, + "highs": -82.6, + "lowBass": -6.4, + "lowMids": -6.9, + "mids": -49.1, + "subBass": -69.1, + "t": 5.17, + "upperMids": -83.2 + }, + { + "brilliance": -75.9, + "highs": -82.8, + "lowBass": -6.5, + "lowMids": -7.0, + "mids": -49.2, + "subBass": -70.0, + "t": 5.21, + "upperMids": -83.2 + }, + { + "brilliance": -75.8, + "highs": -82.4, + "lowBass": -6.5, + "lowMids": -7.0, + "mids": -49.3, + "subBass": -70.4, + "t": 5.26, + "upperMids": -83.5 + }, + { + "brilliance": -75.7, + "highs": -82.8, + "lowBass": -6.3, + "lowMids": -7.0, + "mids": -49.3, + "subBass": -68.4, + "t": 5.31, + "upperMids": -83.2 + }, + { + "brilliance": -75.6, + "highs": -82.4, + "lowBass": -6.0, + "lowMids": -6.9, + "mids": -49.1, + "subBass": -68.9, + "t": 5.35, + "upperMids": -83.4 + }, + { + "brilliance": -75.6, + "highs": -82.3, + "lowBass": -5.6, + "lowMids": -6.9, + "mids": -49.0, + "subBass": -66.1, + "t": 5.4, + "upperMids": -83.2 + }, + { + "brilliance": -75.5, + "highs": -82.5, + "lowBass": -5.3, + "lowMids": -6.8, + "mids": -49.2, + "subBass": -67.9, + "t": 5.45, + "upperMids": -83.2 + }, + { + "brilliance": -75.3, + "highs": -82.1, + "lowBass": -5.1, + "lowMids": -6.8, + "mids": -49.3, + "subBass": -65.8, + "t": 5.49, + "upperMids": -83.1 + }, + { + "brilliance": -75.3, + "highs": -82.2, + "lowBass": -5.0, + "lowMids": -6.8, + "mids": -49.2, + "subBass": -65.5, + "t": 5.54, + "upperMids": -83.0 + }, + { + "brilliance": -75.3, + "highs": -82.3, + "lowBass": -5.0, + "lowMids": -6.8, + "mids": -49.2, + "subBass": -66.2, + "t": 5.58, + "upperMids": -83.2 + }, + { + "brilliance": -75.4, + "highs": -82.3, + "lowBass": -5.1, + "lowMids": -6.8, + "mids": -49.5, + "subBass": -65.6, + "t": 5.63, + "upperMids": -83.3 + }, + { + "brilliance": -75.6, + "highs": -82.7, + "lowBass": -5.3, + "lowMids": -6.9, + "mids": -49.6, + "subBass": -67.8, + "t": 5.68, + "upperMids": -82.8 + }, + { + "brilliance": -75.6, + "highs": -82.4, + "lowBass": -5.6, + "lowMids": -7.0, + "mids": -49.3, + "subBass": -67.2, + "t": 5.72, + "upperMids": -83.3 + }, + { + "brilliance": -75.8, + "highs": -82.6, + "lowBass": -6.0, + "lowMids": -7.0, + "mids": -49.2, + "subBass": -67.4, + "t": 5.77, + "upperMids": -83.5 + }, + { + "brilliance": -75.8, + "highs": -83.1, + "lowBass": -6.4, + "lowMids": -7.1, + "mids": -49.3, + "subBass": -69.3, + "t": 5.82, + "upperMids": -83.3 + }, + { + "brilliance": -76.0, + "highs": -82.7, + "lowBass": -6.6, + "lowMids": -7.2, + "mids": -49.4, + "subBass": -69.0, + "t": 5.86, + "upperMids": -83.2 + }, + { + "brilliance": -75.9, + "highs": -82.7, + "lowBass": -6.8, + "lowMids": -7.2, + "mids": -49.4, + "subBass": -71.1, + "t": 5.91, + "upperMids": -83.0 + }, + { + "brilliance": -74.9, + "highs": -81.1, + "lowBass": -6.7, + "lowMids": -7.2, + "mids": -49.5, + "subBass": -68.6, + "t": 5.96, + "upperMids": -81.4 + }, + { + "brilliance": -43.9, + "highs": -42.1, + "lowBass": -10.3, + "lowMids": -11.1, + "mids": -27.2, + "subBass": -29.3, + "t": 6.0, + "upperMids": -37.5 + } + ], + "spectralDetail": { + "barkBands": [ + -45.1593, + -42.6317, + -42.3887, + -36.5489, + -4.5237, + -9.6341, + -14.0747, + -47.1577, + -50.8125, + -51.0924, + -51.6736, + -53.7595, + -53.7207, + -53.8242, + -58.3335, + -56.6625, + -60.8894, + -61.7893, + -63.6887, + -64.6266, + -65.5867, + -66.9544, + -68.0964, + -68.5598 + ], + "chroma": [ + 0.9735, + 0.127, + 0.252, + 0.2546, + 0.4071, + 0.0896, + 0.0834, + 0.512, + 0.1245, + 0.3609, + 0.1488, + 0.2321 + ], + "erbBands": [ + -22.3962, + -13.5438, + -2.1152, + 14.7352, + 29.8554, + 31.3184, + 30.5029, + 27.1155, + 28.7298, + 18.4024, + 4.4996, + -3.6062, + -5.4928, + -5.2696, + -6.2035, + -7.9285, + -6.8539, + -6.5704, + -10.0156, + -9.673, + -10.4773, + -13.2478, + -14.3336, + -16.0312, + -17.3125, + -17.9026, + -19.0185, + -20.258, + -21.3265, + -22.3991, + -23.6038, + -24.819, + -26.2045, + -27.6921, + -29.5007, + -31.6452, + -34.5713, + -40.1409, + -36.0158, + -32.5441 + ], + "mfcc": [ + -1005.3185, + 246.528, + 138.3856, + 83.2849, + 11.9793, + -44.7499, + -79.8733, + -86.5884, + -75.3388, + -52.4688, + -30.0446, + -11.9677, + -4.1607 + ], + "spectralBandwidth": 616.6, + "spectralCentroid": 279.8, + "spectralContrast": [ + -0.0717, + -0.1777, + -0.8899, + -0.9187, + -0.8591, + -0.8709 + ], + "spectralFlatness": 0.005941, + "spectralRolloff": 336.6, + "spectralValley": [ + -7.5314, + -8.5881, + -11.2139, + -12.0168, + -12.4845, + -13.1331 + ] + }, + "stemAnalysis": null, + "stereoDetail": { + "bandCorrelations": { + "brilliance": 1.0, + "highs": 1.0, + "lowBass": 1.0, + "lowMids": 1.0, + "mids": 1.0, + "subBass": 1.0, + "upperMids": 1.0 + }, + "correlationCurve": [ + { + "full": 1.0, + "sub": 1.0, + "t": 0.5 + }, + { + "full": 1.0, + "sub": 1.0, + "t": 1.5 + }, + { + "full": 1.0, + "sub": 1.0, + "t": 2.5 + }, + { + "full": 1.0, + "sub": 1.0, + "t": 3.5 + }, + { + "full": 1.0, + "sub": 1.0, + "t": 4.5 + }, + { + "full": 1.0, + "sub": 1.0, + "t": 5.5 + } + ], + "stereoCorrelation": 1.0, + "stereoWidth": 0.0, + "subBassCorrelation": 1.0, + "subBassMono": true + }, + "structure": { + "segmentCount": 1, + "segments": [ + { + "end": 6.0, + "index": 0, + "start": 0.0 + } + ] + }, + "supersawDetail": { + "avgDetuneCents": 29.3, + "confidence": 0.84, + "isSupersaw": true, + "spectralComplexity": 11.0, + "voiceCount": 11 + }, + "synthesisCharacter": { + "inharmonicity": 0.1616, + "oddToEvenRatio": 0.7756 + }, + "textureCharacter": { + "highBandFlatness": 0.5903, + "inharmonicity": 0.1616, + "lowBandFlatness": 0.0591, + "midBandFlatness": 0.0263, + "textureScore": 0.3527 + }, + "timeSignature": "4/4", + "timeSignatureConfidence": 0.0, + "timeSignatureSource": "assumed_four_four", + "transcriptionDetail": null, + "transientDensityDetail": { + "brilliance": { + "eventCount": 44, + "meanOnsetStrength": 2.003, + "onsetRatePerSecond": 7.33, + "peakOnsetStrength": 5.462 + }, + "highs": { + "eventCount": 40, + "meanOnsetStrength": 2.936, + "onsetRatePerSecond": 6.67, + "peakOnsetStrength": 7.831 + }, + "lowBass": { + "eventCount": 105, + "meanOnsetStrength": 0.16, + "onsetRatePerSecond": 17.5, + "peakOnsetStrength": 0.305 + }, + "lowMids": { + "eventCount": 111, + "meanOnsetStrength": 0.398, + "onsetRatePerSecond": 18.5, + "peakOnsetStrength": 0.735 + }, + "mids": { + "eventCount": 36, + "meanOnsetStrength": 4.971, + "onsetRatePerSecond": 6.0, + "peakOnsetStrength": 10.989 + }, + "subBass": { + "eventCount": 39, + "meanOnsetStrength": 0.124, + "onsetRatePerSecond": 6.5, + "peakOnsetStrength": 0.263 + }, + "upperMids": { + "eventCount": 38, + "meanOnsetStrength": 5.358, + "onsetRatePerSecond": 6.33, + "peakOnsetStrength": 12.293 + } + }, + "truePeak": 1.0, + "tuningCents": -30.0, + "tuningFrequency": 432.44, + "vocalDetail": { + "confidence": 0.64, + "formantStrength": 0.06, + "hasVocals": false, + "mfccLikelihood": 0.91, + "stemEnergyRatio": null, + "stemOtherCorrelation": null, + "vocalEnergyRatio": 1.0 + } +} diff --git a/apps/backend/tests/test_phase1_golden.py b/apps/backend/tests/test_phase1_golden.py new file mode 100644 index 00000000..8ca4f5ad --- /dev/null +++ b/apps/backend/tests/test_phase1_golden.py @@ -0,0 +1,367 @@ +"""Golden-snapshot regression gate for default-mode ``analyze.py`` output. + +This runs ``analyze.py`` on a fixed, deterministic procedural fixture and asserts the +emitted JSON matches a committed golden snapshot within tolerance. It is a safety net for +changes to ``analyze.py`` / the DSP feature modules: any unintended drift in any output +field surfaces as a failed assertion that names the drifted path. + +Determinism: the default analyze path (no ``--separate`` / ``--transcribe``) is pure-DSP +and contains no RNG / ML / timestamps; output floats are explicitly rounded. Output was +confirmed byte-identical across 5 repeated local runs, so the tolerance below +(ABS_TOL / REL_TOL) is insurance against cross-environment float jitter (e.g. multi-thread +reduction order on the CI runner), not run-to-run noise. One field is excluded: +``melodyDetail.midiFile`` is the path of a transient MIDI artifact analyze writes next to +the input, so it varies per run/location and carries no measurement. Exclude a field by +adding its exact dotted path to ``EXCLUDED_PATHS`` rather than loosening the global +tolerance; excluded paths are stored in the golden as a placeholder so the key-set check +still holds and the comparator skips their values. + +Regenerating the golden after an *intentional* output change (one command, from +``apps/backend/``): + + UPDATE_PHASE1_GOLDEN=1 ./venv/bin/python -m unittest tests.test_phase1_golden + +The main gate test never writes; only ``test_zzz_regenerate_golden`` writes, and only when +that env var is set. +""" + +import copy +import json +import math +import os +import subprocess +import sys +import tempfile +import unittest +import wave +from pathlib import Path + +import numpy as np + + +_BACKEND_ROOT = Path(__file__).resolve().parent.parent +_ANALYZE_PY = _BACKEND_ROOT / "analyze.py" +_GOLDEN_PATH = Path(__file__).resolve().parent / "fixtures" / "golden" / "phase1_default.json" +_UPDATE_ENV = "UPDATE_PHASE1_GOLDEN" + +# Tolerance for numeric leaves: equal iff abs(a-g) <= max(ABS_TOL, REL_TOL*abs(g)). +ABS_TOL = 1e-3 +REL_TOL = 1e-3 + +# Cap on reported mismatches so a large-array drift can't produce a multi-thousand-line +# failure (and so the comparator stops descending once the point is made). +MAX_REPORTED = 40 + +# Exact dotted paths to skip during comparison. Populated only from the determinism +# calibration when a field is shown to vary run-to-run / per-environment. Excluded paths +# are stored in the golden as the placeholder below (keeping the key-set check intact). +# melodyDetail.midiFile -> absolute path of a transient MIDI artifact (per-run/location). +EXCLUDED_PATHS: set[str] = {"melodyDetail.midiFile"} +_EXCLUDED_PLACEHOLDER = "" + + +def _write_golden_fixture(path: Path, sample_rate: int = 44_100, duration_seconds: float = 6.0) -> None: + """Deterministic A-minor harmonic bed (copied from test_analyze._write_key_fixture). + + Inlined rather than imported to keep this module self-contained and free of the + cross-test-module import-path fragility the rest of the suite avoids. No RNG. + """ + total_samples = int(sample_rate * duration_seconds) + time_axis = np.arange(total_samples, dtype=np.float32) / sample_rate + signal = ( + 0.45 * np.sin(2 * np.pi * 220.0 * time_axis) + + 0.3 * np.sin(2 * np.pi * 261.63 * time_axis) + + 0.3 * np.sin(2 * np.pi * 329.63 * time_axis) + + 0.18 * np.sin(2 * np.pi * 440.0 * time_axis) + ).astype(np.float32) + envelope = np.linspace(1.0, 0.8, total_samples, dtype=np.float32) + + stereo = np.stack([signal * envelope, signal * envelope], axis=1) + pcm = np.clip(stereo, -1.0, 1.0) + pcm = (pcm * 32767.0).astype(np.int16) + + with wave.open(str(path), "wb") as wav_file: + wav_file.setnchannels(2) + wav_file.setsampwidth(2) + wav_file.setframerate(sample_rate) + wav_file.writeframes(pcm.tobytes()) + + +def _run_analyze(analyze_path: Path, fixture_path: Path, extra_args: list[str]) -> tuple[str, str]: + """Run analyze.py and return (stdout, stderr). Raises AssertionError on non-zero exit. + + Inlined from test_analyze._run_analyze for self-containment. + """ + try: + completed = subprocess.run( + [sys.executable, str(analyze_path), str(fixture_path), "--yes"] + extra_args, + cwd=analyze_path.parent, + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as error: + raise AssertionError( + f"analyze.py {' '.join(extra_args)} failed.\n" + f"stdout:\n{error.stdout[:800]}\n" + f"stderr:\n{error.stderr[:800]}" + ) from error + return completed.stdout, completed.stderr + + +def _join(path: str, key: str) -> str: + return key if not path else f"{path}.{key}" + + +def _fmt(value: object) -> str: + text = repr(value) + return text if len(text) <= 80 else text[:77] + "..." + + +def _numbers_equal(golden: float, actual: float) -> bool: + g = float(golden) + a = float(actual) + if math.isnan(g) or math.isnan(a): + return math.isnan(g) and math.isnan(a) + if math.isinf(g) or math.isinf(a): + return g == a # both +inf or both -inf + return abs(a - g) <= max(ABS_TOL, REL_TOL * abs(g)) + + +def _diff(golden: object, actual: object, path: str, out: list[str]) -> None: + """Recursively collect mismatches between golden and actual into ``out`` (capped).""" + if len(out) >= MAX_REPORTED or path in EXCLUDED_PATHS: + return + + # None first. + if golden is None or actual is None: + if not (golden is None and actual is None): + out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") + return + + # bool before number: in Python isinstance(True, int) is True and True == 1, so a + # number branch would silently accept a bool/int swap. + if isinstance(golden, bool) or isinstance(actual, bool): + if not (isinstance(golden, bool) and isinstance(actual, bool) and golden == actual): + out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") + return + + if isinstance(golden, str) or isinstance(actual, str): + if not (isinstance(golden, str) and isinstance(actual, str) and golden == actual): + out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") + return + + if isinstance(golden, (int, float)) or isinstance(actual, (int, float)): + if not ( + isinstance(golden, (int, float)) + and isinstance(actual, (int, float)) + and _numbers_equal(golden, actual) + ): + out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") + return + + if isinstance(golden, dict) or isinstance(actual, dict): + if not (isinstance(golden, dict) and isinstance(actual, dict)): + out.append(f"{path or ''}: type mismatch dict vs {type(actual).__name__}") + return + golden_keys = set(golden) + actual_keys = set(actual) + for key in sorted(golden_keys - actual_keys): + if len(out) >= MAX_REPORTED: + return + out.append(f"{_join(path, key)}: missing in actual (removed key)") + for key in sorted(actual_keys - golden_keys): + if len(out) >= MAX_REPORTED: + return + out.append(f"{_join(path, key)}: unexpected in actual (added key)") + for key in sorted(golden_keys & actual_keys): + if len(out) >= MAX_REPORTED: + return + _diff(golden[key], actual[key], _join(path, key), out) + return + + if isinstance(golden, list) or isinstance(actual, list): + if not (isinstance(golden, list) and isinstance(actual, list)): + out.append(f"{path or ''}: type mismatch list vs {type(actual).__name__}") + return + if len(golden) != len(actual): + out.append(f"{path or ''}: list length {len(golden)} != {len(actual)}") + return + for index, (gv, av) in enumerate(zip(golden, actual)): + if len(out) >= MAX_REPORTED: + return + _diff(gv, av, f"{path}[{index}]", out) + return + + # Fallback for any non-JSON scalar. + if golden != actual: + out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") + + +def diff(golden: object, actual: object) -> list[str]: + """Return a (capped) list of human-readable mismatch lines; empty means match.""" + out: list[str] = [] + _diff(golden, actual, "", out) + if len(out) >= MAX_REPORTED: + out = out[:MAX_REPORTED] + out.append(f"... (further mismatches truncated at {MAX_REPORTED})") + return out + + +def _with_excluded_placeholders(payload: dict) -> dict: + """Copy ``payload`` with each present EXCLUDED_PATHS leaf set to the placeholder. + + Keeps the excluded key in the golden (so the dict key-set check still passes) while + not committing the volatile value. The comparator skips the value either way. + """ + result = copy.deepcopy(payload) + for dotted in EXCLUDED_PATHS: + parts = dotted.split(".") + node = result + for key in parts[:-1]: + if isinstance(node, dict) and key in node: + node = node[key] + else: + node = None + break + if isinstance(node, dict) and parts[-1] in node: + node[parts[-1]] = _EXCLUDED_PLACEHOLDER + return result + + +class Phase1GoldenRegressionTests(unittest.TestCase): + """Runs analyze.py once (default mode) and compares to the committed golden.""" + + @classmethod + def setUpClass(cls) -> None: + cls._tmp = tempfile.TemporaryDirectory(prefix="phase1_golden_") + cls.fixture_path = Path(cls._tmp.name) / "fixture.wav" + _write_golden_fixture(cls.fixture_path) + stdout, stderr = _run_analyze(_ANALYZE_PY, cls.fixture_path, []) + try: + cls.actual = json.loads(stdout) + except json.JSONDecodeError as error: + raise AssertionError( + "analyze.py did not emit valid JSON for the golden fixture.\n" + f"stderr:\n{stderr[:800]}" + ) from error + + @classmethod + def tearDownClass(cls) -> None: + cls._tmp.cleanup() + + def test_phase1_output_matches_golden(self) -> None: + if os.environ.get(_UPDATE_ENV): + self.skipTest(f"{_UPDATE_ENV} set; golden written by test_zzz_regenerate_golden") + if not _GOLDEN_PATH.exists(): + self.fail( + f"Golden snapshot missing at {_GOLDEN_PATH}.\n" + f"Generate it with: {_UPDATE_ENV}=1 ./venv/bin/python -m unittest tests.test_phase1_golden" + ) + golden = json.loads(_GOLDEN_PATH.read_text()) + mismatches = diff(golden, self.actual) + if mismatches: + self.fail( + "Phase 1 analyze.py output drifted from the golden snapshot.\n" + "If this change is intentional, regenerate with " + f"{_UPDATE_ENV}=1 ./venv/bin/python -m unittest tests.test_phase1_golden\n\n" + + "\n".join(mismatches) + ) + + def test_zzz_regenerate_golden(self) -> None: + if not os.environ.get(_UPDATE_ENV): + self.skipTest(f"set {_UPDATE_ENV}=1 to regenerate the golden snapshot") + _GOLDEN_PATH.parent.mkdir(parents=True, exist_ok=True) + payload = _with_excluded_placeholders(self.actual) + _GOLDEN_PATH.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + self.skipTest(f"regenerated golden snapshot at {_GOLDEN_PATH}") + + +class GoldenComparatorMetaTests(unittest.TestCase): + """Proves the comparator bites (and doesn't over-bite). Pure-Python, no analyze run.""" + + @staticmethod + def _golden() -> dict: + return { + "bpm": 120.0, + "key": "A minor", + "monoCompatible": True, + "stemAnalysis": None, + "spectralBalance": {"subBass": -12.5, "highs": -30.0}, + "beatGrid": [0.5, 1.0, 1.5, 2.0], + } + + def test_numeric_drift_beyond_tolerance_is_reported(self) -> None: + golden = self._golden() + actual = copy.deepcopy(golden) + actual["bpm"] = 120.0 + 10 * max(ABS_TOL, REL_TOL * 120.0) + mismatches = diff(golden, actual) + self.assertTrue(any(m.startswith("bpm:") for m in mismatches), mismatches) + + def test_within_tolerance_nudge_is_silent(self) -> None: + golden = self._golden() + actual = copy.deepcopy(golden) + actual["bpm"] = 120.0 + 0.5 * max(ABS_TOL, REL_TOL * 120.0) + self.assertEqual(diff(golden, actual), []) + + def test_identical_payloads_match(self) -> None: + golden = self._golden() + self.assertEqual(diff(golden, copy.deepcopy(golden)), []) + + def test_added_and_removed_keys_reported(self) -> None: + golden = self._golden() + actual = copy.deepcopy(golden) + actual["newField"] = 1.0 + del actual["key"] + mismatches = diff(golden, actual) + self.assertTrue(any("newField" in m and "added" in m for m in mismatches), mismatches) + self.assertTrue(any("key" in m and "removed" in m for m in mismatches), mismatches) + + def test_list_length_drift_reported(self) -> None: + golden = self._golden() + actual = copy.deepcopy(golden) + actual["beatGrid"] = [0.5, 1.0] + mismatches = diff(golden, actual) + self.assertTrue(any(m.startswith("beatGrid:") and "length" in m for m in mismatches), mismatches) + + def test_nan_inf_bool_branches(self) -> None: + nan = float("nan") + inf = float("inf") + self.assertEqual(diff({"x": nan}, {"x": nan}), []) # both NaN -> equal + self.assertEqual(diff({"x": inf}, {"x": inf}), []) # both +inf -> equal + self.assertNotEqual(diff({"x": nan}, {"x": 1.0}), []) # NaN vs number + self.assertNotEqual(diff({"x": inf}, {"x": -inf}), []) # +inf vs -inf + self.assertNotEqual(diff({"x": True}, {"x": 1}), []) # bool vs int + self.assertNotEqual(diff({"x": False}, {"x": 0.0}), []) # bool vs float + + def test_type_mismatch_reported(self) -> None: + self.assertNotEqual(diff({"x": {"a": 1}}, {"x": [1]}), []) # dict vs list + self.assertNotEqual(diff({"x": "s"}, {"x": 1.0}), []) # str vs number + self.assertNotEqual(diff({"x": 1.0}, {"x": None}), []) # number vs None + + def test_excluded_path_is_ignored(self) -> None: + self.assertIn("melodyDetail.midiFile", EXCLUDED_PATHS) + golden = {"melodyDetail": {"midiFile": _EXCLUDED_PLACEHOLDER, "confidence": 0.5}} + actual = {"melodyDetail": {"midiFile": "/tmp/whatever/x_melody.mid", "confidence": 0.5}} + self.assertEqual(diff(golden, actual), []) + # A non-excluded sibling still bites. + actual["melodyDetail"]["confidence"] = 0.9 + self.assertNotEqual(diff(golden, actual), []) + + def test_with_excluded_placeholders_sets_leaf(self) -> None: + payload = {"melodyDetail": {"midiFile": "/tmp/abc/x_melody.mid", "confidence": 0.5}} + result = _with_excluded_placeholders(payload) + self.assertEqual(result["melodyDetail"]["midiFile"], _EXCLUDED_PLACEHOLDER) + self.assertEqual(result["melodyDetail"]["confidence"], 0.5) + self.assertEqual(payload["melodyDetail"]["midiFile"], "/tmp/abc/x_melody.mid") # unmutated + + def test_cap_truncation(self) -> None: + golden = {f"k{i:03d}": float(i) for i in range(200)} + actual = {key: value + 5.0 for key, value in golden.items()} + mismatches = diff(golden, actual) + self.assertLessEqual(len(mismatches), MAX_REPORTED + 1) + self.assertTrue(any("truncated" in m for m in mismatches), mismatches) + + +if __name__ == "__main__": + unittest.main() From 35d608f127032ff69abdc7a8640536fcd8b46d59 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 02:12:19 +0000 Subject: [PATCH 3/5] test(backend): make golden gate tolerant of cross-env rounding flips Backend CI failed on the new gate. analyze.py rounds every output float (1-4 decimals); a different BLAS/thread order on the runner can push a raw value across a rounding boundary, flipping the last digit by a whole step (e.g. 135.4 -> 135.5). The 1e-3 tolerance was far tighter than a 0.1 step, so any one of ~1633 boundary-prone leaves was enough to fail in CI while passing locally (same environment, identical rounding). Widen the numeric comparison to also absorb one rounding step at each field's own decimal precision, plus a 1e-9 float-safety epsilon for clean decimal values at the step boundary. Stays portable across environments while still biting any change larger than a field's reported resolution and every string/enum/key/list-length change. Verified by a cross-environment simulation: +/-1 step on all 2154 numeric leaves -> 0 mismatches; a multi-step change still bites. Adds a meta-test pinning the behavior. https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8 --- apps/backend/tests/test_phase1_golden.py | 47 ++++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/backend/tests/test_phase1_golden.py b/apps/backend/tests/test_phase1_golden.py index 8ca4f5ad..9b60ec4f 100644 --- a/apps/backend/tests/test_phase1_golden.py +++ b/apps/backend/tests/test_phase1_golden.py @@ -5,11 +5,15 @@ changes to ``analyze.py`` / the DSP feature modules: any unintended drift in any output field surfaces as a failed assertion that names the drifted path. -Determinism: the default analyze path (no ``--separate`` / ``--transcribe``) is pure-DSP -and contains no RNG / ML / timestamps; output floats are explicitly rounded. Output was -confirmed byte-identical across 5 repeated local runs, so the tolerance below -(ABS_TOL / REL_TOL) is insurance against cross-environment float jitter (e.g. multi-thread -reduction order on the CI runner), not run-to-run noise. One field is excluded: +Determinism vs portability: the default analyze path (no ``--separate`` / ``--transcribe``) +is pure-DSP and contains no RNG / ML / timestamps; output was byte-identical across 5 +repeated runs *in one environment*. Across environments it is not bit-identical: analyze.py +rounds every output float (1-4 decimals), and a different BLAS/thread order on the CI runner +can push a raw value across a rounding boundary, flipping the last digit by a whole step. +The comparator therefore tolerates one rounding step at each field's own precision (see +``_numbers_equal``) plus proportional/absolute float slack -- enough to be portable, while +still biting any change larger than a field's reported resolution and every +string/enum/key/list-length change. One field is excluded: ``melodyDetail.midiFile`` is the path of a transient MIDI artifact analyze writes next to the input, so it varies per run/location and carries no measurement. Exclude a field by adding its exact dotted path to ``EXCLUDED_PATHS`` rather than loosening the global @@ -44,7 +48,14 @@ _GOLDEN_PATH = Path(__file__).resolve().parent / "fixtures" / "golden" / "phase1_default.json" _UPDATE_ENV = "UPDATE_PHASE1_GOLDEN" -# Tolerance for numeric leaves: equal iff abs(a-g) <= max(ABS_TOL, REL_TOL*abs(g)). +# Numeric tolerance. Two numbers are equal iff +# abs(a-g) <= max(ABS_TOL, REL_TOL*abs(g), one rounding step at g's decimal precision). +# The rounding-step term is the load-bearing one for cross-environment portability: +# analyze.py rounds every output float (1-4 decimals), and a different BLAS/thread order on +# another machine can nudge a raw value across a rounding boundary, flipping the last digit +# by a whole step (e.g. 135.4 -> 135.5). Absorbing one step at each field's own precision +# tolerates that while still biting changes larger than a field's reported resolution, plus +# every string/enum/key/list-length change (which are environment-stable). ABS_TOL = 1e-3 REL_TOL = 1e-3 @@ -118,6 +129,14 @@ def _fmt(value: object) -> str: return text if len(text) <= 80 else text[:77] + "..." +def _decimals(value: float) -> int: + """Decimal places in the shortest round-trip repr (0 for ints / scientific notation).""" + text = repr(float(value)) + if "e" in text or "E" in text: + return 0 + return len(text.split(".")[1]) if "." in text else 0 + + def _numbers_equal(golden: float, actual: float) -> bool: g = float(golden) a = float(actual) @@ -125,7 +144,10 @@ def _numbers_equal(golden: float, actual: float) -> bool: return math.isnan(g) and math.isnan(a) if math.isinf(g) or math.isinf(a): return g == a # both +inf or both -inf - return abs(a - g) <= max(ABS_TOL, REL_TOL * abs(g)) + rounding_step = 10.0 ** (-_decimals(g)) + # +1e-9 absorbs binary-float error when comparing clean decimal values at the step + # boundary (e.g. 0.90 - 0.89 == 0.010000000000000009, just over a 0.01 step). + return abs(a - g) <= max(ABS_TOL, REL_TOL * abs(g), rounding_step) + 1e-9 def _diff(golden: object, actual: object, path: str, out: list[str]) -> None: @@ -339,6 +361,17 @@ def test_type_mismatch_reported(self) -> None: self.assertNotEqual(diff({"x": "s"}, {"x": 1.0}), []) # str vs number self.assertNotEqual(diff({"x": 1.0}, {"x": None}), []) # number vs None + def test_rounding_boundary_flip_is_tolerated_but_real_change_bites(self) -> None: + # 1-decimal field (step 0.1): a one-step cross-env flip is tolerated, >1 step bites. + self.assertEqual(diff({"x": 135.4}, {"x": 135.5}), []) + self.assertNotEqual(diff({"x": 135.4}, {"x": 135.6}), []) + # 2-decimal field (step 0.01): one-step tolerated, larger bites. + self.assertEqual(diff({"x": 0.89}, {"x": 0.90}), []) + self.assertNotEqual(diff({"x": 0.89}, {"x": 0.92}), []) + # Small-magnitude 1-decimal value (the case the tight tolerance used to fail on). + self.assertEqual(diff({"x": 0.2}, {"x": 0.3}), []) + self.assertNotEqual(diff({"x": 0.2}, {"x": 0.5}), []) + def test_excluded_path_is_ignored(self) -> None: self.assertIn("melodyDetail.midiFile", EXCLUDED_PATHS) golden = {"melodyDetail": {"midiFile": _EXCLUDED_PLACEHOLDER, "confidence": 0.5}} From d22e87cb6087ab1f572ab4916ffc1016ac4d6fd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 02:13:55 +0000 Subject: [PATCH 4/5] docs(test): clarify fast-mode schema test asserts the shared key set The fast-mode test's docstring/message said fast emits "the same top-level key set as full mode", but full mode emits 6 more detail-only fields (keyProfile, tuningFrequency, tuningCents, lufsMomentaryMax, lufsShortTermMax, pitchDetail). The assertion itself (== EXPECTED_TOP_LEVEL_KEYS, the shared contract) is correct; only the wording was misleading. Surfaced by the new Phase 1 golden snapshot in this PR's review. No behavior change. https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8 --- apps/backend/tests/test_analyze.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/backend/tests/test_analyze.py b/apps/backend/tests/test_analyze.py index 69f39cde..1dc3e4c6 100644 --- a/apps/backend/tests/test_analyze.py +++ b/apps/backend/tests/test_analyze.py @@ -340,12 +340,17 @@ def tearDownClass(cls) -> None: cls.temp_dir.cleanup() def test_output_schema_matches_full_mode(self) -> None: - """Fast mode must emit the same top-level key set as full mode.""" + """Fast mode must emit exactly the shared top-level key set (EXPECTED_TOP_LEVEL_KEYS). + + Full mode emits these keys *plus* a few detail-only fields (keyProfile, + tuningFrequency, tuningCents, lufsMomentaryMax, lufsShortTermMax, pitchDetail), so + this asserts the shared contract — not a byte-for-byte match with full mode. + """ self.assertEqual( set(self.payload.keys()), EXPECTED_TOP_LEVEL_KEYS, - "Fast mode output schema diverged from full mode. Update EXPECTED_TOP_LEVEL_KEYS " - "if the output contract changed intentionally.", + "Fast mode output schema diverged from the shared key set. Update " + "EXPECTED_TOP_LEVEL_KEYS if the output contract changed intentionally.", ) def test_core_fields_are_populated(self) -> None: From a1c650330f38752a3b32a7535b3d5ed12233e1de Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 03:18:18 +0000 Subject: [PATCH 5/5] test(backend): redesign Phase 1 gate to be cross-environment portable The full-output golden snapshot was not reproducible on CI. The fixture WAV is bit-identical across machines, but the runner's CPU/BLAS/FFT order yields last-bit-different floats, and many fine-grained fields (per-beat accent patterns, per-frame spectral series, MFCC/chroma vectors, onset counts) sit on discrete decision boundaries that flip wholesale between machines. No numeric tolerance can absorb that, so the gate failed on CI while passing locally. Redesign (per maintainer decision) to two portable assertions: - contract/structure: exact top-level key set + each field's type category (catches added/removed fields, type changes, a detector returning null); - curated core values: environment-stable, high-value measurements (BPM, key, loudness, true-peak, crest, 7 spectral-balance bands, stereo) compared with the rounding-step tolerance. The curated set was chosen with a local fragility probe (tiny PCM noise): only fields that never flip under perturbation are compared by value. The new gate was validated against noise up to 2 LSB across 9 runs -> 0 mismatches, while real changes still bite (meta-tests). Golden regenerated to the new {topLevelKeys, topLevelTypes, coreValues} form. Full backend suite green (702 ok, 1 skip). https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8 --- .../tests/fixtures/golden/phase1_default.json | 3069 +---------------- apps/backend/tests/test_phase1_golden.py | 417 +-- 2 files changed, 341 insertions(+), 3145 deletions(-) diff --git a/apps/backend/tests/fixtures/golden/phase1_default.json b/apps/backend/tests/fixtures/golden/phase1_default.json index a8b98289..1f520262 100644 --- a/apps/backend/tests/fixtures/golden/phase1_default.json +++ b/apps/backend/tests/fixtures/golden/phase1_default.json @@ -1,2913 +1,164 @@ { - "acidDetail": { - "bassRhythmDensity": 0.2, - "centroidOscillationHz": 4, - "confidence": 0.09, - "isAcid": false, - "resonanceLevel": 0.17 - }, - "arrangementDetail": { - "noveltyCurve": [ - 0.0, - 0.0002, - 0.0012, - 0.0022, - 0.0042, - 0.005, - 0.0043, - 0.0052, - 0.0033, - 0.0025, - 0.0026, - 0.0005, - 0.0004, - 0.0003, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0002, - 0.0002, - 0.0003, - 0.0004, - 0.0002, - 0.0004, - 0.0005, - 0.0004, - 0.001, - 0.0008, - 0.0007, - 0.0008, - 0.0002, - 0.0002, - 0.0001, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0 - ], - "noveltyMean": 0.0059, - "noveltyPeaks": [], - "noveltyStdDev": 0.0692 - }, - "bassDetail": { - "averageDecayMs": 1000, - "fundamentalHz": 120, - "grooveType": "straight", - "swingPercent": 0, - "transientCount": 0, - "transientRatio": 0.2, - "type": "sustained" - }, - "beatsLoudness": { - "accentPattern": [ - 0.9723, - 1.0, - 0.958, - 0.9187 - ], - "beatCount": 13, - "beatLoudnessVariation": 0.1219, - "highBandAccentPattern": [ - 0.9344, - 0.3884, - 1.0, - 0.2675 - ], - "highDominantRatio": 0.0, - "kickDominantRatio": 0.0807, - "lowBandAccentPattern": [ - 0.9687, - 1.0, - 0.9693, - 0.9393 - ], - "meanBeatLoudness": 0.6178, - "midBandAccentPattern": [ - 0.9686, - 1.0, - 0.969, - 0.9392 - ], - "midDominantRatio": 0.0797, - "overallAccentPattern": [ - 0.9723, - 1.0, - 0.958, - 0.9187 - ], - "patternBeatsPerBar": 4 - }, - "bpm": 135.4, - "bpmAgreement": false, - "bpmConfidence": 1.08, - "bpmDoubletime": false, - "bpmPercival": 94.0, - "bpmRawOriginal": 135.4, - "bpmSource": "rhythm_extractor", - "chordDetail": { - "chordChangeCount": 0, - "chordSequence": [ - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "Am", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m", - "F#m" - ], - "chordStrength": 0.7792, - "chordTimeline": [ - { - "confidence": 0.9284, - "endSec": 6.037, - "label": "Am", - "labelLong": "A minor", - "startSec": 0.0 - } - ], - "chordTimelineAgreement": true, - "chordTimelineSource": "librosa_viterbi", - "dominantChords": [ - "Am", - "F#m" - ], - "progression": [ - "Am", - "F#m" - ] - }, - "crestFactor": 7.7, - "danceability": { - "danceability": 0.7086, - "dfa": 1.2499 - }, - "durationSeconds": 6.0, - "dynamicCharacter": { - "attackTimeStdDev": 0.0759, - "dynamicComplexity": 0.4968, - "logAttackTime": -2.9095, - "loudnessDb": -9.8382, - "loudnessVariation": -9.8382, - "spectralFlatness": 0.0059 - }, - "dynamicSpread": 380857.6, - "effectsDetail": { - "gatingDetected": false, - "gatingEventCount": 1, - "gatingRate": null, - "gatingRegularity": 0.0 - }, - "essentiaFeatures": { - "dissonance": 0.0015, - "hfc": 135.3434, - "spectralComplexity": 3.4308, - "zeroCrossingRate": 0.0123 - }, - "genreDetail": { - "confidence": 0.4, - "genre": "ambient-drone", - "genreFamily": "ambient", - "secondaryGenre": "acoustic", - "topScores": [ - { - "genre": "ambient-drone", - "score": 0.6429 - }, - { - "genre": "acoustic", - "score": 0.6378 - }, - { - "genre": "ambient", - "score": 0.524 - }, - { - "genre": "dub-techno", - "score": 0.5079 - }, - { - "genre": "rock", - "score": 0.4341 - } - ] - }, - "grooveDetail": { - "hihatAccent": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - "hihatSwing": 0.0, - "kickAccent": [ - 0.059, - 0.0577, - 0.0562, - 0.0547, - 0.0532, - 0.0516, - 0.05, - 0.0483, - 0.0466, - 0.0451, - 0.0435, - 0.042, - 0.0407 - ], - "kickSwing": 0.0067, - "perDrumSwing": { - "hihat": 0.0, - "kick": 0.0067, - "snare": 0.0067 - } - }, - "hihatDetail": null, - "key": "A Minor", - "keyConfidence": 0.89, - "keyProfile": "edma", - "kickDetail": { - "fundamentalHz": 50.0, - "harmonicRatio": 0.0, - "isDistorted": false, - "kickCount": 0, - "thd": 0.0 - }, - "lufsCurve": { - "momentary": [ - { - "lufs": -4.8, - "t": 0.0 - }, - { - "lufs": -4.8, - "t": 0.1 - }, - { - "lufs": -4.9, - "t": 0.2 - }, - { - "lufs": -4.8, - "t": 0.3 - }, - { - "lufs": -4.8, - "t": 0.4 - }, - { - "lufs": -4.9, - "t": 0.5 - }, - { - "lufs": -4.9, - "t": 0.6 - }, - { - "lufs": -5.0, - "t": 0.7 - }, - { - "lufs": -5.0, - "t": 0.8 - }, - { - "lufs": -5.0, - "t": 0.9 - }, - { - "lufs": -5.0, - "t": 1.0 - }, - { - "lufs": -5.1, - "t": 1.1 - }, - { - "lufs": -5.1, - "t": 1.2 - }, - { - "lufs": -5.2, - "t": 1.3 - }, - { - "lufs": -5.2, - "t": 1.4 - }, - { - "lufs": -5.2, - "t": 1.5 - }, - { - "lufs": -5.2, - "t": 1.6 - }, - { - "lufs": -5.2, - "t": 1.7 - }, - { - "lufs": -5.3, - "t": 1.8 - }, - { - "lufs": -5.4, - "t": 1.9 - }, - { - "lufs": -5.4, - "t": 2.0 - }, - { - "lufs": -5.4, - "t": 2.1 - }, - { - "lufs": -5.4, - "t": 2.2 - }, - { - "lufs": -5.4, - "t": 2.3 - }, - { - "lufs": -5.5, - "t": 2.4 - }, - { - "lufs": -5.5, - "t": 2.5 - }, - { - "lufs": -5.6, - "t": 2.6 - }, - { - "lufs": -5.6, - "t": 2.7 - }, - { - "lufs": -5.6, - "t": 2.8 - }, - { - "lufs": -5.6, - "t": 2.9 - }, - { - "lufs": -5.6, - "t": 3.0 - }, - { - "lufs": -5.7, - "t": 3.1 - }, - { - "lufs": -5.8, - "t": 3.2 - }, - { - "lufs": -5.8, - "t": 3.3 - }, - { - "lufs": -5.8, - "t": 3.4 - }, - { - "lufs": -5.8, - "t": 3.5 - }, - { - "lufs": -5.8, - "t": 3.6 - }, - { - "lufs": -5.9, - "t": 3.7 - }, - { - "lufs": -6.0, - "t": 3.8 - }, - { - "lufs": -6.0, - "t": 3.9 - }, - { - "lufs": -6.0, - "t": 4.0 - }, - { - "lufs": -6.0, - "t": 4.1 - }, - { - "lufs": -6.0, - "t": 4.2 - }, - { - "lufs": -6.1, - "t": 4.3 - }, - { - "lufs": -6.2, - "t": 4.4 - }, - { - "lufs": -6.2, - "t": 4.5 - }, - { - "lufs": -6.2, - "t": 4.6 - }, - { - "lufs": -6.2, - "t": 4.7 - }, - { - "lufs": -6.3, - "t": 4.8 - }, - { - "lufs": -6.3, - "t": 4.9 - }, - { - "lufs": -6.4, - "t": 5.0 - }, - { - "lufs": -6.4, - "t": 5.1 - }, - { - "lufs": -6.4, - "t": 5.2 - }, - { - "lufs": -6.4, - "t": 5.3 - }, - { - "lufs": -6.5, - "t": 5.4 - }, - { - "lufs": -6.5, - "t": 5.5 - }, - { - "lufs": -6.6, - "t": 5.6 - } - ], - "shortTerm": [ - { - "lufs": -5.2, - "t": 0.0 - }, - { - "lufs": -5.2, - "t": 0.1 - }, - { - "lufs": -5.2, - "t": 0.2 - }, - { - "lufs": -5.2, - "t": 0.3 - }, - { - "lufs": -5.3, - "t": 0.4 - }, - { - "lufs": -5.3, - "t": 0.5 - }, - { - "lufs": -5.3, - "t": 0.6 - }, - { - "lufs": -5.4, - "t": 0.7 - }, - { - "lufs": -5.4, - "t": 0.8 - }, - { - "lufs": -5.4, - "t": 0.9 - }, - { - "lufs": -5.4, - "t": 1.0 - }, - { - "lufs": -5.5, - "t": 1.1 - }, - { - "lufs": -5.5, - "t": 1.2 - }, - { - "lufs": -5.5, - "t": 1.3 - }, - { - "lufs": -5.6, - "t": 1.4 - }, - { - "lufs": -5.6, - "t": 1.5 - }, - { - "lufs": -5.6, - "t": 1.6 - }, - { - "lufs": -5.7, - "t": 1.7 - }, - { - "lufs": -5.7, - "t": 1.8 - }, - { - "lufs": -5.7, - "t": 1.9 - }, - { - "lufs": -5.8, - "t": 2.0 - }, - { - "lufs": -5.8, - "t": 2.1 - }, - { - "lufs": -5.8, - "t": 2.2 - }, - { - "lufs": -5.9, - "t": 2.3 - }, - { - "lufs": -5.9, - "t": 2.4 - }, - { - "lufs": -5.9, - "t": 2.5 - }, - { - "lufs": -6.0, - "t": 2.6 - }, - { - "lufs": -6.0, - "t": 2.7 - }, - { - "lufs": -6.0, - "t": 2.8 - }, - { - "lufs": -6.1, - "t": 2.9 - }, - { - "lufs": -6.1, - "t": 3.0 - } - ] - }, - "lufsIntegrated": -5.6, - "lufsMomentaryMax": -4.8, - "lufsRange": 0.8, - "lufsShortTermMax": -5.2, - "melodyDetail": { - "dominantNotes": [ - 45 - ], - "midiFile": "", - "noteCount": 1, - "notes": [ - { - "duration": 5.962, - "midi": 45, - "onset": 0.015 - } - ], - "pitchConfidence": 0.2241, - "pitchRange": { - "max": 45, - "min": 45 - }, - "sourceSeparated": false, - "vibratoConfidence": 0.0, - "vibratoExtent": 0.0, - "vibratoPresent": false, - "vibratoRate": 0.0 - }, - "monoCompatible": true, - "perceptual": { - "roughness": 0.0015, - "sharpness": 0.0014 - }, - "pitchDetail": null, - "plr": 6.6, - "reverbDetail": { - "isWet": false, - "measured": false, - "perBandRt60": null, - "preDelayMs": null, - "rt60": null, - "tailEnergyRatio": null - }, - "rhythmDetail": { - "beatGrid": [ - 0.406, - 0.813, - 1.219, - 1.625, - 2.032, - 2.45, - 2.868, - 3.32, - 3.762, - 4.203, - 4.644, - 5.085, - 5.526 - ], - "beatPositions": [ - 1, - 2, - 3, - 4, - 1, - 2, - 3, - 4, - 1, - 2, - 3, - 4, - 1 - ], - "downbeats": [ - 0.406, - 2.032, - 3.762, - 5.526 - ], - "grooveAmount": 0.0402, - "onsetRate": 0.33, - "phraseGrid": { - "phrases16Bar": [ - 0.406 - ], - "phrases4Bar": [ - 0.406 - ], - "phrases8Bar": [ - 0.406 - ], - "totalBars": 4, - "totalPhrases8Bar": 1 - }, - "tempoCurve": [ - { - "bpm": 147.7, - "t": 0.61 - }, - { - "bpm": 147.7, - "t": 1.02 - }, - { - "bpm": 147.7, - "t": 1.42 - }, - { - "bpm": 147.7, - "t": 1.83 - }, - { - "bpm": 143.6, - "t": 2.24 - }, - { - "bpm": 143.6, - "t": 2.66 - }, - { - "bpm": 136.0, - "t": 3.09 - }, - { - "bpm": 136.0, - "t": 3.54 - }, - { - "bpm": 136.0, - "t": 3.98 - }, - { - "bpm": 136.0, - "t": 4.42 - }, - { - "bpm": 136.0, - "t": 4.86 - }, - { - "bpm": 136.0, - "t": 5.31 - } - ], - "tempoStability": 0.9598 - }, - "rhythmTimeline": { - "availableBars": 3, - "beatsPerBar": 4, - "selectionMethod": "representative_dsp_window", - "stepsPerBeat": 4, - "windows": [ - { - "bars": 3, - "endBar": 3, - "highBandSteps": [ - 0.2572, - 0.0104, - 0.0008, - 0.001, - 0.0266, - 0.2389, - 0.372, - 0.8732, - 0.8013, - 1.0, - 0.5644, - 0.4728, - 0.0968, - 0.0754, - 0.0036, - 0.0042, - 0.0082, - 0.0079, - 0.008, - 0.0342, - 0.1939, - 0.2629, - 0.1855, - 0.2141, - 0.0343, - 0.0128, - 0.0124, - 0.0122, - 0.0117, - 0.0116, - 0.0113, - 0.0116, - 0.0116, - 0.0112, - 0.0303, - 0.0303, - 0.032, - 0.0329, - 0.0349, - 0.0349, - 0.0338, - 0.0323, - 0.0304, - 0.0333, - 0.0315, - 0.0317, - 0.0321, - 0.0342 - ], - "lowBandSteps": [ - 0.8835, - 0.8046, - 0.7568, - 0.7931, - 0.8411, - 0.7872, - 0.8201, - 0.8517, - 0.7307, - 1.0, - 0.8574, - 0.6914, - 0.7552, - 0.7905, - 0.7351, - 0.684, - 0.0007, - 0.0003, - 0.0013, - 0.0004, - 0.0037, - 0.0086, - 0.0057, - 0.0057, - 0.0274, - 0.0306, - 0.0226, - 0.0206, - 0.6697, - 0.6955, - 0.6688, - 0.6594, - 0.6723, - 0.6349, - 0.6641, - 0.635, - 0.6406, - 0.648, - 0.617, - 0.6436, - 0.6087, - 0.617, - 0.6109, - 0.585, - 0.6098, - 0.575, - 0.5924, - 0.5836 - ], - "midBandSteps": [ - 0.9573, - 0.9521, - 0.9451, - 0.9389, - 0.9329, - 0.9238, - 0.9144, - 0.902, - 0.8984, - 0.8883, - 0.8869, - 0.8816, - 0.8802, - 0.8751, - 0.8687, - 0.8619, - 0.9059, - 0.8996, - 0.8918, - 0.8857, - 0.8779, - 0.8691, - 0.864, - 0.8575, - 1.0, - 0.9915, - 0.9836, - 0.9754, - 0.9175, - 0.9106, - 0.9026, - 0.8951, - 0.8881, - 0.8798, - 0.8732, - 0.8652, - 0.8581, - 0.851, - 0.843, - 0.8365, - 0.8286, - 0.8217, - 0.8147, - 0.807, - 0.8007, - 0.7928, - 0.7863, - 0.7791 - ], - "overallSteps": [ - 0.9576, - 0.9523, - 0.9453, - 0.9392, - 0.9331, - 0.924, - 0.9147, - 0.9023, - 0.8986, - 0.8886, - 0.8872, - 0.8818, - 0.8804, - 0.8754, - 0.8689, - 0.8621, - 0.9059, - 0.8995, - 0.8918, - 0.8857, - 0.8779, - 0.8691, - 0.864, - 0.8575, - 1.0, - 0.9915, - 0.9836, - 0.9754, - 0.9177, - 0.9108, - 0.9028, - 0.8953, - 0.8883, - 0.88, - 0.8734, - 0.8654, - 0.8583, - 0.8511, - 0.8432, - 0.8367, - 0.8288, - 0.8219, - 0.8148, - 0.8072, - 0.8008, - 0.793, - 0.7865, - 0.7793 - ], - "startBar": 1 - } - ] - }, - "sampleRate": 44100, - "saturationDetail": { - "clippedSampleCount": 1367, - "clippedSamplePercent": 0.5166, - "nearClippedSampleCount": 2952, - "nearClippedSamplePercent": 1.116, - "peakRatio95to50": 2.681, - "rmsToPeakRatioDb": 7.74, - "saturationLikely": true - }, - "segmentKey": [ - { - "key": "A Minor", - "keyConfidence": 0.89, - "segmentIndex": 0 - } - ], - "segmentLoudness": [ - { - "end": 6.0, - "lra": 0.8, - "lufs": -5.6, - "segmentIndex": 0, - "start": 0.0 - } - ], - "segmentSpectral": [ - { - "barkBands": [ - -45.1593, - -42.6317, - -42.3887, - -36.5489, - -4.5237, - -9.6341, - -14.0747, - -47.1577, - -50.8125, - -51.0924, - -51.6736, - -53.7595, - -53.7207, - -53.8242, - -58.3335, - -56.6625, - -60.8894, - -61.7893, - -63.6887, - -64.6266, - -65.5867, - -66.9544, - -68.0964, - -68.5598 - ], - "segmentIndex": 0, - "spectralCentroid": 279.8, - "spectralRolloff": 336.6, - "stereoCorrelation": 1.0, - "stereoWidth": 0.0 - } - ], - "segmentStereo": [ - { - "segmentIndex": 0, - "stereoCorrelation": 1.0, - "stereoWidth": 0.0 - } - ], - "sidechainDetail": { - "envelopeShape": [ - 1.0, - 0.979, - 0.945, - 0.973, - 0.993, - 0.934, - 0.96, - 0.957, - 0.955, - 0.949, - 0.969, - 0.953, - 0.911, - 0.949, - 0.949, - 0.915 - ], - "envelopeShape32": [ - 0.939, - 1.0, - 0.96, - 0.979, - 0.945, - 0.936, - 0.937, - 0.973, - 0.993, - 0.92, - 0.934, - 0.921, - 0.96, - 0.936, - 0.957, - 0.939, - 0.883, - 0.955, - 0.949, - 0.909, - 0.969, - 0.903, - 0.91, - 0.953, - 0.903, - 0.911, - 0.949, - 0.899, - 0.894, - 0.949, - 0.915, - 0.866 - ], - "pumpingConfidence": 0.0854, - "pumpingRate": null, - "pumpingRegularity": 0.0, - "pumpingStrength": 0.059 - }, - "snareDetail": { - "bandHz": [ - 120.0, - 2000.0 - ], - "hitCount": 26, - "hitsPerSecond": 4.33, - "meanAttackSharpness": 0.0083, - "meanBodyEnergyRatio": 1.0, - "meanCentroidHz": 295.6, - "meanDecayFrames": 57.8, - "meanDecaySeconds": 0.336, - "meanSnapEnergyRatio": 0.0 - }, - "spectralBalance": { - "brilliance": -64.7, - "highs": -62.2, - "lowBass": -4.9, - "lowMids": -6.1, - "mids": -41.0, - "subBass": -41.6, - "upperMids": -53.4 - }, - "spectralBalanceTimeSeries": [ - { - "brilliance": -63.7, - "highs": -56.5, - "lowBass": -4.5, - "lowMids": -6.7, - "mids": -24.4, - "subBass": -21.1, - "t": 0.01, - "upperMids": -43.9 - }, - { - "brilliance": -66.8, - "highs": -58.0, - "lowBass": -3.4, - "lowMids": -5.2, - "mids": -32.3, - "subBass": -45.2, - "t": 0.06, - "upperMids": -44.6 - }, - { - "brilliance": -68.2, - "highs": -59.8, - "lowBass": -3.6, - "lowMids": -5.2, - "mids": -35.0, - "subBass": -48.0, - "t": 0.1, - "upperMids": -45.8 - }, - { - "brilliance": -71.0, - "highs": -60.7, - "lowBass": -3.9, - "lowMids": -5.2, - "mids": -36.7, - "subBass": -48.1, - "t": 0.15, - "upperMids": -47.1 - }, - { - "brilliance": -69.2, - "highs": -61.6, - "lowBass": -4.2, - "lowMids": -5.3, - "mids": -35.6, - "subBass": -49.7, - "t": 0.2, - "upperMids": -47.1 - }, - { - "brilliance": -69.6, - "highs": -61.6, - "lowBass": -4.6, - "lowMids": -5.4, - "mids": -37.6, - "subBass": -50.0, - "t": 0.24, - "upperMids": -47.2 - }, - { - "brilliance": -72.3, - "highs": -63.6, - "lowBass": -4.8, - "lowMids": -5.4, - "mids": -41.0, - "subBass": -52.4, - "t": 0.29, - "upperMids": -49.7 - }, - { - "brilliance": -74.4, - "highs": -67.2, - "lowBass": -5.0, - "lowMids": -5.5, - "mids": -41.7, - "subBass": -54.2, - "t": 0.34, - "upperMids": -51.7 - }, - { - "brilliance": -77.4, - "highs": -67.6, - "lowBass": -4.9, - "lowMids": -5.4, - "mids": -45.0, - "subBass": -59.3, - "t": 0.38, - "upperMids": -52.9 - }, - { - "brilliance": -76.5, - "highs": -68.1, - "lowBass": -4.7, - "lowMids": -5.4, - "mids": -44.1, - "subBass": -56.6, - "t": 0.43, - "upperMids": -54.3 - }, - { - "brilliance": -78.3, - "highs": -70.0, - "lowBass": -4.4, - "lowMids": -5.4, - "mids": -46.9, - "subBass": -64.4, - "t": 0.48, - "upperMids": -57.0 - }, - { - "brilliance": -84.3, - "highs": -74.5, - "lowBass": -4.1, - "lowMids": -5.3, - "mids": -47.6, - "subBass": -66.8, - "t": 0.52, - "upperMids": -64.8 - }, - { - "brilliance": -91.4, - "highs": -95.9, - "lowBass": -3.8, - "lowMids": -5.2, - "mids": -47.8, - "subBass": -64.6, - "t": 0.57, - "upperMids": -99.2 - }, - { - "brilliance": -91.4, - "highs": -95.2, - "lowBass": -3.5, - "lowMids": -5.2, - "mids": -47.5, - "subBass": -65.0, - "t": 0.62, - "upperMids": -97.8 - }, - { - "brilliance": -91.1, - "highs": -95.4, - "lowBass": -3.4, - "lowMids": -5.2, - "mids": -47.4, - "subBass": -64.7, - "t": 0.66, - "upperMids": -96.4 - }, - { - "brilliance": -91.2, - "highs": -95.5, - "lowBass": -3.4, - "lowMids": -5.2, - "mids": -47.7, - "subBass": -63.8, - "t": 0.71, - "upperMids": -96.6 - }, - { - "brilliance": -89.7, - "highs": -95.4, - "lowBass": -3.5, - "lowMids": -5.2, - "mids": -47.9, - "subBass": -66.0, - "t": 0.75, - "upperMids": -96.3 - }, - { - "brilliance": -89.8, - "highs": -95.1, - "lowBass": -3.7, - "lowMids": -5.3, - "mids": -47.7, - "subBass": -64.0, - "t": 0.8, - "upperMids": -96.2 - }, - { - "brilliance": -82.8, - "highs": -73.4, - "lowBass": -4.0, - "lowMids": -5.4, - "mids": -47.5, - "subBass": -65.9, - "t": 0.85, - "upperMids": -64.5 - }, - { - "brilliance": -79.9, - "highs": -72.0, - "lowBass": -4.4, - "lowMids": -5.5, - "mids": -46.5, - "subBass": -61.0, - "t": 0.89, - "upperMids": -57.1 - }, - { - "brilliance": -79.6, - "highs": -73.5, - "lowBass": -4.7, - "lowMids": -5.5, - "mids": -46.9, - "subBass": -63.2, - "t": 0.94, - "upperMids": -57.8 - }, - { - "brilliance": -75.6, - "highs": -68.6, - "lowBass": -5.0, - "lowMids": -5.6, - "mids": -45.9, - "subBass": -66.6, - "t": 0.99, - "upperMids": -53.7 - }, - { - "brilliance": -75.2, - "highs": -67.3, - "lowBass": -5.2, - "lowMids": -5.6, - "mids": -43.7, - "subBass": -55.9, - "t": 1.03, - "upperMids": -53.3 - }, - { - "brilliance": -75.1, - "highs": -66.3, - "lowBass": -5.2, - "lowMids": -5.7, - "mids": -43.5, - "subBass": -56.8, - "t": 1.08, - "upperMids": -51.5 - }, - { - "brilliance": -73.0, - "highs": -63.9, - "lowBass": -5.0, - "lowMids": -5.7, - "mids": -41.1, - "subBass": -53.1, - "t": 1.13, - "upperMids": -49.4 - }, - { - "brilliance": -72.2, - "highs": -63.5, - "lowBass": -4.7, - "lowMids": -5.6, - "mids": -40.2, - "subBass": -55.6, - "t": 1.17, - "upperMids": -48.7 - }, - { - "brilliance": -74.1, - "highs": -63.6, - "lowBass": -4.3, - "lowMids": -5.6, - "mids": -41.0, - "subBass": -51.8, - "t": 1.22, - "upperMids": -49.5 - }, - { - "brilliance": -70.1, - "highs": -62.2, - "lowBass": -4.0, - "lowMids": -5.5, - "mids": -38.3, - "subBass": -54.4, - "t": 1.27, - "upperMids": -46.9 - }, - { - "brilliance": -70.2, - "highs": -61.4, - "lowBass": -3.8, - "lowMids": -5.5, - "mids": -36.8, - "subBass": -48.7, - "t": 1.31, - "upperMids": -46.0 - }, - { - "brilliance": -72.2, - "highs": -62.3, - "lowBass": -3.6, - "lowMids": -5.5, - "mids": -38.4, - "subBass": -53.4, - "t": 1.36, - "upperMids": -47.4 - }, - { - "brilliance": -73.0, - "highs": -63.4, - "lowBass": -3.6, - "lowMids": -5.4, - "mids": -39.8, - "subBass": -51.0, - "t": 1.4, - "upperMids": -48.9 - }, - { - "brilliance": -68.9, - "highs": -60.8, - "lowBass": -3.7, - "lowMids": -5.5, - "mids": -38.1, - "subBass": -53.2, - "t": 1.45, - "upperMids": -46.3 - }, - { - "brilliance": -70.5, - "highs": -61.4, - "lowBass": -3.9, - "lowMids": -5.6, - "mids": -39.2, - "subBass": -52.0, - "t": 1.5, - "upperMids": -47.1 - }, - { - "brilliance": -74.7, - "highs": -64.5, - "lowBass": -4.2, - "lowMids": -5.6, - "mids": -43.0, - "subBass": -55.9, - "t": 1.54, - "upperMids": -51.2 - }, - { - "brilliance": -78.2, - "highs": -68.2, - "lowBass": -4.6, - "lowMids": -5.7, - "mids": -44.2, - "subBass": -55.6, - "t": 1.59, - "upperMids": -54.0 - }, - { - "brilliance": -75.5, - "highs": -67.7, - "lowBass": -5.0, - "lowMids": -5.8, - "mids": -45.5, - "subBass": -62.5, - "t": 1.64, - "upperMids": -52.0 - }, - { - "brilliance": -79.5, - "highs": -72.1, - "lowBass": -5.2, - "lowMids": -5.8, - "mids": -47.3, - "subBass": -64.6, - "t": 1.68, - "upperMids": -58.0 - }, - { - "brilliance": -83.5, - "highs": -77.7, - "lowBass": -5.4, - "lowMids": -5.8, - "mids": -47.8, - "subBass": -68.6, - "t": 1.73, - "upperMids": -63.7 - }, - { - "brilliance": -84.3, - "highs": -75.2, - "lowBass": -5.3, - "lowMids": -5.8, - "mids": -47.9, - "subBass": -67.5, - "t": 1.78, - "upperMids": -68.0 - }, - { - "brilliance": -87.2, - "highs": -85.7, - "lowBass": -5.1, - "lowMids": -5.8, - "mids": -48.1, - "subBass": -69.3, - "t": 1.82, - "upperMids": -94.8 - }, - { - "brilliance": -87.2, - "highs": -85.7, - "lowBass": -4.8, - "lowMids": -5.8, - "mids": -48.2, - "subBass": -65.9, - "t": 1.87, - "upperMids": -94.7 - }, - { - "brilliance": -87.1, - "highs": -85.5, - "lowBass": -4.5, - "lowMids": -5.7, - "mids": -48.1, - "subBass": -67.3, - "t": 1.92, - "upperMids": -94.7 - }, - { - "brilliance": -87.1, - "highs": -85.4, - "lowBass": -4.2, - "lowMids": -5.7, - "mids": -48.0, - "subBass": -64.2, - "t": 1.96, - "upperMids": -94.5 - }, - { - "brilliance": -84.8, - "highs": -83.8, - "lowBass": -3.9, - "lowMids": -5.6, - "mids": -48.2, - "subBass": -65.9, - "t": 2.01, - "upperMids": -85.4 - }, - { - "brilliance": -84.2, - "highs": -83.1, - "lowBass": -3.8, - "lowMids": -5.6, - "mids": -48.3, - "subBass": -65.1, - "t": 2.05, - "upperMids": -83.5 - }, - { - "brilliance": -84.3, - "highs": -83.2, - "lowBass": -3.8, - "lowMids": -5.6, - "mids": -48.0, - "subBass": -64.3, - "t": 2.1, - "upperMids": -83.5 - }, - { - "brilliance": -84.2, - "highs": -83.2, - "lowBass": -3.9, - "lowMids": -5.7, - "mids": -47.9, - "subBass": -65.6, - "t": 2.15, - "upperMids": -83.9 - }, - { - "brilliance": -84.4, - "highs": -83.3, - "lowBass": -4.1, - "lowMids": -5.7, - "mids": -48.2, - "subBass": -65.1, - "t": 2.19, - "upperMids": -83.7 - }, - { - "brilliance": -84.5, - "highs": -83.4, - "lowBass": -4.4, - "lowMids": -5.8, - "mids": -48.4, - "subBass": -66.4, - "t": 2.24, - "upperMids": -83.8 - }, - { - "brilliance": -84.7, - "highs": -83.5, - "lowBass": -4.8, - "lowMids": -5.9, - "mids": -48.2, - "subBass": -67.3, - "t": 2.29, - "upperMids": -83.9 - }, - { - "brilliance": -82.8, - "highs": -75.4, - "lowBass": -5.2, - "lowMids": -6.0, - "mids": -48.1, - "subBass": -66.1, - "t": 2.33, - "upperMids": -67.7 - }, - { - "brilliance": -84.5, - "highs": -80.9, - "lowBass": -5.5, - "lowMids": -6.0, - "mids": -48.2, - "subBass": -68.3, - "t": 2.38, - "upperMids": -75.1 - }, - { - "brilliance": -81.1, - "highs": -71.3, - "lowBass": -5.6, - "lowMids": -6.1, - "mids": -48.2, - "subBass": -68.5, - "t": 2.43, - "upperMids": -64.6 - }, - { - "brilliance": -80.0, - "highs": -74.1, - "lowBass": -5.6, - "lowMids": -6.1, - "mids": -47.6, - "subBass": -65.5, - "t": 2.47, - "upperMids": -58.8 - }, - { - "brilliance": -78.4, - "highs": -71.7, - "lowBass": -5.4, - "lowMids": -6.0, - "mids": -47.4, - "subBass": -63.2, - "t": 2.52, - "upperMids": -56.7 - }, - { - "brilliance": -75.1, - "highs": -68.5, - "lowBass": -5.1, - "lowMids": -6.0, - "mids": -45.9, - "subBass": -61.0, - "t": 2.57, - "upperMids": -53.7 - }, - { - "brilliance": -76.1, - "highs": -68.1, - "lowBass": -4.7, - "lowMids": -6.0, - "mids": -46.0, - "subBass": -61.4, - "t": 2.61, - "upperMids": -52.9 - }, - { - "brilliance": -77.3, - "highs": -69.1, - "lowBass": -4.4, - "lowMids": -5.9, - "mids": -47.7, - "subBass": -62.9, - "t": 2.66, - "upperMids": -56.6 - }, - { - "brilliance": -75.7, - "highs": -67.0, - "lowBass": -4.2, - "lowMids": -5.9, - "mids": -45.3, - "subBass": -61.5, - "t": 2.71, - "upperMids": -51.9 - }, - { - "brilliance": -72.7, - "highs": -65.9, - "lowBass": -4.0, - "lowMids": -5.9, - "mids": -44.6, - "subBass": -57.7, - "t": 2.75, - "upperMids": -51.3 - }, - { - "brilliance": -76.7, - "highs": -71.1, - "lowBass": -4.0, - "lowMids": -5.8, - "mids": -46.5, - "subBass": -61.5, - "t": 2.8, - "upperMids": -55.2 - }, - { - "brilliance": -78.5, - "highs": -72.3, - "lowBass": -4.1, - "lowMids": -5.9, - "mids": -47.7, - "subBass": -63.3, - "t": 2.84, - "upperMids": -57.9 - }, - { - "brilliance": -77.3, - "highs": -68.6, - "lowBass": -4.4, - "lowMids": -5.9, - "mids": -48.2, - "subBass": -67.2, - "t": 2.89, - "upperMids": -59.8 - }, - { - "brilliance": -79.9, - "highs": -72.7, - "lowBass": -4.7, - "lowMids": -6.0, - "mids": -48.3, - "subBass": -64.8, - "t": 2.94, - "upperMids": -66.0 - }, - { - "brilliance": -82.3, - "highs": -80.8, - "lowBass": -5.0, - "lowMids": -6.1, - "mids": -48.3, - "subBass": -66.8, - "t": 2.98, - "upperMids": -77.1 - }, - { - "brilliance": -82.7, - "highs": -82.4, - "lowBass": -5.4, - "lowMids": -6.2, - "mids": -48.5, - "subBass": -67.4, - "t": 3.03, - "upperMids": -81.4 - }, - { - "brilliance": -82.6, - "highs": -82.4, - "lowBass": -5.7, - "lowMids": -6.2, - "mids": -48.6, - "subBass": -69.1, - "t": 3.08, - "upperMids": -81.3 - }, - { - "brilliance": -82.7, - "highs": -82.4, - "lowBass": -5.8, - "lowMids": -6.3, - "mids": -48.5, - "subBass": -69.4, - "t": 3.12, - "upperMids": -81.6 - }, - { - "brilliance": -82.9, - "highs": -82.4, - "lowBass": -5.8, - "lowMids": -6.3, - "mids": -48.4, - "subBass": -68.8, - "t": 3.17, - "upperMids": -81.5 - }, - { - "brilliance": -83.0, - "highs": -82.4, - "lowBass": -5.6, - "lowMids": -6.3, - "mids": -48.3, - "subBass": -68.3, - "t": 3.22, - "upperMids": -81.4 - }, - { - "brilliance": -82.7, - "highs": -82.5, - "lowBass": -5.3, - "lowMids": -6.2, - "mids": -48.3, - "subBass": -66.8, - "t": 3.26, - "upperMids": -81.3 - }, - { - "brilliance": -82.6, - "highs": -82.0, - "lowBass": -4.9, - "lowMids": -6.2, - "mids": -48.6, - "subBass": -67.9, - "t": 3.31, - "upperMids": -81.5 - }, - { - "brilliance": -82.6, - "highs": -82.1, - "lowBass": -4.6, - "lowMids": -6.1, - "mids": -48.7, - "subBass": -65.6, - "t": 3.36, - "upperMids": -81.3 - }, - { - "brilliance": -82.7, - "highs": -82.1, - "lowBass": -4.4, - "lowMids": -6.1, - "mids": -48.5, - "subBass": -65.4, - "t": 3.4, - "upperMids": -81.3 - }, - { - "brilliance": -82.6, - "highs": -82.1, - "lowBass": -4.2, - "lowMids": -6.0, - "mids": -48.5, - "subBass": -65.2, - "t": 3.45, - "upperMids": -81.4 - }, - { - "brilliance": -82.6, - "highs": -82.0, - "lowBass": -4.2, - "lowMids": -6.1, - "mids": -48.7, - "subBass": -64.5, - "t": 3.49, - "upperMids": -81.3 - }, - { - "brilliance": -82.8, - "highs": -82.3, - "lowBass": -4.3, - "lowMids": -6.1, - "mids": -48.7, - "subBass": -66.9, - "t": 3.54, - "upperMids": -81.2 - }, - { - "brilliance": -82.9, - "highs": -82.4, - "lowBass": -4.6, - "lowMids": -6.2, - "mids": -48.4, - "subBass": -65.0, - "t": 3.59, - "upperMids": -81.5 - }, - { - "brilliance": -82.5, - "highs": -82.5, - "lowBass": -4.9, - "lowMids": -6.2, - "mids": -48.4, - "subBass": -66.8, - "t": 3.63, - "upperMids": -81.4 - }, - { - "brilliance": -82.7, - "highs": -82.5, - "lowBass": -5.3, - "lowMids": -6.3, - "mids": -48.6, - "subBass": -67.3, - "t": 3.68, - "upperMids": -81.6 - }, - { - "brilliance": -82.8, - "highs": -82.7, - "lowBass": -5.6, - "lowMids": -6.4, - "mids": -48.8, - "subBass": -68.1, - "t": 3.73, - "upperMids": -81.6 - }, - { - "brilliance": -82.7, - "highs": -82.8, - "lowBass": -5.9, - "lowMids": -6.5, - "mids": -48.8, - "subBass": -70.1, - "t": 3.77, - "upperMids": -81.5 - }, - { - "brilliance": -82.8, - "highs": -82.6, - "lowBass": -6.0, - "lowMids": -6.5, - "mids": -48.7, - "subBass": -68.0, - "t": 3.82, - "upperMids": -81.7 - }, - { - "brilliance": -82.9, - "highs": -82.7, - "lowBass": -6.0, - "lowMids": -6.5, - "mids": -48.7, - "subBass": -69.7, - "t": 3.87, - "upperMids": -81.6 - }, - { - "brilliance": -83.0, - "highs": -82.8, - "lowBass": -5.8, - "lowMids": -6.5, - "mids": -48.6, - "subBass": -67.3, - "t": 3.91, - "upperMids": -81.7 - }, - { - "brilliance": -82.8, - "highs": -82.6, - "lowBass": -5.5, - "lowMids": -6.4, - "mids": -48.7, - "subBass": -69.1, - "t": 3.96, - "upperMids": -81.3 - }, - { - "brilliance": -77.5, - "highs": -82.4, - "lowBass": -5.2, - "lowMids": -6.4, - "mids": -48.8, - "subBass": -65.7, - "t": 4.01, - "upperMids": -82.0 - }, - { - "brilliance": -75.6, - "highs": -82.7, - "lowBass": -4.8, - "lowMids": -6.3, - "mids": -48.6, - "subBass": -67.2, - "t": 4.05, - "upperMids": -82.8 - }, - { - "brilliance": -75.5, - "highs": -82.1, - "lowBass": -4.6, - "lowMids": -6.3, - "mids": -48.6, - "subBass": -64.8, - "t": 4.1, - "upperMids": -82.8 - }, - { - "brilliance": -75.7, - "highs": -82.1, - "lowBass": -4.5, - "lowMids": -6.3, - "mids": -49.0, - "subBass": -66.0, - "t": 4.14, - "upperMids": -83.0 - }, - { - "brilliance": -75.7, - "highs": -82.7, - "lowBass": -4.5, - "lowMids": -6.3, - "mids": -49.1, - "subBass": -66.5, - "t": 4.19, - "upperMids": -82.7 - }, - { - "brilliance": -75.6, - "highs": -81.9, - "lowBass": -4.6, - "lowMids": -6.3, - "mids": -48.8, - "subBass": -65.1, - "t": 4.24, - "upperMids": -83.2 - }, - { - "brilliance": -75.9, - "highs": -82.3, - "lowBass": -4.8, - "lowMids": -6.4, - "mids": -48.6, - "subBass": -66.6, - "t": 4.28, - "upperMids": -82.7 - }, - { - "brilliance": -75.9, - "highs": -82.8, - "lowBass": -5.1, - "lowMids": -6.5, - "mids": -48.8, - "subBass": -66.2, - "t": 4.33, - "upperMids": -83.0 - }, - { - "brilliance": -76.1, - "highs": -82.5, - "lowBass": -5.5, - "lowMids": -6.6, - "mids": -48.9, - "subBass": -67.5, - "t": 4.38, - "upperMids": -83.3 - }, - { - "brilliance": -76.0, - "highs": -82.9, - "lowBass": -5.9, - "lowMids": -6.6, - "mids": -48.8, - "subBass": -68.8, - "t": 4.42, - "upperMids": -82.8 - }, - { - "brilliance": -76.1, - "highs": -82.9, - "lowBass": -6.1, - "lowMids": -6.7, - "mids": -48.9, - "subBass": -68.0, - "t": 4.47, - "upperMids": -83.3 - }, - { - "brilliance": -76.4, - "highs": -82.7, - "lowBass": -6.3, - "lowMids": -6.7, - "mids": -49.0, - "subBass": -69.3, - "t": 4.52, - "upperMids": -83.1 - }, - { - "brilliance": -76.3, - "highs": -83.1, - "lowBass": -6.2, - "lowMids": -6.7, - "mids": -49.0, - "subBass": -69.0, - "t": 4.56, - "upperMids": -83.3 - }, - { - "brilliance": -76.3, - "highs": -82.6, - "lowBass": -6.0, - "lowMids": -6.7, - "mids": -49.0, - "subBass": -69.4, - "t": 4.61, - "upperMids": -83.3 - }, - { - "brilliance": -76.2, - "highs": -82.9, - "lowBass": -5.7, - "lowMids": -6.7, - "mids": -49.0, - "subBass": -67.6, - "t": 4.66, - "upperMids": -83.4 - }, - { - "brilliance": -76.1, - "highs": -82.8, - "lowBass": -5.4, - "lowMids": -6.6, - "mids": -48.8, - "subBass": -67.6, - "t": 4.7, - "upperMids": -83.2 - }, - { - "brilliance": -76.0, - "highs": -82.3, - "lowBass": -5.1, - "lowMids": -6.6, - "mids": -48.8, - "subBass": -66.0, - "t": 4.75, - "upperMids": -83.2 - }, - { - "brilliance": -75.8, - "highs": -82.7, - "lowBass": -4.8, - "lowMids": -6.5, - "mids": -49.1, - "subBass": -66.3, - "t": 4.79, - "upperMids": -82.7 - }, - { - "brilliance": -75.9, - "highs": -82.8, - "lowBass": -4.7, - "lowMids": -6.5, - "mids": -49.3, - "subBass": -66.7, - "t": 4.84, - "upperMids": -82.9 - }, - { - "brilliance": -75.8, - "highs": -82.2, - "lowBass": -4.7, - "lowMids": -6.5, - "mids": -49.0, - "subBass": -64.4, - "t": 4.89, - "upperMids": -82.9 - }, - { - "brilliance": -75.9, - "highs": -82.9, - "lowBass": -4.8, - "lowMids": -6.6, - "mids": -48.9, - "subBass": -67.1, - "t": 4.93, - "upperMids": -82.7 - }, - { - "brilliance": -75.5, - "highs": -82.5, - "lowBass": -5.1, - "lowMids": -6.6, - "mids": -49.2, - "subBass": -65.0, - "t": 4.98, - "upperMids": -83.1 - }, - { - "brilliance": -75.4, - "highs": -82.2, - "lowBass": -5.4, - "lowMids": -6.7, - "mids": -49.2, - "subBass": -68.2, - "t": 5.03, - "upperMids": -83.1 - }, - { - "brilliance": -75.4, - "highs": -82.9, - "lowBass": -5.7, - "lowMids": -6.8, - "mids": -49.0, - "subBass": -67.4, - "t": 5.07, - "upperMids": -82.9 - }, - { - "brilliance": -75.6, - "highs": -82.3, - "lowBass": -6.1, - "lowMids": -6.9, - "mids": -48.9, - "subBass": -68.6, - "t": 5.12, - "upperMids": -83.4 - }, - { - "brilliance": -75.7, - "highs": -82.6, - "lowBass": -6.4, - "lowMids": -6.9, - "mids": -49.1, - "subBass": -69.1, - "t": 5.17, - "upperMids": -83.2 - }, - { - "brilliance": -75.9, - "highs": -82.8, - "lowBass": -6.5, - "lowMids": -7.0, - "mids": -49.2, - "subBass": -70.0, - "t": 5.21, - "upperMids": -83.2 - }, - { - "brilliance": -75.8, - "highs": -82.4, - "lowBass": -6.5, - "lowMids": -7.0, - "mids": -49.3, - "subBass": -70.4, - "t": 5.26, - "upperMids": -83.5 - }, - { - "brilliance": -75.7, - "highs": -82.8, - "lowBass": -6.3, - "lowMids": -7.0, - "mids": -49.3, - "subBass": -68.4, - "t": 5.31, - "upperMids": -83.2 - }, - { - "brilliance": -75.6, - "highs": -82.4, - "lowBass": -6.0, - "lowMids": -6.9, - "mids": -49.1, - "subBass": -68.9, - "t": 5.35, - "upperMids": -83.4 - }, - { - "brilliance": -75.6, - "highs": -82.3, - "lowBass": -5.6, - "lowMids": -6.9, - "mids": -49.0, - "subBass": -66.1, - "t": 5.4, - "upperMids": -83.2 - }, - { - "brilliance": -75.5, - "highs": -82.5, - "lowBass": -5.3, - "lowMids": -6.8, - "mids": -49.2, - "subBass": -67.9, - "t": 5.45, - "upperMids": -83.2 - }, - { - "brilliance": -75.3, - "highs": -82.1, - "lowBass": -5.1, - "lowMids": -6.8, - "mids": -49.3, - "subBass": -65.8, - "t": 5.49, - "upperMids": -83.1 - }, - { - "brilliance": -75.3, - "highs": -82.2, - "lowBass": -5.0, - "lowMids": -6.8, - "mids": -49.2, - "subBass": -65.5, - "t": 5.54, - "upperMids": -83.0 - }, - { - "brilliance": -75.3, - "highs": -82.3, - "lowBass": -5.0, - "lowMids": -6.8, - "mids": -49.2, - "subBass": -66.2, - "t": 5.58, - "upperMids": -83.2 - }, - { - "brilliance": -75.4, - "highs": -82.3, - "lowBass": -5.1, - "lowMids": -6.8, - "mids": -49.5, - "subBass": -65.6, - "t": 5.63, - "upperMids": -83.3 - }, - { - "brilliance": -75.6, - "highs": -82.7, - "lowBass": -5.3, - "lowMids": -6.9, - "mids": -49.6, - "subBass": -67.8, - "t": 5.68, - "upperMids": -82.8 - }, - { - "brilliance": -75.6, - "highs": -82.4, - "lowBass": -5.6, - "lowMids": -7.0, - "mids": -49.3, - "subBass": -67.2, - "t": 5.72, - "upperMids": -83.3 - }, - { - "brilliance": -75.8, - "highs": -82.6, - "lowBass": -6.0, - "lowMids": -7.0, - "mids": -49.2, - "subBass": -67.4, - "t": 5.77, - "upperMids": -83.5 - }, - { - "brilliance": -75.8, - "highs": -83.1, - "lowBass": -6.4, - "lowMids": -7.1, - "mids": -49.3, - "subBass": -69.3, - "t": 5.82, - "upperMids": -83.3 - }, - { - "brilliance": -76.0, - "highs": -82.7, - "lowBass": -6.6, - "lowMids": -7.2, - "mids": -49.4, - "subBass": -69.0, - "t": 5.86, - "upperMids": -83.2 - }, - { - "brilliance": -75.9, - "highs": -82.7, - "lowBass": -6.8, - "lowMids": -7.2, - "mids": -49.4, - "subBass": -71.1, - "t": 5.91, - "upperMids": -83.0 - }, - { - "brilliance": -74.9, - "highs": -81.1, - "lowBass": -6.7, - "lowMids": -7.2, - "mids": -49.5, - "subBass": -68.6, - "t": 5.96, - "upperMids": -81.4 - }, - { - "brilliance": -43.9, - "highs": -42.1, - "lowBass": -10.3, - "lowMids": -11.1, - "mids": -27.2, - "subBass": -29.3, - "t": 6.0, - "upperMids": -37.5 - } + "coreValues": { + "bpm": 135.4, + "crestFactor": 7.7, + "durationSeconds": 6.0, + "key": "A Minor", + "keyConfidence": 0.89, + "lufsIntegrated": -5.6, + "lufsMomentaryMax": -4.8, + "lufsRange": 0.8, + "lufsShortTermMax": -5.2, + "monoCompatible": true, + "plr": 6.6, + "sampleRate": 44100, + "spectralBalance.brilliance": -64.7, + "spectralBalance.highs": -62.2, + "spectralBalance.lowBass": -4.9, + "spectralBalance.lowMids": -6.1, + "spectralBalance.mids": -41.0, + "spectralBalance.subBass": -41.6, + "spectralBalance.upperMids": -53.4, + "stereoDetail.stereoCorrelation": 1.0, + "stereoDetail.stereoWidth": 0.0, + "stereoDetail.subBassCorrelation": 1.0, + "stereoDetail.subBassMono": true, + "timeSignature": "4/4", + "timeSignatureSource": "assumed_four_four", + "truePeak": 1.0 + }, + "topLevelKeys": [ + "acidDetail", + "arrangementDetail", + "bassDetail", + "beatsLoudness", + "bpm", + "bpmAgreement", + "bpmConfidence", + "bpmDoubletime", + "bpmPercival", + "bpmRawOriginal", + "bpmSource", + "chordDetail", + "crestFactor", + "danceability", + "durationSeconds", + "dynamicCharacter", + "dynamicSpread", + "effectsDetail", + "essentiaFeatures", + "genreDetail", + "grooveDetail", + "hihatDetail", + "key", + "keyConfidence", + "keyProfile", + "kickDetail", + "lufsCurve", + "lufsIntegrated", + "lufsMomentaryMax", + "lufsRange", + "lufsShortTermMax", + "melodyDetail", + "monoCompatible", + "perceptual", + "pitchDetail", + "plr", + "reverbDetail", + "rhythmDetail", + "rhythmTimeline", + "sampleRate", + "saturationDetail", + "segmentKey", + "segmentLoudness", + "segmentSpectral", + "segmentStereo", + "sidechainDetail", + "snareDetail", + "spectralBalance", + "spectralBalanceTimeSeries", + "spectralDetail", + "stemAnalysis", + "stereoDetail", + "structure", + "supersawDetail", + "synthesisCharacter", + "textureCharacter", + "timeSignature", + "timeSignatureConfidence", + "timeSignatureSource", + "transcriptionDetail", + "transientDensityDetail", + "truePeak", + "tuningCents", + "tuningFrequency", + "vocalDetail" ], - "spectralDetail": { - "barkBands": [ - -45.1593, - -42.6317, - -42.3887, - -36.5489, - -4.5237, - -9.6341, - -14.0747, - -47.1577, - -50.8125, - -51.0924, - -51.6736, - -53.7595, - -53.7207, - -53.8242, - -58.3335, - -56.6625, - -60.8894, - -61.7893, - -63.6887, - -64.6266, - -65.5867, - -66.9544, - -68.0964, - -68.5598 - ], - "chroma": [ - 0.9735, - 0.127, - 0.252, - 0.2546, - 0.4071, - 0.0896, - 0.0834, - 0.512, - 0.1245, - 0.3609, - 0.1488, - 0.2321 - ], - "erbBands": [ - -22.3962, - -13.5438, - -2.1152, - 14.7352, - 29.8554, - 31.3184, - 30.5029, - 27.1155, - 28.7298, - 18.4024, - 4.4996, - -3.6062, - -5.4928, - -5.2696, - -6.2035, - -7.9285, - -6.8539, - -6.5704, - -10.0156, - -9.673, - -10.4773, - -13.2478, - -14.3336, - -16.0312, - -17.3125, - -17.9026, - -19.0185, - -20.258, - -21.3265, - -22.3991, - -23.6038, - -24.819, - -26.2045, - -27.6921, - -29.5007, - -31.6452, - -34.5713, - -40.1409, - -36.0158, - -32.5441 - ], - "mfcc": [ - -1005.3185, - 246.528, - 138.3856, - 83.2849, - 11.9793, - -44.7499, - -79.8733, - -86.5884, - -75.3388, - -52.4688, - -30.0446, - -11.9677, - -4.1607 - ], - "spectralBandwidth": 616.6, - "spectralCentroid": 279.8, - "spectralContrast": [ - -0.0717, - -0.1777, - -0.8899, - -0.9187, - -0.8591, - -0.8709 - ], - "spectralFlatness": 0.005941, - "spectralRolloff": 336.6, - "spectralValley": [ - -7.5314, - -8.5881, - -11.2139, - -12.0168, - -12.4845, - -13.1331 - ] - }, - "stemAnalysis": null, - "stereoDetail": { - "bandCorrelations": { - "brilliance": 1.0, - "highs": 1.0, - "lowBass": 1.0, - "lowMids": 1.0, - "mids": 1.0, - "subBass": 1.0, - "upperMids": 1.0 - }, - "correlationCurve": [ - { - "full": 1.0, - "sub": 1.0, - "t": 0.5 - }, - { - "full": 1.0, - "sub": 1.0, - "t": 1.5 - }, - { - "full": 1.0, - "sub": 1.0, - "t": 2.5 - }, - { - "full": 1.0, - "sub": 1.0, - "t": 3.5 - }, - { - "full": 1.0, - "sub": 1.0, - "t": 4.5 - }, - { - "full": 1.0, - "sub": 1.0, - "t": 5.5 - } - ], - "stereoCorrelation": 1.0, - "stereoWidth": 0.0, - "subBassCorrelation": 1.0, - "subBassMono": true - }, - "structure": { - "segmentCount": 1, - "segments": [ - { - "end": 6.0, - "index": 0, - "start": 0.0 - } - ] - }, - "supersawDetail": { - "avgDetuneCents": 29.3, - "confidence": 0.84, - "isSupersaw": true, - "spectralComplexity": 11.0, - "voiceCount": 11 - }, - "synthesisCharacter": { - "inharmonicity": 0.1616, - "oddToEvenRatio": 0.7756 - }, - "textureCharacter": { - "highBandFlatness": 0.5903, - "inharmonicity": 0.1616, - "lowBandFlatness": 0.0591, - "midBandFlatness": 0.0263, - "textureScore": 0.3527 - }, - "timeSignature": "4/4", - "timeSignatureConfidence": 0.0, - "timeSignatureSource": "assumed_four_four", - "transcriptionDetail": null, - "transientDensityDetail": { - "brilliance": { - "eventCount": 44, - "meanOnsetStrength": 2.003, - "onsetRatePerSecond": 7.33, - "peakOnsetStrength": 5.462 - }, - "highs": { - "eventCount": 40, - "meanOnsetStrength": 2.936, - "onsetRatePerSecond": 6.67, - "peakOnsetStrength": 7.831 - }, - "lowBass": { - "eventCount": 105, - "meanOnsetStrength": 0.16, - "onsetRatePerSecond": 17.5, - "peakOnsetStrength": 0.305 - }, - "lowMids": { - "eventCount": 111, - "meanOnsetStrength": 0.398, - "onsetRatePerSecond": 18.5, - "peakOnsetStrength": 0.735 - }, - "mids": { - "eventCount": 36, - "meanOnsetStrength": 4.971, - "onsetRatePerSecond": 6.0, - "peakOnsetStrength": 10.989 - }, - "subBass": { - "eventCount": 39, - "meanOnsetStrength": 0.124, - "onsetRatePerSecond": 6.5, - "peakOnsetStrength": 0.263 - }, - "upperMids": { - "eventCount": 38, - "meanOnsetStrength": 5.358, - "onsetRatePerSecond": 6.33, - "peakOnsetStrength": 12.293 - } - }, - "truePeak": 1.0, - "tuningCents": -30.0, - "tuningFrequency": 432.44, - "vocalDetail": { - "confidence": 0.64, - "formantStrength": 0.06, - "hasVocals": false, - "mfccLikelihood": 0.91, - "stemEnergyRatio": null, - "stemOtherCorrelation": null, - "vocalEnergyRatio": 1.0 + "topLevelTypes": { + "acidDetail": "dict", + "arrangementDetail": "dict", + "bassDetail": "dict", + "beatsLoudness": "dict", + "bpm": "number", + "bpmAgreement": "bool", + "bpmConfidence": "number", + "bpmDoubletime": "bool", + "bpmPercival": "number", + "bpmRawOriginal": "number", + "bpmSource": "str", + "chordDetail": "dict", + "crestFactor": "number", + "danceability": "dict", + "durationSeconds": "number", + "dynamicCharacter": "dict", + "dynamicSpread": "number", + "effectsDetail": "dict", + "essentiaFeatures": "dict", + "genreDetail": "dict", + "grooveDetail": "dict", + "hihatDetail": "null", + "key": "str", + "keyConfidence": "number", + "keyProfile": "str", + "kickDetail": "dict", + "lufsCurve": "dict", + "lufsIntegrated": "number", + "lufsMomentaryMax": "number", + "lufsRange": "number", + "lufsShortTermMax": "number", + "melodyDetail": "dict", + "monoCompatible": "bool", + "perceptual": "dict", + "pitchDetail": "null", + "plr": "number", + "reverbDetail": "dict", + "rhythmDetail": "dict", + "rhythmTimeline": "dict", + "sampleRate": "number", + "saturationDetail": "dict", + "segmentKey": "list", + "segmentLoudness": "list", + "segmentSpectral": "list", + "segmentStereo": "list", + "sidechainDetail": "dict", + "snareDetail": "dict", + "spectralBalance": "dict", + "spectralBalanceTimeSeries": "list", + "spectralDetail": "dict", + "stemAnalysis": "null", + "stereoDetail": "dict", + "structure": "dict", + "supersawDetail": "dict", + "synthesisCharacter": "dict", + "textureCharacter": "dict", + "timeSignature": "str", + "timeSignatureConfidence": "number", + "timeSignatureSource": "str", + "transcriptionDetail": "null", + "transientDensityDetail": "dict", + "truePeak": "number", + "tuningCents": "number", + "tuningFrequency": "number", + "vocalDetail": "dict" } } diff --git a/apps/backend/tests/test_phase1_golden.py b/apps/backend/tests/test_phase1_golden.py index 9b60ec4f..18eb0362 100644 --- a/apps/backend/tests/test_phase1_golden.py +++ b/apps/backend/tests/test_phase1_golden.py @@ -1,27 +1,25 @@ -"""Golden-snapshot regression gate for default-mode ``analyze.py`` output. - -This runs ``analyze.py`` on a fixed, deterministic procedural fixture and asserts the -emitted JSON matches a committed golden snapshot within tolerance. It is a safety net for -changes to ``analyze.py`` / the DSP feature modules: any unintended drift in any output -field surfaces as a failed assertion that names the drifted path. - -Determinism vs portability: the default analyze path (no ``--separate`` / ``--transcribe``) -is pure-DSP and contains no RNG / ML / timestamps; output was byte-identical across 5 -repeated runs *in one environment*. Across environments it is not bit-identical: analyze.py -rounds every output float (1-4 decimals), and a different BLAS/thread order on the CI runner -can push a raw value across a rounding boundary, flipping the last digit by a whole step. -The comparator therefore tolerates one rounding step at each field's own precision (see -``_numbers_equal``) plus proportional/absolute float slack -- enough to be portable, while -still biting any change larger than a field's reported resolution and every -string/enum/key/list-length change. One field is excluded: -``melodyDetail.midiFile`` is the path of a transient MIDI artifact analyze writes next to -the input, so it varies per run/location and carries no measurement. Exclude a field by -adding its exact dotted path to ``EXCLUDED_PATHS`` rather than loosening the global -tolerance; excluded paths are stored in the golden as a placeholder so the key-set check -still holds and the comparator skips their values. - -Regenerating the golden after an *intentional* output change (one command, from -``apps/backend/``): +"""Regression gate over default-mode ``analyze.py`` output. + +Runs ``analyze.py`` on a fixed deterministic fixture and asserts two things against a +committed golden: + +1. **Contract / structure** -- the exact set of top-level keys and each one's type category + (null / bool / number / str / list / dict). Catches added/removed fields, a field + changing type, or a detector silently returning null. +2. **Core measurement values** -- a curated set of environment-stable, high-value Phase 1 + measurements (BPM, key, loudness, true-peak, crest, spectral balance, stereo), compared + with tolerance. + +Why not snapshot the whole output? The fixture WAV is bit-identical across machines, but a +different CPU/BLAS/FFT order on the CI runner produces last-bit-different floats. For most +scalar measurements that is absorbed by tolerance, but many *fine-grained* fields +(per-beat accent patterns, per-frame spectral series, MFCC/chroma vectors, onset counts) +sit on discrete decision boundaries and flip wholesale between machines. Snapshotting those +makes the gate flaky, not protective. A local fragility probe (tiny PCM noise) was used to +pick the curated set: only fields that never flip under perturbation are compared by value; +everything else is still covered structurally (presence + type). + +Regenerating the golden after an *intentional* change (one command, from ``apps/backend/``): UPDATE_PHASE1_GOLDEN=1 ./venv/bin/python -m unittest tests.test_phase1_golden @@ -29,7 +27,6 @@ that env var is set. """ -import copy import json import math import os @@ -47,36 +44,34 @@ _ANALYZE_PY = _BACKEND_ROOT / "analyze.py" _GOLDEN_PATH = Path(__file__).resolve().parent / "fixtures" / "golden" / "phase1_default.json" _UPDATE_ENV = "UPDATE_PHASE1_GOLDEN" +_MISSING = object() + +# Curated, environment-stable measurements compared by value. Chosen from a fragility probe +# (tiny-PCM-noise perturbation): these never crossed a tolerance/decision boundary, whereas +# fine-grained arrays/counts/vectors did and are intentionally left to the structural check. +CORE_PATHS = ( + "bpm", "key", "keyConfidence", "timeSignature", "timeSignatureSource", + "durationSeconds", "sampleRate", + "lufsIntegrated", "lufsRange", "lufsMomentaryMax", "lufsShortTermMax", + "truePeak", "plr", "crestFactor", "monoCompatible", + "spectralBalance.subBass", "spectralBalance.lowBass", "spectralBalance.lowMids", + "spectralBalance.mids", "spectralBalance.upperMids", "spectralBalance.highs", + "spectralBalance.brilliance", + "stereoDetail.stereoCorrelation", "stereoDetail.stereoWidth", + "stereoDetail.subBassCorrelation", "stereoDetail.subBassMono", +) # Numeric tolerance. Two numbers are equal iff # abs(a-g) <= max(ABS_TOL, REL_TOL*abs(g), one rounding step at g's decimal precision). -# The rounding-step term is the load-bearing one for cross-environment portability: -# analyze.py rounds every output float (1-4 decimals), and a different BLAS/thread order on -# another machine can nudge a raw value across a rounding boundary, flipping the last digit -# by a whole step (e.g. 135.4 -> 135.5). Absorbing one step at each field's own precision -# tolerates that while still biting changes larger than a field's reported resolution, plus -# every string/enum/key/list-length change (which are environment-stable). +# The rounding-step term absorbs the dominant cross-environment difference: analyze.py rounds +# every output float (1-4 decimals), and a different BLAS/thread order can nudge a raw value +# across a rounding boundary, flipping the last digit by a whole step (e.g. 135.4 -> 135.5). ABS_TOL = 1e-3 REL_TOL = 1e-3 -# Cap on reported mismatches so a large-array drift can't produce a multi-thousand-line -# failure (and so the comparator stops descending once the point is made). -MAX_REPORTED = 40 - -# Exact dotted paths to skip during comparison. Populated only from the determinism -# calibration when a field is shown to vary run-to-run / per-environment. Excluded paths -# are stored in the golden as the placeholder below (keeping the key-set check intact). -# melodyDetail.midiFile -> absolute path of a transient MIDI artifact (per-run/location). -EXCLUDED_PATHS: set[str] = {"melodyDetail.midiFile"} -_EXCLUDED_PLACEHOLDER = "" - def _write_golden_fixture(path: Path, sample_rate: int = 44_100, duration_seconds: float = 6.0) -> None: - """Deterministic A-minor harmonic bed (copied from test_analyze._write_key_fixture). - - Inlined rather than imported to keep this module self-contained and free of the - cross-test-module import-path fragility the rest of the suite avoids. No RNG. - """ + """Deterministic A-minor harmonic bed (copied from test_analyze._write_key_fixture). No RNG.""" total_samples = int(sample_rate * duration_seconds) time_axis = np.arange(total_samples, dtype=np.float32) / sample_rate signal = ( @@ -99,10 +94,7 @@ def _write_golden_fixture(path: Path, sample_rate: int = 44_100, duration_second def _run_analyze(analyze_path: Path, fixture_path: Path, extra_args: list[str]) -> tuple[str, str]: - """Run analyze.py and return (stdout, stderr). Raises AssertionError on non-zero exit. - - Inlined from test_analyze._run_analyze for self-containment. - """ + """Run analyze.py and return (stdout, stderr). Raises AssertionError on non-zero exit.""" try: completed = subprocess.run( [sys.executable, str(analyze_path), str(fixture_path), "--yes"] + extra_args, @@ -120,15 +112,36 @@ def _run_analyze(analyze_path: Path, fixture_path: Path, extra_args: list[str]) return completed.stdout, completed.stderr -def _join(path: str, key: str) -> str: - return key if not path else f"{path}.{key}" - - def _fmt(value: object) -> str: text = repr(value) return text if len(text) <= 80 else text[:77] + "..." +def _type_cat(value: object) -> str: + if value is None: + return "null" + if isinstance(value, bool): + return "bool" + if isinstance(value, (int, float)): + return "number" + if isinstance(value, str): + return "str" + if isinstance(value, list): + return "list" + if isinstance(value, dict): + return "dict" + return type(value).__name__ + + +def _dotted_get(payload: object, path: str) -> object: + current = payload + for key in path.split("."): + if not isinstance(current, dict) or key not in current: + return _MISSING + current = current[key] + return current + + def _decimals(value: float) -> int: """Decimal places in the shortest round-trip repr (0 for ints / scientific notation).""" text = repr(float(value)) @@ -145,110 +158,61 @@ def _numbers_equal(golden: float, actual: float) -> bool: if math.isinf(g) or math.isinf(a): return g == a # both +inf or both -inf rounding_step = 10.0 ** (-_decimals(g)) - # +1e-9 absorbs binary-float error when comparing clean decimal values at the step - # boundary (e.g. 0.90 - 0.89 == 0.010000000000000009, just over a 0.01 step). + # +1e-9 absorbs binary-float error comparing clean decimals at the step boundary + # (e.g. 0.90 - 0.89 == 0.010000000000000009, just over a 0.01 step). return abs(a - g) <= max(ABS_TOL, REL_TOL * abs(g), rounding_step) + 1e-9 -def _diff(golden: object, actual: object, path: str, out: list[str]) -> None: - """Recursively collect mismatches between golden and actual into ``out`` (capped).""" - if len(out) >= MAX_REPORTED or path in EXCLUDED_PATHS: - return +def _value_equal(golden: object, actual: object) -> bool: + # bool before number: isinstance(True, int) is True, so a number path would accept True==1. + if isinstance(golden, bool) or isinstance(actual, bool): + return isinstance(golden, bool) and isinstance(actual, bool) and golden == actual + if isinstance(golden, (int, float)) and isinstance(actual, (int, float)): + return _numbers_equal(golden, actual) + return golden == actual - # None first. - if golden is None or actual is None: - if not (golden is None and actual is None): - out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") - return - # bool before number: in Python isinstance(True, int) is True and True == 1, so a - # number branch would silently accept a bool/int swap. - if isinstance(golden, bool) or isinstance(actual, bool): - if not (isinstance(golden, bool) and isinstance(actual, bool) and golden == actual): - out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") - return - - if isinstance(golden, str) or isinstance(actual, str): - if not (isinstance(golden, str) and isinstance(actual, str) and golden == actual): - out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") - return - - if isinstance(golden, (int, float)) or isinstance(actual, (int, float)): - if not ( - isinstance(golden, (int, float)) - and isinstance(actual, (int, float)) - and _numbers_equal(golden, actual) - ): - out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") - return - - if isinstance(golden, dict) or isinstance(actual, dict): - if not (isinstance(golden, dict) and isinstance(actual, dict)): - out.append(f"{path or ''}: type mismatch dict vs {type(actual).__name__}") - return - golden_keys = set(golden) - actual_keys = set(actual) - for key in sorted(golden_keys - actual_keys): - if len(out) >= MAX_REPORTED: - return - out.append(f"{_join(path, key)}: missing in actual (removed key)") - for key in sorted(actual_keys - golden_keys): - if len(out) >= MAX_REPORTED: - return - out.append(f"{_join(path, key)}: unexpected in actual (added key)") - for key in sorted(golden_keys & actual_keys): - if len(out) >= MAX_REPORTED: - return - _diff(golden[key], actual[key], _join(path, key), out) - return - - if isinstance(golden, list) or isinstance(actual, list): - if not (isinstance(golden, list) and isinstance(actual, list)): - out.append(f"{path or ''}: type mismatch list vs {type(actual).__name__}") - return - if len(golden) != len(actual): - out.append(f"{path or ''}: list length {len(golden)} != {len(actual)}") - return - for index, (gv, av) in enumerate(zip(golden, actual)): - if len(out) >= MAX_REPORTED: - return - _diff(gv, av, f"{path}[{index}]", out) - return - - # Fallback for any non-JSON scalar. - if golden != actual: - out.append(f"{path or ''}: {_fmt(golden)} != {_fmt(actual)}") - - -def diff(golden: object, actual: object) -> list[str]: - """Return a (capped) list of human-readable mismatch lines; empty means match.""" +def compare(golden: dict, actual: dict) -> list[str]: + """Return human-readable mismatch lines (empty == match).""" out: list[str] = [] - _diff(golden, actual, "", out) - if len(out) >= MAX_REPORTED: - out = out[:MAX_REPORTED] - out.append(f"... (further mismatches truncated at {MAX_REPORTED})") - return out + expected_keys = set(golden["topLevelKeys"]) + actual_keys = set(actual) + for key in sorted(expected_keys - actual_keys): + out.append(f"top-level key removed: {key}") + for key in sorted(actual_keys - expected_keys): + out.append(f"top-level key added: {key}") + + expected_types = golden["topLevelTypes"] + for key in sorted(expected_keys & actual_keys): + expected = expected_types.get(key) + actual_cat = _type_cat(actual[key]) + if expected != actual_cat: + out.append(f"{key}: type {expected} -> {actual_cat}") + + for path in sorted(golden["coreValues"]): + golden_value = golden["coreValues"][path] + actual_value = _dotted_get(actual, path) + if actual_value is _MISSING: + out.append(f"{path}: missing in output") + elif not _value_equal(golden_value, actual_value): + out.append(f"{path}: {_fmt(golden_value)} != {_fmt(actual_value)}") + + return out -def _with_excluded_placeholders(payload: dict) -> dict: - """Copy ``payload`` with each present EXCLUDED_PATHS leaf set to the placeholder. - Keeps the excluded key in the golden (so the dict key-set check still passes) while - not committing the volatile value. The comparator skips the value either way. - """ - result = copy.deepcopy(payload) - for dotted in EXCLUDED_PATHS: - parts = dotted.split(".") - node = result - for key in parts[:-1]: - if isinstance(node, dict) and key in node: - node = node[key] - else: - node = None - break - if isinstance(node, dict) and parts[-1] in node: - node[parts[-1]] = _EXCLUDED_PLACEHOLDER - return result +def build_golden(actual: dict) -> dict: + core: dict[str, object] = {} + for path in CORE_PATHS: + value = _dotted_get(actual, path) + if value is _MISSING: + raise AssertionError(f"core path {path!r} not present in analyze.py output") + core[path] = value + return { + "topLevelKeys": sorted(actual), + "topLevelTypes": {key: _type_cat(value) for key, value in actual.items()}, + "coreValues": core, + } class Phase1GoldenRegressionTests(unittest.TestCase): @@ -281,7 +245,7 @@ def test_phase1_output_matches_golden(self) -> None: f"Generate it with: {_UPDATE_ENV}=1 ./venv/bin/python -m unittest tests.test_phase1_golden" ) golden = json.loads(_GOLDEN_PATH.read_text()) - mismatches = diff(golden, self.actual) + mismatches = compare(golden, self.actual) if mismatches: self.fail( "Phase 1 analyze.py output drifted from the golden snapshot.\n" @@ -294,106 +258,87 @@ def test_zzz_regenerate_golden(self) -> None: if not os.environ.get(_UPDATE_ENV): self.skipTest(f"set {_UPDATE_ENV}=1 to regenerate the golden snapshot") _GOLDEN_PATH.parent.mkdir(parents=True, exist_ok=True) - payload = _with_excluded_placeholders(self.actual) - _GOLDEN_PATH.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + _GOLDEN_PATH.write_text(json.dumps(build_golden(self.actual), indent=2, sort_keys=True) + "\n") self.skipTest(f"regenerated golden snapshot at {_GOLDEN_PATH}") class GoldenComparatorMetaTests(unittest.TestCase): - """Proves the comparator bites (and doesn't over-bite). Pure-Python, no analyze run.""" + """Proves the gate bites (and doesn't over-bite). Pure-Python, no analyze run.""" @staticmethod def _golden() -> dict: return { - "bpm": 120.0, - "key": "A minor", - "monoCompatible": True, - "stemAnalysis": None, - "spectralBalance": {"subBass": -12.5, "highs": -30.0}, - "beatGrid": [0.5, 1.0, 1.5, 2.0], + "topLevelKeys": ["bpm", "key", "monoCompatible", "spectralBalance", "stemAnalysis"], + "topLevelTypes": { + "bpm": "number", "key": "str", "monoCompatible": "bool", + "spectralBalance": "dict", "stemAnalysis": "null", + }, + "coreValues": { + "bpm": 135.4, "key": "A Minor", "monoCompatible": True, + "spectralBalance.subBass": -12.5, + }, } - def test_numeric_drift_beyond_tolerance_is_reported(self) -> None: - golden = self._golden() - actual = copy.deepcopy(golden) - actual["bpm"] = 120.0 + 10 * max(ABS_TOL, REL_TOL * 120.0) - mismatches = diff(golden, actual) + @staticmethod + def _actual() -> dict: + return { + "bpm": 135.4, "key": "A Minor", "monoCompatible": True, + "spectralBalance": {"subBass": -12.5}, "stemAnalysis": None, + } + + def test_identical_payload_matches(self) -> None: + self.assertEqual(compare(self._golden(), self._actual()), []) + + def test_core_value_drift_beyond_tolerance_is_reported(self) -> None: + actual = self._actual() + actual["bpm"] = 136.5 + mismatches = compare(self._golden(), actual) self.assertTrue(any(m.startswith("bpm:") for m in mismatches), mismatches) - def test_within_tolerance_nudge_is_silent(self) -> None: - golden = self._golden() - actual = copy.deepcopy(golden) - actual["bpm"] = 120.0 + 0.5 * max(ABS_TOL, REL_TOL * 120.0) - self.assertEqual(diff(golden, actual), []) + def test_core_within_tolerance_nudge_is_silent(self) -> None: + # A one-step cross-env rounding flip on a 1-decimal field must not bite. + actual = self._actual() + actual["bpm"] = 135.5 + self.assertEqual(compare(self._golden(), actual), []) - def test_identical_payloads_match(self) -> None: - golden = self._golden() - self.assertEqual(diff(golden, copy.deepcopy(golden)), []) + def test_string_core_drift_is_reported(self) -> None: + actual = self._actual() + actual["key"] = "C Major" + self.assertTrue(any(m.startswith("key:") for m in compare(self._golden(), actual))) - def test_added_and_removed_keys_reported(self) -> None: - golden = self._golden() - actual = copy.deepcopy(golden) - actual["newField"] = 1.0 + def test_removed_and_added_top_level_keys_reported(self) -> None: + actual = self._actual() del actual["key"] - mismatches = diff(golden, actual) - self.assertTrue(any("newField" in m and "added" in m for m in mismatches), mismatches) - self.assertTrue(any("key" in m and "removed" in m for m in mismatches), mismatches) - - def test_list_length_drift_reported(self) -> None: - golden = self._golden() - actual = copy.deepcopy(golden) - actual["beatGrid"] = [0.5, 1.0] - mismatches = diff(golden, actual) - self.assertTrue(any(m.startswith("beatGrid:") and "length" in m for m in mismatches), mismatches) - - def test_nan_inf_bool_branches(self) -> None: - nan = float("nan") - inf = float("inf") - self.assertEqual(diff({"x": nan}, {"x": nan}), []) # both NaN -> equal - self.assertEqual(diff({"x": inf}, {"x": inf}), []) # both +inf -> equal - self.assertNotEqual(diff({"x": nan}, {"x": 1.0}), []) # NaN vs number - self.assertNotEqual(diff({"x": inf}, {"x": -inf}), []) # +inf vs -inf - self.assertNotEqual(diff({"x": True}, {"x": 1}), []) # bool vs int - self.assertNotEqual(diff({"x": False}, {"x": 0.0}), []) # bool vs float - - def test_type_mismatch_reported(self) -> None: - self.assertNotEqual(diff({"x": {"a": 1}}, {"x": [1]}), []) # dict vs list - self.assertNotEqual(diff({"x": "s"}, {"x": 1.0}), []) # str vs number - self.assertNotEqual(diff({"x": 1.0}, {"x": None}), []) # number vs None - - def test_rounding_boundary_flip_is_tolerated_but_real_change_bites(self) -> None: - # 1-decimal field (step 0.1): a one-step cross-env flip is tolerated, >1 step bites. - self.assertEqual(diff({"x": 135.4}, {"x": 135.5}), []) - self.assertNotEqual(diff({"x": 135.4}, {"x": 135.6}), []) - # 2-decimal field (step 0.01): one-step tolerated, larger bites. - self.assertEqual(diff({"x": 0.89}, {"x": 0.90}), []) - self.assertNotEqual(diff({"x": 0.89}, {"x": 0.92}), []) - # Small-magnitude 1-decimal value (the case the tight tolerance used to fail on). - self.assertEqual(diff({"x": 0.2}, {"x": 0.3}), []) - self.assertNotEqual(diff({"x": 0.2}, {"x": 0.5}), []) - - def test_excluded_path_is_ignored(self) -> None: - self.assertIn("melodyDetail.midiFile", EXCLUDED_PATHS) - golden = {"melodyDetail": {"midiFile": _EXCLUDED_PLACEHOLDER, "confidence": 0.5}} - actual = {"melodyDetail": {"midiFile": "/tmp/whatever/x_melody.mid", "confidence": 0.5}} - self.assertEqual(diff(golden, actual), []) - # A non-excluded sibling still bites. - actual["melodyDetail"]["confidence"] = 0.9 - self.assertNotEqual(diff(golden, actual), []) - - def test_with_excluded_placeholders_sets_leaf(self) -> None: - payload = {"melodyDetail": {"midiFile": "/tmp/abc/x_melody.mid", "confidence": 0.5}} - result = _with_excluded_placeholders(payload) - self.assertEqual(result["melodyDetail"]["midiFile"], _EXCLUDED_PLACEHOLDER) - self.assertEqual(result["melodyDetail"]["confidence"], 0.5) - self.assertEqual(payload["melodyDetail"]["midiFile"], "/tmp/abc/x_melody.mid") # unmutated - - def test_cap_truncation(self) -> None: - golden = {f"k{i:03d}": float(i) for i in range(200)} - actual = {key: value + 5.0 for key, value in golden.items()} - mismatches = diff(golden, actual) - self.assertLessEqual(len(mismatches), MAX_REPORTED + 1) - self.assertTrue(any("truncated" in m for m in mismatches), mismatches) + actual["newField"] = 1.0 + mismatches = compare(self._golden(), actual) + self.assertTrue(any("removed: key" in m for m in mismatches), mismatches) + self.assertTrue(any("added: newField" in m for m in mismatches), mismatches) + + def test_top_level_type_change_reported(self) -> None: + actual = self._actual() + actual["stemAnalysis"] = {"kick": {}} # null -> dict (e.g. separation accidentally ran) + self.assertTrue(any(m.startswith("stemAnalysis: type null -> dict") for m in compare(self._golden(), actual))) + + def test_missing_core_path_reported(self) -> None: + actual = self._actual() + actual["spectralBalance"] = {} # subBass gone + mismatches = compare(self._golden(), actual) + self.assertTrue(any("spectralBalance.subBass: missing" in m for m in mismatches), mismatches) + + def test_bool_core_not_equal_to_number(self) -> None: + actual = self._actual() + actual["monoCompatible"] = 1 # bool True vs int 1 must bite + self.assertTrue(any(m.startswith("monoCompatible:") for m in compare(self._golden(), actual))) + + def test_numbers_equal_rounding_and_nonfinite(self) -> None: + self.assertTrue(_numbers_equal(135.4, 135.5)) # one 0.1 step -> tolerated + self.assertFalse(_numbers_equal(135.4, 135.7)) # >1 step -> bites + self.assertTrue(_numbers_equal(0.89, 0.90)) # one 0.01 step -> tolerated + self.assertFalse(_numbers_equal(0.89, 0.92)) # >1 step -> bites + self.assertTrue(_numbers_equal(float("nan"), float("nan"))) + self.assertFalse(_numbers_equal(float("nan"), 1.0)) + self.assertTrue(_numbers_equal(float("inf"), float("inf"))) + self.assertFalse(_numbers_equal(float("inf"), float("-inf"))) if __name__ == "__main__":