CC-430: Audio normalization (Replaygain Support)

-Added support on media-monitor's side
This commit is contained in:
Martin Konecny 2012-07-05 21:54:44 -04:00
parent 1cbb0345b3
commit a687e48d80
7 changed files with 195 additions and 116 deletions

View File

@ -5,9 +5,6 @@ import time
import pyinotify import pyinotify
import shutil import shutil
from subprocess import Popen, PIPE
from api_clients import api_client
class AirtimeMediaMonitorBootstrap(): class AirtimeMediaMonitorBootstrap():
"""AirtimeMediaMonitorBootstrap constructor """AirtimeMediaMonitorBootstrap constructor
@ -78,7 +75,7 @@ class AirtimeMediaMonitorBootstrap():
""" """
def sync_database_to_filesystem(self, dir_id, dir): def sync_database_to_filesystem(self, dir_id, dir):
dir = os.path.normpath(dir)+"/" dir = os.path.normpath(dir) + "/"
""" """
@ -109,7 +106,7 @@ class AirtimeMediaMonitorBootstrap():
if os.path.exists(self.mmc.timestamp_file): if os.path.exists(self.mmc.timestamp_file):
"""find files that have been modified since the last time media-monitor process started.""" """find files that have been modified since the last time media-monitor process started."""
time_diff_sec = time.time() - os.path.getmtime(self.mmc.timestamp_file) time_diff_sec = time.time() - os.path.getmtime(self.mmc.timestamp_file)
command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable -mmin -%d" % (dir, time_diff_sec/60+1) command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable -mmin -%d" % (dir, time_diff_sec / 60 + 1)
else: else:
command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable" % dir command = "find '%s' -iname '*.ogg' -o -iname '*.mp3' -type f -readable" % dir

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import replay_gain
import os import os
import hashlib import hashlib
import mutagen import mutagen
import logging import logging
import math import math
import re
import traceback import traceback
from api_clients import api_client
""" """
list of supported easy tags in mutagen version 1.20 list of supported easy tags in mutagen version 1.20
@ -18,39 +19,39 @@ class AirtimeMetadata:
def __init__(self): def __init__(self):
self.airtime2mutagen = {\ self.airtime2mutagen = {\
"MDATA_KEY_TITLE": "title",\ "MDATA_KEY_TITLE": "title", \
"MDATA_KEY_CREATOR": "artist",\ "MDATA_KEY_CREATOR": "artist", \
"MDATA_KEY_SOURCE": "album",\ "MDATA_KEY_SOURCE": "album", \
"MDATA_KEY_GENRE": "genre",\ "MDATA_KEY_GENRE": "genre", \
"MDATA_KEY_MOOD": "mood",\ "MDATA_KEY_MOOD": "mood", \
"MDATA_KEY_TRACKNUMBER": "tracknumber",\ "MDATA_KEY_TRACKNUMBER": "tracknumber", \
"MDATA_KEY_BPM": "bpm",\ "MDATA_KEY_BPM": "bpm", \
"MDATA_KEY_LABEL": "organization",\ "MDATA_KEY_LABEL": "organization", \
"MDATA_KEY_COMPOSER": "composer",\ "MDATA_KEY_COMPOSER": "composer", \
"MDATA_KEY_ENCODER": "encodedby",\ "MDATA_KEY_ENCODER": "encodedby", \
"MDATA_KEY_CONDUCTOR": "conductor",\ "MDATA_KEY_CONDUCTOR": "conductor", \
"MDATA_KEY_YEAR": "date",\ "MDATA_KEY_YEAR": "date", \
"MDATA_KEY_URL": "website",\ "MDATA_KEY_URL": "website", \
"MDATA_KEY_ISRC": "isrc",\ "MDATA_KEY_ISRC": "isrc", \
"MDATA_KEY_COPYRIGHT": "copyright",\ "MDATA_KEY_COPYRIGHT": "copyright", \
} }
self.mutagen2airtime = {\ self.mutagen2airtime = {\
"title": "MDATA_KEY_TITLE",\ "title": "MDATA_KEY_TITLE", \
"artist": "MDATA_KEY_CREATOR",\ "artist": "MDATA_KEY_CREATOR", \
"album": "MDATA_KEY_SOURCE",\ "album": "MDATA_KEY_SOURCE", \
"genre": "MDATA_KEY_GENRE",\ "genre": "MDATA_KEY_GENRE", \
"mood": "MDATA_KEY_MOOD",\ "mood": "MDATA_KEY_MOOD", \
"tracknumber": "MDATA_KEY_TRACKNUMBER",\ "tracknumber": "MDATA_KEY_TRACKNUMBER", \
"bpm": "MDATA_KEY_BPM",\ "bpm": "MDATA_KEY_BPM", \
"organization": "MDATA_KEY_LABEL",\ "organization": "MDATA_KEY_LABEL", \
"composer": "MDATA_KEY_COMPOSER",\ "composer": "MDATA_KEY_COMPOSER", \
"encodedby": "MDATA_KEY_ENCODER",\ "encodedby": "MDATA_KEY_ENCODER", \
"conductor": "MDATA_KEY_CONDUCTOR",\ "conductor": "MDATA_KEY_CONDUCTOR", \
"date": "MDATA_KEY_YEAR",\ "date": "MDATA_KEY_YEAR", \
"website": "MDATA_KEY_URL",\ "website": "MDATA_KEY_URL", \
"isrc": "MDATA_KEY_ISRC",\ "isrc": "MDATA_KEY_ISRC", \
"copyright": "MDATA_KEY_COPYRIGHT",\ "copyright": "MDATA_KEY_COPYRIGHT", \
} }
self.logger = logging.getLogger() self.logger = logging.getLogger()
@ -67,9 +68,9 @@ class AirtimeMetadata:
## return format hh:mm:ss.uuu ## return format hh:mm:ss.uuu
def format_length(self, mutagen_length): def format_length(self, mutagen_length):
t = float(mutagen_length) t = float(mutagen_length)
h = int(math.floor(t/3600)) h = int(math.floor(t / 3600))
t = t % 3600 t = t % 3600
m = int(math.floor(t/60)) m = int(math.floor(t / 60))
s = t % 60 s = t % 60
# will be ss.uuu # will be ss.uuu
@ -123,8 +124,14 @@ class AirtimeMetadata:
self.logger.info("getting info from filepath %s", filepath) self.logger.info("getting info from filepath %s", filepath)
md = {}
replay_gain_val = replay_gain.calculate_replay_gain(filepath)
self.logger.info('ReplayGain calculated as %s for %s' % (replay_gain_val, filepath))
md['MDATA_KEY_REPLAYGAIN'] = replay_gain_val
try: try:
md = {}
md5 = self.get_md5(filepath) md5 = self.get_md5(filepath)
md['MDATA_KEY_MD5'] = md5 md['MDATA_KEY_MD5'] = md5
@ -135,6 +142,7 @@ class AirtimeMetadata:
self.logger.error("Exception %s", e) self.logger.error("Exception %s", e)
return None return None
#check if file has any metadata #check if file has any metadata
if file_info is None: if file_info is None:
return None return None
@ -149,6 +157,7 @@ class AirtimeMetadata:
except Exception, e: except Exception, e:
self.logger.error('Exception: %s', e) self.logger.error('Exception: %s', e)
self.logger.error("traceback: %s", traceback.format_exc()) self.logger.error("traceback: %s", traceback.format_exc())
if 'MDATA_KEY_TITLE' not in md: if 'MDATA_KEY_TITLE' not in md:
#get rid of file extension from original name, name might have more than 1 '.' in it. #get rid of file extension from original name, name might have more than 1 '.' in it.
original_name = os.path.basename(filepath) original_name = os.path.basename(filepath)
@ -165,8 +174,6 @@ class AirtimeMetadata:
pass pass
if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring): if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring):
match = re.search('^(\d*/\d*)?', md['MDATA_KEY_TRACKNUMBER'])
try: try:
md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'].split("/")[0], 10) md['MDATA_KEY_TRACKNUMBER'] = int(md['MDATA_KEY_TRACKNUMBER'].split("/")[0], 10)
except Exception, e: except Exception, e:
@ -223,7 +230,7 @@ class AirtimeMetadata:
md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", None) md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", None)
md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", None) md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", None)
self.logger.info( "Bitrate: %s , Samplerate: %s", md['MDATA_KEY_BITRATE'], md['MDATA_KEY_SAMPLERATE'] ) self.logger.info("Bitrate: %s , Samplerate: %s", md['MDATA_KEY_BITRATE'], md['MDATA_KEY_SAMPLERATE'])
try: md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length) try: md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length)
except Exception as e: self.logger.warn("File: '%s' raises: %s", filepath, str(e)) except Exception as e: self.logger.warn("File: '%s' raises: %s", filepath, str(e))

View File

@ -8,13 +8,11 @@ import traceback
# For RabbitMQ # For RabbitMQ
from kombu.connection import BrokerConnection from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue, Consumer, Producer from kombu.messaging import Exchange, Queue, Consumer
import pyinotify import pyinotify
from pyinotify import Notifier from pyinotify import Notifier
#from api_clients import api_client
from api_clients import api_client
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
class AirtimeNotifier(Notifier): class AirtimeNotifier(Notifier):
@ -153,7 +151,6 @@ class AirtimeNotifier(Notifier):
md.update(file_md) md.update(file_md)
else: else:
file_md = None file_md = None
data = None
if (os.path.exists(filepath) and (mode == self.config.MODE_CREATE)): if (os.path.exists(filepath) and (mode == self.config.MODE_CREATE)):
if file_md is None: if file_md is None:

View File

@ -9,7 +9,6 @@ import difflib
import traceback import traceback
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import pyinotify
from pyinotify import ProcessEvent from pyinotify import ProcessEvent
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
@ -59,7 +58,7 @@ class AirtimeProcessEvent(ProcessEvent):
if "-unknown-path" in path: if "-unknown-path" in path:
unknown_path = path unknown_path = path
pos = path.find("-unknown-path") pos = path.find("-unknown-path")
path = path[0:pos]+"/" path = path[0:pos] + "/"
list = self.api_client.list_all_watched_dirs() list = self.api_client.list_all_watched_dirs()
# case where the dir that is being watched is moved to somewhere # case where the dir that is being watched is moved to somewhere
@ -241,7 +240,7 @@ class AirtimeProcessEvent(ProcessEvent):
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
self.logger.info("process_IN_MOVED_TO: %s", event) self.logger.info("process_IN_MOVED_TO: %s", event)
# if /etc/mtab is modified # if /etc/mtab is modified
filename = self.mount_file_dir +"/mtab" filename = self.mount_file_dir + "/mtab"
if event.pathname in filename: if event.pathname in filename:
self.handle_mount_change() self.handle_mount_change()
@ -404,7 +403,7 @@ class AirtimeProcessEvent(ProcessEvent):
if os.path.exists(k): if os.path.exists(k):
# check if file is open # check if file is open
try: try:
command = "lsof "+k command = "lsof " + k
#f = os.popen(command) #f = os.popen(command)
f = Popen(command, shell=True, stdout=PIPE).stdout f = Popen(command, shell=True, stdout=PIPE).stdout
except Exception, e: except Exception, e:

View File

@ -10,7 +10,6 @@ import traceback
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from airtimemetadata import AirtimeMetadata from airtimemetadata import AirtimeMetadata
from api_clients import api_client
import pyinotify import pyinotify
class MediaMonitorCommon: class MediaMonitorCommon:
@ -75,7 +74,7 @@ class MediaMonitorCommon:
try: try:
return self.is_user_readable(item, 'www-data', 'www-data') \ return self.is_user_readable(item, 'www-data', 'www-data') \
and self.is_user_readable(item, 'pypo', 'pypo') and self.is_user_readable(item, 'pypo', 'pypo')
except Exception, e: except Exception:
self.logger.warn(u"Failed to check owner/group/permissions for %s", item) self.logger.warn(u"Failed to check owner/group/permissions for %s", item)
return False return False
@ -110,7 +109,7 @@ class MediaMonitorCommon:
else: else:
pathname = dirname pathname = dirname
is_dir = True is_dir = True
except Exception, e: except Exception:
#something went wrong while we were trying to make world readable. #something went wrong while we were trying to make world readable.
return False return False
@ -194,7 +193,7 @@ class MediaMonitorCommon:
break break
except Exception, e: except Exception, e:
self.logger.error("Exception %s", e) self.logger.error("Exception %s", e)
return filepath return filepath
@ -273,7 +272,7 @@ class MediaMonitorCommon:
File name charset encoding is UTF-8. File name charset encoding is UTF-8.
""" """
stdout = stdout.decode("UTF-8") stdout = stdout.decode("UTF-8")
except Exception, e: except Exception:
stdout = None stdout = None
self.logger.error("Could not decode %s using UTF-8" % stdout) self.logger.error("Could not decode %s using UTF-8" % stdout)

View File

@ -0,0 +1,81 @@
from subprocess import Popen, PIPE
import re
import os
import sys
def get_process_output(command):
"""
Run subprocess and return stdout
"""
p = Popen(command, shell=True, stdout=PIPE)
return p.communicate()[0].strip()
def run_process(command):
"""
Run subprocess and return "return code"
"""
p = Popen(command, shell=True)
return os.waitpid(p.pid, 0)[1]
def get_mime_type(file_path):
"""
Attempts to get the mime type but will return prematurely if the process
takes longer than 5 seconds. Note that this function should only be called
for files which do not have a mp3/ogg/flac extension.
"""
return get_process_output("timeout 5 file -b --mime-type %s" % file_path)
def calculate_replay_gain(file_path):
"""
This function accepts files of type mp3/ogg/flac and returns a calculated ReplayGain value.
If the value cannot be calculated for some reason, then we default to 1.
TODO:
Currently some of the subprocesses called will actually insert metadata into the file itself,
which we do *not* want as this changes the file's hash. Need to make a copy of the file before
we run this function.
http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification
"""
search = None
if re.search(r'mp3$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "audio/mpeg":
if run_process("which mp3gain > /dev/null") == 0:
out = get_process_output('mp3gain -q "%s" 2> /dev/null' % file_path)
search = re.search(r'Recommended "Track" dB change: (.*)', out)
else:
print "mp3gain not found"
#Log warning
elif re.search(r'ogg$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "application/ogg":
if run_process("which vorbisgain > /dev/null && which ogginfo > dev/null") == 0:
run_process('vorbisgain -q -f "%s" 2>/dev/null >/dev/null' % file_path)
out = get_process_output('ogginfo "%s"' % file_path)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
print "vorbisgain/ogginfo not found"
#Log warning
elif re.search(r'flac$', file_path, re.IGNORECASE) or get_mime_type(file_path) == "audio/x-flac":
if run_process("which metaflac > /dev/null") == 0:
out = get_process_output('metaflac --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' % file_path)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
print "metaflac not found"
#Log warning
else:
pass
#Log unknown file type.
replay_gain = 1
if search:
matches = search.groups()
if len(matches) == 1:
replay_gain = matches[0]
return replay_gain
# Example of running from command line:
# python replay_gain.py /path/to/filename.mp3
if __name__ == "__main__":
print calculate_replay_gain(sys.argv[1])

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from mediaconfig import AirtimeMediaConfig
import traceback import traceback
import os import os