Skip to content

Commit

Permalink
Merge pull request #1343 from jmwatte/bs1770gainsupport
Browse files Browse the repository at this point in the history
Bs1770gainsupport
  • Loading branch information
sampsyo committed Mar 3, 2015
2 parents 8bd0633 + 5d7d402 commit 293e44f
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 6 deletions.
121 changes: 118 additions & 3 deletions beetsplug/replaygain.py
Expand Up @@ -21,6 +21,7 @@
import itertools
import sys
import warnings
import re

from beets import logging
from beets import ui
Expand All @@ -32,12 +33,14 @@
# Utilities.

class ReplayGainError(Exception):

"""Raised when a local (to a track or an album) error occurs in one
of the backends.
"""


class FatalReplayGainError(Exception):

"""Raised when a fatal error occurs in one of the backends.
"""

Expand Down Expand Up @@ -66,8 +69,10 @@ def call(args):


class Backend(object):

"""An abstract class representing engine for calculating RG values.
"""

def __init__(self, config, log):
"""Initialize the backend with the configuration view for the
plugin.
Expand All @@ -83,10 +88,114 @@ def compute_album_gain(self, album):
raise NotImplementedError()


# mpgain/aacgain CLI tool backend.
# bsg1770gain backend
class Bs1770gainBackend(Backend):

"""bs1770gain is a loudness scanner compliant with ITU-R BS.1770 and its
flavors EBU R128,ATSC A/85 and Replaygain 2.0. It uses a special
designed algorithm to normalize audio to the same level.
"""

def __init__(self, config, log):
super(Bs1770gainBackend, self).__init__(config, log)
cmd = 'bs1770gain'

try:
self.method = '--' + config['method'].get(unicode)
except:
self.method = '--replaygain'

try:
call([cmd, self.method])
self.command = cmd
except OSError:
raise FatalReplayGainError(
'Is bs1770gain installed? Is your method in config correct?'
)
if not self.command:
raise FatalReplayGainError(
'no replaygain command found: install bs1770gain'
)

def compute_track_gain(self, items):
"""Computes the track gain of the given tracks, returns a list
of TrackGain objects.
"""

output = self.compute_gain(items, False)
return output

def compute_album_gain(self, album):
"""Computes the album gain of the given album, returns an
AlbumGain object.
"""
# TODO: What should be done when not all tracks in the album are
# supported?

supported_items = album.items()
output = self.compute_gain(supported_items, True)

return AlbumGain(output[-1], output[:-1])

def compute_gain(self, items, is_album):
"""Computes the track or album gain of a list of items, returns
a list of TrackGain objects.
When computing album gain, the last TrackGain object returned is
the album gain
"""

if len(items) == 0:
return []

"""Compute ReplayGain values and return a list of results
dictionaries as given by `parse_tool_output`.
"""
# Construct shell command.
cmd = [self.command]
cmd = cmd + [self.method]
cmd = cmd + ['-it']
cmd = cmd + [syspath(i.path) for i in items]

self._log.debug(u'analyzing {0} files', len(items))
self._log.debug(u"executing {0}", " ".join(map(displayable_path, cmd)))
output = call(cmd)
self._log.debug(u'analysis finished')
results = self.parse_tool_output(output,
len(items) + is_album)
return results

def parse_tool_output(self, text, num_lines):
"""Given the output from bs1770gain, parse the text and
return a list of dictionaries
containing information about each analyzed file.
"""
out = []
data = text.decode('utf8', errors='ignore')
regex = ("(\s{2,2}\[\d+\/\d+\].*?|\[ALBUM\].*?)(?=\s{2,2}\[\d+\/\d+\]"
"|\s{2,2}\[ALBUM\]:|done\.$)")

results = re.findall(regex, data, re.S | re.M)
for ll in results[0:num_lines]:
parts = ll.split(b'\n')
if len(parts) == 0:
self._log.debug(u'bad tool output: {0!r}', text)
raise ReplayGainError('bs1770gain failed')

d = {
'file': parts[0],
'gain': float((parts[1].split('/'))[1].split('LU')[0]),
'peak': float(parts[2].split('/')[1]),
}

self._log.info('analysed {}gain={};peak={}',
d['file'].rstrip(), d['gain'], d['peak'])
out.append(Gain(d['gain'], d['peak']))
return out


# mpgain/aacgain CLI tool backend.
class CommandBackend(Backend):

def __init__(self, config, log):
super(CommandBackend, self).__init__(config, log)
config.add({
Expand Down Expand Up @@ -218,6 +327,7 @@ def parse_tool_output(self, text, num_lines):
# GStreamer-based backend.

class GStreamerBackend(Backend):

def __init__(self, config, log):
super(GStreamerBackend, self).__init__(config, log)
self._import_gst()
Expand Down Expand Up @@ -466,10 +576,12 @@ def _on_pad_removed(self, decbin, pad):


class AudioToolsBackend(Backend):

"""ReplayGain backend that uses `Python Audio Tools
<http://audiotools.sourceforge.net/>`_ and its capabilities to read more
file formats and compute ReplayGain values using it replaygain module.
"""

def __init__(self, config, log):
super(AudioToolsBackend, self).__init__(config, log)
self._import_audiotools()
Expand Down Expand Up @@ -594,13 +706,15 @@ def compute_album_gain(self, album):
# Main plugin logic.

class ReplayGainPlugin(BeetsPlugin):

"""Provides ReplayGain analysis.
"""

backends = {
"command": CommandBackend,
"command": CommandBackend,
"gstreamer": GStreamerBackend,
"audiotools": AudioToolsBackend
"audiotools": AudioToolsBackend,
"bs1770gain": Bs1770gainBackend
}

def __init__(self):
Expand Down Expand Up @@ -688,6 +802,7 @@ def handle_album(self, album, write):
)

self.store_album_gain(album, album_gain.album_gain)

for item, track_gain in itertools.izip(album.items(),
album_gain.track_gains):
self.store_track_gain(item, track_gain)
Expand Down
31 changes: 28 additions & 3 deletions docs/plugins/replaygain.rst
Expand Up @@ -10,9 +10,9 @@ playback levels.
Installation
------------

This plugin can use one of three backends to compute the ReplayGain values:
GStreamer, mp3gain (and its cousin, aacgain), and Python Audio Tools. mp3gain
can be easier to install but GStreamer and Audio Tools support more audio
This plugin can use one of four backends to compute the ReplayGain values:
GStreamer, mp3gain (and its cousin, aacgain), Python Audio Tools and bs1770gain. mp3gain
can be easier to install but GStreamer, Audio Tools and bs1770gain support more audio
formats.

Once installed, this plugin analyzes all files during the import process. This
Expand Down Expand Up @@ -75,6 +75,22 @@ On OS X, most of the dependencies can be installed with `Homebrew`_::

.. _Python Audio Tools: http://audiotools.sourceforge.net

bs1770gain
``````````

In order to use this backend, you will need to install the bs1770gain command-line tool. Here are some hints:

* goto `bs1770gain`_ and follow the download instructions
* make sure it is in your $PATH

.. _bs1770gain: bs1770gain.sourceforge.net

Then, enable the plugin (see :ref:`using-plugins`) and specify the
backend in your configuration file::

replaygain:
backend: bs1770gain
Configuration
-------------

Expand All @@ -100,6 +116,15 @@ These options only work with the "command" backend:
would keep clipping from occurring.
Default: ``yes``.

This option only works with the "bs1770gain" backend:

- **method**: The loudness scanning standard: either 'replaygain' for ReplayGain 2.0,
'ebu' for EBU R128 or 'atsc' for ATSC A/85.
This dictates the reference level: -18, -23, or -24 LUFS respectively. Default: replaygain




Manual Analysis
---------------

Expand Down

0 comments on commit 293e44f

Please sign in to comment.