mirror of
https://github.com/beetbox/beets.git
synced 2026-01-04 23:12:51 +01:00
Adds support for SoundCheck
* Adds 2 functions to convert between SoundCheck and ReplayGain * Adds relevant StorageStyle to rg_track_* for SoundCheck * Adds a new packing type for SoundCheck * Modifies StorageStyle to accept a lang argument * Modifies StorageStyle to allow specifying Packed out_type * Modifies MediaField to write COMM frames with a lang attribute
This commit is contained in:
parent
6a7034d31e
commit
8a3221abd2
1 changed files with 92 additions and 22 deletions
|
|
@ -38,6 +38,7 @@ import mutagen.asf
|
|||
import datetime
|
||||
import re
|
||||
import base64
|
||||
import math
|
||||
import struct
|
||||
import imghdr
|
||||
import os
|
||||
|
|
@ -181,13 +182,58 @@ def _pack_asf_image(mime, data, type=3, description=""):
|
|||
tag_data += data
|
||||
return tag_data
|
||||
|
||||
# SoundCheck/ReplayGain conversion
|
||||
|
||||
def _sc2rg(soundcheck):
|
||||
"""Convert a SoundCheck tag to ReplayGain values"""
|
||||
# SoundCheck tags consist 10 numbers, each represented by 8 characters
|
||||
# of ASCII hex preceded by a space.
|
||||
soundcheck = soundcheck.replace(' ', '')
|
||||
soundcheck = struct.unpack('!iiiiiiiiii', soundcheck.decode('hex'))
|
||||
# SoundCheck stores absolute calculated/measured RMS value in an unknown
|
||||
# unit. We need to find the ratio of this measurement compared to a
|
||||
# reference value of 1000 to get our gain in dB.
|
||||
left = math.log10(soundcheck[0] / 1000.0) * -10
|
||||
right = math.log10(soundcheck[1] / 1000.0) * -10
|
||||
# We play it safe by using the smallest value (i.e., largest reduction)
|
||||
gain = round(min(left, right), 2)
|
||||
# SoundCheck stores peak values as the actual value of the sample, and
|
||||
# again separately for the left and right channels. We need to convert
|
||||
# this to a percentage of full scale, which is 32768 for a 16 bit sample.
|
||||
# Once again, we play it safe by using the larger of the two values.
|
||||
peak = round(max(soundcheck[6], soundcheck[7]) / 32768.0, 6)
|
||||
return (gain, peak)
|
||||
|
||||
|
||||
def _rg2sc(gain, peak):
|
||||
"""Convert ReplayGain values to a SoundCheck tag"""
|
||||
if not isinstance(gain, float):
|
||||
gain = float(gain.lower().strip(' db'))
|
||||
# SoundCheck stores the peak value as the actual value of the sample,
|
||||
# rather than the percentage of full scale that RG uses, so we do
|
||||
# a simple conversion assuming 16 bit samples.
|
||||
peak = float(peak) * 32768.0
|
||||
# SoundCheck stores absolute RMS values in some unknown units rather than
|
||||
# the dB values RG uses. We can calculate these absolute values from the
|
||||
# gain ratio using a reference value of 1000 units. We also enforce the
|
||||
# maximum value here, which is equivalent to about -18.2dB.
|
||||
g1 = min(round((10 ** (gain / -10)) * 1000), 65534)
|
||||
# Same as above, except our reference level is 2500 units.
|
||||
g2 = min(round((10 ** (gain / -10)) * 2500), 65534)
|
||||
# The purpose of these values are unknown, but they also seem to be
|
||||
# unused so we just pick a sensible number.
|
||||
uk = 150696
|
||||
values = (g1, g1, g2, g2, uk, uk, peak, peak, uk, uk)
|
||||
soundcheck = (' %08X' * 10) % values
|
||||
return soundcheck
|
||||
|
||||
# Flags for encoding field behavior.
|
||||
|
||||
# Determine style of packing, if any.
|
||||
packing = enum('SLASHED', # pair delimited by /
|
||||
'TUPLE', # a python tuple of 2 items
|
||||
'DATE', # YYYY-MM-DD
|
||||
packing = enum('SLASHED', # pair delimited by /
|
||||
'TUPLE', # a python tuple of 2 items
|
||||
'DATE', # YYYY-MM-DD
|
||||
'SC', # 10 numbers as space preceded 8 char ASCII hex
|
||||
name='packing')
|
||||
|
||||
class StorageStyle(object):
|
||||
|
|
@ -207,15 +253,18 @@ class StorageStyle(object):
|
|||
as the key.
|
||||
"""
|
||||
def __init__(self, key, list_elem = True, as_type = unicode,
|
||||
packing = None, pack_pos = 0, id3_desc = None,
|
||||
id3_frame_field = 'text'):
|
||||
packing = None, pack_pos = 0, pack_type = int,
|
||||
id3_desc = None, id3_frame_field = 'text',
|
||||
id3_lang = None):
|
||||
self.key = key
|
||||
self.list_elem = list_elem
|
||||
self.as_type = as_type
|
||||
self.packing = packing
|
||||
self.pack_pos = pack_pos
|
||||
self.pack_type = pack_type
|
||||
self.id3_desc = id3_desc
|
||||
self.id3_frame_field = id3_frame_field
|
||||
self.id3_lang = id3_lang
|
||||
|
||||
|
||||
# Dealing with packings.
|
||||
|
|
@ -228,7 +277,7 @@ class Packed(object):
|
|||
"""Create a Packed object for subscripting the packed values in
|
||||
items. The items are packed using packstyle, which is a value
|
||||
from the packing enum. none_val is returned from a request when
|
||||
no suitable value is found in the items. Vales are converted to
|
||||
no suitable value is found in the items. Values are converted to
|
||||
out_type before they are returned.
|
||||
"""
|
||||
self.items = items
|
||||
|
|
@ -256,7 +305,8 @@ class Packed(object):
|
|||
seq = unicode(items).split('-')
|
||||
elif self.packstyle == packing.TUPLE:
|
||||
seq = items # tuple: items is already indexable
|
||||
|
||||
elif self.packstyle == packing.SC:
|
||||
seq = _sc2rg(items)
|
||||
try:
|
||||
out = seq[index]
|
||||
except:
|
||||
|
|
@ -268,8 +318,8 @@ class Packed(object):
|
|||
return _safe_cast(self.out_type, out)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if self.packstyle in (packing.SLASHED, packing.TUPLE):
|
||||
# SLASHED and TUPLE are always two-item packings
|
||||
if self.packstyle in (packing.SLASHED, packing.TUPLE, packing.SC):
|
||||
# SLASHED, TUPLE and SC are always two-item packings
|
||||
length = 2
|
||||
else:
|
||||
# DATE can have up to three fields
|
||||
|
|
@ -302,6 +352,8 @@ class Packed(object):
|
|||
self.items = '-'.join(elems)
|
||||
elif self.packstyle == packing.TUPLE:
|
||||
self.items = new_items
|
||||
elif self.packstyle == packing.SC:
|
||||
self.items = _rg2sc(*new_items)
|
||||
|
||||
|
||||
# The field itself.
|
||||
|
|
@ -397,11 +449,19 @@ class MediaField(object):
|
|||
# need to make a new frame?
|
||||
if not found:
|
||||
assert isinstance(style.id3_frame_field, str) # Keyword.
|
||||
frame = mutagen.id3.Frames[style.key](
|
||||
encoding=3,
|
||||
desc=style.id3_desc,
|
||||
**{style.id3_frame_field: val}
|
||||
)
|
||||
if style.id3_lang:
|
||||
frame = mutagen.id3.Frames[style.key](
|
||||
encoding=3,
|
||||
desc=style.id3_desc,
|
||||
lang=style.id3_lang,
|
||||
**{style.id3_frame_field: val}
|
||||
)
|
||||
else:
|
||||
frame = mutagen.id3.Frames[style.key](
|
||||
encoding=3,
|
||||
desc=style.id3_desc,
|
||||
**{style.id3_frame_field: val}
|
||||
)
|
||||
obj.mgfile.tags.add(frame)
|
||||
|
||||
# Try to match on "owner" field.
|
||||
|
|
@ -458,7 +518,7 @@ class MediaField(object):
|
|||
break
|
||||
|
||||
if style.packing:
|
||||
out = Packed(out, style.packing)[style.pack_pos]
|
||||
out = Packed(out, style.packing, out_type=style.pack_type)[style.pack_pos]
|
||||
|
||||
# MPEG-4 freeform frames are (should be?) encoded as UTF-8.
|
||||
if obj.type == 'mp4' and style.key.startswith('----:') and \
|
||||
|
|
@ -478,7 +538,7 @@ class MediaField(object):
|
|||
for style in styles:
|
||||
|
||||
if style.packing:
|
||||
p = Packed(self._fetchdata(obj, style), style.packing)
|
||||
p = Packed(self._fetchdata(obj, style), style.packing, out_type=style.pack_type)
|
||||
p[style.pack_pos] = val
|
||||
out = p.items
|
||||
|
||||
|
|
@ -489,6 +549,8 @@ class MediaField(object):
|
|||
if out is None:
|
||||
if self.out_type == int:
|
||||
out = 0
|
||||
elif self.out_type == float:
|
||||
out = 0.0
|
||||
elif self.out_type == bool:
|
||||
out = False
|
||||
elif self.out_type == unicode:
|
||||
|
|
@ -1103,9 +1165,13 @@ class MediaFile(object):
|
|||
|
||||
# ReplayGain fields.
|
||||
rg_track_gain = FloatValueField(2, 'dB',
|
||||
mp3 = StorageStyle('TXXX', id3_desc=u'REPLAYGAIN_TRACK_GAIN'),
|
||||
mp4 = StorageStyle('----:com.apple.iTunes:replaygain_track_gain',
|
||||
as_type=str),
|
||||
mp3 = [StorageStyle('TXXX', id3_desc=u'REPLAYGAIN_TRACK_GAIN'),
|
||||
StorageStyle('COMM', id3_desc=u'iTunNORM', id3_lang='eng',
|
||||
packing=packing.SC, pack_pos=0, pack_type=float)],
|
||||
mp4 = [StorageStyle('----:com.apple.iTunes:replaygain_track_gain',
|
||||
as_type=str),
|
||||
StorageStyle('----:com.apple.iTunes:iTunNORM',
|
||||
packing=packing.SC, pack_pos=0, pack_type=float)],
|
||||
etc = StorageStyle(u'REPLAYGAIN_TRACK_GAIN'),
|
||||
asf = StorageStyle(u'replaygain_track_gain'),
|
||||
)
|
||||
|
|
@ -1117,9 +1183,13 @@ class MediaFile(object):
|
|||
asf = StorageStyle(u'replaygain_album_gain'),
|
||||
)
|
||||
rg_track_peak = FloatValueField(6, None,
|
||||
mp3 = StorageStyle('TXXX', id3_desc=u'REPLAYGAIN_TRACK_PEAK'),
|
||||
mp4 = StorageStyle('----:com.apple.iTunes:replaygain_track_peak',
|
||||
as_type=str),
|
||||
mp3 = [StorageStyle('TXXX', id3_desc=u'REPLAYGAIN_TRACK_PEAK'),
|
||||
StorageStyle('COMM', id3_desc=u'iTunNORM', id3_lang='eng',
|
||||
packing=packing.SC, pack_pos=1, pack_type=float)],
|
||||
mp4 = [StorageStyle('----:com.apple.iTunes:replaygain_track_peak',
|
||||
as_type=str),
|
||||
StorageStyle('----:com.apple.iTunes:iTunNORM',
|
||||
packing=packing.SC, pack_pos=1, pack_type=float)],
|
||||
etc = StorageStyle(u'REPLAYGAIN_TRACK_PEAK'),
|
||||
asf = StorageStyle(u'replaygain_track_peak'),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue