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:
parent
393b6b3571
commit
3357a0ded6
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png binary
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,6 +10,7 @@ __pycache__
|
|||||||
cython_debug
|
cython_debug
|
||||||
lasp/config.pxi
|
lasp/config.pxi
|
||||||
lasp/wrappers.c
|
lasp/wrappers.c
|
||||||
|
lasp/resources_rc.py
|
||||||
*.so
|
*.so
|
||||||
test/test_bf
|
test/test_bf
|
||||||
test/test_fft
|
test/test_fft
|
||||||
@ -19,3 +20,4 @@ doc
|
|||||||
LASP.egg-info
|
LASP.egg-info
|
||||||
lasp_octave_fir.*
|
lasp_octave_fir.*
|
||||||
lasp/ui_*
|
lasp/ui_*
|
||||||
|
resources_rc.py
|
||||||
|
@ -19,16 +19,26 @@ include_directories(
|
|||||||
# DEPENDS MakeTable
|
# 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(
|
add_custom_command(
|
||||||
OUTPUT "aps_ui.py"
|
OUTPUT "${fn}.py"
|
||||||
COMMAND pyside-uic ${CMAKE_CURRENT_SOURCE_DIR}/ui/aps_ui.ui -o ${CMAKE_CURRENT_SOURCE_DIR}/aps_ui.py
|
COMMAND pyside-uic ${CMAKE_CURRENT_SOURCE_DIR}/ui/${fn}.ui -o ${CMAKE_CURRENT_SOURCE_DIR}/${fn}.py
|
||||||
DEPENDS "ui/aps_ui.ui"
|
DEPENDS "ui/${fn}.ui"
|
||||||
)
|
)
|
||||||
add_custom_target(ui ALL DEPENDS "aps_ui.py")
|
add_custom_target(${fn} ALL DEPENDS "${fn}.py")
|
||||||
|
endforeach(fn)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
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)
|
cython_add_module(wrappers wrappers.pyx)
|
||||||
target_link_libraries(wrappers lasp_lib )
|
target_link_libraries(wrappers lasp_lib )
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ add_library(lasp_lib
|
|||||||
lasp_worker.c
|
lasp_worker.c
|
||||||
lasp_dfifo.c
|
lasp_dfifo.c
|
||||||
lasp_filterbank.c
|
lasp_filterbank.c
|
||||||
lasp_octave_fir.c
|
# lasp_octave_fir.c
|
||||||
lasp_decimation.c
|
lasp_decimation.c
|
||||||
lasp_sp_lowpass.c
|
lasp_sp_lowpass.c
|
||||||
)
|
)
|
||||||
|
@ -1,21 +1,56 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from .wrappers import Window as wWindow
|
||||||
"""
|
"""
|
||||||
Common definitions used throughout the code.
|
Common definitions used throughout the code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = ['P_REF','FreqWeighting','TimeWeighting','getTime']
|
__all__ = ['P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'calfile',
|
||||||
|
'sens']
|
||||||
|
|
||||||
# Reference sound pressure level
|
# Reference sound pressure level
|
||||||
P_REF = 2e-5
|
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:
|
class TimeWeighting:
|
||||||
none = (None, 'Raw (no time weighting)')
|
none = (None, 'Raw (no time weighting)')
|
||||||
|
uufast = (1e-4, '0.1 ms')
|
||||||
ufast = (30e-3, '30 ms')
|
ufast = (30e-3, '30 ms')
|
||||||
fast = (0.125, 'Fast')
|
fast = (0.125, 'Fast')
|
||||||
slow = (1.0, 'Slow')
|
slow = (1.0, 'Slow')
|
||||||
types = (none, ufast, fast, slow)
|
types = (none, uufast, ufast, fast, slow)
|
||||||
default = 2
|
default = 2
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -34,6 +69,7 @@ class TimeWeighting:
|
|||||||
def getCurrent(cb):
|
def getCurrent(cb):
|
||||||
return TimeWeighting.types[cb.currentIndex()]
|
return TimeWeighting.types[cb.currentIndex()]
|
||||||
|
|
||||||
|
|
||||||
class FreqWeighting:
|
class FreqWeighting:
|
||||||
"""
|
"""
|
||||||
Frequency weighting types
|
Frequency weighting types
|
||||||
@ -60,6 +96,7 @@ class FreqWeighting:
|
|||||||
def getCurrent(cb):
|
def getCurrent(cb):
|
||||||
return FreqWeighting.types[cb.currentIndex()]
|
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.
|
Return a time array for given number of points and sampling frequency.
|
||||||
|
@ -15,3 +15,10 @@ def ones(shape):
|
|||||||
return np.ones(shape, dtype=LASP_NUMPY_FLOAT_TYPE)
|
return np.ones(shape, dtype=LASP_NUMPY_FLOAT_TYPE)
|
||||||
def empty(shape):
|
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'
|
||||||
|
@ -4,20 +4,67 @@
|
|||||||
Author: J.A. de Jong - ASCEE
|
Author: J.A. de Jong - ASCEE
|
||||||
|
|
||||||
Description: Measurement class
|
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 h5py as h5
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
|
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
|
||||||
import wave,os
|
import wave
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class BlockIter:
|
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.i = 0
|
||||||
self.nblocks = nblocks
|
self.nblocks = faudio.shape[0]
|
||||||
self.fa = faudio
|
self.fa = faudio
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __next__(self):
|
def __next__(self):
|
||||||
|
"""
|
||||||
|
Return the next block
|
||||||
|
"""
|
||||||
if self.i == self.nblocks:
|
if self.i == self.nblocks:
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
self.i += 1
|
self.i += 1
|
||||||
@ -25,10 +72,21 @@ class BlockIter:
|
|||||||
|
|
||||||
|
|
||||||
def getSampWidth(dtype):
|
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:
|
if dtype == np.int32:
|
||||||
return 4
|
return 4
|
||||||
elif dtype == np.int16:
|
elif dtype == np.int16:
|
||||||
return 2
|
return 2
|
||||||
|
elif dtype == np.float64:
|
||||||
|
return 8
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid data type: %s' % dtype)
|
raise ValueError('Invalid data type: %s' % dtype)
|
||||||
|
|
||||||
@ -49,15 +107,29 @@ def exportAsWave(fn,fs,data,force=False):
|
|||||||
|
|
||||||
|
|
||||||
class Measurement:
|
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'
|
fn += '.h5'
|
||||||
|
|
||||||
|
# Full filepath
|
||||||
self.fn = fn
|
self.fn = fn
|
||||||
|
# Base filename
|
||||||
|
self.fn_base = os.path.split(fn)[1]
|
||||||
|
|
||||||
|
# Open the h5 file in read-plus mode, to allow for changing the
|
||||||
|
# measurement comment.
|
||||||
f = h5.File(fn, 'r+')
|
f = h5.File(fn, 'r+')
|
||||||
self.f = f
|
self.f = f
|
||||||
|
|
||||||
|
# Check for video data
|
||||||
try:
|
try:
|
||||||
f['video']
|
f['video']
|
||||||
self.has_video = True
|
self.has_video = True
|
||||||
@ -74,17 +146,19 @@ class Measurement:
|
|||||||
|
|
||||||
# comment = read-write thing
|
# comment = read-write thing
|
||||||
try:
|
try:
|
||||||
self.f.attrs['comment']
|
self._comment = self.f.attrs['comment']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.f.attrs['comment'] = ''
|
self.f.attrs['comment'] = ''
|
||||||
|
self._comment = ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comment(self):
|
def comment(self):
|
||||||
return self.f.attrs['comment']
|
return self._comment
|
||||||
|
|
||||||
@comment.setter
|
@comment.setter
|
||||||
def comment(self, cmt):
|
def comment(self, cmt):
|
||||||
self.f.attrs['comment'] = cmt
|
self.f.attrs['comment'] = cmt
|
||||||
|
self._comment = cmt
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recTime(self):
|
def recTime(self):
|
||||||
@ -92,23 +166,37 @@ class Measurement:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def time(self):
|
def time(self):
|
||||||
|
"""
|
||||||
|
Returns the measurement time in seconds since the epoch.
|
||||||
|
"""
|
||||||
return self.f.attrs['time']
|
return self.f.attrs['time']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prms(self):
|
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:
|
try:
|
||||||
return self._prms
|
return self._prms
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sens = self.sensitivity
|
sens = self.sensitivity
|
||||||
pms = 0.
|
pms = 0.
|
||||||
for block in self.iterBlocks():
|
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)
|
self._prms = np.sqrt(pms)
|
||||||
return self._prms
|
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:
|
if block is not None:
|
||||||
blocks = self.f['audio'][block]
|
blocks = self.f['audio'][block]
|
||||||
else:
|
else:
|
||||||
@ -119,50 +207,171 @@ class Measurement:
|
|||||||
|
|
||||||
blocks = blocks.reshape(self.nblocks*self.blocksize,
|
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:
|
if blocks.dtype == np.int32:
|
||||||
fac = 2**31
|
fac = 2**31
|
||||||
elif blocks.dtype == np.int16:
|
elif blocks.dtype == np.int16:
|
||||||
fac = 2**15
|
fac = 2**15
|
||||||
|
elif blocks.dtype == np.float64:
|
||||||
|
fac = 1.0
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('Unimplemented data type from recording: %s' %str(blocks.dtype))
|
raise RuntimeError(
|
||||||
|
f'Unimplemented data type from recording: {blocks.dtype}.')
|
||||||
blocks = blocks.astype(LASP_NUMPY_FLOAT_TYPE)/fac/self.sensitivity
|
sens = self.sensitivity
|
||||||
|
blocks = blocks.astype(LASP_NUMPY_FLOAT_TYPE)/fac/sens[np.newaxis, :]
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def iterBlocks(self):
|
def iterBlocks(self):
|
||||||
return BlockIter(self.nblocks,self.f['audio'])
|
return BlockIter(self.f['audio'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sensitivity(self):
|
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:
|
try:
|
||||||
return self.f.attrs['sensitivity']
|
return self.f.attrs['sensitivity']
|
||||||
except:
|
except KeyError:
|
||||||
return 1.0
|
return np.ones(self.nchannels)
|
||||||
|
|
||||||
@sensitivity.setter
|
@sensitivity.setter
|
||||||
def sensitivity(self, sens):
|
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
|
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:
|
if fn is None:
|
||||||
fn = self.fn
|
fn = self.fn
|
||||||
fn = os.path.splitext(fn)[0]
|
fn = os.path.splitext(fn)[0]
|
||||||
|
|
||||||
if not '.wav' in fn[-4:]:
|
if '.wav' not in fn[-4:]:
|
||||||
fn += '.wav'
|
fn += '.wav'
|
||||||
|
|
||||||
if os.path.exists(fn) and not force:
|
if os.path.exists(fn) and not force:
|
||||||
raise RuntimeError('File already exists: %s', fn)
|
raise RuntimeError(f'File already exists: {fn}')
|
||||||
|
|
||||||
|
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:
|
with wave.open(fn, 'w') as wf:
|
||||||
wf.setparams((self.nchannels,self.sampwidth,self.samplerate,0,'NONE','NONE'))
|
wf.setparams((self.nchannels, self.sampwidth,
|
||||||
|
self.samplerate, 0, 'NONE', 'NONE'))
|
||||||
for block in self.iterBlocks():
|
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())
|
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):
|
def __del__(self):
|
||||||
try:
|
try:
|
||||||
|
@ -3,20 +3,23 @@
|
|||||||
"""
|
"""
|
||||||
Created on Sat Mar 10 08:28:03 2018
|
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
|
import numpy as np
|
||||||
from .lasp_atomic import Atomic
|
from .lasp_atomic import Atomic
|
||||||
from threading import Condition
|
from threading import Condition
|
||||||
from .lasp_avstream import AvType
|
from .lasp_avstream import AvType
|
||||||
import h5py, time
|
import h5py
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
# %%
|
|
||||||
class Recording:
|
class Recording:
|
||||||
def __init__(self, fn, stream, rectime=None):
|
def __init__(self, fn, stream, rectime=None):
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
ext = '.h5'
|
ext = '.h5'
|
||||||
if not ext in fn:
|
if ext not in fn:
|
||||||
fn += ext
|
fn += ext
|
||||||
self._stream = stream
|
self._stream = stream
|
||||||
self.blocksize = stream.blocksize
|
self.blocksize = stream.blocksize
|
||||||
@ -47,7 +50,8 @@ class Recording:
|
|||||||
self._vd = f.create_dataset('video',
|
self._vd = f.create_dataset('video',
|
||||||
(1, video_y, video_x, 3),
|
(1, video_y, video_x, 3),
|
||||||
dtype='uint8',
|
dtype='uint8',
|
||||||
maxshape=(None,video_y,video_x,3),
|
maxshape=(
|
||||||
|
None, video_y, video_x, 3),
|
||||||
compression='gzip'
|
compression='gzip'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,7 +80,6 @@ class Recording:
|
|||||||
if stream.hasVideo():
|
if stream.hasVideo():
|
||||||
f['video_frame_positions'] = self._video_frame_positions
|
f['video_frame_positions'] = self._video_frame_positions
|
||||||
|
|
||||||
|
|
||||||
print('\nEnding record')
|
print('\nEnding record')
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -110,7 +113,6 @@ class Recording:
|
|||||||
self._ad[self._aframeno(), :, :] = frames
|
self._ad[self._aframeno(), :, :] = frames
|
||||||
self._aframeno += 1
|
self._aframeno += 1
|
||||||
|
|
||||||
|
|
||||||
def _vCallback(self, frame):
|
def _vCallback(self, frame):
|
||||||
self._video_frame_positions.append(self._aframeno())
|
self._video_frame_positions.append(self._aframeno())
|
||||||
vframeno = self._vframeno
|
vframeno = self._vframeno
|
||||||
@ -118,6 +120,7 @@ class Recording:
|
|||||||
self._vd[vframeno, :, :] = frame
|
self._vd[vframeno, :, :] = frame
|
||||||
self._vframeno += 1
|
self._vframeno += 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
rec = Recording('test', 5)
|
rec = Recording('test', 5)
|
||||||
|
81
lasp/lasp_reverb.py
Normal file
81
lasp/lasp_reverb.py
Normal 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}
|
137
lasp/lasp_slm.py
137
lasp/lasp_slm.py
@ -4,38 +4,73 @@
|
|||||||
Sound level meter implementation
|
Sound level meter implementation
|
||||||
@author: J.A. de Jong - ASCEE
|
@author: J.A. de Jong - ASCEE
|
||||||
"""
|
"""
|
||||||
from .wrappers import FilterBank, SPLowpass
|
from .wrappers import SPLowpass
|
||||||
|
from .lasp_computewidget import ComputeWidget
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .lasp_config import zeros
|
from .lasp_config import zeros
|
||||||
from .lasp_common import TimeWeighting, P_REF
|
from .lasp_common import (FreqWeighting, sens, calfile,
|
||||||
__all__ = ['SLM']
|
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:
|
class Dummy:
|
||||||
|
"""
|
||||||
|
Emulate filtering, but does not filter anything at all.
|
||||||
|
"""
|
||||||
|
|
||||||
def filter_(self, data):
|
def filter_(self, data):
|
||||||
return data
|
return data[:, np.newaxis]
|
||||||
|
|
||||||
|
|
||||||
class SLM:
|
class SLM:
|
||||||
"""
|
"""
|
||||||
Sound Level Meter, implements the single pole lowpass filter
|
Sound Level Meter, implements the single pole lowpass filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, fs, weighcal,
|
def __init__(self, fs, weighcal,
|
||||||
tw=TimeWeighting.default,
|
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
|
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)]
|
self._lps = [SPLowpass(fs, tw[0]) for i in range(nchannels)]
|
||||||
else:
|
else:
|
||||||
self._lpw = [Dummy() for i in range(nchannels)]
|
self._lps = [Dummy() for i in range(nchannels)]
|
||||||
self._Lmax = zeros(nchannels)
|
self._Lmax = zeros(nchannels)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Lmax(self):
|
def Lmax(self):
|
||||||
|
"""
|
||||||
|
Returns the currently maximum recorded level
|
||||||
|
"""
|
||||||
return self._Lmax
|
return self._Lmax
|
||||||
|
|
||||||
def addData(self, data):
|
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)
|
data_weighted = self._weighcal.filter_(data)
|
||||||
|
|
||||||
# Squared
|
# Squared
|
||||||
@ -44,9 +79,11 @@ class SLM:
|
|||||||
return np.array([])
|
return np.array([])
|
||||||
|
|
||||||
tw = []
|
tw = []
|
||||||
|
|
||||||
# Time-weight the signal
|
# Time-weight the signal
|
||||||
for chan, lp in enumerate(self._lps):
|
for chan, lp in enumerate(self._lps):
|
||||||
tw.append(lp.filter_(sq[:, chan])[:, 0])
|
tw.append(lp.filter_(sq[:, chan])[:, 0])
|
||||||
|
|
||||||
tw = np.asarray(tw).transpose()
|
tw = np.asarray(tw).transpose()
|
||||||
|
|
||||||
Level = 10*np.log10(tw/P_REF**2)
|
Level = 10*np.log10(tw/P_REF**2)
|
||||||
@ -56,3 +93,85 @@ class SLM:
|
|||||||
self._Lmax = curmax
|
self._Lmax = curmax
|
||||||
|
|
||||||
return Level
|
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)
|
||||||
|
@ -63,8 +63,19 @@ class WeighCal:
|
|||||||
def filter_(self, data):
|
def filter_(self, data):
|
||||||
"""
|
"""
|
||||||
Filter data using the calibration and frequency weighting filter.
|
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
|
nchan = self.nchannels
|
||||||
|
if data.ndim == 1:
|
||||||
|
data = data[:, np.newaxis]
|
||||||
assert data.shape[1] == nchan
|
assert data.shape[1] == nchan
|
||||||
assert data.shape[0] > 0
|
assert data.shape[0] > 0
|
||||||
|
|
||||||
@ -76,7 +87,6 @@ class WeighCal:
|
|||||||
filtered = filtered[:, np.newaxis]
|
filtered = filtered[:, np.newaxis]
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def frpCalObj(self, freq_design):
|
def frpCalObj(self, freq_design):
|
||||||
"""
|
"""
|
||||||
Computes the objective frequency response of the calibration filter
|
Computes the objective frequency response of the calibration filter
|
||||||
@ -93,7 +103,8 @@ class WeighCal:
|
|||||||
filter_calfac = empty((freq_design.shape[0], self.nchannels))
|
filter_calfac = empty((freq_design.shape[0], self.nchannels))
|
||||||
|
|
||||||
for chan in range(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:
|
else:
|
||||||
filter_calfac = ones((freq_design.shape[0], self.nchannels,))
|
filter_calfac = ones((freq_design.shape[0], self.nchannels,))
|
||||||
@ -133,4 +144,5 @@ class WeighCal:
|
|||||||
"""
|
"""
|
||||||
if freq is None:
|
if freq is None:
|
||||||
freq = np.logspace(1, np.log10(self.fs/2), 500)
|
freq = np.logspace(1, np.log10(self.fs/2), 500)
|
||||||
return freq,frp(self.fs,freq,self._firs[chan]),self.frpObj(freq)[:,chan]
|
return (freq, frp(self.fs, freq, self._firs[chan]),
|
||||||
|
self.frpObj(freq)[:, chan])
|
||||||
|
Loading…
Reference in New Issue
Block a user