Source code for loudnessPlotter.loudness

#/usr/bin/python
# -*- coding: utf-8 -*-

from string import Template
import os
import subprocess
import glob
import sys

__version__='0.2.0'
__author__='seb@mikrolax.me'


tpl_CDN='''<!DOCTYPE html>
<html lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <title>loudnessPlotter</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="plot loudness mesurement">
    <meta name="author" content="seb 'mikrolax'">
    <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap-combined.min.css" rel="stylesheet">

    <style>
      body {
        padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
      }
    </style>
    <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
      <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
  <!--[if lte IE 8]><script language="javascript" type="text/javascript" src="excanvas.min.js"></script><![endif]-->
    <link rel="shortcut icon" href="favicon.ico">
  </head>
  <body>

    <div class="navbar navbar-inverse navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <a class="brand" href="#">loudnessPlotter</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <!-- <li class="active"><a href="https://github.com/mikrolax/loudnessPlotter/issues">Bugs</a></li> -->
            </ul>
          </div><!--/.nav-collapse -->
        </div>
      </div>
    </div>
    
  <div class="container">
    <h3> $filename </h3><hr>
      $htmlstats
      <div id="placeholder" style="width:900px;height:450px"></div>
      
            
    <footer>
      <hr>
      <p class="pull-right"> <a href="https://github.com/mikrolax/loudnessPlotter">loudnessPlotter</a> - 2012 </p>
    </footer>
  </div>
  
  <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
  <script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/js/bootstrap.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/flot/0.7/jquery.flot.min.js"></script>  
  <script language="javascript" type="text/javascript">
  $(document).ready(function() {
  $.plot($("#placeholder"), $datas,$options);
  });
  </script>

  </body></html>
'''


tpl_multi_fromCDN='''<!DOCTYPE html>
<html lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <title>loudnessPlotter</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="plot loudness mesurement">
    <meta name="author" content="seb 'mikrolax'">
    <!-- Le styles -->
    <style>
      body {
        padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
      }
    </style>
    <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap-combined.min.css" rel="stylesheet">
    <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
      <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
  <!--[if lte IE 8]><script language="javascript" type="text/javascript" src="excanvas.min.js"></script><![endif]-->
    <link rel="shortcut icon" href="favicon.ico">
    
  </head>
  <body>

    <div class="navbar navbar-inverse navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <a class="brand" href="#">loudnessPlotter</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <!-- <li class="active"><a href="#">Home</a></li> -->
            </ul>
          </div><!--/.nav-collapse -->
        </div>
      </div>
    </div>
    
  <div class="container">
  $tabbedplaceholder
  <footer>
  <hr>
  <p class="pull-right"> <a href="https://github.com/mikrolax/loudnessPlotter">loudnessPlotter</a> - 2012 </p>
  </footer>
  </div>
  
  <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
  <script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/js/bootstrap.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/flot/0.7/jquery.flot.min.js"></script>
<script language="javascript" type="text/javascript">
$(document).ready(function() {
$plots
});
</script>
</body></html>
'''
    

#for py2exe freezing support 
[docs]def we_are_frozen(): """Returns whether we are frozen via py2exe. This will affect how we find out where we are located.""" return hasattr(sys, "frozen")
[docs]def module_path(): """ This will get us the program's directory, even if we are frozen using py2exe""" if we_are_frozen(): return os.path.dirname(unicode(sys.executable, sys.getfilesystemencoding( ))) else: return os.path.dirname(os.path.abspath(__file__))
[docs]def writeHTML(loudnessdata,htmlout): #pass snippet? """ Write a single self-contained HTML page with graph. """ #print 'writeHTML %s' %os.getcwd() fout=open(htmlout+'.html','w') htmlstats=HTMLstats(stats(loudnessdata)) datas=[] # getData(self.loudnessdata) M=[] S=[] I=[] idx_m=0 idx_s=0 idx_i=0 for val in loudnessdata['M']: M.append([idx_m*2*0.1,loudnessdata['M'][idx_m]]) idx_m+=1 for val in loudnessdata['S']: S.append([idx_s*0.1,loudnessdata['S'][idx_s]]) idx_s+=1 for val in loudnessdata['M']: I.append([idx_i*2*0.1,loudnessdata['I'][0]]) idx_i+=1 Mdict={} Mdict['label']='Momentary' Mdict['data']=M Sdict={} Sdict['label']='Short-Term' Sdict['data']=S Idict={} Idict['label']='Integrated' Idict['data']=I datas.append(Mdict) datas.append(Sdict) datas.append(Idict) options='''{ series: {lines: {show: true, steps: true },points: { show: false },bars: { show: false }}, yaxis: { tickFormatter: function (v) { return v + " LUFS"; } }, xaxis: { tickFormatter: function (v) { return v + " s"; } }, legend: { position: 'se' } }''' #if snippet ==True: # s = Template(snippet) # page=s.safe_substitute(filename=os.path.basename(htmlout),htmlstats=htmlstats,datas=datas,options=options) #,options=options #else: s = Template(tpl_CDN) page=s.safe_substitute(filename=os.path.basename(htmlout),htmlstats=htmlstats,datas=datas,options=options) #,options=options print 'write %s' %htmlout+'.html' fout.write(str(page)) return page#not really needed...
[docs]def stats(loudnessdata): """ get min/max/average value of M,S,(I) value. Return a dictionnary """ stats={} stats['M']={} stats['S']={} idx=0 maxVal=-100 minVal=0 avgVal=0 for item in loudnessdata['M']: try: data=float(loudnessdata['M'][idx]) except: #print loudnessdata['M'][idx] #weird => : 1.#F data=-100.0 pass if data > maxVal: maxVal=data elif data < minVal: minVal=data avgVal+=data/len(loudnessdata['M']) idx+=1 stats['M']['min']=minVal stats['M']['max']=maxVal stats['M']['avg']='{0:.2f}'.format(avgVal) idx=0 maxVal=-100 minVal=0 avgVal=0 for item in loudnessdata['S']: try: data=float(loudnessdata['S'][idx]) except: #print loudnessdata['S'][idx] data=-100 pass if data > maxVal: maxVal=data elif data < minVal: minVal=data avgVal+=data/len(loudnessdata['S']) idx+=1 stats['S']['min']=minVal stats['S']['max']=maxVal stats['S']['avg']='{0:.2f}'.format(avgVal) try: data=float(loudnessdata['I'][0]) except: data=-100.0 stats['I']=data #import pprint #pprint.pprint(stats) stats['LRA']=LRA(loudnessdata) return stats
[docs]def parseLoudnessLog(filepath): """ return dict : { 'M' : [val,val2], 'S' : [value,value], 'I' : [integratedvalue]} value are string reprensenting LUFS value """ loudnessdata={} loudnessdata['M']=[] loudnessdata['S']=[] loudnessdata['I']=[] key='' lines = open(filepath,'r').readlines() for line in lines: if 'ebu_mode=s' in line: key='S' if 'ebu_mode=m' in line: key='M' if 'ebu_mode=i' in line: key='I' if 'Lk=' in line: data=line.rsplit()[1] loudnessdata[key].append(data.rsplit('Lk=')[1]) return loudnessdata
[docs]def HTMLstats(stats): """ return html from M,S,I stats dictionnary (returned by stats()) """ html='''<dl class="dl-horizontal">''' s=m=i='' for key in stats.keys(): if key=='M': m='''<dt>Momentary max</dt> <dd>'''+str(stats['M']['max'])+''' LUFS</dd>''' m+='''<dt>Momentary min</dt> <dd>'''+str(stats['M']['min'])+''' LUFS</dd>''' m+='''<dt>Momentary average</dt> <dd>'''+str(stats['M']['avg'])+''' LUFS</dd>''' elif key=='S': s='''<dt>Short-term max</dt> <dd>'''+str(stats['S']['max'])+''' LUFS</dd>''' s+='''<dt>Short-term min</dt> <dd>'''+str(stats['S']['min'])+''' LUFS</dd>''' s+='''<dt>Short-term average</dt> <dd>'''+str(stats['S']['avg'])+''' LUFS</dd>''' elif key=='I': i='''<dt>Integrated</dt> <dd>'''+str(stats['I'])+''' LUFS</dd>''' elif key=='LRA': lra='''<dt>Loudness Range</dt> <dd>'''+str(stats['LRA'])+''' LU</dd>''' else: pass html+=m+s+i+lra html+='''</dl>''' return html
[docs]def LRA(loudnessdata): import math absThreshold=-70.0 relThreshold=-20.0 absGated=[] idx=0 for item in loudnessdata['S']: try: data=float(loudnessdata['S'][idx]) except: data=-100.0 #will not be taken into account or exit? if data > absThreshold: absGated.append(data) idx+=1 n=len(absGated) if n==0: return 'nan' power=0 for item in absGated: power+=pow(10,item/10) power=power/n integrated=10*math.log10(power) relGated=[] for data in absGated: if data > integrated+ relThreshold: relGated.append(data) n=len(relGated) relGated.sort() idx=(n-1)*0.1+1 perclow=relGated[int(idx)] idx=(n-1)*0.95+1 perchigh=relGated[int(idx)] lra=perchigh-perclow return lra
[docs]class LoudnessPlotter(object): """ base class for launching executable, parse log and write output HTML file """ def __init__(self,filelist,outpath): """ init some self used value """ self.filelist=filelist self.outpath=outpath self.loudnessdata={} self.toolspath=module_path() if sys.platform == 'win32': self.wavtoolpath=os.path.join(self.toolspath,'wave_analyze.exe') else: self.wavtoolpath=os.path.join(self.toolspath,'wave_analyze') self.processed=[] self.failed=[] # options self.snippet=False self.autoscale=True self.outfilename=None self.template=None
[docs] def analyse(self): """ launch wave_analyze executable on each file of self.filelist putting stdout in a file fills self.processsed file path list """ done=0 for item in self.filelist: logfile=item+'_loudness.txt' if len(self.filelist) > 1: print 'loudness analyse :: %s :: %s' %(str(done*100/len(self.filelist)),os.path.basename(item)) #else: # print 'loudness analyse :: %s' %os.path.basename(item) if os.path.isfile(logfile): #print 'remove %s' %logfile os.remove(logfile) error_mode=0 for ebu_mode in ['m','s','i']: #print 'ebu mode %s' %ebu_mode log=open(logfile,'a') log.write('ebu_mode=%s\n' %ebu_mode) log.flush() cmd=self.wavtoolpath+' "'+item+'" '+ebu_mode #print cmd res=subprocess.call(cmd,stdout=log,shell=True) if res!=0: #print 'error analysing %s' %item error_mode+=1 log.close() if error_mode>0: self.failed.append(item) #print 'error analysing %s' %item else: self.processed.append(item) done+=1 for f in self.processed: self.loudnessdata[os.path.basename(f)]=parseLoudnessLog(f+'_loudness.txt') self.loudnessdata[os.path.basename(f)]['LRA']=LRA(self.loudnessdata[os.path.basename(f)]) return self.loudnessdata
[docs] def process(self): """ base function to parse and write HTML based on internal config """ if len(self.filelist)==0: print 'No file to process. Abort' return 1 self.analyse() #fills self.processed else flot bugs (no data on one placeholder for width/height seems to affect them all) if len(self.processed)==1: self.writeIndividual() else : return self.write()
[docs] def writeIndividual(self): """ write single HTML file for an individual file""" for key in self.loudnessdata.keys(): writeHTML(self.loudnessdata[key],os.path.join(self.outpath,key))
[docs] def getHtmlContent(self): """ get content for multi-files, return tabed content and the plots (to put in js)""" tabbedplaceholder='' if len(self.failed)>0: #if some test failed display notif tabbedplaceholder+='''<div class="alert"> <button type="button" class="close" data-dismiss="alert">×</button> <strong>Warning! </strong>''' for item in self.failed: tabbedplaceholder+='''<br>error while processing :'''+os.path.basename(item) tabbedplaceholder+='''</div>''' tabbedplaceholder+='''<div class="tabbable"> <!-- <ul class="nav nav-tabs"> --> <ul class="nav nav-pills"> ''' i=0 for f in self.loudnessdata.keys(): name=os.path.splitext(os.path.basename(f))[0] if i==0: tabbedplaceholder+='''<li class="active"><a href="#'''+name.replace(' ','')+'''" data-toggle="tab">'''+name+'''</a></li> ''' else: tabbedplaceholder+='''<li><a href="#'''+name.replace(' ','')+'''" data-toggle="tab">'''+name+'''</a></li> ''' i+=1 tabbedplaceholder+='''</ul><hr> ''' tabbedplaceholder+='''<div class="tab-content"> ''' j=0 for tab in self.loudnessdata.keys(): name=os.path.splitext(os.path.basename(tab))[0] if j==0: tabbedplaceholder+='''<div class="tab-pane active" id="'''+name.replace(' ','')+'''"> ''' else: tabbedplaceholder+='''<div class="tab-pane" id="'''+name.replace(' ','')+'''"> ''' #print tab #only for min/max/average, do not compute LRA again tabbedplaceholder+=HTMLstats(stats(self.loudnessdata[tab]))+''' <div id="'''+'placeholder'+name.replace(' ','')+'''" style="width:1000px;height:450px"></div></div>''' j+=1 tabbedplaceholder+='''</div> </div> ''' plots='' for item in self.loudnessdata.keys(): datas=[] #getData(self.loudnessdata[item]) M=[] S=[] I=[] idx_m=0 idx_s=0 idx_i=0 for val in self.loudnessdata[item]['M']: M.append([idx_m*2*0.1,self.loudnessdata[item]['M'][idx_m]]) idx_m+=1 for val in self.loudnessdata[item]['S']: S.append([idx_s*0.1,self.loudnessdata[item]['S'][idx_s]]) idx_s+=1 for val in self.loudnessdata[item]['M']: I.append([idx_i*2*0.1,self.loudnessdata[item]['I'][0]],) idx_i+=1 Mdict={} Mdict['label']='Momentary' Mdict['data']=M Sdict={} Sdict['label']='Short-Term' Sdict['data']=S Idict={} Idict['label']='Integrated' Idict['data']=I #Idict['points']={'show':'false'} #Idict['lines']={'show':'true'} #Idict['bars']={'show':'false','horizontal':'false'} datas.append(Mdict) datas.append(Sdict) datas.append(Idict) if self.autoscale==True: options='''{ series: {lines: {show: true, steps: true },points: { show: false },bars: { show: false }}, yaxis: { tickFormatter: function (v) { return v + " LUFS"; } }, xaxis: { tickFormatter: function (v) { return v + " s"; } }, legend: { position: 'se' }}''' else: options='''{ series: {lines: {show: true, steps: true },points: { show: false },bars: { show: false }}, yaxis: { min:-70, max: 0, tickFormatter: function (v) { return v + " LUFS"; } }, xaxis: { tickFormatter: function (v) { return v + " s"; } }, legend: { position: 'se' }}''' name=os.path.splitext(os.path.basename(item))[0] plots+='''$.plot($("#placeholder'''+name.replace(' ','')+'''"), '''+str(datas)+''','''+options+'''); ''' return (tabbedplaceholder,plots)
[docs] def write(self): """ write single HTML file for a list of file""" tabbedplaceholder,plots =self.getHtmlContent() if self.template != None and os.path.exists(self.template): print 'using template file : %s' %self.template s = Template( open(self.template,'r').read() ) else: s = Template(tpl_multi_fromCDN) #options= page=s.safe_substitute(tabbedplaceholder=tabbedplaceholder,plots=plots) if self.outfilename != None: print 'write %s' %os.path.join(self.outpath,self.outfilename) fout=os.path.join(self.outpath,self.outfilename) else: print 'write %s' %os.path.join(self.outpath,'loudness.html') fout=os.path.join(self.outpath,'loudness.html') open(fout,'w').write(page) return page #not needed but usefull...
[docs]def cli(): """ Simple command line interface. Process file or path, based on input args """ if we_are_frozen(): msg=''' loudnessPlotter : analyse and plot loudness in HTML\n usage: loudnessPlotter.py [inpath] [outpath]''' else: msg=''' loudnessPlotter : analyse and plot loudness in HTML\n usage: python loudness.py [inpath] [outpath]''' if len(sys.argv) > 1: pass else: print msg return 1 inpath=os.path.abspath(sys.argv[1]) outpath=os.path.abspath(sys.argv[2]) wavfilelist=[] if os.path.isdir(inpath): wavfilelist=glob.glob(os.path.join(inpath,'*.wav')) print 'nb file to process: %s' %len(wavfilelist) elif os.path.isfile(inpath) and os.path.splitext(inpath)[1]=='.wav' : wavfilelist.append(inpath) else: print msg return 1 loud=LoudnessPlotter(wavfilelist,outpath) loud.process()
if __name__ == "__main__": cli()