2018-04-23 07:29:21 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
Sound level meter implementation
|
|
|
|
@author: J.A. de Jong - ASCEE
|
|
|
|
"""
|
2020-01-20 11:10:24 +00:00
|
|
|
from .wrappers import Slm as pyxSlm
|
2018-04-23 07:29:21 +00:00
|
|
|
import numpy as np
|
2020-08-22 09:00:08 +00:00
|
|
|
from .lasp_common import (TimeWeighting, FreqWeighting, P_REF)
|
2020-01-21 20:10:38 +00:00
|
|
|
from .filter import SPLFilterDesigner
|
2018-09-13 11:56:05 +00:00
|
|
|
|
|
|
|
__all__ = ['SLM', 'Dummy']
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2018-04-23 07:29:21 +00:00
|
|
|
|
|
|
|
class Dummy:
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
|
|
|
Emulate filtering, but does not filter anything at all.
|
|
|
|
"""
|
|
|
|
|
2018-04-23 07:29:21 +00:00
|
|
|
def filter_(self, data):
|
2018-05-02 14:29:53 +00:00
|
|
|
return data[:, np.newaxis]
|
|
|
|
|
2018-04-23 07:29:21 +00:00
|
|
|
|
|
|
|
class SLM:
|
|
|
|
"""
|
2018-12-29 14:34:24 +00:00
|
|
|
Multi-channel Sound Level Meter. Input data: time data with a certain
|
2018-07-17 09:52:02 +00:00
|
|
|
sampling frequency. Output: time-weighted (fast/slow) sound pressure
|
2020-01-20 11:10:24 +00:00
|
|
|
levels in dB(A/C/Z). Possibly in octave bands.
|
2018-07-17 09:52:02 +00:00
|
|
|
|
2018-04-23 07:29:21 +00:00
|
|
|
"""
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
def __init__(self,
|
2020-01-22 20:11:20 +00:00
|
|
|
fs,
|
2020-10-16 16:33:22 +00:00
|
|
|
fbdesigner=None,
|
2020-01-21 20:10:38 +00:00
|
|
|
tw=TimeWeighting.fast,
|
|
|
|
fw=FreqWeighting.A,
|
2020-01-22 20:11:20 +00:00
|
|
|
xmin = None,
|
|
|
|
xmax = None,
|
2020-08-22 09:00:08 +00:00
|
|
|
include_overall=True,
|
|
|
|
level_ref_value=P_REF):
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
2018-07-17 09:52:02 +00:00
|
|
|
Initialize a sound level meter object.
|
2018-04-23 07:29:21 +00:00
|
|
|
|
2018-05-02 14:29:53 +00:00
|
|
|
Args:
|
2020-01-22 20:11:20 +00:00
|
|
|
fs: Sampling frequency of input data [Hz]
|
2020-01-21 20:10:38 +00:00
|
|
|
fbdesigner: FilterBankDesigner to use for creating the
|
2020-01-22 20:11:20 +00:00
|
|
|
(fractional) octave bank filters. Set this one to None to only do
|
|
|
|
overalls
|
2018-05-02 14:29:53 +00:00
|
|
|
fs: Sampling frequency [Hz]
|
2018-07-17 09:52:02 +00:00
|
|
|
tw: Time Weighting to apply
|
2020-01-21 20:10:38 +00:00
|
|
|
fw: Frequency weighting to apply
|
2020-01-22 20:11:20 +00:00
|
|
|
xmin: Filter designator of lowest band
|
|
|
|
xmax: Filter designator of highest band
|
2020-01-21 20:10:38 +00:00
|
|
|
include_overall: If true, a non-functioning filter is added which
|
|
|
|
is used to compute the overall level.
|
2020-08-22 09:00:08 +00:00
|
|
|
level_ref_value: Reference value for computing the levels in dB
|
2020-01-21 20:10:38 +00:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.fbdesigner = fbdesigner
|
2020-10-16 16:33:22 +00:00
|
|
|
if xmin is None and fbdesigner is not None:
|
2020-01-22 20:11:20 +00:00
|
|
|
xmin = fbdesigner.xs[0]
|
2020-10-16 16:33:22 +00:00
|
|
|
elif fbdesigner is None:
|
|
|
|
xmin = 0
|
|
|
|
|
|
|
|
if xmax is None and self.fbdesigner is not None:
|
2020-01-22 20:11:20 +00:00
|
|
|
xmax = fbdesigner.xs[-1]
|
2020-10-16 16:33:22 +00:00
|
|
|
elif fbdesigner is None:
|
|
|
|
xmax = 0
|
|
|
|
|
2020-01-22 20:11:20 +00:00
|
|
|
self.xs = list(range(xmin, xmax + 1))
|
2020-01-21 20:10:38 +00:00
|
|
|
|
|
|
|
nfilters = len(self.xs)
|
|
|
|
if include_overall: nfilters +=1
|
|
|
|
self.include_overall = include_overall
|
|
|
|
|
|
|
|
spld = SPLFilterDesigner(fs)
|
|
|
|
if fw == FreqWeighting.A:
|
|
|
|
prefilter = spld.A_Sos_design().flatten()
|
|
|
|
elif fw == FreqWeighting.C:
|
|
|
|
prefilter = spld.C_Sos_design().flatten()
|
|
|
|
elif fw == FreqWeighting.Z:
|
|
|
|
prefilter = None
|
|
|
|
else:
|
2020-01-22 20:11:20 +00:00
|
|
|
raise ValueError(f'Not implemented prefilter {fw}')
|
2020-01-21 20:10:38 +00:00
|
|
|
|
|
|
|
# 'Probe' size of filter coefficients
|
|
|
|
self.nom_txt = []
|
|
|
|
|
2020-10-16 16:33:22 +00:00
|
|
|
# This is a bit of a hack, as the 5 is hard-encoded here, but should in
|
|
|
|
# fact be coming from somewhere else..
|
|
|
|
sos_overall = np.array([1,0,0,1,0,0]*5, dtype=float)
|
|
|
|
|
2020-01-22 20:11:20 +00:00
|
|
|
if fbdesigner is not None:
|
|
|
|
assert fbdesigner.fs == fs
|
2020-10-16 16:33:22 +00:00
|
|
|
sos_firstx = fbdesigner.createSOSFilter(self.xs[0]).flatten()
|
2020-01-24 19:44:12 +00:00
|
|
|
self.nom_txt.append(fbdesigner.nominal_txt(self.xs[0]))
|
2020-10-16 16:33:22 +00:00
|
|
|
sos = np.empty((nfilters, sos_firstx.size), dtype=float, order='C')
|
|
|
|
sos[0, :] = sos_firstx
|
2020-01-22 20:11:20 +00:00
|
|
|
|
|
|
|
for i, x in enumerate(self.xs[1:]):
|
2020-01-24 19:44:12 +00:00
|
|
|
sos[i+1, :] = fbdesigner.createSOSFilter(x).flatten()
|
2020-01-22 20:11:20 +00:00
|
|
|
self.nom_txt.append(fbdesigner.nominal_txt(x))
|
|
|
|
|
|
|
|
if include_overall:
|
|
|
|
# Create a unit impulse response filter, every third index equals
|
|
|
|
# 1, so b0 = 1 and a0 is 1 (by definition)
|
2020-10-16 16:33:22 +00:00
|
|
|
# a0 = 1, b0 = 1, rest is zero
|
|
|
|
sos[-1,:] = sos_overall
|
2020-01-22 20:11:20 +00:00
|
|
|
self.nom_txt.append('overall')
|
2020-10-16 16:33:22 +00:00
|
|
|
|
2020-01-22 20:11:20 +00:00
|
|
|
else:
|
|
|
|
# No filterbank, means we do only compute the overall values. This
|
|
|
|
# means that in case of include_overall, it creates two overall
|
|
|
|
# channels. That would be confusing, so we do not allow it.
|
2020-10-16 16:33:22 +00:00
|
|
|
if include_overall:
|
|
|
|
sos = sos_overall[np.newaxis,:]
|
|
|
|
self.nom_txt.append('overall')
|
2020-01-21 20:10:38 +00:00
|
|
|
|
|
|
|
self.slm = pyxSlm(prefilter, sos,
|
2020-08-22 09:00:08 +00:00
|
|
|
fs, tw[0], level_ref_value)
|
2020-01-21 20:10:38 +00:00
|
|
|
|
|
|
|
dsfac = self.slm.downsampling_fac
|
|
|
|
if dsfac > 0:
|
|
|
|
# Not unfiltered data
|
|
|
|
self.fs_slm = fs / self.slm.downsampling_fac
|
2018-04-23 07:29:21 +00:00
|
|
|
else:
|
2020-01-21 20:10:38 +00:00
|
|
|
self.fs_slm = fs
|
|
|
|
|
|
|
|
# Initialize counter to 0
|
|
|
|
self.N = 0
|
2018-07-17 09:52:02 +00:00
|
|
|
|
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
def run(self, data):
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
|
|
|
Add new fresh timedata to the Sound Level Meter
|
|
|
|
|
|
|
|
Args:
|
2020-01-21 20:10:38 +00:00
|
|
|
data: one-dimensional input data
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
2020-01-21 20:10:38 +00:00
|
|
|
|
2018-07-17 09:52:02 +00:00
|
|
|
assert data.ndim == 2
|
2020-01-21 20:10:38 +00:00
|
|
|
assert data.shape[1] == 1, "invalid number of channels, should be 1"
|
|
|
|
|
|
|
|
if data.shape[0] == 0:
|
|
|
|
return {}
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
levels = self.slm.run(data)
|
2018-04-23 07:29:21 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
tstart = self.N / self.fs_slm
|
|
|
|
Ncur = levels.shape[0]
|
|
|
|
tend = tstart + Ncur / self.fs_slm
|
2018-04-23 07:29:21 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
t = np.linspace(tstart, tend, Ncur, endpoint=False)
|
|
|
|
self.N += Ncur
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
output = {}
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
for i, x in enumerate(self.xs):
|
|
|
|
# '31.5' to '16k'
|
|
|
|
output[self.nom_txt[i]] = {'t': t,
|
2020-01-24 19:44:12 +00:00
|
|
|
'data': levels[:, [i]],
|
2020-01-21 20:10:38 +00:00
|
|
|
'x': x}
|
2020-01-22 20:11:20 +00:00
|
|
|
if self.include_overall and self.fbdesigner is not None:
|
2020-02-09 14:08:48 +00:00
|
|
|
output['overall'] = {'t': t, 'data': levels[:, [i+1]], 'x': 0}
|
2020-01-24 19:44:12 +00:00
|
|
|
|
2020-01-21 20:10:38 +00:00
|
|
|
return output
|
2018-04-23 07:29:21 +00:00
|
|
|
|
|
|
|
|
2020-01-24 19:44:12 +00:00
|
|
|
def return_as_dict(self, dat):
|
|
|
|
"""
|
|
|
|
Helper function used to
|
|
|
|
"""
|
|
|
|
output = {}
|
|
|
|
for i, x in enumerate(self.xs):
|
|
|
|
# '31.5' to '16k'
|
|
|
|
output[self.nom_txt[i]] = { 'data': dat[i],
|
|
|
|
'x': x}
|
|
|
|
if self.include_overall and self.fbdesigner is not None:
|
|
|
|
output['overall'] = {'data': dat[i+1], 'x': 0}
|
|
|
|
return output
|
|
|
|
|
|
|
|
def Leq(self):
|
|
|
|
"""
|
|
|
|
Returns the computed equivalent levels for each filter channel
|
|
|
|
"""
|
|
|
|
return self.return_as_dict(self.slm.Leq())
|
|
|
|
|
|
|
|
def Lmax(self):
|
|
|
|
"""
|
|
|
|
Returns the computed max levels for each filter channel
|
|
|
|
"""
|
|
|
|
return self.return_as_dict(self.slm.Lmax())
|
|
|
|
|
|
|
|
def Lpeak(self):
|
|
|
|
"""
|
|
|
|
Returns the computed peak levels for each filter channel
|
|
|
|
"""
|
|
|
|
return self.return_as_dict(self.slm.Lpeak())
|
|
|
|
|
|
|
|
def Leq_array(self):
|
|
|
|
return self.slm.Leq()
|
|
|
|
|
|
|
|
def Lmax_array(self):
|
|
|
|
return self.slm.Lmax()
|
|
|
|
|
|
|
|
def Lpeak_array(self):
|
|
|
|
return self.slm.Lpeak()
|