A python script to count bpm (beats per minute) on Linux

published Aug 18, 2010 11:53   by admin ( last modified Aug 18, 2010 11:53 )

As I have written before, soundstretch can do bpm analysis on soundfiles on Linux. However:

  • it needs wav rather than mp3 files.
  • For my music it sometimes report half the correct bpm

 

I have written a script that wraps soundstretch and lame into a command that takes an mp3 file as input and outputs the bpm.

Internally, it converts the mp3 into temporary wav file with lame, runs soundstretch on the file, gets the bpm guess from soundstretch and fits that into a window of bpms.

I do not usually write command line scripts, so there may be problems in the below code, and there are certainly some hard coded assumptions. Use at your own risk. I intend to package this up and release it in some way, but until then, here is is in its present state:

#!/usr/bin/python
DOC = ''' This program detects bpm for mp3 files. It relies on lame and soundstretch being
installed on your system.


Usage:

%s <filename>

-or pipe filenames to it.

Example:

find . -name "*.mp3"| %s\n''' % (__file__, __file__)

import os
import pipes
import select
import shutil
import subprocess
import sys
import tempfile

## Define the window for sane bpm values. This may depend on genre of music. ##
BPM_WINDOW_MAX = 240
# Do not change this one
BPM_WINDOW_MIN = BPM_WINDOW_MAX/2
###############################################################################

def _get_bpm_from_soundstretch(output):
"""Gets bpm value from soundstretch output"""

output = output.split("\n")
for line in output:
if 'Detected BPM rate ' in line:
bpm = line[18:]
return float(bpm)
return None # Could not parse output

def fit_bpm_in_window(bpm_suggestion):
"""Double or halve a bpm suggestion until it fits inside the bpm window"""

if bpm_suggestion is not None:
while bpm_suggestion < (BPM_WINDOW_MIN):
bpm_suggestion = bpm_suggestion * 2
while bpm_suggestion > (BPM_WINDOW_MAX):
bpm_suggestion = bpm_suggestion / 2
return bpm_suggestion

def analyze_mp3(mp3filespec):
"""Uses lame and soundstretch to analyze an mp3 file for its bpm rate"""

# Make a temporary working directory for storing the wav file
# that soundstretch should analyze
workingdir = tempfile.mkdtemp()
wavfilespec = workingdir + "/tempwavfile.wav"

# Use lame to make a wav representation of the mp3 file to be analyzed
wav_command = 'lame --decode %s %s' % (mp3filespec, wavfilespec)
subprocess.call([wav_command], shell=True, stderr=open(os.devnull, 'w'))

# Call soundstretch to analyze the wav file
bpm_command = 'soundstretch %s -bpm' % wavfilespec
p = subprocess.Popen([bpm_command], shell=True,stdout=subprocess.PIPE)
output = p.communicate()[0]

# Delete temporary working directory and its contents
shutil.rmtree(workingdir)

bpm_suggestion = _get_bpm_from_soundstretch(output)

return fit_bpm_in_window(bpm_suggestion)

def process_input(mp3filespec):
bpm_suggestion = analyze_mp3(pipes.quote(mp3filespec))
if bpm_suggestion is None:
print "Unable to detect bpm for file %s" % mp3filespec
else:
print "BPM rate for %s is estimated to be %s" % (mp3filespec, bpm_suggestion)

if __name__ == "__main__":
argv = sys.argv[1:]
if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []): # input is piped to program
mp3filespecs = sys.stdin.readlines()
for mp3filespec in mp3filespecs:
process_input(mp3filespec.rstrip('\n'))
elif len(argv) < 1: # No pipe and no input file, print help text and exit
print DOC
sys.exit()
else: # Input file present
mp3filespec = os.path.abspath(argv[0])
process_input(mp3filespec)

It needs soundstretch and lame to be installed.