replaygain: introduce Task objects to bundle the state related to computations

Renames *GainHandler -> *Task and instead of having a singleton
instance, creates a *Task object for each album/item to process. The
advantage is that now, related data can be bundled in the instance,
instead of passing multiple arguments around.
This commit is contained in:
wisp3rwind 2021-03-19 11:18:01 +01:00
parent ae3e95f9d3
commit 67d85d18ad

View file

@ -103,18 +103,15 @@ ALL_PEAK_METHODS = ["true", "sample"]
Peak = enum.Enum("Peak", ["none"] + ALL_PEAK_METHODS)
class GainHandler():
def __init__(self, config, peak, log):
self.config = config
class RgTask():
def __init__(self, items, album, target_level, peak, log):
self.items = items
self.album = album
self.target_level = target_level
self.peak = peak
self._log = log
@property
def target_level(self):
"""This currently needs to reloaded from the config since the tests
modify its value on-the-fly.
"""
return self.config['targetlevel'].as_number()
self.album_gain = None
self.track_gains = None
def store_track_gain(self, item, track_gain):
item.rg_track_gain = track_gain.gain
@ -131,18 +128,7 @@ class GainHandler():
item.rg_album_gain, item.rg_album_peak)
class R128GainHandler(GainHandler):
def __init__(self, config, log):
# R128_* tags do not store the track/album peak
super().__init__(config, Peak.none, log)
@property
def target_level(self):
"""This currently needs to reloaded from the config since the tests
modify its value on-the-fly.
"""
return self.config['r128_targetlevel'].as_number()
class R128Task(RgTask):
def store_track_gain(self, item, track_gain):
item.r128_track_gain = track_gain.gain
item.store()
@ -169,15 +155,15 @@ class Backend:
"""
self._log = log
def compute_track_gain(self, items, target_level, peak):
"""Computes the track gain of the given tracks, returns a list
of Gain objects.
def compute_track_gain(self, task):
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
raise NotImplementedError()
def compute_album_gain(self, items, target_level, peak):
"""Computes the album gain of the given album, returns an
AlbumGain object.
def compute_album_gain(self, task):
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
raise NotImplementedError()
@ -218,27 +204,28 @@ class FfmpegBackend(Backend):
"the --enable-libebur128 configuration option is required."
)
def compute_track_gain(self, items, target_level, peak):
"""Computes the track gain of the given tracks, returns a list
of Gain objects (the track gains).
def compute_track_gain(self, task):
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
gains = []
for item in items:
for item in task.items:
gains.append(
self._analyse_item(
item,
target_level,
peak,
task.target_level,
task.peak,
count_blocks=False,
)[0] # take only the gain, discarding number of gating blocks
)
return gains
task.track_gains = gains
return task
def compute_album_gain(self, items, target_level, peak):
"""Computes the album gain of the given album, returns an
AlbumGain object.
def compute_album_gain(self, task):
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
target_level_lufs = db_to_lufs(target_level)
target_level_lufs = db_to_lufs(task.target_level)
# analyse tracks
# list of track Gain objects
@ -250,9 +237,9 @@ class FfmpegBackend(Backend):
# total number of BS.1770 gating blocks
n_blocks = 0
for item in items:
for item in task.items:
track_gain, track_n_blocks = self._analyse_item(
item, target_level, peak
item, task.target_level, task.peak
)
track_gains.append(track_gain)
@ -287,10 +274,11 @@ class FfmpegBackend(Backend):
self._log.debug(
"{}: gain {} LU, peak {}"
.format(items, album_gain, album_peak)
.format(task.items, album_gain, album_peak)
)
return AlbumGain(Gain(album_gain, album_peak), track_gains)
task.album_gain = AlbumGain(Gain(album_gain, album_peak), track_gains)
return task
def _construct_cmd(self, item, peak_method):
"""Construct the shell command to analyse items."""
@ -466,28 +454,30 @@ class CommandBackend(Backend):
self.noclip = config['noclip'].get(bool)
def compute_track_gain(self, items, target_level, peak):
"""Computes the track gain of the given tracks, returns a list
of TrackGain objects.
def compute_track_gain(self, task):
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
supported_items = list(filter(self.format_supported, items))
output = self.compute_gain(supported_items, target_level, False)
return output
supported_items = list(filter(self.format_supported, task.items))
output = self.compute_gain(supported_items, task.target_level, False)
task.track_gains = output
return task
def compute_album_gain(self, items, target_level, peak):
"""Computes the album gain of the given album, returns an
AlbumGain object.
def compute_album_gain(self, task):
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
# TODO: What should be done when not all tracks in the album are
# supported?
supported_items = list(filter(self.format_supported, items))
if len(supported_items) != len(items):
supported_items = list(filter(self.format_supported, task.items))
if len(supported_items) != len(task.items):
self._log.debug('tracks are of unsupported format')
return AlbumGain(None, [])
output = self.compute_gain(supported_items, target_level, True)
return AlbumGain(output[-1], output[:-1])
output = self.compute_gain(supported_items, task.target_level, True)
task.album_gain = AlbumGain(output[-1], output[:-1])
return task
def format_supported(self, item):
"""Checks whether the given item is supported by the selected tool.
@ -668,21 +658,28 @@ class GStreamerBackend(Backend):
if self._error is not None:
raise self._error
def compute_track_gain(self, items, target_level, peak):
self.compute(items, target_level, False)
if len(self._file_tags) != len(items):
def compute_track_gain(self, task):
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
self.compute(task.items, task.target_level, False)
if len(self._file_tags) != len(task.items):
raise ReplayGainError("Some tracks did not receive tags")
ret = []
for item in items:
for item in task.items:
ret.append(Gain(self._file_tags[item]["TRACK_GAIN"],
self._file_tags[item]["TRACK_PEAK"]))
return ret
task.track_gains = ret
return task
def compute_album_gain(self, items, target_level, peak):
items = list(items)
self.compute(items, target_level, True)
def compute_album_gain(self, task):
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
items = list(task.items)
self.compute(items, task.target_level, True)
if len(self._file_tags) != len(items):
raise ReplayGainError("Some items in album did not receive tags")
@ -704,7 +701,8 @@ class GStreamerBackend(Backend):
except KeyError:
raise ReplayGainError("results missing for album")
return AlbumGain(Gain(gain, peak), track_gains)
task.album_gain = AlbumGain(Gain(gain, peak), track_gains)
return task
def close(self):
self._bus.remove_signal_watch()
@ -897,12 +895,14 @@ class AudioToolsBackend(Backend):
return
return rg
def compute_track_gain(self, items, target_level, peak):
"""Compute ReplayGain values for the requested items.
:return list: list of :class:`Gain` objects
def compute_track_gain(self, task):
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
return [self._compute_track_gain(item, target_level) for item in items]
gains = [self._compute_track_gain(i, task.target_level)
for i in task.items]
task.track_gains = gains
return task
def _with_target_level(self, gain, target_level):
"""Return `gain` relative to `target_level`.
@ -947,23 +947,22 @@ class AudioToolsBackend(Backend):
item.artist, item.title, rg_track_gain, rg_track_peak)
return Gain(gain=rg_track_gain, peak=rg_track_peak)
def compute_album_gain(self, items, target_level, peak):
"""Compute ReplayGain values for the requested album and its items.
:rtype: :class:`AlbumGain`
def compute_album_gain(self, task):
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
# The first item is taken and opened to get the sample rate to
# initialize the replaygain object. The object is used for all the
# tracks in the album to get the album values.
item = list(items)[0]
item = list(task.items)[0]
audiofile = self.open_audio_file(item)
rg = self.init_replaygain(audiofile, item)
track_gains = []
for item in items:
for item in task.items:
audiofile = self.open_audio_file(item)
rg_track_gain, rg_track_peak = self._title_gain(
rg, audiofile, target_level
rg, audiofile, task.target_level
)
track_gains.append(
Gain(gain=rg_track_gain, peak=rg_track_peak)
@ -974,14 +973,16 @@ class AudioToolsBackend(Backend):
# After getting the values for all tracks, it's possible to get the
# album values.
rg_album_gain, rg_album_peak = rg.album_gain()
rg_album_gain = self._with_target_level(rg_album_gain, target_level)
rg_album_gain = self._with_target_level(
rg_album_gain, task.target_level)
self._log.debug('ReplayGain for album {0}: {1:.2f}, {2:.2f}',
items[0].album, rg_album_gain, rg_album_peak)
task.items[0].album, rg_album_gain, rg_album_peak)
return AlbumGain(
task.album_gain = AlbumGain(
Gain(gain=rg_album_gain, peak=rg_album_peak),
track_gains=track_gains
)
return task
class ExceptionWatcher(Thread):
@ -1070,17 +1071,10 @@ class ReplayGainPlugin(BeetsPlugin):
)
)
# The key in this dict is the `use_r128` flag.
self.gain_handlers = {
True: R128GainHandler(
self.config,
self._log,
),
False: GainHandler(
self.config,
Peak[peak_method],
self._log,
),
# The key in these dicts is the `use_r128` flag.
self.peak_methods = {
True: Peak.none,
False: Peak[peak_method]
}
# On-import analysis.
@ -1150,6 +1144,22 @@ class ReplayGainPlugin(BeetsPlugin):
return False
def create_task(self, items, use_r128, album=None):
if use_r128:
return R128Task(
items, album,
self.config["r128_targetlevel"].as_number(),
Peak.none, # R128_* tags do not store the track/album peak
self._log,
)
else:
return RgTask(
items, album,
self.config["targetlevel"].as_number(),
self.peak_methods[use_r128],
self._log,
)
def handle_album(self, album, write, force=False):
"""Compute album and track replay gain store it in all of the
album's items.
@ -1172,8 +1182,6 @@ class ReplayGainPlugin(BeetsPlugin):
self._log.info('analyzing {0}', album)
handler = self.gain_handlers[use_r128]
discs = {}
if self.per_disc:
for item in album.items():
@ -1184,33 +1192,30 @@ class ReplayGainPlugin(BeetsPlugin):
discs[1] = album.items()
for discnumber, items in discs.items():
def _store_album(album_gain):
if not album_gain or not album_gain.album_gain \
or len(album_gain.track_gains) != len(items):
def _store_album(task):
if task.album_gain is None or not task.album_gain.album_gain \
or len(task.album_gain.track_gains) != len(task.items):
# In some cases, backends fail to produce a valid
# `album_gain` without throwing FatalReplayGainError
# => raise non-fatal exception & continue
raise ReplayGainError(
"ReplayGain backend `{}` failed "
"for some tracks in album {}"
.format(self.backend_name, album)
.format(self.backend_name, task.album)
)
for item, track_gain in zip(items,
album_gain.track_gains):
handler.store_track_gain(item, track_gain)
handler.store_album_gain(item, album_gain.album_gain)
for item, track_gain in zip(task.items,
task.album_gain.track_gains):
task.store_track_gain(item, track_gain)
task.store_album_gain(item, task.album_gain.album_gain)
if write:
item.try_write()
self._log.debug('done analyzing {0}', item)
task = self.create_task(items, use_r128, album=album)
try:
self._apply(
self.backend_instance.compute_album_gain, args=(),
kwds={
"items": list(items),
"target_level": handler.target_level,
"peak": handler.peak,
},
self.backend_instance.compute_album_gain,
args=[task], kwds={},
callback=_store_album
)
except ReplayGainError as e:
@ -1231,10 +1236,9 @@ class ReplayGainPlugin(BeetsPlugin):
return
use_r128 = self.should_use_r128(item)
handler = self.gain_handlers[use_r128]
def _store_track(track_gains):
if not track_gains or len(track_gains) != 1:
def _store_track(task):
if task.track_gains is None or len(task.track_gains) != 1:
# In some cases, backends fail to produce a valid
# `track_gains` without throwing FatalReplayGainError
# => raise non-fatal exception & continue
@ -1243,19 +1247,16 @@ class ReplayGainPlugin(BeetsPlugin):
.format(self.backend_name, item)
)
handler.store_track_gain(item, track_gains[0])
task.store_track_gain(item, task.track_gains[0])
if write:
item.try_write()
self._log.debug('done analyzing {0}', item)
task = self.create_task([item], use_r128)
try:
self._apply(
self.backend_instance.compute_track_gain, args=(),
kwds={
"items": [item],
"target_level": handler.target_level,
"peak": handler.peak,
},
self.backend_instance.compute_track_gain,
args=[task], kwds={},
callback=_store_track
)
except ReplayGainError as e: