Merge pull request #2560 from autrimpo/r128

RFC: replaygain: R128 support
This commit is contained in:
Adrian Sampson 2017-06-10 20:32:42 -04:00
commit 0e47095bb0
5 changed files with 127 additions and 10 deletions

View file

@ -454,6 +454,8 @@ class Item(LibModel):
'rg_track_peak': types.NULL_FLOAT,
'rg_album_gain': types.NULL_FLOAT,
'rg_album_peak': types.NULL_FLOAT,
'r128_track_gain': types.PaddedInt(6),
'r128_album_gain': types.PaddedInt(6),
'original_year': types.PaddedInt(4),
'original_month': types.PaddedInt(2),
'original_day': types.PaddedInt(2),
@ -898,6 +900,7 @@ class Album(LibModel):
'albumdisambig': types.STRING,
'rg_album_gain': types.NULL_FLOAT,
'rg_album_peak': types.NULL_FLOAT,
'r128_album_gain': types.PaddedInt(6),
'original_year': types.PaddedInt(4),
'original_month': types.PaddedInt(2),
'original_day': types.PaddedInt(2),
@ -941,6 +944,7 @@ class Album(LibModel):
'albumdisambig',
'rg_album_gain',
'rg_album_peak',
'r128_album_gain',
'original_year',
'original_month',
'original_day',

View file

@ -154,10 +154,12 @@ def _safe_cast(out_type, val):
return int(val)
else:
# Process any other type as a string.
if not isinstance(val, six.string_types):
if isinstance(val, bytes):
val = val.decode('utf-8', 'ignore')
elif not isinstance(val, six.string_types):
val = six.text_type(val)
# Get a number from the front of the string.
val = re.match(r'[0-9]*', val.strip()).group(0)
val = re.match(r'[\+-]?[0-9]*', val.strip()).group(0)
if not val:
return 0
else:
@ -2005,6 +2007,38 @@ class MediaFile(object):
out_type=float,
)
# EBU R128 fields.
r128_track_gain = MediaField(
MP3DescStorageStyle(
u'R128_TRACK_GAIN'
),
MP4StorageStyle(
'----:com.apple.iTunes:R128_TRACK_GAIN'
),
StorageStyle(
u'R128_TRACK_GAIN'
),
ASFStorageStyle(
u'R128_TRACK_GAIN'
),
out_type=int,
)
r128_album_gain = MediaField(
MP3DescStorageStyle(
u'R128_ALBUM_GAIN'
),
MP4StorageStyle(
'----:com.apple.iTunes:R128_ALBUM_GAIN'
),
StorageStyle(
u'R128_ALBUM_GAIN'
),
ASFStorageStyle(
u'R128_ALBUM_GAIN'
),
out_type=int,
)
initial_key = MediaField(
MP3StorageStyle('TKEY'),
MP4StorageStyle('----:com.apple.iTunes:initialkey'),

View file

@ -805,6 +805,7 @@ class ReplayGainPlugin(BeetsPlugin):
'auto': True,
'backend': u'command',
'targetlevel': 89,
'r128': ['Opus'],
})
self.overwrite = self.config['overwrite'].get(bool)
@ -822,6 +823,9 @@ class ReplayGainPlugin(BeetsPlugin):
if self.config['auto']:
self.import_stages = [self.imported]
# Formats to use R128.
self.r128_whitelist = self.config['r128'].as_str_seq()
try:
self.backend_instance = self.backends[backend_name](
self.config, self._log
@ -830,9 +834,19 @@ class ReplayGainPlugin(BeetsPlugin):
raise ui.UserError(
u'replaygain initialization failed: {0}'.format(e))
self.r128_backend_instance = ''
def should_use_r128(self, item):
"""Checks the plugin setting to decide whether the calculation
should be done using the EBU R128 standard and use R128_ tags instead.
"""
return item.format in self.r128_whitelist
def track_requires_gain(self, item):
return self.overwrite or \
(not item.rg_track_gain or not item.rg_track_peak)
(self.should_use_r128(item) and not item.r128_track_gain) or \
(not self.should_use_r128(item) and
(not item.rg_track_gain or not item.rg_track_peak))
def album_requires_gain(self, album):
# Skip calculating gain only when *all* files don't need
@ -840,8 +854,12 @@ class ReplayGainPlugin(BeetsPlugin):
# needs recalculation, we still get an accurate album gain
# value.
return self.overwrite or \
any([not item.rg_album_gain or not item.rg_album_peak
for item in album.items()])
any([self.should_use_r128(item) and
(not item.r128_item_gain or not item.r128_album_gain)
for item in album.items()]) or \
any([not self.should_use_r128(item) and
(not item.rg_album_gain or not item.rg_album_peak)
for item in album.items()])
def store_track_gain(self, item, track_gain):
item.rg_track_gain = track_gain.gain
@ -851,6 +869,12 @@ class ReplayGainPlugin(BeetsPlugin):
self._log.debug(u'applied track gain {0}, peak {1}',
item.rg_track_gain, item.rg_track_peak)
def store_track_r128_gain(self, item, track_gain):
item.r128_track_gain = int(round(track_gain.gain * pow(2, 8)))
item.store()
self._log.debug(u'applied 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
@ -859,6 +883,12 @@ class ReplayGainPlugin(BeetsPlugin):
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()
self._log.debug(u'applied album gain {0}', album.r128_album_gain)
def handle_album(self, album, write):
"""Compute album and track replay gain store it in all of the
album's items.
@ -873,17 +903,35 @@ class ReplayGainPlugin(BeetsPlugin):
self._log.info(u'analyzing {0}', album)
if (any([self.should_use_r128(item) for item in album.items()]) and
all(([self.should_use_r128(item) for item in album.items()]))):
raise ReplayGainError(
u"Mix of ReplayGain and EBU R128 detected"
u"for some tracks in album {0}".format(album)
)
if any([self.should_use_r128(item) for item in album.items()]):
if self.r128_backend_instance == '':
self.init_r128_backend()
backend_instance = self.r128_backend_instance
store_track_gain = self.store_track_r128_gain
store_album_gain = self.store_album_r128_gain
else:
backend_instance = self.backend_instance
store_track_gain = self.store_track_gain
store_album_gain = self.store_album_gain
try:
album_gain = self.backend_instance.compute_album_gain(album)
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)
)
self.store_album_gain(album, album_gain.album_gain)
store_album_gain(album, album_gain.album_gain)
for item, track_gain in zip(album.items(), album_gain.track_gains):
self.store_track_gain(item, track_gain)
store_track_gain(item, track_gain)
if write:
item.try_write()
except ReplayGainError as e:
@ -905,14 +953,23 @@ class ReplayGainPlugin(BeetsPlugin):
self._log.info(u'analyzing {0}', item)
if self.should_use_r128(item):
if self.r128_backend_instance == '':
self.init_r128_backend()
backend_instance = self.r128_backend_instance
store_track_gain = self.store_track_r128_gain
else:
backend_instance = self.backend_instance
store_track_gain = self.store_track_gain
try:
track_gains = self.backend_instance.compute_track_gain([item])
track_gains = backend_instance.compute_track_gain([item])
if len(track_gains) != 1:
raise ReplayGainError(
u"ReplayGain backend failed for track {0}".format(item)
)
self.store_track_gain(item, track_gains[0])
store_track_gain(item, track_gains[0])
if write:
item.try_write()
except ReplayGainError as e:
@ -921,6 +978,19 @@ class ReplayGainPlugin(BeetsPlugin):
raise ui.UserError(
u"Fatal replay gain error: {0}".format(e))
def init_r128_backend(self):
backend_name = 'bs1770gain'
try:
self.r128_backend_instance = self.backends[backend_name](
self.config, self._log
)
except (ReplayGainError, FatalReplayGainError) as e:
raise ui.UserError(
u'replaygain initialization failed: {0}'.format(e))
self.r128_backend_instance.method = '--ebu'
def imported(self, session, task):
"""Add replay gain info to items or albums of ``task``.
"""

View file

@ -108,6 +108,10 @@ configuration file. The available options are:
Default: ``no``.
- **targetlevel**: A number of decibels for the target loudness level.
Default: 89.
- **r128**: A space separated list of formats that will use ``R128_`` tags with
integer values instead of the common ``REPLAYGAIN_`` tags with floating point
values. Requires the "bs1770gain" backend.
Default: ``Opus``.
These options only work with the "command" backend:

View file

@ -374,6 +374,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'rg_track_gain',
'rg_album_peak',
'rg_album_gain',
'r128_track_gain',
'r128_album_gain',
'albumartist',
'mb_albumartistid',
'artist_sort',
@ -672,6 +674,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
if key.startswith('rg_'):
# ReplayGain is float
tags[key] = 1.0
elif key.startswith('r128_'):
# R128 is int
tags[key] = -1
else:
tags[key] = 'value\u2010%s' % key