Skip to content

feat(meter): loudness-accent stream for meter + downbeats on non-4/4 (PR-G4) - #213

Merged
slittycode merged 4 commits into
mainfrom
accuracy/genre-meter-downbeats
Jul 13, 2026
Merged

feat(meter): loudness-accent stream for meter + downbeats on non-4/4 (PR-G4)#213
slittycode merged 4 commits into
mainfrom
accuracy/genre-meter-downbeats

Conversation

@slittycode

Copy link
Copy Markdown
Owner

What

PR-G4 of the genre-generalization program (stacked on #212): meter and downbeat detection learn to hear loudness accents, closing the measured odd-meter gaps. The onset-count accent stream that analyze_time_signature ran on is loudness-blind — a kick on every beat yields identical counts at every bar position no matter which beat is accented, which is exactly how the corpus odd-meter clips (and most real four-on-the-floor material in 3/4, 6/8, 7/8) are shaped. Measured before this PR: 3/4's best candidate edged 4/4 by 0.8% (dominance 1.067 vs 1.059, position means all ~1.0), nowhere near the 20% override margin, shipping 4/4 at confidence 0.03.

Design

  1. Loudness-accent stream: the per-beat low-band (kick) loudness from beat_data — the same signal the downbeat phase heuristic already uses — folded at each candidate bar length, dominance maximized over bar phase, clamped to 10 (near-silent off-beat positions explode the ratio without carrying more information), neutral (1.0) when the fold has fewer than 4 bars or the caller has no beat_data.
  2. Harmonic collapse: a 3-periodic accent folded at bar 6 scores high but describes the shorter bar twice. The discriminator is the echo position's prominence — how far the position at (peak + divisor) rises above the remaining positions — because the raw echo/peak ratio measures ~0.72 for BOTH true 3/4 and true 6/8 (baseline noise), while prominence measures 0.262 vs 0.000 on the corpus clips.
  3. Decision: the shipped meter now picks the best count-dominance × loudness-dominance product, overriding 4/4 at a 15% margin when loudness contributed (calibrated: 6/8's true combined margin is ~17%) and the original 20% count-only margin otherwise — legacy callers and --fast (which pass no beat_data) are byte-identical.
  4. Pipeline: beat_data extraction is hoisted above the meter call in analyze.py (one extraction, same object reused by rhythm detail/groove/sidechain as before).
  5. Evidence surface: timeSignatureCandidates entries gain an additive loudnessDominance key, landed on both sides (schema doc, TS type, client parser — which would otherwise have silently dropped it, tripwire Preserve signal monitor playback during analysis and harden run flow #3 — and the full-payload fixture).

Measured burn-down (the PR's gate)

Clip Before After
grid_3_4_90 meter 4/4 ✗ (gap), downbeats F1 0.286 (gap) meter 3/4 ✓, downbeats 0.875 ✓ — both now gating
grid_6_8_110 meter 4/4 ✗ (gap), downbeats 0.400 (gap) meter 6/8 ✓, downbeats 0.875 ✓ — both now gating
grid_7_8_140 meter 4/4 ✗ (gap), downbeats 0.182 (gap) meter 7/8 ✓, downbeats 0.875 ✓ — both now gating (tempo smear 142.6 stays a known gap)
all ten 4/4 rhythm clips (incl. 2-step, halftime, breakbeat broken kicks) 4/4 4/4 — no regressions, broken-kick clips at confidence 1.0

_KNOWN_GAPS_BY_ID shrinks from 5 entries / 11 checks to 4 entries / 9 checks; every remaining entry is tempo-family (octave halving at 174/190, halftime-174's 2:3 error, 7/8's smear) owned by the PR-G3 promotion gate, plus the PR-G5 shuffle gap.

Behavior change disclosure

This PR changes the shipped timeSignature (and therefore downbeats) on material with loudness-accented non-4/4 bars — that is its purpose. The pre-registered gate is the synthetic corpus: all 38 clips green, 113 active checks passed / 0 failed / 10 informational. What it does NOT change: any caller without beat_data (including --fast), the golden (passes without re-baseline — the pipeline reorder shifted no curated value), or 4/4 material with flat or broken-kick accents (measured: all ten 4/4 rhythm clips unchanged). Honest caveat: the 15% accent margin is calibrated on synthetic accents; real-audio meter accuracy has no staged corpus yet (GTZAN-Rhythm is beat/downbeat-annotated and can serve the downbeat half via the existing beat-eval harness when the beat_this gate work resumes).

Validation

  1. Backend: full unittest discover — 1357 tests, OK (3 skipped) (exit code captured directly, not piped).
  2. Synthetic eval: 38/38 clips, 113 active passed / 0 failed / 10 informational.
  3. CI-mirror fundamentals gate (default manifest): 15/15, allPassed.
  4. UI npm run verify: 51 smoke passed (one deep-equal unit test updated for the new candidate key).
  5. New unit tests: 8 in test_time_signature_accent.py (dominance math, neutrality guards, cap, harmonic collapse both directions, no-beat_data fallback).

🤖 Generated with Claude Code

slittycode and others added 3 commits July 13, 2026 05:59
…acy PR-G3)

Adds bpmOctaveEvidence (full-mode only): simple-ratio candidates
(1:2, 2:3, 1:1, 3:2, 2:1 of the shipped bpm, clamped 40-220) scored
against two inter-onset streams — low-band kick articulation (integer
multiples, fit divided by the median multiple) plus the full-band
hat/snare grid (admits the 8th-note 0.5 multiple), which pins the
notated tempo when the kick only marks bars. Never overrides bpm
(invariant #1); abstains (null) on sparse/beatless material.

Measured on the corpus: recovers all three broken octave cases
(174 -> 173.8, 190 -> 189.8, halftime-174 -> 175.5 via 3:2) with zero
false alarms on the 10 correctly-shipped rhythm clips; the beatless
ambient clip abstains. Promoted as an active tempoOctave:preferredBpm
gate on 14 rhythm clips (13 pass; grid_7_8_140 stays informational —
its bpm is meter-smeared, not octave-wrong: PR-G4 territory).

Both sides landed: JSON_SCHEMA.md + full-only docstring, measurement.ts
types, backendPhase1Client reconstructor, phase1FullPayload fixture;
golden deliberately re-baselined (bpmOctaveEvidence key + one
rounding-step lufsShortTermMax nudge within designed tolerance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o-fixture key set

Two omissions from PR-G3 that CI caught on the rebased branch:
- server_phase1._build_phase1 did not forward bpmOctaveEvidence to the
  frontend (test_phase1_golden.BuildPhase1KeySupersetTests caught it)
- EXPECTED_TOP_LEVEL_KEYS in test_audio_fixture.py did not include it
  (AudioFixtureSmokeTest.test_output_contains_all_expected_keys caught it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCtxwnhn8nzHBjzeehPtf8
…(accuracy PR-G4)

The onset-count accent stream is loudness-blind: a kick on every beat
yields identical counts at every bar position regardless of which beat
is accented — measured on the odd-meter corpus clips as 3/4 edging 4/4
by 0.8%, shipping 4/4 at confidence 0.03. analyze_time_signature now
folds the per-beat low-band loudness (beat_data, hoisted above the
meter call) at each candidate bar length as a second accent stream:
max-over-phase dominance, clamped at 10, neutral under 4 bars, harmonic
folds collapsed by echo PROMINENCE (0.262 true-3/4 vs 0.000 true-6/8
measured; raw echo/peak is ~0.72 for both). Decision runs on the
count x loudness product with a 15% override margin when loudness
contributed; count-only callers (including --fast) keep the 20% margin
byte-identical.

Burn-down: 3/4, 6/8, 7/8 meter AND downbeats (F1 0.875 each) flip from
knownGaps to gating checks; all ten 4/4 rhythm clips (incl. broken-kick
2-step/halftime/breakbeat) unchanged. Remaining gaps are all
tempo-family. timeSignatureCandidates entries gain an additive
loudnessDominance key, landed on both sides; golden passes without
re-baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from accuracy/genre-tempo-octave-evidence to main July 13, 2026 06:37
…R-G4

# Conflicts:
#	apps/backend/scripts/build_synthetic_corpus.py
#	apps/backend/tests/fixtures/fundamentals_eval_manifest.synthetic.json
@slittycode
slittycode merged commit b5bb5c5 into main Jul 13, 2026
5 checks passed
@slittycode
slittycode deleted the accuracy/genre-meter-downbeats branch July 13, 2026 09:39
slittycode added a commit that referenced this pull request Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants