Source code for munin.provider.bpm

#!/usr/bin/env python
# encoding: utf-8

"""
Overview
========

This provider is able to analyze an audio file path in order to find the
average beats-per-minute rate.

**Usage Example:**

.. code-block:: python

    >>> from munin.provider import BPMProvider
    >>> p = BPMProvider()
    >>> p.do_process('/tmp/some_file.mp3')
    (123.456, )  # beats per minute

The potential Information you can get from this is: Songs of different genre
often have differente tempi, therefore are less similar. This is no rule of course.

**Problems to consider:**

    * Noisy Live-data often gets high BPM counts
    * Speed-Metal (as an example) ranges in the same

**Prerequisites:**

To function properly this poorly implemented provider needs two external utils:

    * http://sox.sourceforge.net/
    * http://www.pogo.org.uk/~mark/bpm-tools/

Reference
=========
"""


import os
import pipes
import shutil
import subprocess

import logging
LOGGER = logging.getLogger(__name__)

# Fix for Python 3.2
try:
    from subprocess import DEVNULL
except ImportError:
    subprocess.DEVNULL = open(os.devnull, 'w')

from munin.provider import Provider
from munin.helper import float_cmp


def check_for_bpmtools():
    """Check if all required tools are installed for this Provider.

        - ``bpm-tools`` (``bpm`` executable)
        - ``sox`` (``sox`` executable)

    :returns: True if both binaries were found.
    """
    return shutil.which('bpm') and shutil.which('sox')


BPM_COMMAND = \
    "sox -v 1.0 {path} -t raw -r 44100 -e float -c 1 - | bpm -m 60 -x 350"


[docs]class BPMProvider(Provider): """A Beats-per-minute provider. Currently, this is stupidly implemented as a call to an external util: .. code-block:: bash $ sox -v 1.0 path -t raw -r 44100 -e float -c 1 - | bpm -m 60 -x 350 More Information on beats per minute: http://en.wikipedia.org/wiki/Tempo#Beats_per_minute """ def do_process(self, audio_path): try: stdout = subprocess.check_output( BPM_COMMAND.format(path=pipes.quote(audio_path)), shell=True, stderr=DEVNULL ) converted = float(stdout.decode('utf-8').strip()) # Check if the maximum value is reached (which usually means an error) if converted > 340: return None return (converted, ) except subprocess.CalledProcessError as err: LOGGER.debug('"{cmd}" failed with {e}'.format( cmd=err.cmd, e=err.returncode )) except UnicodeDecodeError: LOGGER.debug('could not convert input to valid utf-8') return None
[docs]class BPMCachedProvider(BPMProvider): """Same as :class:`BPMProvider`, but adds a caching layer. A .bpm file with the calculated value will be stored along the audio file, and the same place will be checked before actually calculating it. """ def __init__(self, cache_invalid=False, **kwargs): """ :param cache_invalid: Also cache invalid results of failed calculations? """ Provider.__init__(self, **kwargs) self._cache_invalid = cache_invalid def do_process(self, audio_path): try: cache_path = audio_path + '.bpm' print(cache_path) if os.access(cache_path, os.R_OK): with open(cache_path, 'r') as handle: content = handle.read() if content: LOGGER.debug('bpm for {} was cached.'.format(audio_path)) return (float(content), ) else: return None LOGGER.debug('calculating bpm for {}'.format(audio_path)) bpm = BPMProvider.do_process(self, audio_path) if self._cache_invalid or bpm is not None: with open(cache_path, 'w') as handle: if bpm is not None: handle.write(str(bpm[0])) return bpm except OSError: pass return () ########################################################################### # Tests # ###########################################################################
if __name__ == '__main__': from sys import argv from munin.helper import AudioFileWalker if '--cli' in argv: try: bpms = [] provider = BPMCachedProvider(cache_invalid=True) for audio_path in AudioFileWalker(argv[2]): bpm = provider.do_process(audio_path) bpms.append(( bpm[0] if bpm else 0.0, audio_path )) for bpm, audio_path in sorted(bpms, key=lambda elem: elem[0]): print('{:<10} {}'.format(bpm, audio_path)) except KeyboardInterrupt: pass

Related Topics

Useful links:

Package:

Github: