Skip to content

Measure tempo, key and energy from a track's sound - #47

Merged
davior merged 1 commit into
mainfrom
claude/admiring-planck-ggdijx
Oct 26, 2026
Merged

davior merged 1 commit into
mainfrom
claude/admiring-planck-ggdijx

Conversation

@davior

@davior davior commented Oct 26, 2026

Copy link
Copy Markdown
Owner

This fills the music profile (davior/gam#46) for tracks whose tags say nothing, which is most music from free libraries. GAM measures tempo, key and energy from the audio itself, using plain numpy over ffmpeg-decoded PCM, with no new dependency.

What it measures (app/ingest/audio_analysis.py)

How
Tempo An onset curve (how much new sound arrives in each frame) is autocorrelated to find the beat period. Each candidate between 50 and 210 BPM is scored together with its first four multiples, and weighted towards 110 BPM to settle octave ties.
Key The track's 12-note pitch profile is compared against the 24 Krumhansl–Kessler key profiles.
Energy low / medium / high, from a blend of loudness, brightness and drive. Drive comes from the measured tempo and beat strength; a beatless track has none.

Not attempted:

  • Vocals. Telling singing from a lead instrument takes a trained model, and a wrong guess would put songs under narration.
  • Genre and mood. These need a person or a tagger.

Gates, so talks and noise get nothing:

  • A tempo needs a beat strength of at least 0.3. A slow pad whose chords change every few seconds reached 0.24 in testing.
  • Without a beat, a key only counts as music at a correlation of 0.75 or more. Pitched speech reached 0.65.
  • If neither is found, nothing is written.

How it's used

  • Fills blanks only, after the file's tags have been applied, and is stamped analysis in field_provenance. That is a new fourth value beside human, ai and embedded. A tag or a hand edit always wins.
  • After upload: queued automatically for audio between 8 s and 20 min long. Longer files are recordings, not tracks.
  • Per track: a "Measure tempo, key and energy" button in the Music tab (POST /api/assets/{id}/analyze-music).
  • Whole library: a new Settings → Fill in metadata panel (POST /api/assets/analyze-music). The same panel gives the existing tag harvest (harvest_attribution) its first button; until now it could only be started through the API.
  • The activity feed gets labels for all three jobs.

Accuracy

I had no access to real music from the build environment, so this was checked against synthetic tracks with a known tempo and key:

  • Tempo: exact from 92 to 150 BPM. 72 and 170 can read an octave off (144 and 85), the ambiguity every tempo method has. The tests accept an octave there and nothing else.
  • Key: exact on major progressions and on minor ones with a harmonic-minor V.
    • Three of the four minor progressions in my first test set (i–VI–III–VII) were reported as the relative major. That progression uses the same four chords as its relative major, so it really is ambiguous.
  • Speed: about a second per track. It reads two minutes, starting after a 20-second intro.
  • Worth trying on your own library: real recordings will correlate lower than synthetic ones, so the gates may turn out a little strict.

Design notes are in docs/music-profile.md.

Testing

  • pytest -q: 1344 passed. The new tests/test_audio_analysis.py uses seeded synthesised audio from tests/music_synth.py and covers:
    • tempo, key and energy measurement;
    • pads, speech, noise and silence;
    • decoding an encoded MP3;
    • the job running after upload;
    • tags and hand edits being kept;
    • the endpoints and the library run.
  • npm run format:check, npm run lint, npm test (418 passed, including the new LibraryScanPanel.test.tsx and Music tab button tests) and npm run build all clean.

Generated by Claude Code

For the music whose tags say nothing. Plain numpy over ffmpeg-decoded PCM,
no new dependency:

- Tempo: autocorrelation of a spectral-flux onset curve, scored with its
  first four multiples and a log-normal prior at 110 BPM.
- Key: pitch-class profile against the 24 Krumhansl-Kessler profiles.
- Energy: loudness, brightness and drive, in three levels.

Gated so talks and noise get nothing: a tempo needs a real beat, and a key
without one needs a strong correlation. Fills blanks only, after tags, and
is stamped "analysis" in field_provenance.

Queued after every song-length audio upload, runnable per track from the
Music tab, and library-wide from a new Settings panel, which also gives
the existing tag harvest its first button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCL4jm6ogZBBhnjEfBBaXU
@davior
davior marked this pull request as ready for review October 26, 2026 10:50
@davior
davior merged commit 33e3436 into main Oct 26, 2026
3 checks passed
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