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

RFC: replaygain: R128 support #2560

Merged
merged 4 commits into from
Jun 11, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -941,6 +944,7 @@ class Album(LibModel):
'albumdisambig',
'rg_album_gain',
'rg_album_peak',
'r128_album_gain',
'original_year',
'original_month',
'original_day',
Expand Down
38 changes: 36 additions & 2 deletions beets/mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -2005,6 +2007,38 @@ def update(self, dict):
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'),
Expand Down
86 changes: 78 additions & 8 deletions beetsplug/replaygain.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ def __init__(self):
'auto': True,
'backend': u'command',
'targetlevel': 89,
'r128': ['Opus'],
})

self.overwrite = self.config['overwrite'].get(bool)
Expand All @@ -822,6 +823,9 @@ def __init__(self):
if self.config['auto']:
self.import_stages = [self.imported]

# Formats to use R128.
self.r128_whitelist = self.config['r128'].as_str_seq()
Copy link
Member

Choose a reason for hiding this comment

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

Cool! This configurable format list makes sense to me. (We'll need to add it to the documentation eventually—unless we want to leave it undocumented, if we think people shouldn't be fiddling with it.)


try:
self.backend_instance = self.backends[backend_name](
self.config, self._log
Expand All @@ -830,18 +834,32 @@ def __init__(self):
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
# recalculation. This way, if any file among an album's tracks
# 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
Expand All @@ -851,6 +869,12 @@ def store_track_gain(self, item, track_gain):
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
Expand All @@ -859,6 +883,12 @@ def store_album_gain(self, album, album_gain):
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.
Expand All @@ -873,17 +903,35 @@ def handle_album(self, album, write):

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:
Expand All @@ -905,14 +953,23 @@ def handle_track(self, item, write):

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:
Expand All @@ -921,6 +978,19 @@ def handle_track(self, item, write):
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``.
"""
Expand Down
4 changes: 4 additions & 0 deletions docs/plugins/replaygain.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
5 changes: 5 additions & 0 deletions test/test_mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -672,6 +674,9 @@ def _generate_tags(self, base=None):
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

Expand Down