From 3d842db8d8507828ec1e0f6f961ac3e710fea683 Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 09:27:24 +0100 Subject: [PATCH 1/8] Added per disc album_gain support --- beetsplug/replaygain.py | 92 +++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index ac45aa4f83..868800d9b5 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -83,7 +83,7 @@ def __init__(self, config, log): def compute_track_gain(self, items): raise NotImplementedError() - def compute_album_gain(self, album): + def compute_album_gain(self, items): # TODO: implement album gain in terms of track gain of the # individual tracks which can be used for any backend. raise NotImplementedError() @@ -125,15 +125,14 @@ def compute_track_gain(self, items): output = self.compute_gain(items, False) return output - def compute_album_gain(self, album): + def compute_album_gain(self, items): """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) + output = self.compute_gain(items, True) if not output: raise ReplayGainError(u'no output from bs1770gain') @@ -316,15 +315,15 @@ def compute_track_gain(self, items): output = self.compute_gain(supported_items, False) return output - def compute_album_gain(self, album): + def compute_album_gain(self, items): """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 = list(filter(self.format_supported, album.items())) - if len(supported_items) != len(album.items()): + supported_items = list(filter(self.format_supported, items)) + if len(supported_items) != len(items): self._log.debug(u'tracks are of unsupported format') return AlbumGain(None, []) @@ -521,8 +520,8 @@ def compute_track_gain(self, items): return ret - def compute_album_gain(self, album): - items = list(album.items()) + def compute_album_gain(self, items): + items = list(items) self.compute(items, True) if len(self._file_tags) != len(items): raise ReplayGainError(u"Some items in album did not receive tags") @@ -777,22 +776,20 @@ def _compute_track_gain(self, item): 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, album): + def compute_album_gain(self, items): """Compute ReplayGain values for the requested album and its items. :rtype: :class:`AlbumGain` """ - self._log.debug(u'Analysing album {0}', album) - # 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(album.items())[0] + item = list(items)[0] audiofile = self.open_audio_file(item) rg = self.init_replaygain(audiofile, item) track_gains = [] - for item in album.items(): + for item in items: audiofile = self.open_audio_file(item) rg_track_gain, rg_track_peak = self._title_gain(rg, audiofile) track_gains.append( @@ -836,9 +833,11 @@ def __init__(self): 'backend': u'command', 'targetlevel': 89, 'r128': ['Opus'], + 'per_disc': False }) self.overwrite = self.config['overwrite'].get(bool) + self.per_disc = self.config['per_disc'].get(bool) backend_name = self.config['backend'].as_str() if backend_name not in self.backends: raise ui.UserError( @@ -919,6 +918,21 @@ def store_album_r128_gain(self, album, album_gain): self._log.debug(u'applied r128 album gain {0}', album.r128_album_gain) + def store_album_gain_item_level(self, items, album_gain): + for item in items: + item.rg_album_gain = album_gain.gain + item.rg_album_peak = album_gain.peak + item.store() + self._log.debug(u'applied album gain {0}, peak {1}', + item.rg_album_gain, item.rg_album_peak) + + def store_album_r128_gain_item_level(self, items, album_gain): + for item in items: + item.r128_album_gain = album_gain.gain + item.store() + self._log.debug(u'applied r128 album gain {0}', + item.r128_album_gain) + def handle_album(self, album, write, force=False): """Compute album and track replay gain store it in all of the album's items. @@ -946,29 +960,45 @@ def handle_album(self, album, write, force=False): backend_instance = self.r128_backend_instance store_track_gain = self.store_track_r128_gain store_album_gain = self.store_album_r128_gain + store_album_gain_item_level = self.store_album_r128_gain_item_level else: backend_instance = self.backend_instance store_track_gain = self.store_track_gain store_album_gain = self.store_album_gain + store_album_gain_item_level = self.store_album_gain_item_level + + discs = dict() + if self.per_disc: + for item in album.items(): + if discs.get(item.disc) is None: + discs[item.disc] = [] + discs[item.disc].append(item) + else: + discs[1] = album.items() - try: - album_gain = backend_instance.compute_album_gain(album) - if len(album_gain.track_gains) != len(album.items()): - raise ReplayGainError( - u"ReplayGain backend failed " - u"for some tracks in album {0}".format(album) - ) + for discnumber in discs: + try: + items = discs[discnumber] + album_gain = backend_instance.compute_album_gain(items) + if len(album_gain.track_gains) != len(items): + raise ReplayGainError( + u"ReplayGain backend failed " + u"for some tracks in album {0}".format(album) + ) - store_album_gain(album, album_gain.album_gain) - for item, track_gain in zip(album.items(), album_gain.track_gains): - store_track_gain(item, track_gain) - if write: - item.try_write() - except ReplayGainError as e: - self._log.info(u"ReplayGain error: {0}", e) - except FatalReplayGainError as e: - raise ui.UserError( - u"Fatal replay gain error: {0}".format(e)) + if len(items) == len(album.items()): + store_album_gain(album, album_gain.album_gain) + else: + store_album_gain_item_level(items, album_gain.album_gain) + for item, track_gain in zip(items, album_gain.track_gains): + store_track_gain(item, track_gain) + if write: + item.try_write() + except ReplayGainError as e: + self._log.info(u"ReplayGain error: {0}", e) + except FatalReplayGainError as e: + raise ui.UserError( + u"Fatal replay gain error: {0}".format(e)) def handle_track(self, item, write, force=False): """Compute track replay gain and store it in the item. From 1619761bd6bcd4ac4f3aa8d04e615cbb8f9f4535 Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 09:38:03 +0100 Subject: [PATCH 2/8] Updated docs with per_disc ReplayGain configuration. --- docs/plugins/replaygain.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/plugins/replaygain.rst b/docs/plugins/replaygain.rst index ad0e50e22f..3f1667c8f0 100644 --- a/docs/plugins/replaygain.rst +++ b/docs/plugins/replaygain.rst @@ -93,6 +93,8 @@ configuration file. The available options are: integer values instead of the common ``REPLAYGAIN_`` tags with floating point values. Requires the "ffmpeg" backend. Default: ``Opus``. +- **per_disc**: Calculate album ReplayGain on disc level instead of album level. + Default: ``no`` These options only work with the "command" backend: From 31326ebb20438f96702824f78df67764b58718fa Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 10:06:48 +0100 Subject: [PATCH 3/8] Simplified album ReplayGain code --- beetsplug/replaygain.py | 43 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index dba1f6baa5..660bf8c1aa 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -904,34 +904,18 @@ def store_track_r128_gain(self, item, track_gain): self._log.debug(u'applied r128 track gain {0}', item.r128_track_gain) - def store_album_gain(self, album, album_gain): - album.rg_album_gain = album_gain.gain - album.rg_album_peak = album_gain.peak - album.store() - + def store_album_gain(self, item, album_gain): + item.rg_album_gain = album_gain.gain + item.rg_album_peak = album_gain.peak + item.store() self._log.debug(u'applied album gain {0}, peak {1}', - album.rg_album_gain, album.rg_album_peak) - - def store_album_r128_gain(self, album, album_gain): - album.r128_album_gain = int(round(album_gain.gain * pow(2, 8))) - album.store() + item.rg_album_gain, item.rg_album_peak) - self._log.debug(u'applied r128 album gain {0}', album.r128_album_gain) - - def store_album_gain_item_level(self, items, album_gain): - for item in items: - item.rg_album_gain = album_gain.gain - item.rg_album_peak = album_gain.peak - item.store() - self._log.debug(u'applied album gain {0}, peak {1}', - item.rg_album_gain, item.rg_album_peak) - - def store_album_r128_gain_item_level(self, items, album_gain): - for item in items: - item.r128_album_gain = album_gain.gain - item.store() - self._log.debug(u'applied r128 album gain {0}', - item.r128_album_gain) + def store_album_r128_gain(self, item, album_gain): + item.r128_album_gain = album_gain.gain + item.store() + self._log.debug(u'applied r128 album gain {0}', + item.r128_album_gain) def handle_album(self, album, write, force=False): """Compute album and track replay gain store it in all of the @@ -960,12 +944,10 @@ def handle_album(self, album, write, force=False): backend_instance = self.r128_backend_instance store_track_gain = self.store_track_r128_gain store_album_gain = self.store_album_r128_gain - store_album_gain_item_level = self.store_album_r128_gain_item_level else: backend_instance = self.backend_instance store_track_gain = self.store_track_gain store_album_gain = self.store_album_gain - store_album_gain_item_level = self.store_album_gain_item_level discs = dict() if self.per_disc: @@ -986,12 +968,9 @@ def handle_album(self, album, write, force=False): u"for some tracks in album {0}".format(album) ) - if len(items) == len(album.items()): - store_album_gain(album, album_gain.album_gain) - else: - store_album_gain_item_level(items, album_gain.album_gain) for item, track_gain in zip(items, album_gain.track_gains): store_track_gain(item, track_gain) + store_album_gain(item, album_gain.album_gain) if write: item.try_write() except ReplayGainError as e: From 24f02cb5cd6cc51c5efd3d064ee9fcc0988628ec Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 10:12:06 +0100 Subject: [PATCH 4/8] ReplayGain refactoring --- beetsplug/replaygain.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 660bf8c1aa..2122be7a49 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -958,9 +958,8 @@ def handle_album(self, album, write, force=False): else: discs[1] = album.items() - for discnumber in discs: + for discnumber, items in discs.items(): try: - items = discs[discnumber] album_gain = backend_instance.compute_album_gain(items) if len(album_gain.track_gains) != len(items): raise ReplayGainError( From 413147d3c98562452c18fa84ac9944dfb4067681 Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 12:42:58 +0100 Subject: [PATCH 5/8] ReplayGain: Updated changelog with per_disc option. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9cab4a1e06..e5d1507a7d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -65,6 +65,10 @@ New features: :bug:`3123` * :doc:`/plugins/ipfs`: The plugin now supports a ``nocopy`` option which passes that flag to ipfs. Thanks to :user:`wildthyme`. +* :doc:`/plugins/replaygain`: The plugin now supports a ``per_disc`` option + which enables calculation of album ReplayGain on disc level instead of album + level. + Thanks to :user:`samuelnilsson` Changes: From 93007bfdd5a305a83c6a55457e1d19574444c280 Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Wed, 6 Feb 2019 13:17:34 +0100 Subject: [PATCH 6/8] ReplayGain: fixed error caused by per_disc option --- beetsplug/replaygain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 2122be7a49..40d228490c 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -802,7 +802,7 @@ def compute_album_gain(self, items): # album values. rg_album_gain, rg_album_peak = rg.album_gain() self._log.debug(u'ReplayGain for album {0}: {1:.2f}, {2:.2f}', - album, rg_album_gain, rg_album_peak) + items[0].album, rg_album_gain, rg_album_peak) return AlbumGain( Gain(gain=rg_album_gain, peak=rg_album_peak), From 2c0d9b07dbf4f5581ed186032177d2d61720d781 Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Sat, 8 Jun 2019 16:23:24 +0200 Subject: [PATCH 7/8] Fixed changelog for replaygain per_disc option --- docs/changelog.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 59def9fae9..4265646bc2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,11 @@ New features: to MPD commands we don't support yet. Let us know if you find an MPD client that doesn't get along with BPD! :bug:`3214` :bug:`800` +* :doc:`/plugins/replaygain`: The plugin now supports a ``per_disc`` option + which enables calculation of album ReplayGain on disc level instead of album + level. + Thanks to :user:`samuelnilsson` + :bug:`293` Fixes: @@ -140,10 +145,6 @@ The new core features are: :bug:`3123` * :doc:`/plugins/ipfs`: The plugin now supports a ``nocopy`` option which passes that flag to ipfs. Thanks to :user:`wildthyme`. -* :doc:`/plugins/replaygain`: The plugin now supports a ``per_disc`` option - which enables calculation of album ReplayGain on disc level instead of album - level. - Thanks to :user:`samuelnilsson` ======= * A new importer option, :ref:`ignore_data_tracks`, lets you skip audio tracks From f5c8650dc9f5a846f099af8779cecc18d40bc5ff Mon Sep 17 00:00:00 2001 From: Samuel Nilsson Date: Sat, 8 Jun 2019 16:44:03 +0200 Subject: [PATCH 8/8] Fixed merge issue regarding replaygain per_disc option --- docs/changelog.rst | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4265646bc2..baef715084 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -126,27 +126,6 @@ The new core features are: example, ``beet modify -a artist:beatles artpath!`` resets ``artpath`` attribute from matching albums back to the default value. :bug:`2497` -* Modify selection can now be applied early without selecting every item. - :bug:`3083` -* :doc:`/plugins/chroma`: Fingerprint values are now properly stored as - strings, which prevents strange repeated output when running ``beet write``. - Thanks to :user:`Holzhaus`. - :bug:`3097` :bug:`2942` -* The ``move`` command now lists the number of items already in-place. - Thanks to :user:`RollingStar`. - :bug:`3117` -* :doc:`/plugins/spotify`: The plugin now uses OAuth for authentication to the - Spotify API. - Thanks to :user:`rhlahuja`. - :bug:`2694` :bug:`3123` -* :doc:`/plugins/spotify`: The plugin now works as an import metadata - provider: you can match tracks and albums using the Spotify database. - Thanks to :user:`rhlahuja`. - :bug:`3123` -* :doc:`/plugins/ipfs`: The plugin now supports a ``nocopy`` option which passes that flag to ipfs. - Thanks to :user:`wildthyme`. - -======= * A new importer option, :ref:`ignore_data_tracks`, lets you skip audio tracks contained in data files. :bug:`3021`