foo_rubato ~ Foobar2000 Rubato BPM Analyzer


Detects the tempo of a track and which of Tango, Vals, Milonga it is – or none of the three – from the audio alone, without reading the genre tag.

The two answers are linked. The tempo a dancer taps is not a property of the audio by itself: a tango is tapped on the beat, a vals once per 3/4 bar, a milonga once per 2/4 bar, a reggae on the quarter note – under the skank rather than on it. So the rhythm is settled first and the tempo reported on the level that rhythm implies. Measured against 3,692 hand-tapped tracks the estimate lands within 2 BPM of the tap 88.7% of the time, which is about as close as the same person tapping the same track twice; rhythm classification is 93.6% accurate. docs/tango-analysis.md has the method and the full numbers, and docs/cortina-genres.md covers the cortina genres that were measured and left alone.

Originally written by Michael Balzer+Holger Stenger as BPM Analyser, but bpmcore engine was completely rewritten.

github.com/shaforostoff/foo_rubato
hydrogenaudio.org/index.php/topic,129923.0

References:

en.wikipedia.org/wiki/Tempo_rubato

earchord ~ Find Song Chords Online


Drop a track. EarChord reads the chord progression, the key and the tempo. The audio is analysed on this device and never leaves your browser.

earchord.com

acidcat ~ Audio File Forensics


A pure-Python inspector, editor, and forensic tool for audio files and synth/DAW presets: read the metadata, decode the format structure byte by byte, flag anomalies, repair broken containers, and identify how a file was made.

Reads BPM, key, duration, tags, and format info from WAV, AIFF, MP3, FLAC, OGG, Opus, M4A, MIDI, and Serum presets. Also structurally decodes Bitwig (.bwpreset/.bwclip), Native Instruments (Massive/Absynth/Kontakt/NKS/KORE), Vital, NCW, SoundFont (SF2/SF3), tracker modules (MOD/XM/IT), MP4, VST FXP, ReCycle RX2, and RMID containers via inspect. Beyond reading: repair and validate fix and check container structure (stale sizes, offset tables, counts, pad bytes) without touching a byte of audio, and audit gives a forensic verdict (structure, integrity, hidden data, and the writing tool). One pure-Python dependency (mutagen); the native inspect walkers need nothing. Optional librosa analysis for BPM/key detection and ML feature extraction.

Also ships per-library SQLite indexes (acidcat index) tracked in a small global registry, plus an MCP server (acidcat-mcp) so an LLM can query your whole collection across libraries by bpm, key, tags, or full-text.

github.com/hed0rah/acidcat
github.com/hed0rah/acidcat-playground
www.databurn.org

Pitch Playground ~ Intonation, Pitch, Rhythm & Tuning Training


Pitch Playground is a free, ad-free collection of simple tools for musical tuning, rhythm, pitch, and intonation ear training.

Tuning and reference pitches

Use the tuner to check the pitch of a note with your microphone, or play reference tones to practice matching and tuning by ear. The reference pitch is adjustable, so you are not limited to A440.

Pitch and intonation ear training

Practice hearing small differences in pitch and recognizing whether a note is sharp or flat. The exercises are intended to develop pitch discrimination and a more accurate sense of intonation without requiring perfect pitch.

Metronome and rhythm practice

Use the metronome for a steady beat while practicing, choose from common simple, compound, and odd meters, or tap a tempo to set the BPM by feel.

Simple by design

Pitch Playground is designed to be a small, straightforward set of music practice tools: open it, choose a tab, and start practicing. Works well on desktop and mobile.

Free forever

Pitch Playground will always remain free, ad-free, and without in-app purchases. To run this site locally, download and unzip the latest release, which can be hosted on a webserver like nginx or by directly opening index.html.

p.jeltz.org
github.com/qwell/pitch-playground

Tonalux Audio Analyzer ~ Clientside Analyzation


LUFS, True Peak, BPM, Key, Stereo Width, Frequency Spectrum — everything runs locally in your browser. No uploads.

tonalux.org/analyzer

Drum Machine ~ Create & Play Drum Beats


Drum Machine is a modern and intuitive application for creating, playing, and managing drum patterns. Perfect for musicians, producers, and anyone interested in rhythm creation, this application provides a simple interface for drum pattern programming. Have fun!

Features:

  • Intuitive grid-based pattern editor with infinite pages
  • Custom samples: Add your own drum sounds with MIDI note mapping
  • Adjustable BPM control
  • Volume control for overall mix
  • Save and load patterns
  • Multiple drum sounds including kick, snare, hi-hat, and more
  • Audio and MIDI export with support for WAV, FLAC, Ogg, MP3, and MIDI formats
  • Metadata support for embedding artist name, song title, and cover art
  • Keyboard shortcuts for quick access to all functions

apps.gnome.org/DrumMachine
github.com/revisto/drum-machine

Foobar2000 ~ Album Based Beats Per Minute


Beats Per minute is great for playlist of songs sorted by BPM but it would be useful to sort albums by BPM as well. To do this the following script calculates an average of BPM information from each song of the album and write that value to ALBUM_AVG_BPM. Then just create a playlist sorted by artist then album_avg_bpm

SMP Album Average BPM Calculator Script

In the DUI, toggle layout and create a new panel or tab, then right click the new area and add a New UI Element, scroll down to the Utility section and select Spider Monkey Panel and click OK. Now right click the new Spider Monkey Panel and select Edit Panel Script. Select the entire contents on the new ‘Temporary File’ window and replace them with the following script.

Show full script

'use strict';

/*
====================================================
Album Average BPM Panel - FULL PRO v5
- Immediate feedback for buttons
- Status line in report section
- Progress bars appear immediately
- Logs skipped files
- Inconsistent albums listed
====================================================
*/

function RGB(r,g,b){ return (0xff000000 | (r<<16) | (g<<8) | b); }

////////////////////////////////////////////////////
// TitleFormats
////////////////////////////////////////////////////
const tf_album = fb.TitleFormat("%album%");
const tf_albumArtist = fb.TitleFormat("%album artist%");
const tf_artist = fb.TitleFormat("%artist%");
const tf_bpm = fb.TitleFormat("%bpm%");
const tf_existing = fb.TitleFormat("%album_avg_bpm%");

////////////////////////////////////////////////////
// UI Colours + Font
////////////////////////////////////////////////////
function getUIColours(){
    try{
        return { bg: window.GetColourDUI(1), text: window.GetColourDUI(0), accent: window.GetColourDUI(2) };
    }catch(e){
        return { bg: RGB(30,30,30), text: RGB(255,255,255), accent: RGB(0,200,0) };
    }
}

function getUIFont(){ try{ return window.GetFontDUI(0); } catch(e){ return gdi.Font("Segoe UI",13,0); } }

////////////////////////////////////////////////////
// Library cache
////////////////////////////////////////////////////
let libHandles = null;
function loadLibraryOnce(){ if(!libHandles) libHandles = fb.GetLibraryItems(); }

////////////////////////////////////////////////////
// State
////////////////////////////////////////////////////
let runningUpdate=false;
let reporting=false;
let updateIndex=0, updateTimer=0;
let reportIndex=0, reportTimer=0;
let currentAlbum="Idle", currentFile="";
let updates=0;

let totalFiles=0, totalAlbums={};
let missingBPM=0, zeroBPM=0, missingAlbumAvg=0;
let minBPM=999999, maxBPM=0;
let albumConsistency={}, inconsistentAlbums=0;
let skippedFiles=[];

let forceRecalc=false, useMedian=false;

////////////////////////////////////////////////////
// Layout
////////////////////////////////////////////////////
const LEFT_WIDTH = 280;
const buttonScan={x:20,y:50,w:220,h:50};
const buttonUpdate={x:20,y:110,w:220,h:50};
const buttonCopy={x:20,y:170,w:220,h:50};
const checkboxForce={x:20,y:240,size:20};
const checkboxMedian={x:20,y:270,size:20};

////////////////////////////////////////////////////
// Album BPM Update Logic
////////////////////////////////////////////////////
let albumsMap=[];

function buildAlbums(){
    loadLibraryOnce();
    albumsMap = [];
    let map={};

    for(let i=0;i400) continue; // Filter extreme BPMs

        let groupingArtist = albumArtist ? albumArtist : artist;
        let key = groupingArtist+"|||"+album;

        if(!map[key]){
            map[key]={ handles:new FbMetadbHandleList(), sum:0, count:0, hasExisting:true, existingValues:{} };
        }

        map[key].handles.Add(h);
        map[key].sum += bpm;
        map[key].count++;

        let albumAvg = tf_existing.EvalWithMetadb(h);
        if(albumAvg) map[key].existingValues[albumAvg]=true;

        if(!albumAvg) map[key].hasExisting=false;
    }

    albumsMap = Object.values(map);
}

function stopUpdateTimer(){ if(updateTimer){ window.ClearInterval(updateTimer); updateTimer=0; } }

function processUpdateNext(){
    if(!runningUpdate) return;

    if(updateIndex>=albumsMap.length){
        runningUpdate=false;
        stopUpdateTimer();
        currentAlbum="Finished";
        fb.ShowPopupMessage("Album BPM Update Finished. Updated Albums: "+updates);
        window.Repaint();
        return;
    }

    let data = albumsMap[updateIndex++];
    currentAlbum = tf_album.EvalWithMetadb(data.handles[0]);

    // Determine if update needed
    let inconsistent = Object.keys(data.existingValues).length > 1;
    if(!forceRecalc && !inconsistent) return;

    let avg=0;
    if(useMedian){
        let arr=[];
        for(let i=0;i<data.handles.Count;i++){
            let val=parseFloat(tf_bpm.EvalWithMetadb(data.handles[i]));
            if(!isNaN(val) && vala-b);
        let mid=Math.floor(arr.length/2);
        avg = arr.length%2===0 ? (arr[mid-1]+arr[mid])/2 : arr[mid];
    }else{
        avg = data.sum / data.count;
    }
    avg = avg.toFixed(2);

    let json=[];
    for(let i=0;i<data.handles.Count;i++){
        try{
            json.push({"ALBUM_AVG_BPM":avg});
        }catch(e){
            skippedFiles.push(data.handles[i].Path);
        }
    }

    try{
        data.handles.UpdateFileInfoFromJSON(JSON.stringify(json));
        updates++;
    }catch(e){
        for(let i=0;i0){
        currentAlbum = tf_album.EvalWithMetadb(albumsMap[0].handles[0]);
    }

    window.Repaint();
    updateTimer = window.SetInterval(processUpdateNext,100);
}

////////////////////////////////////////////////////
// Report Logic
////////////////////////////////////////////////////
let reportText="Press Scan Library.";

function stopReportTimer(){ if(reportTimer){ window.ClearInterval(reportTimer); reportTimer=0; } }

function processReportNext(){
    if(!reporting) return;
    let batch=500;

    for(let c=0;c<batch && reportIndex0){
                if(bpmmaxBPM) maxBPM=bpm;
            }
        }
        if(!albumAvg) missingAlbumAvg++;

        if(!albumConsistency[key]) albumConsistency[key]={};
        if(albumAvg) albumConsistency[key][albumAvg]=true;

        currentFile = groupingArtist+" - "+album;
    }

    if(reportIndex>=libHandles.Count){
        reporting=false;
        stopReportTimer();

        inconsistentAlbums=0;
        let inconsistentList=[];
        for(let k in albumConsistency){
            let vals=Object.keys(albumConsistency[k]);
            if(vals.length>1){
                inconsistentAlbums++;
                if(inconsistentList.length0?inconsistentList.join("\n"):"None"}

Skipped / Inaccessible Files:
${skippedFiles.length>0?skippedFiles.join("\n"):"None"}

Current Status: Idle

BPM Range:
- Lowest BPM found: ${minBPM===999999?"N/A":minBPM}
- Highest BPM found: ${maxBPM}`;
    }

    window.Repaint();
}

function startReport(){
    if(reporting) return;
    loadLibraryOnce();
    reportIndex=0;
    totalFiles=0; totalAlbums={};
    missingBPM=0; zeroBPM=0; missingAlbumAvg=0;
    minBPM=999999; maxBPM=0; albumConsistency={};
    skippedFiles=[];

    reporting=true;
    reportText="Scanning library...";

    // show first file immediately
    if(libHandles.Count>0){
        currentFile = tf_album.EvalWithMetadb(libHandles[0]) || "(No Album)";
    }

    window.Repaint();
    reportTimer = window.SetInterval(processReportNext,10);
}

////////////////////////////////////////////////////
// Drawing
////////////////////////////////////////////////////
function drawButton(gr,b,label,ui,font){
    gr.FillSolidRect(b.x,b.y,b.w,b.h,ui.accent);
    gr.DrawString(label,gdi.Font(font.Name,14,1),ui.text,b.x,b.y,b.w,b.h,0x11000000);
}

function drawCheckbox(gr,c,label,state,ui,font){
    gr.DrawRect(c.x,c.y,c.size,c.size,1,ui.text);
    if(state) gr.FillSolidRect(c.x+4,c.y+4,c.size-8,c.size-8,ui.accent);
    gr.DrawString(label,font,ui.text,c.x+28,c.y-2,300,24,0);
}

function on_paint(gr){
    let ui=getUIColours();
    let font=getUIFont();
    let titleFont=gdi.Font(font.Name,16,1);

    gr.FillSolidRect(0,0,window.Width,window.Height,ui.bg);

    // LEFT PANEL
    gr.DrawString("Album Avg BPM Tools",titleFont,ui.text,20,10,LEFT_WIDTH,30,0);
    drawButton(gr,buttonScan,"Scan Library",ui,font);
    drawButton(gr,buttonUpdate,"Run Album BPM Update",ui,font);
    drawButton(gr,buttonCopy,"Copy Report",ui,font);
    drawCheckbox(gr,checkboxForce,"Force Recalculate",forceRecalc,ui,font);
    drawCheckbox(gr,checkboxMedian,"Use Median Averaging",useMedian,ui,font);

    gr.DrawLine(LEFT_WIDTH,0,LEFT_WIDTH,window.Height,1,ui.text);

    // RIGHT PANEL - report text
    let statusText = reporting ? "Scanning: "+currentFile : runningUpdate ? "Updating Album BPM: "+currentAlbum : "Idle";
    let reportWithStatus = reportText.replace("Current Status: Idle","Current Status: "+statusText);

    gr.GdiDrawText(reportWithStatus,font,ui.text,
        LEFT_WIDTH+20,20,
        window.Width-(LEFT_WIDTH+40),
        window.Height-160,0); // leave 160px for progress bars

    // Progress bars
    const barHeight = 18;
    const margin = 8;
    const barYUpdate = window.Height - 2*barHeight - 2*margin;
    const barYScan   = window.Height - barHeight - margin;

    if(runningUpdate){
        let prog=Math.floor((updateIndex/albumsMap.length)*(window.Width-LEFT_WIDTH-40));
        gr.FillSolidRect(LEFT_WIDTH+20,barYUpdate,prog,barHeight,ui.accent);
        gr.DrawRect(LEFT_WIDTH+20,barYUpdate,window.Width-LEFT_WIDTH-40,barHeight,1,ui.text);
        gr.DrawString("Updating Album BPM: "+currentAlbum,font,ui.text,LEFT_WIDTH+20,barYUpdate-25,window.Width-LEFT_WIDTH-40,20,0);
    }

    if(reporting){
        let prog=Math.floor((reportIndex/libHandles.Count)*(window.Width-LEFT_WIDTH-40));
        gr.FillSolidRect(LEFT_WIDTH+20,barYScan,prog,barHeight,ui.accent);
        gr.DrawRect(LEFT_WIDTH+20,barYScan,window.Width-LEFT_WIDTH-40,barHeight,1,ui.text);
        gr.DrawString("Scanning: "+currentFile,font,ui.text,LEFT_WIDTH+20,barYScan-25,window.Width-LEFT_WIDTH-40,20,0);
    }
}

////////////////////////////////////////////////////
// Mouse
////////////////////////////////////////////////////
function on_mouse_lbtn_up(x,y){
    if(hit(buttonScan,x,y)) {
        startReport();
        reportText = "Scanning library...";
        currentFile = libHandles.Count>0 ? tf_album.EvalWithMetadb(libHandles[0]) || "(No Album)" : "Idle";
        window.Repaint();
    }
    if(hit(buttonUpdate,x,y)) {
        startUpdate();
        currentAlbum = albumsMap.length>0 ? tf_album.EvalWithMetadb(albumsMap[0].handles[0]) : "Preparing update...";
        window.Repaint();
    }
    if(hit(buttonCopy,x,y) && reportText){
        utils.SetClipboardText(reportText);
        fb.ShowPopupMessage("Report copied to clipboard.");
    }

    if(hitSquare(checkboxForce,x,y)){ forceRecalc=!forceRecalc; window.Repaint(); }
    if(hitSquare(checkboxMedian,x,y)){ useMedian=!useMedian; window.Repaint(); }
}

function hit(b,x,y){ return x>=b.x && x=b.y && y=c.x && x=c.y && y<=c.y+c.size; }

Album Avg BPM Tools Script

github.com/tom2tec/foobar2000-smp-album-average-bpm

Resources:

https://github.com/stengerh/foo_bpm
https://github.com/NotSimone/foo_cnn_bpm
https://github.com/tom2tec/foobar2000-smp-album-average-bpm

Foobar2000 ~ Not-A-Waveform-Seekbar-SMP


Seekbar for foobar2000, using Spider Monkey and ffmpeg or audiowaveform. It’s based on RMS or peak levels, instead of the actual waveform.

Features:

  • Uses audiowaveform by default (included).
  • ffprobe can be used if desired. Download it and copy ffprobe.exe into ‘helpers-external\ffprobe’.
  • Visualizer mode to simply show an animation which changes according to BPM (if tag exists).
  • VU Meter mode by RMS or peak levels.
  • Fully configurable using the R. Click menu:
    • Colors
    • Waveform modes
    • Analysis modes
    • VU Meter
    • Animations
    • Multi-channel display
    • Refresh rate (not recommended anything below 100 ms except on really modern CPUs)

github.com/regorxxx/Not-A-Waveform-Seekbar-SMP

FRKB ~ Rapid Audio Organization Tool


FRKB is a cross-platform desktop application designed for audio professionals (such as DJs). The current beta version is compatible with Windows and will be adapted for macOS once stable. It is still under active development.

Core Features:

  • Portable: Easily transfer the database to mobile devices for on-the-go use.
  • Audio Fingerprint Deduplication: Identify and exclude duplicate tracks using audio fingerprint technology, providing prompts during import to keep your music collection clean and efficient.
  • Ergonomic Shortcuts: Ergonomically designed shortcuts that allow most operations to be performed with the left hand, making the organization process smoother and more efficient.
  • Direct File Management: When adding tracks, FRKB directly manages the audio files themselves, ensuring that the organization results are immediately reflected in the computer’s folders, achieving a “what you see is what you get” effect.
  • Waveform Visualization: Provides audio waveform display.
  • BPM Analysis: Displays BPM information.

github.com/coderDJing/FRKB_Rapid-Audio-Organization-Tool

Tunebat ~ Song Key & BPM Finder


Upload your audio files to find the key and tempo of the tracks in your library. This is a tool for DJs interested in harmonic mixing, producers looking to remix songs, and anyone trying to understand their music a little better.

tunebat.com/Analyzer

BeatRoot ~ Beat Tracking & Visualization


BeatRoot is an interactive beat tracking and visualisation system.

code.soundsoftware.ac.uk/projects/beatroot
www.eecs.qmul.ac.uk/~simond/beatroot
en.wikipedia.org/wiki/BeatRoot