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

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

Metronomek ~ Digital Metronome That Tries To Be Analogue


Trivial looking metronome with natural sounds and ‘classical’ approach.

Features:

  • High quality recorded natural sounds of different beat kinds
  • Handling similar to mechanical devices
  • Changing time signature and visual counting

metronomek.sourceforge.io
sourceforge.net/projects/metronomek
github.com/SeeLook/metronomek
flathub.org/en/apps/net.sf.metronomek

Transport ~ Transport Info Display


A handy little tool which provides all the information about measure, time, sample count etc that most hosts usually hide somewhere in a tiny little part of the screen. Transport is resizable and you can pick your own colours. Ideally, you’d drag it onto an external monitor in your 1000 square feet wooden studio and make it fullscreen so that your recording artists always stay oriented, but it works in smaller settings as well ;-). Made upon request by a DDMF user, we thought this gem is too good to not make it available for everybody. Transport is available as a 32 and 64 bit plugin in VST, RTAS, AAX (Win + Mac) and AU (Mac) format. MacOSX 10.6 is required.

ddmf.eu/freeware

AGS ~ Advanced Gtk+ Sequencer


Advanced GTK+ Sequencer is intended to use for music composition. It features a piano roll, as well a synth, matrix editor, drum machine, soundfont2 player, mixer and an output panel.

It’s designed to be highly configurable, you may add effects to its effect chain, add or remove audio channels/pads.

You may set up a fully functional network of engines, therefore exists a link editor for linking audio lines.

In conjunction with AGS you need a realtime kernel and alsa support. `ags` uses conditional locks to keep several threads in sync that’s why you need at least a preemptible kernel.

Features:

  • save or open Advanced Gtk+ Sequencer XML files with XPath support
  • add or remove audio engines with adjustable audio channels and pads
  • link channels with property dialog
  • output panel, mixer, drum and matrix sequencer, soft synth and audio file player
  • piano roll with basic notation editing supporting copy & paste
  • adjustable BPM
  • LADSPA, DSSI and Lv2 support
  • export to WAV, FLAC, OGG and others
  • multiple sinks like Pulseaudio, JACK, ALSA and OSS
  • automation editor
  • waveform editor with copy & paste
  • capture sound with AgsAudiorec machine
  • MIDI instrument input
  • import/export to Standard MIDI Files
  • OSC content format support
  • OSC server for remote control and monitoring

nongnu.org/gsequencer/

sourceforge.net/projects/ags/

Drumstick Metronome ~ Linux Precision Beats


Drumstick Metronome is a MIDI based metronome using the ALSA sequencer. It’s intended for musicians and music students, as a tool to keep the rhythm while playing musical instruments. It uses MIDI for sound generation instead of digital audio, allowing low CPU usage and very accurate timing, thanks to the ALSA sequencer.

Features:

  • Easy to use graphic user interface.
  • MIDI only. Can be used with software or external MIDI synthesizers.
  • Based on ALSA sequencer. Provides input and output ports
  • Highly customizable parameters.
  • External control: D-Bus and MIDI realtime.
  • Custom rhythm patterns.

sourceforge.net/projects/kmetronome

BPM Online ~ Beats Per Minute Webservice


Free online Tap BPM tool allows you to calculate tempo and count Beats Per Minute (BPM) by tapping any key to the rhythm or beat. Tap for a few seconds to quickly calculate BPM without waiting the whole minute. You may optionally configure it for Beats Per Second (BPS) or Beats Per Hour (BPH).

www.beatsperminuteonline.com

TempoPerfect ~ Cross-platform Metronome


TempoPerfect is a free software metronome. Unlike mechanical metronomes, our software metronome provides a clear and precise beat that won’t wind down, making it an essential tool for any musician.

  • Simple and intuitive interface
  • Highly accurate beat simulation
  • Accent the first beat in a measure
  • Preset tempos include Largo, Allegro, Presto and more
  • Visual beat indicator bar provides a helpful visual cue
  • Create accurate beat patterns for simple or complex rhythms
  • Subdivide beats to hear difficult patterns, such as triplets
  • BPM Tempo Guide chart for each speed of music (e.g., Allegro=120-168)
  • Subdivide beats into accented beats and regular beats to emphasize different patterns
  • Use on your computer or as a metronome app on your mobile device
  • TempoPerfect also includes a tempo guide within the program which is a helpful resource for remembering the BPM for particular speed markings (e.g., Allegro).
tempoperfect

www.nch.com.au/metronome