Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bs1770gainsupport #1343

Merged
merged 9 commits into from Mar 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extra whitespace is not needed. Can you please delete it again?

"""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.
"""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty line is fine, though—it's actually in line with PEP 8 to have a blank line after docstrings in class declarations.

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a brief docstring here describing what "bs1770" is and roughly how it's used?


"""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