LASP 1.0
Library for Acoustic Signal Processing
Loading...
Searching...
No Matches
lasp_common.py
Go to the documentation of this file.
1# -*- coding: utf-8 -*-
2import shelve, logging, sys, appdirs, os, platform
3import numpy as np
4
5from collections import namedtuple
6from dataclasses import dataclass
7from dataclasses_json import dataclass_json
8from enum import Enum, unique, auto
9from .lasp_cpp import DaqChannel
10
11"""
12Common definitions used throughout the code.
13"""
14
15__all__ = [
16 'P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'getFreq', 'Qty',
17 'SIQtys',
18 'lasp_shelve', 'this_lasp_shelve', 'W_REF', 'U_REF', 'I_REF', 'dBFS_REF',
19 'AvType'
20]
21
22# Reference sound pressure level
23P_REF = 2e-5 # 20 micropascal
24
25W_REF = 1e-12 # 1 picoWatt
26I_REF = 1e-12 # 1 picoWatt/ m^2
27
28# Reference velocity for sound velocity level
29U_REF = 5e-8 # 50 nano meter / s
30
31# Wiki: The unit dB FS or dBFS is defined in AES Standard AES17-1998,[13]
32# IEC 61606,[14] and ITU-T P.38x,[15][16] such that the RMS value of a
33# full-scale sine wave is designated 0 dB FS
34
35# The LASP code converts integer samples to floats, where the mapping is such
36# that the maximum value (POSITIVE) that can be represented by the integer data
37# is mapped to the value of 1.0. Following this convention, a sine wave with
38# amplitude of 1.0 is 0 dB FS, and subsequently, its rms value is 0.5*sqrt(2),
39# hence this is the reference level as specified below.
40dBFS_REF = 0.5*2**0.5 # Which level would be -3.01 dBFS
41
42
43@unique
44class AvType(Enum):
45 """Specificying the type of data, for adding and removing callbacks from
46 the stream."""
47
48 # Input stream
49 audio_input = (0, 'input')
50
51 # Output stream
52 audio_output = (1, 'output')
53
54 # Both input as well as output
55 audio_duplex = (2, 'duplex')
56
57
58@dataclass_json
59@dataclass(eq=False)
60class Qty:
61 # Name, i.e.: Acoustic Pressure
62 name: str
63 # I.e.: Pascal
64 unit_name: str
65 # I.e.: Pa
66 unit_symb: str
67 # I.e.: ('dB SPL') <== tuple of possible level units
68 level_unit: object
69 # Contains a tuple of possible level names, including its reference value.
70 # For now, it is only able to compute for the first one. I.e.e we are not
71 # yet able to compute `dBPa's'
72 level_ref_name: object
73 level_ref_value: object
74 cpp_enum: DaqChannel.Qty
75
76 def __str__(self):
77 return f'{self.name} [{self.unit_symb}]'
78
79 def __eq__(self, other):
80 """
81 Comparison breaks for the other objects, level unit, level ref name,
82 etc as these are tuples / a single string.
83
84 """
85 # logging.debug(f'eq() {self.name} {other.name}')
86 return (self.namename == other.name and
87 self.unit_nameunit_name == other.unit_name)
88
89 def toInt(self):
90 """
91 Convert quantity to its specific enum integer value
92 """
93 return self.cpp_enum.value
94
95
96
97@unique
98class SIQtys(Enum):
99 N = Qty(name='Number',
100 unit_name='No unit / full scale',
101 unit_symb='-',
102 level_unit=('dBFS',),
103 level_ref_name=('Relative to full scale sine wave',),
104 level_ref_value=(dBFS_REF,),
105 cpp_enum = DaqChannel.Qty.Number
106 )
107 AP = Qty(name='Acoustic Pressure',
108 unit_name='Pascal',
109 unit_symb='Pa',
110 level_unit=('dB SPL','dBPa'),
111 level_ref_name=('2 micropascal', '1 pascal',),
112 level_ref_value=(P_REF, 1),
113 cpp_enum = DaqChannel.Qty.AcousticPressure
114 )
115
116 V = Qty(name='Voltage',
117 unit_name='Volt',
118 unit_symb=('V'),
119 level_unit=('dBV',), # dBV
120 level_ref_name=('1V',),
121 level_ref_value=(1.0,),
122 cpp_enum = DaqChannel.Qty.Voltage
123 )
124
125 @staticmethod
126 def default():
127 return SIQtys.N.value
128
129 @staticmethod
130 def fromCppEnum(enum):
131 """
132 Convert enumeration index from - say - a measurement file back into
133 physical quantity information.
134 """
135 for qty in SIQtys:
136 if qty.value.cpp_enum == enum:
137 return qty.value
138 raise RuntimeError(f'Qty corresponding to enum {enum} not found')
139
140 @staticmethod
141 def fromInt(val):
142 """
143 Convert integer index from - say - a measurement file back into
144 physical quantity information.
145 """
146 for qty in SIQtys:
147 if qty.value.cpp_enum.value == val:
148 return qty.value
149 raise RuntimeError(f'Qty corresponding to integer {val} not found')
150
151
152@dataclass
154 name: str
155 cal_value_dB: float
156 cal_value_linear: float
157 qty: Qty
158
160 one = CalSetting('94 dB SPL', 94.0 , 10**(94/20)*2e-5, SIQtys.AP.value)
161 two = CalSetting('1 Pa rms', 93.98, 1.0, SIQtys.AP.value)
162 three = CalSetting('114 dB SPL', 114.0 , 10**(114/20)*2e-5, SIQtys.AP.value)
163 four = CalSetting('10 Pa rms', 113.98, 10.0, SIQtys.AP.value)
164 five = CalSetting('93.7 dB SPL', 93.7 , 1.0, SIQtys.AP.value)
165
166 types = (one, two, three, four, five)
167 default = one
168 default_index = 0
169
170 @staticmethod
172 """
173 Fill Calibration Settings to a combobox
174
175 Args:
176 cb: QComboBox to fill
177 """
178 cb.clear()
179 for ty in CalibrationSettings.types:
180 cb.addItem(f'{ty.cal_value_dB}')
181 cb.setCurrentIndex(CalibrationSettings.default_index)
182
183 @staticmethod
184 def getCurrent(cb):
185 if cb.currentIndex() < len(CalibrationSettings.types):
186 return CalibrationSettings.types[cb.currentIndex()]
187 else:
188 return None
189
190lasp_appdir = appdirs.user_data_dir('Lasp', 'ASCEE')
191
192if not os.path.exists(lasp_appdir):
193 try:
194 os.makedirs(lasp_appdir, exist_ok=True)
195 except:
196 print('Fatal error: could not create application directory')
197 sys.exit(1)
198
199
200
201class Shelve:
202 def load(self, key, default_value):
203 """
204 Load data from a given key, if key is not found, returns the
205 default value if key is not found
206 """
207 if key in self.shelve.keys():
208 return self.shelve[key]
209 else:
210 return default_value
211
212 def __enter__(self):
213 self.incref()
214 return self
215
216 def store(self, key, val):
217 self._shelve[key] = val
218
219 def deleteIfPresent(self, key):
220 try:
221 del self._shelve[key]
222 except:
223 pass
224
225 def printAllKeys(self):
226 print(list(self.shelve.keys()))
227
228 def incref(self):
229 if self.shelve is None:
230 assert self.refcount == 0
231 self.shelve = shelve.open(self.shelve_fn())
232 self.refcount += 1
233
234 def decref(self):
235 self.refcount -= 1
236 if self.refcount == 0:
237 self.shelve.close()
238 self.shelve = None
239
240 def __exit__(self, type, value, traceback):
241 self.decref()
242
243
245 _refcount = 0
246 _shelve = None
247
248 @property
249 def refcount(self):
250 return lasp_shelve._refcount
251
252 @refcount.setter
253 def refcount(self, val):
254 lasp_shelve._refcount = val
255
256 @property
257 def shelve(self):
258 return lasp_shelve._shelve
259
260 @shelve.setter
261 def shelve(self, shelve):
262 lasp_shelve._shelve = shelve
263
264 def shelve_fn(self):
265 return os.path.join(lasp_appdir, 'config.shelve')
266
267
269 _refcount = 0
270 _shelve = None
271
272 @property
273 def refcount(self):
274 return this_lasp_shelve._refcount
275
276 @refcount.setter
277 def refcount(self, val):
278 this_lasp_shelve._refcount = val
279
280 @property
281 def shelve(self):
282 return this_lasp_shelve._shelve
283
284 @shelve.setter
285 def shelve(self, shelve):
286 this_lasp_shelve._shelve = shelve
287
288 def shelve_fn(self):
289 node = platform.node()
290 return os.path.join(lasp_appdir, f'{node}_config.shelve')
291
293 # This is not actually a time weighting
294 none = (0, 'Raw (no time weighting)')
295
296 uufast = (1e-4, '0.1 ms')
297 ufast = (35e-3, 'Impulse (35 ms)')
298 fast = (0.125, 'Fast (0.125 s)')
299 slow = (1.0, 'Slow (1.0 s)')
300 tens = (10., '10 s')
301
302 # All-averaging: compute the current average of all history. Kind of the
303 # equivalent level. Only useful for power spectra. For Sound Level Meter,
304 # equivalent levels does that thing.
305 averaging = (-1, 'All-averaging')
306
307 types_all = (none, uufast, ufast, fast, slow, tens, averaging)
308
309 # Used for combobox of realt time power spectra
310 types_realtimeaps = (ufast, fast, slow, tens, averaging)
311 # Used for combobox of realt time sound level meter
312 types_realtimeslm = (ufast, fast, slow, tens)
313
314 # Used for combobox of sound level meter - time dependent
315 types_slmt = (none, uufast, ufast, fast, slow, tens)
316
317 # Used for combobox of sound level meter - Lmax statistics
318 types_slmstats = (uufast, ufast, fast, slow, tens)
319
320 default = fast
321
322 @staticmethod
323 def fillComboBox(cb, types):
324 """
325 Fill TimeWeightings to a combobox
326
327 Args:
328 cb: QComboBox to fill
329 types: The types to fill it with
330 """
331 cb.clear()
332
333 logging.debug(f'{types}')
334 for tw in types:
335 cb.addItem(tw[1], tw)
336 if TimeWeighting.default == tw:
337 cb.setCurrentIndex(cb.count()-1)
338
339 @staticmethod
340 def getCurrent(cb):
341 for type in TimeWeighting.types_all:
342 if cb.currentText() == type[1]:
343 return type
344
345
347 """
348 Frequency weighting types
349 """
350 Z = ('Z', 'Z-weighting')
351 A = ('A', 'A-weighting')
352 C = ('C', 'C-weighting')
353 types = (A, C, Z)
354 default = Z
355 default_index = 0
356
357 @staticmethod
359 """
360 Fill FreqWeightings to a combobox
361
362 Args:
363 cb: QComboBox to fill
364 """
365 cb.clear()
366 for fw in FreqWeighting.types:
367 cb.addItem(fw[1], fw)
368 cb.setCurrentIndex(FreqWeighting.default_index)
369
370 @staticmethod
371 def getCurrent(cb):
372 return FreqWeighting.types[cb.currentIndex()]
373
374
375def getTime(fs, N, start=0):
376 """
377 Return a time array for given number of points and sampling frequency.
378
379 Args:
380 fs: Sampling frequency [Hz]
381 N: Number of time samples
382 start: Optional start ofset in number of samples
383 """
384 assert N > 0 and fs > 0
385 return np.linspace(start, start + N/fs, N, endpoint=False)
386
387
388def getFreq(fs, nfft):
389 """
390 return an array of frequencies for single-sided spectra
391
392 Args:
393 fs: Sampling frequency [Hz]
394 nfft: Fft length (int)
395 """
396 df = fs/nfft # frequency resolution
397 K = nfft//2+1 # number of frequency bins
398 return np.linspace(0, (K-1)*df, K)
Specificying the type of data, for adding and removing callbacks from the stream.
fillComboBox(cb)
Fill Calibration Settings to a combobox.
Frequency weighting types.
fillComboBox(cb)
Fill FreqWeightings to a combobox.
toInt(self)
Convert quantity to its specific enum integer value.
__eq__(self, other)
Comparison breaks for the other objects, level unit, level ref name, etc as these are tuples / a sing...
fromCppEnum(enum)
Convert enumeration index from - say - a measurement file back into physical quantity information.
fromInt(val)
Convert integer index from - say - a measurement file back into physical quantity information.
__exit__(self, type, value, traceback)
store(self, key, val)
load(self, key, default_value)
Load data from a given key, if key is not found, returns the default value if key is not found.
fillComboBox(cb, types)
Fill TimeWeightings to a combobox.