mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 21:14:19 +01:00
Merge pull request #3844 from SamuelCook/remove_bs1770gain
Removes support for bs1770gain.
This commit is contained in:
commit
04ea754d00
3 changed files with 17 additions and 335 deletions
|
|
@ -15,26 +15,23 @@
|
|||
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import collections
|
||||
import enum
|
||||
import math
|
||||
import os
|
||||
import signal
|
||||
import six
|
||||
import subprocess
|
||||
import sys
|
||||
import warnings
|
||||
import enum
|
||||
import re
|
||||
import xml.parsers.expat
|
||||
from six.moves import zip, queue
|
||||
import six
|
||||
|
||||
from multiprocessing.pool import ThreadPool, RUN
|
||||
from six.moves import zip, queue
|
||||
from threading import Thread, Event
|
||||
import signal
|
||||
|
||||
from beets import ui
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.util import (syspath, command_output, bytestring_path,
|
||||
displayable_path, py3_path, cpu_count)
|
||||
from beets.util import (syspath, command_output, displayable_path,
|
||||
py3_path, cpu_count)
|
||||
|
||||
|
||||
# Utilities.
|
||||
|
|
@ -136,252 +133,6 @@ class Backend(object):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
# 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.
|
||||
"""
|
||||
|
||||
methods = {
|
||||
-24: "atsc",
|
||||
-23: "ebu",
|
||||
-18: "replaygain",
|
||||
}
|
||||
|
||||
do_parallel = True
|
||||
|
||||
def __init__(self, config, log):
|
||||
super(Bs1770gainBackend, self).__init__(config, log)
|
||||
config.add({
|
||||
'chunk_at': 5000,
|
||||
'method': '',
|
||||
})
|
||||
self.chunk_at = config['chunk_at'].as_number()
|
||||
# backward compatibility to `method` config option
|
||||
self.__method = config['method'].as_str()
|
||||
|
||||
cmd = 'bs1770gain'
|
||||
try:
|
||||
version_out = call([cmd, '--version'])
|
||||
self.command = cmd
|
||||
self.version = re.search(
|
||||
'bs1770gain ([0-9]+.[0-9]+.[0-9]+), ',
|
||||
version_out.stdout.decode('utf-8')
|
||||
).group(1)
|
||||
except OSError:
|
||||
raise FatalReplayGainError(
|
||||
u'Is bs1770gain installed?'
|
||||
)
|
||||
if not self.command:
|
||||
raise FatalReplayGainError(
|
||||
u'no replaygain command found: install bs1770gain'
|
||||
)
|
||||
|
||||
def compute_track_gain(self, items, target_level, peak):
|
||||
"""Computes the track gain of the given tracks, returns a list
|
||||
of TrackGain objects.
|
||||
"""
|
||||
|
||||
output = self.compute_gain(items, target_level, False)
|
||||
return output
|
||||
|
||||
def compute_album_gain(self, items, target_level, peak):
|
||||
"""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?
|
||||
|
||||
output = self.compute_gain(items, target_level, True)
|
||||
|
||||
if not output:
|
||||
raise ReplayGainError(u'no output from bs1770gain')
|
||||
return AlbumGain(output[-1], output[:-1])
|
||||
|
||||
def isplitter(self, items, chunk_at):
|
||||
"""Break an iterable into chunks of at most size `chunk_at`,
|
||||
generating lists for each chunk.
|
||||
"""
|
||||
iterable = iter(items)
|
||||
while True:
|
||||
result = []
|
||||
for i in range(chunk_at):
|
||||
try:
|
||||
a = next(iterable)
|
||||
except StopIteration:
|
||||
break
|
||||
else:
|
||||
result.append(a)
|
||||
if result:
|
||||
yield result
|
||||
else:
|
||||
break
|
||||
|
||||
def compute_gain(self, items, target_level, 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 []
|
||||
|
||||
albumgaintot = 0.0
|
||||
albumpeaktot = 0.0
|
||||
returnchunks = []
|
||||
|
||||
# In the case of very large sets of music, we break the tracks
|
||||
# into smaller chunks and process them one at a time. This
|
||||
# avoids running out of memory.
|
||||
if len(items) > self.chunk_at:
|
||||
i = 0
|
||||
for chunk in self.isplitter(items, self.chunk_at):
|
||||
i += 1
|
||||
returnchunk = self.compute_chunk_gain(
|
||||
chunk,
|
||||
is_album,
|
||||
target_level
|
||||
)
|
||||
albumgaintot += returnchunk[-1].gain
|
||||
albumpeaktot = max(albumpeaktot, returnchunk[-1].peak)
|
||||
returnchunks = returnchunks + returnchunk[0:-1]
|
||||
returnchunks.append(Gain(albumgaintot / i, albumpeaktot))
|
||||
return returnchunks
|
||||
else:
|
||||
return self.compute_chunk_gain(items, is_album, target_level)
|
||||
|
||||
def compute_chunk_gain(self, items, is_album, target_level):
|
||||
"""Compute ReplayGain values and return a list of results
|
||||
dictionaries as given by `parse_tool_output`.
|
||||
"""
|
||||
# choose method
|
||||
target_level = db_to_lufs(target_level)
|
||||
if self.__method != "":
|
||||
# backward compatibility to `method` option
|
||||
method = self.__method
|
||||
gain_adjustment = target_level \
|
||||
- [k for k, v in self.methods.items() if v == method][0]
|
||||
elif target_level in self.methods:
|
||||
method = self.methods[target_level]
|
||||
gain_adjustment = 0
|
||||
else:
|
||||
lufs_target = -23
|
||||
method = self.methods[lufs_target]
|
||||
gain_adjustment = target_level - lufs_target
|
||||
|
||||
# Construct shell command.
|
||||
cmd = [self.command]
|
||||
cmd += ["--" + method]
|
||||
cmd += ['--xml', '-p']
|
||||
if after_version(self.version, '0.6.0'):
|
||||
cmd += ['--unit=ebu'] # set units to LU
|
||||
cmd += ['--suppress-progress'] # don't print % to XML output
|
||||
|
||||
# Workaround for Windows: the underlying tool fails on paths
|
||||
# with the \\?\ prefix, so we don't use it here. This
|
||||
# prevents the backend from working with long paths.
|
||||
args = cmd + [syspath(i.path, prefix=False) for i in items]
|
||||
path_list = [i.path for i in items]
|
||||
|
||||
# Invoke the command.
|
||||
self._log.debug(
|
||||
u'executing {0}', u' '.join(map(displayable_path, args))
|
||||
)
|
||||
output = call(args).stdout
|
||||
|
||||
self._log.debug(u'analysis finished: {0}', output)
|
||||
results = self.parse_tool_output(output, path_list, is_album)
|
||||
|
||||
if gain_adjustment:
|
||||
results = [
|
||||
Gain(res.gain + gain_adjustment, res.peak)
|
||||
for res in results
|
||||
]
|
||||
|
||||
self._log.debug(u'{0} items, {1} results', len(items), len(results))
|
||||
return results
|
||||
|
||||
def parse_tool_output(self, text, path_list, is_album):
|
||||
"""Given the output from bs1770gain, parse the text and
|
||||
return a list of dictionaries
|
||||
containing information about each analyzed file.
|
||||
"""
|
||||
per_file_gain = {}
|
||||
album_gain = {} # mutable variable so it can be set from handlers
|
||||
parser = xml.parsers.expat.ParserCreate(encoding='utf-8')
|
||||
state = {'file': None, 'gain': None, 'peak': None}
|
||||
album_state = {'gain': None, 'peak': None}
|
||||
|
||||
def start_element_handler(name, attrs):
|
||||
if name == u'track':
|
||||
state['file'] = bytestring_path(attrs[u'file'])
|
||||
if state['file'] in per_file_gain:
|
||||
raise ReplayGainError(
|
||||
u'duplicate filename in bs1770gain output')
|
||||
elif name == u'integrated':
|
||||
if 'lu' in attrs:
|
||||
state['gain'] = float(attrs[u'lu'])
|
||||
elif name == u'sample-peak':
|
||||
if 'factor' in attrs:
|
||||
state['peak'] = float(attrs[u'factor'])
|
||||
elif 'amplitude' in attrs:
|
||||
state['peak'] = float(attrs[u'amplitude'])
|
||||
|
||||
def end_element_handler(name):
|
||||
if name == u'track':
|
||||
if state['gain'] is None or state['peak'] is None:
|
||||
raise ReplayGainError(u'could not parse gain or peak from '
|
||||
'the output of bs1770gain')
|
||||
per_file_gain[state['file']] = Gain(state['gain'],
|
||||
state['peak'])
|
||||
state['gain'] = state['peak'] = None
|
||||
elif name == u'summary':
|
||||
if state['gain'] is None or state['peak'] is None:
|
||||
raise ReplayGainError(u'could not parse gain or peak from '
|
||||
'the output of bs1770gain')
|
||||
album_gain["album"] = Gain(state['gain'], state['peak'])
|
||||
state['gain'] = state['peak'] = None
|
||||
elif len(per_file_gain) == len(path_list):
|
||||
if state['gain'] is not None:
|
||||
album_state['gain'] = state['gain']
|
||||
if state['peak'] is not None:
|
||||
album_state['peak'] = state['peak']
|
||||
if album_state['gain'] is not None \
|
||||
and album_state['peak'] is not None:
|
||||
album_gain["album"] = Gain(
|
||||
album_state['gain'], album_state['peak'])
|
||||
state['gain'] = state['peak'] = None
|
||||
|
||||
parser.StartElementHandler = start_element_handler
|
||||
parser.EndElementHandler = end_element_handler
|
||||
|
||||
try:
|
||||
parser.Parse(text, True)
|
||||
except xml.parsers.expat.ExpatError:
|
||||
raise ReplayGainError(
|
||||
u'The bs1770gain tool produced malformed XML. '
|
||||
u'Using version >=0.4.10 may solve this problem.')
|
||||
|
||||
if len(per_file_gain) != len(path_list):
|
||||
raise ReplayGainError(
|
||||
u'the number of results returned by bs1770gain does not match '
|
||||
'the number of files passed to it')
|
||||
|
||||
# bs1770gain does not return the analysis results in the order that
|
||||
# files are passed on the command line, because it is sorting the files
|
||||
# internally. We must recover the order from the filenames themselves.
|
||||
try:
|
||||
out = [per_file_gain[os.path.basename(p)] for p in path_list]
|
||||
except KeyError:
|
||||
raise ReplayGainError(
|
||||
u'unrecognized filename in bs1770gain output '
|
||||
'(bs1770gain can only deal with utf-8 file names)')
|
||||
if is_album:
|
||||
out.append(album_gain["album"])
|
||||
return out
|
||||
|
||||
|
||||
# ffmpeg backend
|
||||
class FfmpegBackend(Backend):
|
||||
"""A replaygain backend using ffmpeg's ebur128 filter.
|
||||
|
|
@ -1216,7 +967,6 @@ class ReplayGainPlugin(BeetsPlugin):
|
|||
"command": CommandBackend,
|
||||
"gstreamer": GStreamerBackend,
|
||||
"audiotools": AudioToolsBackend,
|
||||
"bs1770gain": Bs1770gainBackend,
|
||||
"ffmpeg": FfmpegBackend,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,14 +64,12 @@ New features:
|
|||
Thanks to :user:`samuelnilsson`
|
||||
:bug:`293`
|
||||
* :doc:`/plugins/replaygain`: The new ``ffmpeg`` ReplayGain backend supports
|
||||
``R128_`` tags, just like the ``bs1770gain`` backend.
|
||||
``R128_`` tags.
|
||||
:bug:`3056`
|
||||
* :doc:`plugins/replaygain`: ``r128_targetlevel`` is a new configuration option
|
||||
for the ReplayGain plugin: It defines the reference volume for files using
|
||||
``R128_`` tags. ``targetlevel`` only configures the reference volume for
|
||||
``REPLAYGAIN_`` files.
|
||||
This also deprecates the ``bs1770gain`` ReplayGain backend's ``method``
|
||||
option. Use ``targetlevel`` and ``r128_targetlevel`` instead.
|
||||
:bug:`3065`
|
||||
* A new :doc:`/plugins/parentwork` gets information about the original work,
|
||||
which is useful for classical music.
|
||||
|
|
@ -176,8 +174,10 @@ New features:
|
|||
https://github.com/alastair/python-musicbrainzngs/pull/266 .
|
||||
Thanks to :user:`aereaux`.
|
||||
* :doc:`/plugins/replaygain` now does its analysis in parallel when using
|
||||
the ``command``, ``ffmpeg`` or ``bs1770gain`` backends.
|
||||
the ``command`` or ``ffmpeg`` backends.
|
||||
:bug:`3478`
|
||||
* Removes usage of the bs1770gain replaygain backend.
|
||||
Thanks to :user:`SamuelCook`.
|
||||
|
||||
Fixes:
|
||||
|
||||
|
|
@ -239,8 +239,6 @@ Fixes:
|
|||
:bug:`3437`
|
||||
* :doc:`/plugins/lyrics`: Fix a corner-case with Genius lowercase artist names
|
||||
:bug:`3446`
|
||||
* :doc:`/plugins/replaygain`: Support ``bs1770gain`` v0.6.0 and up
|
||||
:bug:`3480`
|
||||
* :doc:`/plugins/parentwork`: Don't save tracks when nothing has changed.
|
||||
:bug:`3492`
|
||||
* Added a warning when configuration files defined in the `include` directive
|
||||
|
|
@ -352,6 +350,7 @@ For packagers:
|
|||
or `repair <https://build.opensuse.org/package/view_file/openSUSE:Factory/beets/fix_test_command_line_option_relative_to_working_dir.diff?expand=1>`_
|
||||
the test may no longer be necessary.
|
||||
* This version drops support for Python 3.4.
|
||||
* Removes the optional dependency on bs1770gain.
|
||||
|
||||
.. _Fish shell: https://fishshell.com/
|
||||
.. _MediaFile: https://github.com/beetbox/mediafile
|
||||
|
|
|
|||
|
|
@ -16,18 +16,14 @@
|
|||
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import unittest
|
||||
import six
|
||||
|
||||
from mock import patch
|
||||
from test.helper import TestHelper, capture_log, has_program
|
||||
import unittest
|
||||
from mediafile import MediaFile
|
||||
|
||||
from beets import config
|
||||
from beets.util import CommandOutput
|
||||
from mediafile import MediaFile
|
||||
from beetsplug.replaygain import (FatalGstreamerPluginReplayGainError,
|
||||
GStreamerBackend)
|
||||
|
||||
from test.helper import TestHelper, has_program
|
||||
|
||||
try:
|
||||
import gi
|
||||
|
|
@ -41,11 +37,6 @@ if any(has_program(cmd, ['-v']) for cmd in ['mp3gain', 'aacgain']):
|
|||
else:
|
||||
GAIN_PROG_AVAILABLE = False
|
||||
|
||||
if has_program('bs1770gain'):
|
||||
LOUDNESS_PROG_AVAILABLE = True
|
||||
else:
|
||||
LOUDNESS_PROG_AVAILABLE = False
|
||||
|
||||
FFMPEG_AVAILABLE = has_program('ffmpeg', ['-version'])
|
||||
|
||||
|
||||
|
|
@ -153,9 +144,7 @@ class ReplayGainCliTestBase(TestHelper):
|
|||
self.assertEqual(max(gains), min(gains))
|
||||
|
||||
self.assertNotEqual(max(gains), 0.0)
|
||||
if not self.backend == "bs1770gain":
|
||||
# Actually produces peaks == 0.0 ~ self.add_album_fixture
|
||||
self.assertNotEqual(max(peaks), 0.0)
|
||||
self.assertNotEqual(max(peaks), 0.0)
|
||||
|
||||
def test_cli_writes_only_r128_tags(self):
|
||||
if self.backend == "command":
|
||||
|
|
@ -219,62 +208,6 @@ class ReplayGainCmdCliTest(ReplayGainCliTestBase, unittest.TestCase):
|
|||
backend = u'command'
|
||||
|
||||
|
||||
@unittest.skipIf(not LOUDNESS_PROG_AVAILABLE, u'bs1770gain cannot be found')
|
||||
class ReplayGainLdnsCliTest(ReplayGainCliTestBase, unittest.TestCase):
|
||||
backend = u'bs1770gain'
|
||||
|
||||
|
||||
class ReplayGainLdnsCliMalformedTest(TestHelper, unittest.TestCase):
|
||||
@patch('beetsplug.replaygain.call')
|
||||
def setUp(self, call_patch):
|
||||
self.setup_beets()
|
||||
self.config['replaygain']['backend'] = 'bs1770gain'
|
||||
|
||||
# Patch call to return nothing, bypassing the bs1770gain installation
|
||||
# check.
|
||||
call_patch.return_value = CommandOutput(
|
||||
stdout=b'bs1770gain 0.0.0, ', stderr=b''
|
||||
)
|
||||
try:
|
||||
self.load_plugins('replaygain')
|
||||
except Exception:
|
||||
import sys
|
||||
exc_info = sys.exc_info()
|
||||
try:
|
||||
self.tearDown()
|
||||
except Exception:
|
||||
pass
|
||||
six.reraise(exc_info[1], None, exc_info[2])
|
||||
|
||||
for item in self.add_album_fixture(2).items():
|
||||
reset_replaygain(item)
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
self.unload_plugins()
|
||||
|
||||
@patch('beetsplug.replaygain.call')
|
||||
def test_malformed_output(self, call_patch):
|
||||
# Return malformed XML (the ampersand should be &)
|
||||
call_patch.return_value = CommandOutput(stdout=b"""
|
||||
<album>
|
||||
<track total="1" number="1" file="&">
|
||||
<integrated lufs="0" lu="0" />
|
||||
<sample-peak spfs="0" factor="0" />
|
||||
</track>
|
||||
</album>
|
||||
""", stderr="")
|
||||
|
||||
with capture_log('beets.replaygain') as logs:
|
||||
self.run_command('replaygain')
|
||||
|
||||
# Count how many lines match the expected error.
|
||||
matching = [line for line in logs if
|
||||
'malformed XML' in line]
|
||||
|
||||
self.assertEqual(len(matching), 2)
|
||||
|
||||
|
||||
@unittest.skipIf(not FFMPEG_AVAILABLE, u'ffmpeg cannot be found')
|
||||
class ReplayGainFfmpegTest(ReplayGainCliTestBase, unittest.TestCase):
|
||||
backend = u'ffmpeg'
|
||||
|
|
|
|||
Loading…
Reference in a new issue