Converted daqconfig to Python3.7 dataclass. Changed revtime name to revtimewidget. Added real time APS dialog. But contains bug on close! Split up SLMWidget and SLM into different files for accessing the SLM class from scripts. Changed lower y-limits of figure plot in GUI to something more sensible. Added umik to list of devices, but does not work yet due to 24-bits audio bug. First steps to change legend position in bar plot.

This commit is contained in:
Anne de Jong 2018-09-13 13:56:05 +02:00 committed by J.A. de Jong - ASCEE
parent 889a1898f5
commit a10326d617
11 changed files with 189 additions and 380 deletions

View File

@ -20,7 +20,8 @@ include_directories(
# DEPENDS MakeTable # DEPENDS MakeTable
# ) # )
set(ui_files ui_apsrtsettings ui_mainwindow ui_figure ui_about ui_apswidget ui_revtime ui_slmwidget ui_daq ui_apssettings) set(ui_files ui_apsrtsettings ui_mainwindow ui_figure ui_about
ui_apswidget ui_revtimewidget ui_slmwidget ui_daqwidget ui_apssettings)
foreach(fn ${ui_files}) foreach(fn ${ui_files})
add_custom_command( add_custom_command(
OUTPUT "${fn}.py" OUTPUT "${fn}.py"

View File

@ -8,24 +8,15 @@ Description:
Data Acquistiion (DAQ) device descriptors, and the DAQ devices themselves Data Acquistiion (DAQ) device descriptors, and the DAQ devices themselves
""" """
__all__ = ['DAQConfiguration', 'roga_plugndaq', 'default_soundcard'] from dataclasses import dataclass, field
from .lasp_daqdevice import query_devices, DeviceInfo
__all__ = ['DAQConfiguration', 'roga_plugndaq', 'umik',
'default_soundcard', 'configs',
'findDAQDevice']
@dataclass
class DAQConfiguration: class DAQConfiguration:
def __init__(self, name,
cardname,
cardlongnamematch,
device_name,
en_format,
en_input_rate,
en_input_channels,
input_sensitivity,
input_gain_settings,
en_input_gain_setting,
en_output_rate,
en_output_channels):
""" """
Initialize a device descriptor Initialize a device descriptor
@ -40,7 +31,7 @@ class DAQConfiguration:
en_input_channels: list of channel indices which are used to en_input_channels: list of channel indices which are used to
acquire data from. acquire data from.
input_sensitivity: List of sensitivity values, in units of [Pa^-1] input_sensitivity: List of sensitivity values, in units of [Pa^-1]
input_gain_setting: If a DAQ supports it, list of indices which input_gain_settings: If a DAQ supports it, list of indices which
corresponds to a position in the possible input corresponds to a position in the possible input
gains for each channel. Should only be not equal gains for each channel. Should only be not equal
to None when the hardware supports changing the to None when the hardware supports changing the
@ -51,33 +42,19 @@ class DAQConfiguration:
""" """
self.name = name
self.cardlongnamematch = cardlongnamematch
self.cardname = cardname
self.device_name = device_name
self.en_format = en_format
self.en_input_rate = en_input_rate
self.en_input_channels = en_input_channels
self.input_sensitivity = input_sensitivity
self.input_gain_settings = input_gain_settings
self.en_output_rate = en_output_rate
self.en_output_channels = en_output_channels
def __repr__(self):
"""
String representation of configuration
"""
rep = f"""Name: {self.name}
Enabled input channels: {self.en_input_channels}
Enabled input sampling frequency: {self.en_input_rate}
Input gain settings: {self.input_gain_settings}
Sensitivity: {self.input_sensitivity}
"""
return rep
name: str
cardname: str
cardlongnamematch: str
device_name: str
en_format: int
en_input_rate: int
en_input_channels: list
input_sensitivity: list
input_gain_settings: list
en_input_gain_settings: list = field(default_factory=list)
en_output_rate: int = -1
en_output_channels: list = field(default_factory=list)
def match(self, device): def match(self, device):
""" """
@ -117,10 +94,23 @@ roga_plugndaq = DAQConfiguration(name='Roga-instruments Plug.n.DAQ USB',
en_input_channels=[0], en_input_channels=[0],
input_sensitivity=[46.92e-3, 46.92e-3], input_sensitivity=[46.92e-3, 46.92e-3],
input_gain_settings=[-20, 0, 20], input_gain_settings=[-20, 0, 20],
en_input_gain_setting=[1, 1], en_input_gain_settings=[1, 1],
en_output_rate=1, en_output_rate=1,
en_output_channels=[False, False] en_output_channels=[False, False]
) )
umik = DAQConfiguration(name='UMIK-1',
cardname='Umik-1 Gain: 18dB',
cardlongnamematch='miniDSP Umik-1 Gain: 18dB',
device_name='iec958:CARD=U18dB,DEV=0',
en_format=0,
en_input_rate=0,
en_input_channels=[0],
input_sensitivity=[1., 1.],
input_gain_settings=[0., 0.],
en_input_gain_settings=[0, 0],
en_output_rate=0,
en_output_channels=[True, True]
)
default_soundcard = DAQConfiguration(name="Default device", default_soundcard = DAQConfiguration(name="Default device",
cardname=None, cardname=None,
@ -131,8 +121,23 @@ default_soundcard = DAQConfiguration(name="Default device",
en_input_channels=[0], en_input_channels=[0],
input_sensitivity=[1.0, 1.0], input_sensitivity=[1.0, 1.0],
input_gain_settings=[0], input_gain_settings=[0],
en_input_gain_setting=[0, 0], en_input_gain_settings=[0, 0],
en_output_rate=1, en_output_rate=1,
en_output_channels=[] en_output_channels=[]
) )
configs = (roga_plugndaq, default_soundcard) configs = (roga_plugndaq, default_soundcard)
def findDAQDevice(config: DAQConfiguration) -> DeviceInfo:
"""
Search for a DaQ device for the given configuration.
Args:
config: configuration to search a device for
"""
devices = query_devices()
for device in devices:
if config.match(device):
return device
return None

View File

@ -129,8 +129,13 @@ class DeviceInfo:
Will later be replaced by a dataclass. Storage container for a lot of Will later be replaced by a dataclass. Storage container for a lot of
device parameters. device parameters.
""" """
def __repr__(self): def __repr__(self):
rep = f"""Device name: {self.device_name} rep = f"""Device name: {self.device_name}
Card name: {self.cardname}
Available sample formats: {self.available_formats}
Max input channels: {self.max_input_channels}
""" """
return rep return rep

View File

@ -148,7 +148,7 @@ class AvStream:
self._vframectr <<= 0 self._vframectr <<= 0
self._video_started <<= False self._video_started <<= False
def isStarted(self): def isRunning(self):
return self._running() return self._running()
def hasVideo(self): def hasVideo(self):

View File

@ -7,11 +7,11 @@ Common definitions used throughout the code.
""" """
__all__ = ['P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'calfile', __all__ = ['P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'calfile',
] 'W_REF']
# Reference sound pressure level # Reference sound pressure level
P_REF = 2e-5 P_REF = 2e-5
W_REF = 1e-12 # 1 picoWatt
# Todo: fix This # Todo: fix This
# calfile = '/home/anne/wip/UMIK-1/cal/7027430_90deg.txt' # calfile = '/home/anne/wip/UMIK-1/cal/7027430_90deg.txt'
calfile = None calfile = None

View File

@ -67,7 +67,7 @@ class Recording:
self._running <<= True self._running <<= True
# Videothread is going to start # Videothread is going to start
if not stream.isStarted(): if not stream.isRunning():
stream.start() stream.start()
stream.addCallback(self._callback) stream.addCallback(self._callback)
@ -93,7 +93,7 @@ class Recording:
self._running_cond.notify() self._running_cond.notify()
def _callback(self, _type, data, aframe, vframe): def _callback(self, _type, data, aframe, vframe):
if not self._stream.isStarted(): if not self._stream.isRunning():
self._running <<= False self._running <<= False
with self._running_cond: with self._running_cond:
self._running_cond.notify() self._running_cond.notify()

View File

@ -5,18 +5,10 @@ Sound level meter implementation
@author: J.A. de Jong - ASCEE @author: J.A. de Jong - ASCEE
""" """
from .wrappers import 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_common import (TimeWeighting, P_REF)
from .lasp_common import (FreqWeighting, calfile,
TimeWeighting, getTime, P_REF) __all__ = ['SLM', 'Dummy']
from .lasp_weighcal import WeighCal
from .lasp_gui_tools import wait_cursor
from .lasp_figure import PlotOptions, Plotable
from .ui_slmwidget import Ui_SlmWidget
from .filter.bandpass_fir import OctaveBankDesigner, ThirdOctaveBankDesigner
from .lasp_octavefilter import OctaveFilterBank, ThirdOctaveFilterBank
__all__ = ['SLM', 'SlmWidget']
class Dummy: class Dummy:
@ -103,289 +95,3 @@ class SLM:
self._Lmax = curmax self._Lmax = curmax
return Level return Level
class SlmWidget(ComputeWidget, Ui_SlmWidget):
def __init__(self, parent=None):
"""
Initialize the SlmWidget.
"""
super().__init__(parent)
self.setupUi(self)
self.eqFreqBandChanged(0)
self.tFreqBandChanged(0)
self.setMeas(None)
def init(self, fm):
"""
Register combobox of the figure dialog to plot to in the FigureManager
"""
super().init(fm)
fm.registerCombo(self.tfigure)
fm.registerCombo(self.eqfigure)
self.tbandstart.setEnabled(False)
self.tbandstop.setEnabled(False)
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.tstarttime.setRange(0, rt, 0)
self.tstoptime.setRange(0, rt, rt)
self.eqstarttime.setRange(0, rt, 0)
self.eqstoptime.setRange(0, rt, rt)
self.tchannel.clear()
self.eqchannel.clear()
for i in range(meas.nchannels):
self.tchannel.addItem(str(i))
self.eqchannel.addItem(str(i))
self.tchannel.setCurrentIndex(0)
self.eqchannel.setCurrentIndex(0)
def computeEq(self):
"""
Compute equivalent levels for a piece of time
"""
meas = self.meas
fs = meas.samplerate
channel = self.eqchannel.currentIndex()
fw = FreqWeighting.getCurrent(self.eqfreqweighting)
istart, istop = self.getStartStopIndices(meas, self.eqstarttime,
self.eqstoptime)
bands = self.eqfreqband.currentIndex()
if bands == 0:
# 1/3 Octave bands
filt = ThirdOctaveFilterBank(fs)
xs = filt.xs
xmin = xs[0] + self.eqbandstart.currentIndex()
xmax = xs[0] + self.eqbandstop.currentIndex()
if bands == 1:
# Octave bands
filt = OctaveFilterBank(fs)
xs = filt.xs
xmin = xs[0] + self.eqbandstart.currentIndex()
xmax = xs[0] + self.eqbandstop.currentIndex()
leveltype = self.eqleveltype.currentIndex()
if leveltype == 0:
# equivalent levels
tw = TimeWeighting.fast
elif leveltype == 1:
# fast time weighting
tw = TimeWeighting.fast
elif leveltype == 2:
# slow time weighting
tw = TimeWeighting.slow
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)
praw = meas.praw()[istart:istop, [channel]]
weighted = weighcal.filter_(praw)
filtered_out = filt.filter_(weighted)
levels = np.empty((xmax - xmin + 1))
xlabels = []
for i, x in enumerate(range(xmin, xmax+1)):
nom = filt.nominal(x)
xlabels.append(nom)
filt_x = filtered_out[nom]['data']
slm = SLM(filt.fs, tw)
slm.addData(filt_x)
if leveltype > 0:
level = slm.Lmax
else:
level = slm.Leq
levels[i] = level
pto = PlotOptions.forLevelBars()
pta = Plotable(xlabels, levels)
fig, new = self.getFigure(self.eqfigure, pto, 'bar')
fig.fig.add(pta)
fig.show()
def computeT(self):
"""
Compute sound levels as a function of time.
"""
meas = self.meas
fs = meas.samplerate
channel = self.tchannel.currentIndex()
tw = TimeWeighting.getCurrent(self.ttimeweighting)
fw = FreqWeighting.getCurrent(self.tfreqweighting)
istart, istop = self.getStartStopIndices(meas, self.tstarttime,
self.tstoptime)
bands = self.tfreqband.currentIndex()
if bands == 0:
# Overall
filt = Dummy()
else:
# Octave bands
filt = OctaveFilterBank(
fs) if bands == 1 else ThirdOctaveFilterBank(fs)
xs = filt.xs
xmin = xs[0] + self.tbandstart.currentIndex()
xmax = xs[0] + self.tbandstop.currentIndex()
# Downsampling factor of result
dsf = self.tdownsampling.value()
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.
praw = meas.praw()[istart:istop, [channel]]
weighcal = WeighCal(fw, nchannels=1,
fs=fs, calfile=calfile)
weighted = weighcal.filter_(praw)
if bands == 0:
slm = SLM(fs, tw)
level = slm.addData(weighted)[::dsf]
# Filter, downsample data
N = level.shape[0]
time = getTime(float(fs)/dsf, N)
Lmax = slm.Lmax
pta = Plotable(time, level,
name=f'Overall level [dB([fw[0]])]')
pto = PlotOptions()
pto.ylabel = f'L{fw[0]} [dB({fw[0]})]'
pto.xlim = (time[0], time[-1])
fig, new = self.getFigure(self.tfigure, pto, 'line')
fig.fig.add(pta)
else:
pto = PlotOptions()
fig, new = self.getFigure(self.tfigure, pto, 'line')
pto.ylabel = f'L{fw[0]} [dB({fw[0]})]'
out = filt.filter_(weighted)
tmin = 0
tmax = 0
for x in range(xmin, xmax+1):
dec = np.prod(filt.decimation(x))
fd = filt.fs/dec
# Nominal frequency text
nom = filt.nominal(x)
leg = f'{nom} Hz - [dB({fw[0]})]'
# Find global tmin and tmax, used for xlim
time = out[nom]['t']
tmin = min(tmin, time[0])
tmax = max(tmax, time[-1])
slm = SLM(fd, tw)
level = slm.addData(out[nom]['data'])
plotable = Plotable(time[::dsf//dec],
level[::dsf//dec],
name=leg)
fig.fig.add(plotable)
pto.xlim = (tmin, tmax)
fig.fig.setPlotOptions(pto)
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)
def compute(self):
"""
Compute Sound Level using settings. This method is
called whenever the Compute button is pushed in the SLM tab
"""
if self.ttab.isVisible():
self.computeT()
elif self.eqtab.isVisible():
self.computeEq()
def eqFreqBandChanged(self, idx):
"""
User changes frequency bands to plot time-dependent values for
"""
self.eqbandstart.clear()
self.eqbandstop.clear()
if idx == 1:
# 1/3 Octave bands
o = OctaveBankDesigner()
for x in o.xs:
nom = o.nominal(x)
self.eqbandstart.addItem(nom)
self.eqbandstop.addItem(nom)
self.eqbandstart.setCurrentIndex(0)
self.eqbandstop.setCurrentIndex(len(o.xs)-1)
elif idx == 0:
# Octave bands
o = ThirdOctaveBankDesigner()
for x in o.xs:
nom = o.nominal(x)
self.eqbandstart.addItem(nom)
self.eqbandstop.addItem(nom)
self.eqbandstart.setCurrentIndex(2)
self.eqbandstop.setCurrentIndex(len(o.xs) - 3)
def tFreqBandChanged(self, idx):
"""
User changes frequency bands to plot time-dependent values for
"""
self.tbandstart.clear()
self.tbandstop.clear()
enabled = False
if idx == 1:
# Octave bands
enabled = True
o = OctaveBankDesigner()
for x in o.xs:
nom = o.nominal(x)
self.tbandstart.addItem(nom)
self.tbandstop.addItem(nom)
self.tbandstart.setCurrentIndex(2)
self.tbandstop.setCurrentIndex(len(o.xs)-1)
elif idx == 2:
# Octave bands
enabled = True
o = ThirdOctaveBankDesigner()
for x in o.xs:
nom = o.nominal(x)
self.tbandstart.addItem(nom)
self.tbandstop.addItem(nom)
self.tbandstart.setCurrentIndex(2)
self.tbandstop.setCurrentIndex(len(o.xs) - 3)
self.tbandstart.setEnabled(enabled)
self.tbandstop.setEnabled(enabled)

View File

@ -47,7 +47,8 @@ class BarScene(QGraphicsScene):
ylabel=None, ylabel=None,
title=None, title=None,
colors=DEFAULT_COLORS, size=(1200, 600), colors=DEFAULT_COLORS, size=(1200, 600),
legend=None): legend=None,
legendpos=None):
""" """
Initialize a bar scene Initialize a bar scene
@ -61,6 +62,7 @@ class BarScene(QGraphicsScene):
colors: color cycler colors: color cycler
size: size of the plot in pixels size: size of the plot in pixels
legend: list of legend strings to show. legend: list of legend strings to show.
legendpos: position of legend w.r.t. default position, in pixels
""" """
super().__init__(parent=parent) super().__init__(parent=parent)
self.setSceneRect(QRect(0,0,*size)) self.setSceneRect(QRect(0,0,*size))
@ -171,8 +173,11 @@ class BarScene(QGraphicsScene):
if legend is not None: if legend is not None:
maxlegtxtwidth = 0 maxlegtxtwidth = 0
legpos = (xsize-rightoffset-300, legposx = 0 if legendpos is None else legendpos[0]
ysize-topoffset-30) legposy = 0 if legendpos is None else legendpos[1]
legpos = (xsize-rightoffset-300+legposx,
ysize-topoffset-30+legposy)
dyleg = 15 dyleg = 15
dylegtxt = dyleg dylegtxt = dyleg

View File

@ -1,22 +1,37 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import argparse
from lasp.lasp_rtapsdialog import RealTimeAPSDialog from lasp.lasp_rtapsdialog import RealTimeAPSDialog
from lasp.lasp_avstream import AvStream from lasp.lasp_avstream import AvStream
from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq, umik
from lasp.lasp_gui_tools import Branding, ASCEEColors, warningdialog from lasp.lasp_gui_tools import Branding, warningdialog
from PySide import QtGui from PySide import QtGui
def main(): def main():
parser = argparse.ArgumentParser(
description='Run real time power spectra monitor')
device_help = 'Device to record from'
parser.add_argument('-d', '--device', help=device_help, type=str,
choices=['roga', 'umik', 'default'], default='roga')
args = parser.parse_args()
device_str = args.device
if 'roga' == device_str:
device = roga_plugndaq
elif 'default' == device_str:
device = default_soundcard
elif 'umik' == device_str:
device = umik
app = QtGui.QApplication(sys.argv) # A new instance of QApplication app = QtGui.QApplication(sys.argv) # A new instance of QApplication
app.setFont(Branding.font()) app.setFont(Branding.font())
stream = AvStream(default_soundcard) # stream = AvStream(default_soundcard)
# stream = AvStream(roga_plugndaq) stream = AvStream(device)
mw = RealTimeAPSDialog(None, stream) mw = RealTimeAPSDialog(None, stream)
mw.show() # Show the form
# Install exception hook to catch exceptions # Install exception hook to catch exceptions
def excepthook(cls, exception, traceback): def excepthook(cls, exception, traceback):
""" """
@ -33,7 +48,8 @@ def main():
# Set custom exception hook that catches all exceptions # Set custom exception hook that catches all exceptions
sys.excepthook = excepthook sys.excepthook = excepthook
stream.start() stream.start()
app.exec_() # and execute the app mw.show() # Show the window
app.exec_() # and start the event loop
stream.stop() stream.stop()

58
scripts/lasp_calibrate.py Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Aug 14 12:49:27 2018
@author: J.A. de Jong - ASCEE
Description: calibrate device using measurement
"""
import numpy as np
import argparse
from lasp.lasp_measurement import Measurement
from lasp.lasp_common import P_REF
import os
spl_default = 94.
gain_default = 0.
parser = argparse.ArgumentParser('Calibrate device using'
' calibration measurement')
parser.add_argument('--gain-setting','-g',
help='DAQ Input gain setting during calibration in [dB]' +
f' default = {gain_default} dB.',
type=float, default=gain_default)
parser.add_argument('fn',help='File name of calibration measurement', type=str)
parser.add_argument('--channel',help='Channel of the device to calibrate, default = 0',
type=int, default=0)
parser.add_argument('--spl','-s',help='Applied sound pressure level to the'
f' microphone in dB, default = {spl_default}',
default=spl_default)
args = parser.parse_args()
m = Measurement(args.fn)
nchannels = m.nchannels
# Reset measurement sensitivity, in case it was set wrongly
m.sensitivity = np.ones(nchannels)
# Compute Vrms
Vrms = m.prms * 10**(args.gain_setting/20.)
prms = P_REF*10**(args.spl/20)
sens = Vrms / prms
print(f'Computed sensitivity: {sens[args.channel]:.5} V/Pa')
print('Searching for files in directory to apply sensitivity value to...')
dir_ = os.path.dirname(args.fn)
for f in os.listdir(dir_):
yn = input(f'Apply sensitivity to {f}? [Y/n]')
if yn in ['','Y','y']:
meas = Measurement(os.path.join(dir_,f))
meas.sensitivity = sens

View File

@ -2,7 +2,7 @@
import argparse import argparse
from lasp.lasp_record import Recording from lasp.lasp_record import Recording
from lasp.lasp_avstream import AvStream from lasp.lasp_avstream import AvStream
from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq, umik
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Acquire data and store a measurement file' description='Acquire data and store a measurement file'
) )
@ -13,9 +13,22 @@ parser.add_argument('--duration', '-d', type=float,
help='The recording duration in [s]') help='The recording duration in [s]')
parser.add_argument('--comment', '-c', type=str, parser.add_argument('--comment', '-c', type=str,
help='Add a measurement comment, optionally') help='Add a measurement comment, optionally')
device_help = 'DAQ Device to record from'
parser.add_argument('--input-daq','-i', help=device_help, type=str,
choices=['roga', 'umik', 'default'], default='roga')
args = parser.parse_args() args = parser.parse_args()
stream = AvStream() device_str = args.input_daq
if 'roga' == device_str:
device = roga_plugndaq
elif 'default' == device_str:
device = default_soundcard
elif 'umik' == device_str:
device = umik
stream = AvStream(device)
rec = Recording(args.filename, stream, args.duration) rec = Recording(args.filename, stream, args.duration)
rec.start() rec.start()
stream.stop() stream.stop()