#!/usr/bin/env python

"""Perceptual loudness measurement.
This is an implementation of
"Percieved Level of Noise by Mark VII and Decibels (E)"
S. S. Stevens,
J. Acoustical Soc. Am. v. 51(2, part 2) 1972
pages 575-602.

The algorithm matches the paper for FractOct==ThirdOct or OneOct.
The algorithm assumes that the level defined by SIXTY_EIGHT
corresponds to 68dB relative to 20 micro-Newtons per square meter
sound pressure.

USER INFORMATION:

You'll find, somewhere, a subdirectory named
speechresearch/voicing .
Within this is a script called emphasis_stevens.py .
This can be run as
python emphasis_stevens.py -o outputfile -write BITPIX=0 -c channelnumber inputfile
(there are a couple of other flags, too).

It will then read the input file (which is an audio recording)
and produce a time-series of the loudness.   Data input and
output is via the gpkio library, in .../speechresearch/gpkio.
That library has many virtues, but reading WAV files is not
one of them, so you have to convert .wav files to
the "GPK ASCII image" format (or one of several other formats),
using  .../speechresearch/lib/wavio.py .

The output will be in an ASCII version of the format,
which should be reasonably intelligible.    (The GPK ASCII image
format is based on the FITS astronomy format from NASA.)

However, that script uses the gpkio and gpklib libraries
(also under ../speechresearch).  These need to be compiled,
and it uses the gpk_img_python package that needs to be
installed via   "python setup.py install".
Oh, and .../speechresearch/gmisclib needs to be in
your PYTHONPATH.

Give it a try, and I'll be happy to help, and will
incorporate the troubles you have into some form of
documentation.    Sorry, I have nothing better yet.
"""

import math
import numpy
from gmisclib import Numeric_gpk
from gmisclib import die
from gmisclib import gpkmisc

from gpk_voicing import voice_misc



FAC = 0.5
ThirdOct = 2.0**(1.0/3.0)
OneOct = 2.0
FracOct = 2.0**0.7
Fmax = 12500.0
Fmin = 50.0
EXFAC = 2
E = 0.3333

# 1.0 would cheat on the low frequency end a little.
TAUFAC = 1.2


SIXTY_EIGHT = 6000.0
E = 0.3333



class extra_c:
	def __init__(self, fmin, fmax, frac_oct, ffac):
		"""
		@param ffac: How far apart should be frequency bins, relative to their width.
		"""
		self.frac_oct = frac_oct
		self.fmin = fmin * frac_oct**0.5	# Put lower edge at fmin
		fmax *= frac_oct**-0.5			# Put upper edge at fmax
		self.n = int(round(math.log(fmax/fmin)/(math.log(frac_oct)*ffac)))
		#: Fsum is correct at 68Db SOUND PRESSURE LEVEL AT 3150Hz
		self.Fsum = 0.23*((1+math.log(self.frac_oct)/math.log(ThirdOct))/2.0)
		self.Band_ctr = [ self.band_ctr(i*FAC) for i in range(self.n) ]
		self.Band_h_width = [ 0.5*(self.band_ctr(i*FAC+0.5) - self.band_ctr(i*FAC-0.5)) for i in range(self.n) ]
		self.ffac = ffac
		self.Ear = self.calc_ear()

	def band_ctr(self, i):
		return self.fmin * (self.frac_oct**i)


	def calc_ear(self):
		"""At 68dB SPL."""
		F0 = 50.0
		app_A = [0.729, 1.33, 2.28, 2.92, 3.65, 4.47, 5.40,
			6.40, 7.48, 8.64, 8.64, 8.64, 8.64, 8.64, 8.64,
			10.1, 11.8, 13.7, 16.0, 16.0, 16.0, 16.0, 16.0,
			11.8, 8.64 ]
		o = numpy.zeros((self.n,))
		for i in range(self.n):
			f = self.Band_ctr[i]
			idx = math.log(f/F0)/math.log(self.frac_oct)
			iidx = int(round(idx))
			# print i, f, iidx, len(app_A)
			assert iidx>=0 and iidx<len(app_A)
			fidx = idx - iidx
			if abs(fidx) < 0.01:
				o[i] = app_A[iidx]
			elif fidx < 0:
				assert i > 0
				o[i] = app_A[iidx]*(1-abs(fidx)) + app_A[iidx-1]*abs(fidx)
			else:
				assert i+1 < len(app_A)
				o[i] = app_A[iidx]*(1-abs(fidx)) + app_A[iidx+1]*abs(fidx)
		return o

Fullband = extra_c(Fmin, Fmax, FracOct, FAC)
Speechband = extra_c(300.0, 4000.0, FracOct, FAC)

def filter_fcn(f, fc, w):
	Exp = 6	# 18 dB/octave
	return 1.0/(((f-fc)/w)**Exp + 1)


_ff_cache = {}
def cached_filter_fcn(f, fc, w):
	key = (f.shape, fc, w)
	try:
		return _ff_cache[key]
	except KeyError:
		pass
	tmp = filter_fcn(f, fc, w)
	_ff_cache[key] = tmp
	return tmp




def one_loud(d, extra):
	"""Approximate loudness of the sound in data array d.
	Extra contains misc. parameters."""

	n = d.shape[0]
	dt = extra.dt
	assert d.shape == (n,)
	ss = numpy.fft.fft(Numeric_gpk.zero_pad_end(d*voice_misc.window(n, 0), EXFAC-1))
	ss[0] = 0	# Remove DC offset.
	# print "# ss.shape=", ss.shape
	f = numpy.absolute(voice_misc.fft_freq(ss.shape[0], d=dt))
	# assert abs(gpkmisc.N_maximum(f)-0.5/dt) < 0.05/dt
	nyquist = 0.5/dt
	q = numpy.zeros((extra.n,))
	for i in range( extra.n ):
		fc = extra.Band_ctr[i]
		w = extra.Band_h_width[i]
		if fc+w > nyquist:
			break
		q[i] = numpy.square(numpy.absolute(ss * cached_filter_fcn(f, fc, w))).sum()
	S = extra.Ear * (q**E)
	Smax = S[S.argmax()]
	Ssum = S.sum()/extra.ffac
	loud = Smax + extra.Fsum*(Ssum - Smax)
	return loud


def printit(title, vec):
	vt = ' '.join(["%6g"%q for q in vec])
	print title, vt


def emphasis(data, extra, DT=0.01):
	"""Measure the time-series of loudness of a data array data.
	DT is the sampling interval for the resulting loudness time series."""
	ns = int(math.floor(extra.dt*data.shape[0]/DT))
	o = numpy.zeros((ns,))
	for t in range(ns):
		s, e = voice_misc.start_end(t*DT, extra.dt, extra.window, data.shape[0])
		o[t] = one_loud(data[s:e], extra)
	return o


def simple_emphasis(data, dt, DT=0.01, extra=Fullband):
	extra.dt = dt
	wmin = 2*extra.Band_h_width[0]
	# Ideally, we'd use a different window size for each frequency...
	taumax = max(TAUFAC / wmin, 1.5*DT)
	extra.window = voice_misc.near_win_size(near=taumax/extra.dt, tol=0.1*taumax/extra.dt)
	# print 'win', extra.window, taumax/extra.dt, wmin, extra.Band_h_width[0], extra.Band_ctr[0]
	return emphasis(data/SIXTY_EIGHT, extra, DT)


def speechband_loudness(data, dt, DT=0.01):
	return simple_emphasis(data, dt, DT, extra=Speechband)
