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:
Dave Hayes 2013-02-06 13:57:20 -06:00
parent 6a7034d31e
commit 8a3221abd2

View file

@ -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'),
)