Split up GUI in different Widgets, added revtime, added figure list possibilities, added Qt Resources, added About panel, added lots of comments, export and import of measurements

This commit is contained in:
Anne de Jong 2018-05-02 16:29:53 +02:00 committed by J.A. de Jong - ASCEE
parent 393b6b3571
commit 3357a0ded6
11 changed files with 606 additions and 125 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.png binary

2
.gitignore vendored
View File

@ -10,6 +10,7 @@ __pycache__
cython_debug
lasp/config.pxi
lasp/wrappers.c
lasp/resources_rc.py
*.so
test/test_bf
test/test_fft
@ -19,3 +20,4 @@ doc
LASP.egg-info
lasp_octave_fir.*
lasp/ui_*
resources_rc.py

View File

@ -18,17 +18,27 @@ include_directories(
# COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
# DEPENDS MakeTable
# )
set(ui_files ui_apsrt ui_mainwindow ui_figure ui_about ui_apswidget ui_revtime ui_slmwidget)
foreach(fn ${ui_files})
add_custom_command(
OUTPUT "${fn}.py"
COMMAND pyside-uic ${CMAKE_CURRENT_SOURCE_DIR}/ui/${fn}.ui -o ${CMAKE_CURRENT_SOURCE_DIR}/${fn}.py
DEPENDS "ui/${fn}.ui"
)
add_custom_target(${fn} ALL DEPENDS "${fn}.py")
endforeach(fn)
add_custom_command(
OUTPUT "aps_ui.py"
COMMAND pyside-uic ${CMAKE_CURRENT_SOURCE_DIR}/ui/aps_ui.ui -o ${CMAKE_CURRENT_SOURCE_DIR}/aps_ui.py
DEPENDS "ui/aps_ui.ui"
)
add_custom_target(ui ALL DEPENDS "aps_ui.py")
OUTPUT "${PROJECT_SOURCE_DIR}/resources_rc.py"
COMMAND pyside-rcc -py3 ui/resources.qrc -o ${PROJECT_SOURCE_DIR}/resources_rc.py
DEPENDS "ui/resources.qrc"
)
add_custom_target(resources_rc ALL DEPENDS "${PROJECT_SOURCE_DIR}/resources_rc.py")
set_source_files_properties(wrappers.c PROPERTIES COMPILE_FLAGS "${CMAKE_C_FLAGS} ${CYTHON_EXTRA_C_FLAGS}")
set_source_files_properties(wrappers.c PROPERTIES COMPILE_FLAGS "${CMAKE_C_FLAGS} ${CYTHON_EXTRA_C_FLAGS}")
cython_add_module(wrappers wrappers.pyx)
target_link_libraries(wrappers lasp_lib )

View File

@ -17,7 +17,7 @@ add_library(lasp_lib
lasp_worker.c
lasp_dfifo.c
lasp_filterbank.c
lasp_octave_fir.c
# lasp_octave_fir.c
lasp_decimation.c
lasp_sp_lowpass.c
)

View File

@ -1,21 +1,56 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
import numpy as np
from .wrappers import Window as wWindow
"""
Common definitions used throughout the code.
"""
__all__ = ['P_REF','FreqWeighting','TimeWeighting','getTime']
__all__ = ['P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'calfile',
'sens']
# Reference sound pressure level
P_REF = 2e-5
# Todo: fix This
calfile = '/home/anne/wip/UMIK-1/cal/7027430_90deg.txt'
sens = 0.053690387255872614
class Window:
hann = (wWindow.hann, 'Hann')
hamming = (wWindow.hamming, 'Hamming')
rectangular = (wWindow.rectangular, 'Rectangular')
bartlett = (wWindow.bartlett, 'Bartlett')
blackman = (wWindow.blackman, 'Blackman')
types = (hann, hamming, rectangular, bartlett, blackman)
default = 0
@staticmethod
def fillComboBox(cb):
"""
Fill Windows to a combobox
Args:
cb: QComboBox to fill
"""
cb.clear()
for tw in Window.types:
cb.addItem(tw[1], tw)
cb.setCurrentIndex(Window.default)
def getCurrent(cb):
return Window.types[cb.currentIndex()]
class TimeWeighting:
none = (None,'Raw (no time weighting)')
ufast = (30e-3,'30 ms')
fast = (0.125,'Fast')
slow = (1.0,'Slow')
types = (none, ufast, fast, slow)
none = (None, 'Raw (no time weighting)')
uufast = (1e-4, '0.1 ms')
ufast = (30e-3, '30 ms')
fast = (0.125, 'Fast')
slow = (1.0, 'Slow')
types = (none, uufast, ufast, fast, slow)
default = 2
@staticmethod
@ -28,20 +63,21 @@ class TimeWeighting:
"""
cb.clear()
for tw in TimeWeighting.types:
cb.addItem(tw[1],tw)
cb.addItem(tw[1], tw)
cb.setCurrentIndex(TimeWeighting.default)
def getCurrent(cb):
return TimeWeighting.types[cb.currentIndex()]
class FreqWeighting:
"""
Frequency weighting types
"""
A=('A','A-weighting')
C=('C','C-weighting')
Z=('Z','Z-weighting')
types = (A,C,Z)
A = ('A', 'A-weighting')
C = ('C', 'C-weighting')
Z = ('Z', 'Z-weighting')
types = (A, C, Z)
default = 0
@staticmethod
@ -54,13 +90,14 @@ class FreqWeighting:
"""
cb.clear()
for fw in FreqWeighting.types:
cb.addItem(fw[1],fw)
cb.addItem(fw[1], fw)
cb.setCurrentIndex(FreqWeighting.default)
def getCurrent(cb):
return FreqWeighting.types[cb.currentIndex()]
def getTime(fs,N,start=0):
def getTime(fs, N, start=0):
"""
Return a time array for given number of points and sampling frequency.
@ -70,7 +107,7 @@ def getTime(fs,N,start=0):
start: Optional start ofset in number of samples
"""
return np.linspace(start/fs,N/fs,N,endpoint=False)
return np.linspace(start/fs, N/fs, N, endpoint=False)
def getFreq(fs, nfft):
@ -81,6 +118,6 @@ def getFreq(fs, nfft):
fs: Sampling frequency [Hz]
nfft: Fft length (int)
"""
df = fs/nfft # frequency resolution
K = nfft//2+1 # number of frequency bins
df = fs/nfft # frequency resolution
K = nfft//2+1 # number of frequency bins
return np.linspace(0, (K-1)*df, K)

View File

@ -10,8 +10,15 @@ import numpy as np
LASP_NUMPY_FLOAT_TYPE = np.float64
def zeros(shape):
return np.zeros(shape,dtype=LASP_NUMPY_FLOAT_TYPE)
return np.zeros(shape, dtype=LASP_NUMPY_FLOAT_TYPE)
def ones(shape):
return np.ones(shape,dtype=LASP_NUMPY_FLOAT_TYPE)
return np.ones(shape, dtype=LASP_NUMPY_FLOAT_TYPE)
def empty(shape):
return np.empty(shape,dtype=LASP_NUMPY_FLOAT_TYPE)
return np.empty(shape, dtype=LASP_NUMPY_FLOAT_TYPE)
class config:
default_figsize = (12,7)
default_comment_lenght_measoverview = 50
default_xlim_freq_plot = (50, 10000)
default_ylim_freq_plot = (20, 100)
versiontxt = '0.1'

View File

@ -4,36 +4,94 @@
Author: J.A. de Jong - ASCEE
Description: Measurement class
The ASCEE hdf5 measurement file format contains the following fields:
- Attributes:
'samplerate': The audio data sample rate in Hz.
'nchannels': The number of audio channels in the file
'sensitivity': (Optionally) the stored sensitivity of the record channels.
This can be a single value, or an array of sensitivities for
each channel. Both representations are allowed.
- Datasets:
'audio': 3-dimensional array of blocks of audio data. The first axis is the
block index, the second axis the sample number and the third axis is the channel
number. The data type is either int16, int32 or float64 / float32. In case the
data is stored as integers. The raw data should be scaled with the maximum value
that can be stored for the integer bit depth to get a number between -1.0 and
1.0.
'video': 4-dimensional array of video frames. The first index is the frame
number, the second the x-value of the pixel and the third is the
y-value of the pixel. Then, the last axis is the color. This axis has
length 3 and the colors are stored as (r,g,b). Where typically a
color depth of 256 is used (np.uint8 data format)
The video dataset can possibly be not present in the data.
"""
import h5py as h5
import numpy as np
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
import wave,os
import wave
import os
class BlockIter:
def __init__(self,nblocks,faudio):
"""
Iterate over the blocks in the audio data of a h5 file
"""
def __init__(self, faudio):
"""
Initialize a BlockIter object
Args:
faudio: Audio dataset in the h5 file, accessed as f['audio']
"""
self.i = 0
self.nblocks = nblocks
self.nblocks = faudio.shape[0]
self.fa = faudio
def __iter__(self):
return self
def __next__(self):
"""
Return the next block
"""
if self.i == self.nblocks:
raise StopIteration
self.i+=1
return self.fa[self.i-1][:,:]
self.i += 1
return self.fa[self.i-1][:, :]
def getSampWidth(dtype):
"""
Returns the width of a single sample in bytes.
Args:
dtype: numpy dtype
Returns:
Size of a sample in bytes (int)
"""
if dtype == np.int32:
return 4
elif dtype == np.int16:
return 2
elif dtype == np.float64:
return 8
else:
raise ValueError('Invalid data type: %s' %dtype)
raise ValueError('Invalid data type: %s' % dtype)
def exportAsWave(fn,fs,data,force=False):
def exportAsWave(fn, fs, data, force=False):
if not '.wav' in fn[-4:]:
fn += '.wav'
@ -43,21 +101,35 @@ def exportAsWave(fn,fs,data,force=False):
if os.path.exists(fn) and not force:
raise RuntimeError('File already exists: %s', fn)
with wave.open(fn,'w') as wf:
wf.setparams((nchannels,sampwidth,fs,0,'NONE','NONE'))
with wave.open(fn, 'w') as wf:
wf.setparams((nchannels, sampwidth, fs, 0, 'NONE', 'NONE'))
wf.writeframes(np.asfortranarray(data).tobytes())
class Measurement:
def __init__(self, fn):
"""
Provides access to measurement data stored in the h5 measurement file
format.
"""
if not '.h5' in fn:
def __init__(self, fn):
"""
Initialize a Measurement object based on the filename
"""
if '.h5' not in fn:
fn += '.h5'
# Full filepath
self.fn = fn
# Base filename
self.fn_base = os.path.split(fn)[1]
f = h5.File(fn,'r+')
# Open the h5 file in read-plus mode, to allow for changing the
# measurement comment.
f = h5.File(fn, 'r+')
self.f = f
# Check for video data
try:
f['video']
self.has_video = True
@ -74,17 +146,19 @@ class Measurement:
# comment = read-write thing
try:
self.f.attrs['comment']
self._comment = self.f.attrs['comment']
except KeyError:
self.f.attrs['comment'] = ''
self._comment = ''
@property
def comment(self):
return self.f.attrs['comment']
return self._comment
@comment.setter
def comment(self, cmt):
self.f.attrs['comment'] = cmt
self._comment = cmt
@property
def recTime(self):
@ -92,23 +166,37 @@ class Measurement:
@property
def time(self):
"""
Returns the measurement time in seconds since the epoch.
"""
return self.f.attrs['time']
@property
def prms(self):
"""
Returns the root mean square of the uncalibrated rms sound pressure
level (equivalend SPL).
Returns:
1D array with rms values for each channel
"""
#
try:
return self._prms
except AttributeError:
pass
sens = self.sensitivity
pms = 0.
for block in self.iterBlocks():
pms += np.sum(block/sens)**2/self.N
pms += np.sum(block/sens[np.newaxis, :], axis=0)**2/self.N
self._prms = np.sqrt(pms)
return self._prms
def praw(self,block=None):
def praw(self, block=None):
"""
Returns the raw uncalibrated data, converted to floating point format.
"""
if block is not None:
blocks = self.f['audio'][block]
else:
@ -118,51 +206,172 @@ class Measurement:
blocks = np.asarray(blocks)
blocks = blocks.reshape(self.nblocks*self.blocksize,
self.nchannels)
self.nchannels)
# When the data is stored as integers, we assume dB full-scale scaling.
# Hence, when we convert the data to floats, we divide by the maximum
# possible value.
if blocks.dtype == np.int32:
fac = 2**31
elif blocks.dtype == np.int16:
fac = 2**15
elif blocks.dtype == np.float64:
fac = 1.0
else:
raise RuntimeError('Unimplemented data type from recording: %s' %str(blocks.dtype))
blocks = blocks.astype(LASP_NUMPY_FLOAT_TYPE)/fac/self.sensitivity
raise RuntimeError(
f'Unimplemented data type from recording: {blocks.dtype}.')
sens = self.sensitivity
blocks = blocks.astype(LASP_NUMPY_FLOAT_TYPE)/fac/sens[np.newaxis, :]
return blocks
def iterBlocks(self):
return BlockIter(self.nblocks,self.f['audio'])
return BlockIter(self.f['audio'])
@property
def sensitivity(self):
"""
Sensitivity of the data in Pa^-1, from floating point data scaled
between -1.0 and 1.0 to Pascal. If the sensitivity is not stored in
the measurement file, this function returns 1.0
"""
try:
return self.f.attrs['sensitivity']
except:
return 1.0
except KeyError:
return np.ones(self.nchannels)
@sensitivity.setter
def sensitivity(self, sens):
"""
Set the sensitivity of the measurement in the file
Args:
sens: sensitivity data, should be a float, or an array of floats
equal to the number of channels.
"""
if isinstance(sens, float):
sens = sens*np.ones(self.nchannels)
valid = sens.ndim == 1
valid &= sens.shape[0] == self.nchannels
valid &= isinstance(sens.dtype, float)
if not valid:
raise ValueError('Invalid sensitivity value(s) given')
self.f.attrs['sensitivity'] = sens
def exportAsWave(self, fn=None, force=False):
def exportAsWave(self, fn=None, force=False, sampwidth=None):
"""
Export measurement file as wave
Export measurement file as wave. In case the measurement data is stored
as floats, the values are scaled between 0 and 1
Args:
fn: If given, this will be the filename to write to. If the
filename does not end with '.wav', this extension is added.
force: If True, overwrites any existing files with the given name
, otherwise a RuntimeError is raised.
sampwidth: sample width in bytes with which to export the data.
This should only be given in case the measurement data is stored as
floating point values, otherwise an
"""
if fn is None:
fn = self.fn
fn = os.path.splitext(fn)[0]
if not '.wav' in fn[-4:]:
if '.wav' not in fn[-4:]:
fn += '.wav'
if os.path.exists(fn) and not force:
raise RuntimeError('File already exists: %s', fn)
raise RuntimeError(f'File already exists: {fn}')
with wave.open(fn,'w') as wf:
wf.setparams((self.nchannels,self.sampwidth,self.samplerate,0,'NONE','NONE'))
audio = self.f['audio']
if isinstance(audio.dtype, float):
if sampwidth is None:
raise ValueError('sampwidth parameter should be given '
'for float data in measurement file')
elif sampwidth == 2:
itype = np.int16
elif sampwidth == 4:
itype = np.int32
else:
raise ValueError('Invalid sample width, should be 2 or 4')
# Find maximum
max = 0.
for block in self.iterBlocks():
blockmax = np.max(np.abs(block))
if blockmax > max:
max = blockmax
# Scale with maximum value only if we have a nonzero maximum value.
if max == 0.:
max = 1.
scalefac = 2**(8*sampwidth)/max
with wave.open(fn, 'w') as wf:
wf.setparams((self.nchannels, self.sampwidth,
self.samplerate, 0, 'NONE', 'NONE'))
for block in self.iterBlocks():
if isinstance(block.dtype, float):
# Convert block to integral data type
block = (block*scalefac).astype(itype)
wf.writeframes(np.asfortranarray(block).tobytes())
@staticmethod
def fromtxt(fn, skiprows, samplerate, sensitivity, mfn=None,
timestamp=None,
delimiter='\t', firstcoltime=True):
"""
Converts a txt file to a LASP Measurement object and returns the
measurement.
Args:
fn: Filename of text file
skiprows: Number of header rows in text file to skip
samplerate: Sampling frequency in [Hz]
sensitivity: 1D array of channel sensitivities
mfn: Filepath where measurement file is stored. If not given,
a h5 file will be created along fn, which shares its basename
timestamp: If given, a custom timestamp for the measurement
(integer containing seconds since epoch). If not given, the
timestamp is obtained from the last modification time.
delimiter: Column delimiter
firstcoltime: If true, the first column is the treated as the
sample time.
"""
if not os.path.exists(fn):
raise ValueError(f'File {fn} does not exist.')
if timestamp is None:
timestamp = os.path.getmtime(fn)
if mfn is None:
mfn = os.path.splitext(fn)[0] + '.h5'
dat = np.loadtxt(fn, skiprows=skiprows, delimiter=delimiter)
if firstcoltime:
time = dat[:, 0]
if not np.isclose(time[1] - time[0], 1/samplerate):
raise ValueError('Samplerate given does not agree with '
'samplerate in file')
dat = dat[:, 1:]
nchannels = dat.shape[1]
with h5.File(mfn, 'w') as hf:
hf.attrs['samplerate'] = samplerate
hf.attrs['sensitivity'] = sensitivity
hf.attrs['time'] = timestamp
hf.attrs['blocksize'] = 1
hf.attrs['nchannels'] = nchannels
ad = hf.create_dataset('audio',
(1, dat.shape[0], dat.shape[1]),
dtype=dat.dtype,
maxshape=(1, dat.shape[0], dat.shape[1]),
compression='gzip')
ad[0] = dat
return Measurement(mfn)
def __del__(self):
try:

View File

@ -3,20 +3,23 @@
"""
Created on Sat Mar 10 08:28:03 2018
@author: Read data from image stream and record sound at the same time
Read data from stream and record sound and video at the same time
"""
import numpy as np
from .lasp_atomic import Atomic
from threading import Condition
from .lasp_avstream import AvType
import h5py, time
import h5py
import time
# %%
class Recording:
def __init__(self, fn, stream, rectime = None):
def __init__(self, fn, stream, rectime=None):
"""
"""
ext = '.h5'
if not ext in fn:
if ext not in fn:
fn += ext
self._stream = stream
self.blocksize = stream.blocksize
@ -25,42 +28,43 @@ class Recording:
self._running_cond = Condition()
self.rectime = rectime
self._fn = fn
self._video_frame_positions = []
self._aframeno = Atomic(0)
self._vframeno = 0
def start(self):
stream = self._stream
with h5py.File(self._fn,'w') as f:
with h5py.File(self._fn, 'w') as f:
self._ad = f.create_dataset('audio',
(1,stream.blocksize,stream.nchannels),
(1, stream.blocksize, stream.nchannels),
dtype=stream.dtype,
maxshape=(None,stream.blocksize,
maxshape=(None, stream.blocksize,
stream.nchannels),
compression='gzip'
)
if stream.hasVideo():
video_x,video_y = stream.video_x,stream.video_y
video_x, video_y = stream.video_x, stream.video_y
self._vd = f.create_dataset('video',
(1,video_y,video_x,3),
(1, video_y, video_x, 3),
dtype='uint8',
maxshape=(None,video_y,video_x,3),
maxshape=(
None, video_y, video_x, 3),
compression='gzip'
)
f.attrs['samplerate'] = stream.samplerate
f.attrs['nchannels'] = stream.nchannels
f.attrs['blocksize'] = stream.blocksize
f.attrs['time'] = time.time()
self._running <<= True
# Videothread is going to start
if not stream.isStarted():
stream.start()
stream.addCallback(self._callback)
with self._running_cond:
try:
@ -70,34 +74,33 @@ class Recording:
except KeyboardInterrupt:
print("Keyboard interrupt on record")
self._running <<= False
stream.removeCallback(self._callback)
if stream.hasVideo():
f['video_frame_positions'] = self._video_frame_positions
print('\nEnding record')
def stop(self):
self._running <<= False
with self._running_cond:
self._running_cond.notify()
def _callback(self, _type, data, aframe, vframe):
if not self._stream.isStarted():
self._running <<= False
with self._running_cond:
self._running_cond.notify()
if _type == AvType.audio:
self._aCallback(data, aframe)
elif _type == AvType.video:
self._vCallback(data)
def _aCallback(self, frames, aframe):
# print(self._aframeno())
print('.',end='')
# print(self._aframeno())
print('.', end='')
curT = self._aframeno()*self.blocksize/self.samplerate
if self.rectime is not None and curT > self.rectime:
# We are done!
@ -105,20 +108,20 @@ class Recording:
with self._running_cond:
self._running_cond.notify()
return
self._ad.resize(self._aframeno()+1,axis=0)
self._ad[self._aframeno(),:,:] = frames
self._ad.resize(self._aframeno()+1, axis=0)
self._ad[self._aframeno(), :, :] = frames
self._aframeno += 1
def _vCallback(self,frame):
def _vCallback(self, frame):
self._video_frame_positions.append(self._aframeno())
vframeno = self._vframeno
self._vd.resize(vframeno+1,axis=0)
self._vd[vframeno,:,:] = frame
self._vd.resize(vframeno+1, axis=0)
self._vd[vframeno, :, :] = frame
self._vframeno += 1
if __name__ == '__main__':
rec = Recording('test',5)
rec = Recording('test', 5)
rec.start()

81
lasp/lasp_reverb.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""!
Author: J.A. de Jong - ASCEE
Description:
Reverberation time estimation tool using least squares
"""
from .lasp_common import getTime
import numpy as np
class ReverbTime:
"""
Tool to estimate the reverberation time
"""
def __init__(self, fs, level):
"""
Initialize Reverberation time computer.
Args:
fs: Sampling frequency [Hz]
level: (Optionally weighted) level values as a function of time, in
dB.
"""
assert level.ndim == 1, 'Invalid number of dimensions in level'
self._level = level
# Number of time samples
self._N = self._level.shape[0]
self._t = getTime(fs, self._N, 0)
def compute(self, tstart, tstop):
"""
Compute the reverberation time using a least-squares solver
Args:
tstart: Start time of reverberation interval
stop: Stop time of reverberation interval
Returns:
dictionary with result values, contains:
- istart: start index of reberberation interval
- istop: stop index of reverb. interval
- T60: Reverberation time
- const: Constant value
- derivative: rate of change of the level in dB/s.
"""
# Find start and stop indices. Coerce if they are outside
# of the valid domain
istart = np.where(self._t >= tstart)[0][0]
istop = np.where(self._t >= tstop)[0]
if istop.shape[0] == 0:
istop = self._level.shape[0]
else:
istop = istop[0]
points = self._level[istart:istop]
x = self._t[istart:istop][:, np.newaxis]
# Solve the least-squares problem, by creating a matrix of
A = np.hstack([x, np.ones((x.shape))])
# derivative is dB/s of increase/decrease
sol, residuals, rank, s = np.linalg.lstsq(A, points)
# Derivative of the decay in dB/s
derivative = sol[0]
# Start level in dB
const = sol[1]
# Time to reach a decay of 60 dB (reverb. time)
T60 = -60./derivative
return {'istart': istart,
'istop': istop,
'const': const,
'derivative': derivative,
'T60': T60}

View File

@ -4,38 +4,73 @@
Sound level meter implementation
@author: J.A. de Jong - ASCEE
"""
from .wrappers import FilterBank, SPLowpass
from .wrappers import SPLowpass
from .lasp_computewidget import ComputeWidget
import numpy as np
from .lasp_config import zeros
from .lasp_common import TimeWeighting, P_REF
__all__ = ['SLM']
from .lasp_common import (FreqWeighting, sens, calfile,
TimeWeighting, getTime, P_REF)
from .lasp_weighcal import WeighCal
from .lasp_gui_tools import wait_cursor
from .lasp_figure import FigureDialog, PlotOptions
from .ui_slmwidget import Ui_SlmWidget
__all__ = ['SLM', 'SlmWidget']
class Dummy:
"""
Emulate filtering, but does not filter anything at all.
"""
def filter_(self, data):
return data
return data[:, np.newaxis]
class SLM:
"""
Sound Level Meter, implements the single pole lowpass filter
"""
def __init__(self, fs, weighcal,
tw=TimeWeighting.default,
nchannels = 1,
freq=None, cal=None):
):
"""
Initialize a sound level meter object. Number of channels comes from
the weighcal object
Args:
fs: Sampling frequency [Hz]
weighcal: WeighCal instance used for calibration and frequency
weighting.
nchannels: Number of channels to allocate filters for
"""
nchannels = weighcal.nchannels
self.nchannels = nchannels
self._weighcal = weighcal
if tw is not TimeWeighting.none:
if tw[0] is not TimeWeighting.none[0]:
self._lps = [SPLowpass(fs, tw[0]) for i in range(nchannels)]
else:
self._lpw = [Dummy() for i in range(nchannels)]
self._lps = [Dummy() for i in range(nchannels)]
self._Lmax = zeros(nchannels)
@property
def Lmax(self):
"""
Returns the currently maximum recorded level
"""
return self._Lmax
def addData(self, data):
assert data.ndim == 2
"""
Add new fresh timedata to the Sound Level Meter
Args:
data:
"""
if data.ndim == 1:
data = data[:, np.newaxis]
data_weighted = self._weighcal.filter_(data)
# Squared
@ -44,9 +79,11 @@ class SLM:
return np.array([])
tw = []
# Time-weight the signal
for chan,lp in enumerate(self._lps):
tw.append(lp.filter_(sq[:,chan])[:,0])
for chan, lp in enumerate(self._lps):
tw.append(lp.filter_(sq[:, chan])[:, 0])
tw = np.asarray(tw).transpose()
Level = 10*np.log10(tw/P_REF**2)
@ -56,3 +93,85 @@ class SLM:
self._Lmax = curmax
return Level
class SlmWidget(ComputeWidget, Ui_SlmWidget):
def __init__(self):
"""
Initialize the SlmWidget.
"""
super().__init__()
self.setupUi(self)
FreqWeighting.fillComboBox(self.freqweighting)
self.setMeas(None)
def setMeas(self, meas):
"""
Set the current measurement for this widget.
Args:
meas: if None, the Widget is disabled
"""
self.meas = meas
if meas is None:
self.setEnabled(False)
else:
self.setEnabled(True)
rt = meas.recTime
self.starttime.setRange(0, rt, 0)
self.stoptime.setRange(0, rt, rt)
self.channel.clear()
for i in range(meas.nchannels):
self.channel.addItem(str(i))
self.channel.setCurrentIndex(0)
def compute(self):
"""
Compute Sound Level using settings. This method is
called whenever the Compute button is pushed in the SLM tab
"""
meas = self.meas
fs = meas.samplerate
channel = self.channel.currentIndex()
tw = TimeWeighting.getCurrent(self.timeweighting)
fw = FreqWeighting.getCurrent(self.freqweighting)
# Downsampling factor of result
dsf = self.downsampling.value()
# gb = self.slmFre
with wait_cursor():
# This one exctracts the calfile and sensitivity from global
# variables defined at the top. # TODO: Change this to a more
# robust variant.
weighcal = WeighCal(fw, nchannels=1,
fs=fs, calfile=calfile,
sens=sens)
slm = SLM(fs, weighcal, tw)
praw = meas.praw()[:, [channel]]
# Filter, downsample data
filtered = slm.addData(praw)[::dsf, :]
N = filtered.shape[0]
time = getTime(float(fs)/dsf, N)
Lmax = slm.Lmax
pto = PlotOptions()
pto.ylabel = f'L{fw[0]} [dB({fw[0]})]'
pto.xlim = (time[0], time[-1])
fig, new = self.getFigure(pto)
fig.fig.plot(time, filtered)
fig.show()
stats = f"""Statistical results:
=============================
Applied frequency weighting: {fw[1]}
Applied time weighting: {tw[1]}
Applied Downsampling factor: {dsf}
Maximum level (L{fw[0]} max): {Lmax:4.4} [dB({fw[0]})]
"""
self.results.setPlainText(stats)

View File

@ -19,10 +19,10 @@ class WeighCal:
"""
Time weighting and calibration FIR filter
"""
def __init__(self, fw = FreqWeighting.default,
nchannels = 1,
fs = 48000.,
calfile = None,
def __init__(self, fw=FreqWeighting.default,
nchannels=1,
fs=48000.,
calfile=None,
sens=1.0):
"""
Initialize the frequency weighting and calibration FIR filters.
@ -42,7 +42,7 @@ class WeighCal:
self.calfile = calfile
# Frequencies used for the filter design
freq_design = np.linspace(0,17e3,3000)
freq_design = np.linspace(0, 17e3, 3000)
freq_design[-1] = fs/2
# Objective function for the frequency response
@ -51,52 +51,63 @@ class WeighCal:
self._firs = []
self._fbs = []
for chan in range(self.nchannels):
fir = arbitrary_fir_design(fs,2048,freq_design,
frp_obj[:,chan],
window='rectangular')
fir = arbitrary_fir_design(fs, 2048, freq_design,
frp_obj[:, chan],
window='rectangular')
self._firs.append(fir)
self._fbs.append(FilterBank(fir[:,np.newaxis],4096))
self._fbs.append(FilterBank(fir[:, np.newaxis], 4096))
self._freq_design = freq_design
def filter_(self,data):
def filter_(self, data):
"""
Filter data using the calibration and frequency weighting filter.
Args:
data: (Weighted) raw time data that needs to be filtered, should
have the same number of columns as the number of channels, or should
have dimension 1 in case of a single channel.
Retuns:
Filtered data for each channel
"""
nchan = self.nchannels
if data.ndim == 1:
data = data[:, np.newaxis]
assert data.shape[1] == nchan
assert data.shape[0] > 0
filtered = []
for chan in range(nchan):
filtered.append(self._fbs[chan].filter_(data[:,chan])[:,0])
filtered.append(self._fbs[chan].filter_(data[:, chan])[:, 0])
filtered = np.asarray(filtered).transpose()/self.sens
if filtered.ndim == 1:
filtered = filtered[:,np.newaxis]
filtered = filtered[:, np.newaxis]
return filtered
def frpCalObj(self, freq_design):
"""
Computes the objective frequency response of the calibration filter
"""
calfile = self.calfile
if calfile is not None:
cal = np.loadtxt(calfile,skiprows=2)
freq = cal[:,0]
cal = cal[:,1:]
cal = np.loadtxt(calfile, skiprows=2)
freq = cal[:, 0]
cal = cal[:, 1:]
if cal.shape[1] != self.nchannels:
raise ValueError('Number of channels in calibration file does'
' not equal to given number of channels')
calfac = 10**(-cal/20)
filter_calfac = empty((freq_design.shape[0],self.nchannels))
filter_calfac = empty((freq_design.shape[0], self.nchannels))
for chan in range(self.nchannels):
filter_calfac[:,chan] = np.interp(freq_design,freq,calfac[:,chan])
filter_calfac[:, chan] = np.interp(freq_design, freq,
calfac[:, chan])
else:
filter_calfac = ones((freq_design.shape[0],self.nchannels,))
filter_calfac = ones((freq_design.shape[0], self.nchannels,))
return filter_calfac
@ -122,7 +133,7 @@ class WeighCal:
"""
# Objective function for the frequency response
frp_objective = self.frpCalObj(freq_design) * \
self.frpWeightingObj(freq_design)[:,np.newaxis]
self.frpWeightingObj(freq_design)[:, np.newaxis]
frp_objective[-1] = 0.
return frp_objective
@ -132,5 +143,6 @@ class WeighCal:
Returns the frequency response of the designed FIR filter
"""
if freq is None:
freq = np.logspace(1,np.log10(self.fs/2),500)
return freq,frp(self.fs,freq,self._firs[chan]),self.frpObj(freq)[:,chan]
freq = np.logspace(1, np.log10(self.fs/2), 500)
return (freq, frp(self.fs, freq, self._firs[chan]),
self.frpObj(freq)[:, chan])