LASP 1.0
Library for Acoustic Signal Processing
Loading...
Searching...
No Matches
lasp_slm.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Sound level meter implementation
5@author: J.A. de Jong - ASCEE
6"""
7from .lasp_cpp import cppSLM
8import numpy as np
9from .lasp_common import (TimeWeighting, FreqWeighting, P_REF)
10from .filter import SPLFilterDesigner
11import logging
12
13__all__ = ['SLM', 'Dummy']
14
15
16class Dummy:
17 """
18 Emulate filtering, but does not filter anything at all.
19 """
20
21 def filter_(self, data):
22 return data[:, np.newaxis]
23
24
25class SLM:
26 """
27 Multi-channel Sound Level Meter. Input data: time data with a certain
28 sampling frequency. Output: time-weighted (fast/slow) sound pressure
29 levels in dB(A/C/Z). Possibly in octave bands.
30 """
31
32 def __init__(self,
33 fs,
34 fbdesigner=None,
35 tw: TimeWeighting = TimeWeighting.fast,
36 fw: FreqWeighting = FreqWeighting.A,
37 xmin=None,
38 xmax=None,
39 include_overall=True,
40 level_ref_value=P_REF,
41 offset_t=0):
42 """
43 Initialize a sound level meter object.
44
45 Args:
46 fs: Sampling frequency of input data [Hz]
47 fbdesigner: FilterBankDesigner to use for creating the
48 (fractional) octave bank filters. Set this one to None to only do
49 overalls
50 fs: Sampling frequency [Hz]
51 tw: Time Weighting to apply
52 fw: Frequency weighting to apply
53 xmin: Filter designator of lowest band.
54 xmax: Filter designator of highest band
55 include_overall: If true, a non-functioning filter is added which
56 is used to compute the overall level.
57 level_ref_value: Reference value for computing the levels in dB
58 offset_t: Offset to be added to output time data [s]
59 """
60
61 self.fbdesigner = fbdesigner
62 if xmin is None and fbdesigner is not None:
63 xmin = fbdesigner.xs[0]
64 elif fbdesigner is None:
65 xmin = 0
66
67 if xmax is None and self.fbdesigner is not None:
68 xmax = fbdesigner.xs[-1]
69 elif fbdesigner is None:
70 xmax = 0
71
72 self.xs = list(range(xmin, xmax + 1))
73
74 nfilters = len(self.xs)
75 if include_overall:
76 nfilters += 1
77 self.include_overall = include_overall
78 self.offset_t = offset_t
79
80 spld = SPLFilterDesigner(fs)
81 if fw == FreqWeighting.A:
82 prefilter = spld.A_Sos_design().flatten()
83 elif fw == FreqWeighting.C:
84 prefilter = spld.C_Sos_design().flatten()
85 elif fw == FreqWeighting.Z:
86 prefilter = None
87 else:
88 raise ValueError(f'Not implemented prefilter {fw}')
89
90 # 'Probe' size of filter coefficients
91 self.nom_txt = []
92
93 # This is a bit of a hack, as the 5 is hard-encoded here, but should in
94 # fact be coming from somewhere else..
95 sos_overall = np.array([1, 0, 0, 1, 0, 0]*5, dtype=float)
96
97 if fbdesigner is not None:
98 assert fbdesigner.fs == fs
99 sos_firstx = fbdesigner.createSOSFilter(self.xs[0]).flatten()
100 self.nom_txt.append(fbdesigner.nominal_txt(self.xs[0]))
101 sos = np.empty((sos_firstx.size, nfilters), dtype=float, order='C')
102 sos[:, 0] = sos_firstx
103
104 for i, x in enumerate(self.xs[1:]):
105 sos[:, i+1] = fbdesigner.createSOSFilter(x).flatten()
106 self.nom_txt.append(fbdesigner.nominal_txt(x))
107
108 if include_overall:
109 # Create a unit impulse response filter, every third index equals
110 # 1, so b0 = 1 and a0 is 1 (by definition)
111 # a0 = 1, b0 = 1, rest is zero
112 sos[:, -1] = sos_overall
113 self.nom_txt.append('overall')
114
115 else:
116 # No filterbank, means we do only compute the overall values. This
117 # means that in case of include_overall, it creates two overall
118 # channels. That would be confusing, so we do not allow it.
119 sos = sos_overall[:, np.newaxis]
120 self.nom_txt.append('overall')
121
122 # Downsampling factor, determine from single pole low pass filter time
123 # constant, such that aliasing is ~ allowed at 20 dB lower value
124 # and
125 dsfac = cppSLM.suggestedDownSamplingFac(fs, tw[0])
126
127 if prefilter is not None:
128 self.slm = cppSLM.fromBiquads(fs, level_ref_value, dsfac,
129 tw[0],
130 prefilter.flatten(), sos)
131 else:
132 self.slm = cppSLM.fromBiquads(fs, level_ref_value, dsfac,
133 tw[0],
134 sos)
135
136 self.fs_slm = fs / dsfac
137
138 # Initialize counter to 0
139 self.N = 0
140
141 def run(self, data):
142 """
143 Add new fresh timedata to the Sound Level Meter
144
145 Args:
146 data: one-dimensional input data
147 """
148
149 assert data.ndim == 1
150
151 levels = self.slm.run(data)
152
153 tstart = self.N / self.fs_slm
154 Ncur = levels.shape[0]
155 tend = tstart + Ncur / self.fs_slm
156
157 t = np.linspace(tstart, tend, Ncur, endpoint=False) + self.offset_t
158 self.N += Ncur
159
160 output = {}
161
162 for i, x in enumerate(self.xs):
163 # '31.5' to '16k'
164 output[self.nom_txt[i]] = {'t': t,
165 'data': levels[:, [i]],
166 'x': x}
167 if self.include_overall and self.fbdesigner is not None:
168 output['overall'] = {'t': t, 'data': levels[:, [i+1]], 'x': 0}
169
170 return output
171
172 def return_as_dict(self, dat):
173 """
174 Helper function used to return resulting data in a proper way.
175
176 Returns a dictionary with the following keys:
177 'data': The y-values of Lmax, Lpeak, etc
178 'overall': The overall value, in [dB] **COULD BE NOT PART OF
179 OUTPUT**
180 'x': The exponents of the octave, such that the midband frequency
181 corresponds to 1000*G**(x/b), where b is the bands, either 1, 3, or
182 6
183 'mid': The center frequencies of each band, as a numpy float array
184 'nom': The nominal frequency array text, as textual output corresponding
185 to the frequencies in x, they are '16', .. up to '16k'
186
187 """
188 output = {}
189 output['nom'] = self.nom_txt
190 output['x'] = list(self.xs)
191 output['mid'] = self.fbdesigner.fm(list(self.xs))
192 logging.debug(list(self.xs))
193 logging.debug(output['mid'])
194
195 if self.include_overall and self.fbdesigner is not None:
196 output['overall'] = dat[-1]
197 output['y'] = np.asarray(dat[:-1])
198 else:
199 output['y'] = np.asarray(dat[:])
200 return output
201
202 def Leq(self):
203 """
204 Returns the computed equivalent levels for each filter channel
205 """
206 return self.return_as_dict(self.slm.Leq())
207
208 def Lmax(self):
209 """
210 Returns the computed max levels for each filter channel
211 """
212 return self.return_as_dict(self.slm.Lmax())
213
214 def Lpeak(self):
215 """
216 Returns the computed peak levels for each filter channel
217 """
218 return self.return_as_dict(self.slm.Lpeak())
219
220 def Leq_array(self):
221 return self.slm.Leq()
222
223 def Lmax_array(self):
224 return self.slm.Lmax()
225
226 def Lpeak_array(self):
227 return self.slm.Lpeak()
Emulate filtering, but does not filter anything at all.
Definition lasp_slm.py:16
filter_(self, data)
Definition lasp_slm.py:21
Multi-channel Sound Level Meter.
Definition lasp_slm.py:25
Leq(self)
Returns the computed equivalent levels for each filter channel.
Definition lasp_slm.py:202
Lpeak(self)
Returns the computed peak levels for each filter channel.
Definition lasp_slm.py:214
__init__(self, fs, fbdesigner=None, TimeWeighting tw=TimeWeighting.fast, FreqWeighting fw=FreqWeighting.A, xmin=None, xmax=None, include_overall=True, level_ref_value=P_REF, offset_t=0)
Initialize a sound level meter object.
Definition lasp_slm.py:41
run(self, data)
Add new fresh timedata to the Sound Level Meter.
Definition lasp_slm.py:141
Lmax(self)
Returns the computed max levels for each filter channel.
Definition lasp_slm.py:208
return_as_dict(self, dat)
Helper function used to return resulting data in a proper way.
Definition lasp_slm.py:172