diff --git a/beetsplug/autobpm.py b/beetsplug/autobpm.py new file mode 100644 index 000000000..80e7910a8 --- /dev/null +++ b/beetsplug/autobpm.py @@ -0,0 +1,80 @@ +# This file is part of beets. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Uses Librosa to calculate the `bpm` field. +""" + + +from beets import ui +from beets import util +from beets.plugins import BeetsPlugin + +from librosa import load, beat +from soundfile import LibsndfileError + + +class AutoBPMPlugin(BeetsPlugin): + + def __init__(self): + super().__init__() + self.config.add({ + 'auto': True, + 'overwrite': False, + }) + + if self.config['auto'].get(bool): + self.import_stages = [self.imported] + + def commands(self): + cmd = ui.Subcommand('autobpm', + help='detect and add bpm from audio using Librosa') + cmd.func = self.command + return [cmd] + + def command(self, lib, opts, args): + self.calculate_bpm(lib.items(ui.decargs(args)), + write=ui.should_write()) + + def imported(self, session, task): + self.calculate_bpm(task.imported_items()) + + def calculate_bpm(self, items, write=False): + overwrite = self.config['overwrite'].get(bool) + + for item in items: + if item['bpm']: + self._log.info('found bpm {0} for {1}', + item['bpm'], util.displayable_path(item.path)) + if not overwrite: + continue + + try: + y, sr = load(util.syspath(item.path), res_type='kaiser_fast') + except LibsndfileError as exc: + self._log.error('LibsndfileError: failed to load {0} {1}', + util.displayable_path(item.path), exc) + continue + except ValueError as exc: + self._log.error('ValueError: failed to load {0} {1}', + util.displayable_path(item.path), exc) + continue + + tempo, _ = beat.beat_track(y=y, sr=sr) + bpm = round(tempo) + item['bpm'] = bpm + self._log.info('added computed bpm {0} for {1}', + bpm, util.displayable_path(item.path)) + + if write: + item.try_write() + item.store() diff --git a/docs/changelog.rst b/docs/changelog.rst index 330e74f22..afb6cc3de 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -122,14 +122,17 @@ New features: :bug:`2786` * Add support for ``artists`` and ``albumartists`` multi-valued tags. :bug:`505` +* :doc:`/plugins/autobpm`: Add the `autobpm` plugin which uses Librosa to + calculate the BPM of the audio. + :bug:`3856` Bug fixes: -* :doc:`/plugins/scrub`: Fixed the import behavior where scrubbed database tags - were restored to newly imported tracks with config settings ``scrub.auto: yes`` +* :doc:`/plugins/scrub`: Fixed the import behavior where scrubbed database tags + were restored to newly imported tracks with config settings ``scrub.auto: yes`` and ``import.write: no``. :bug:`4326` -* :doc:`/plugins/deezer`: Fixed the error where Deezer plugin would crash if non-Deezer id is passed during import. +* :doc:`/plugins/deezer`: Fixed the error where Deezer plugin would crash if non-Deezer id is passed during import. * :doc:`/plugins/fetchart`: Fix fetching from Cover Art Archive when the `maxwidth` option is set to one of the supported Cover Art Archive widths. * :doc:`/plugins/discogs`: Fix "Discogs plugin replacing Feat. or Ft. with diff --git a/docs/plugins/autobpm.rst b/docs/plugins/autobpm.rst new file mode 100644 index 000000000..caf128b8c --- /dev/null +++ b/docs/plugins/autobpm.rst @@ -0,0 +1,25 @@ +AutoBPM Plugin +============== + +The `autobpm` plugin uses the `Librosa`_ library to calculate the BPM +of a track from its audio data and store it in the `bpm` field of your +database. It does so automatically when importing music or through +the ``beet autobpm [QUERY]`` command. + +To use the ``autobpm`` plugin, enable it in your configuration (see +:ref:`using-plugins`). + +Configuration +------------- + +To configure the plugin, make a ``autobpm:`` section in your +configuration file. The available options are: + +- **auto**: Analyze every file on import. + Otherwise, you need to use the ``beet autobpm`` command explicitly. + Default: ``yes`` +- **overwrite**: Calculate a BPM even for files that already have a + `bpm` value. + Default: ``no``. + +.. _Librosa: https://github.com/librosa/librosa/ diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index bbb4bcf34..b56c50225 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -63,6 +63,7 @@ following to your configuration:: acousticbrainz albumtypes aura + autobpm badfiles bareasc beatport @@ -164,6 +165,9 @@ Metadata :doc:`acousticbrainz ` Fetch various AcousticBrainz metadata +:doc:`autobpm ` + Use `Librosa`_ to calculate the BPM from the audio. + :doc:`bpm ` Measure tempo using keystrokes. @@ -222,6 +226,7 @@ Metadata :doc:`zero ` Nullify fields by pattern or unconditionally. +.. _Librosa: https://github.com/librosa/librosa/ .. _KeyFinder: http://www.ibrahimshaath.co.uk/keyfinder/ .. _streaming_extractor_music: https://acousticbrainz.org/download diff --git a/setup.cfg b/setup.cfg index 874edaa58..b260190be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ per-file-ignores = ./extra/release.py:D ./beetsplug/duplicates.py:D ./beetsplug/bpm.py:D + ./beetsplug/autobpm.py:D ./beetsplug/convert.py:D ./beetsplug/info.py:D ./beetsplug/parentwork.py:D