diff --git a/lasp/CMakeLists.txt b/lasp/CMakeLists.txt index be47c4d..8ce373e 100644 --- a/lasp/CMakeLists.txt +++ b/lasp/CMakeLists.txt @@ -20,7 +20,8 @@ include_directories( # 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}) add_custom_command( OUTPUT "${fn}.py" diff --git a/lasp/device/lasp_daqconfig.py b/lasp/device/lasp_daqconfig.py index 7faff98..b8efb4e 100644 --- a/lasp/device/lasp_daqconfig.py +++ b/lasp/device/lasp_daqconfig.py @@ -8,76 +8,53 @@ Description: 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: - def __init__(self, name, - cardname, - cardlongnamematch, - device_name, - en_format, - en_input_rate, - en_input_channels, + """ + Initialize a device descriptor - input_sensitivity, - input_gain_settings, - en_input_gain_setting, - - en_output_rate, - en_output_channels): - """ - Initialize a device descriptor - - Args: - name: Name of the device to appear to the user - cardname: ALSA name identifier - cardlongnamematch: Long name according to ALSA - device_name: ASCII name with which to open the device when connected - en_format: index in the list of sample formats - en_input_rate: index of enabled input sampling frequency [Hz] - in the list of frequencies. - en_input_channels: list of channel indices which are used to - acquire data from. - input_sensitivity: List of sensitivity values, in units of [Pa^-1] - input_gain_setting: If a DAQ supports it, list of indices which - corresponds to a position in the possible input - gains for each channel. Should only be not equal - to None when the hardware supports changing the - input gain. - en_output_rate: index in the list of possible output sampling - frequencies. - en_output_channels: list of enabled output channels + Args: + name: Name of the device to appear to the user + cardname: ALSA name identifier + cardlongnamematch: Long name according to ALSA + device_name: ASCII name with which to open the device when connected + en_format: index in the list of sample formats + en_input_rate: index of enabled input sampling frequency [Hz] + in the list of frequencies. + en_input_channels: list of channel indices which are used to + acquire data from. + input_sensitivity: List of sensitivity values, in units of [Pa^-1] + input_gain_settings: If a DAQ supports it, list of indices which + corresponds to a position in the possible input + gains for each channel. Should only be not equal + to None when the hardware supports changing the + input gain. + en_output_rate: index in the list of possible output sampling + frequencies. + en_output_channels: list of enabled output channels - """ - 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): """ @@ -117,10 +94,23 @@ roga_plugndaq = DAQConfiguration(name='Roga-instruments Plug.n.DAQ USB', en_input_channels=[0], input_sensitivity=[46.92e-3, 46.92e-3], input_gain_settings=[-20, 0, 20], - en_input_gain_setting=[1, 1], + en_input_gain_settings=[1, 1], en_output_rate=1, 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", cardname=None, @@ -131,8 +121,23 @@ default_soundcard = DAQConfiguration(name="Default device", en_input_channels=[0], input_sensitivity=[1.0, 1.0], input_gain_settings=[0], - en_input_gain_setting=[0, 0], + en_input_gain_settings=[0, 0], en_output_rate=1, en_output_channels=[] ) 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 diff --git a/lasp/device/lasp_daqdevice.pyx b/lasp/device/lasp_daqdevice.pyx index 59c6ed6..8cf6c29 100644 --- a/lasp/device/lasp_daqdevice.pyx +++ b/lasp/device/lasp_daqdevice.pyx @@ -129,8 +129,13 @@ class DeviceInfo: Will later be replaced by a dataclass. Storage container for a lot of device parameters. """ + def __repr__(self): 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 diff --git a/lasp/lasp_avstream.py b/lasp/lasp_avstream.py index 6959ea0..d813afd 100644 --- a/lasp/lasp_avstream.py +++ b/lasp/lasp_avstream.py @@ -148,7 +148,7 @@ class AvStream: self._vframectr <<= 0 self._video_started <<= False - def isStarted(self): + def isRunning(self): return self._running() def hasVideo(self): diff --git a/lasp/lasp_common.py b/lasp/lasp_common.py index 5ecdff6..7988ca7 100644 --- a/lasp/lasp_common.py +++ b/lasp/lasp_common.py @@ -7,11 +7,11 @@ Common definitions used throughout the code. """ __all__ = ['P_REF', 'FreqWeighting', 'TimeWeighting', 'getTime', 'calfile', - ] + 'W_REF'] # Reference sound pressure level P_REF = 2e-5 - +W_REF = 1e-12 # 1 picoWatt # Todo: fix This # calfile = '/home/anne/wip/UMIK-1/cal/7027430_90deg.txt' calfile = None diff --git a/lasp/lasp_record.py b/lasp/lasp_record.py index 588560b..0e7c55b 100644 --- a/lasp/lasp_record.py +++ b/lasp/lasp_record.py @@ -67,7 +67,7 @@ class Recording: self._running <<= True # Videothread is going to start - if not stream.isStarted(): + if not stream.isRunning(): stream.start() stream.addCallback(self._callback) @@ -93,7 +93,7 @@ class Recording: self._running_cond.notify() def _callback(self, _type, data, aframe, vframe): - if not self._stream.isStarted(): + if not self._stream.isRunning(): self._running <<= False with self._running_cond: self._running_cond.notify() diff --git a/lasp/lasp_slm.py b/lasp/lasp_slm.py index f6f51bf..ce2e02e 100644 --- a/lasp/lasp_slm.py +++ b/lasp/lasp_slm.py @@ -5,18 +5,10 @@ Sound level meter implementation @author: J.A. de Jong - ASCEE """ from .wrappers import SPLowpass -from .lasp_computewidget import ComputeWidget import numpy as np -from .lasp_config import zeros -from .lasp_common import (FreqWeighting, calfile, - TimeWeighting, getTime, P_REF) -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'] +from .lasp_common import (TimeWeighting, P_REF) + +__all__ = ['SLM', 'Dummy'] class Dummy: @@ -103,289 +95,3 @@ class SLM: self._Lmax = curmax 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) diff --git a/lasp/plot/bar.py b/lasp/plot/bar.py index b8b09ad..cdc5c19 100644 --- a/lasp/plot/bar.py +++ b/lasp/plot/bar.py @@ -47,7 +47,8 @@ class BarScene(QGraphicsScene): ylabel=None, title=None, colors=DEFAULT_COLORS, size=(1200, 600), - legend=None): + legend=None, + legendpos=None): """ Initialize a bar scene @@ -61,6 +62,7 @@ class BarScene(QGraphicsScene): colors: color cycler size: size of the plot in pixels legend: list of legend strings to show. + legendpos: position of legend w.r.t. default position, in pixels """ super().__init__(parent=parent) self.setSceneRect(QRect(0,0,*size)) @@ -171,8 +173,11 @@ class BarScene(QGraphicsScene): if legend is not None: maxlegtxtwidth = 0 - legpos = (xsize-rightoffset-300, - ysize-topoffset-30) + legposx = 0 if legendpos is None else legendpos[0] + legposy = 0 if legendpos is None else legendpos[1] + + legpos = (xsize-rightoffset-300+legposx, + ysize-topoffset-30+legposy) dyleg = 15 dylegtxt = dyleg diff --git a/scripts/lasp_apsrt b/scripts/lasp_apsrt index 2d5a3e0..eca2401 100755 --- a/scripts/lasp_apsrt +++ b/scripts/lasp_apsrt @@ -1,22 +1,37 @@ #!/usr/bin/env python import sys +import argparse from lasp.lasp_rtapsdialog import RealTimeAPSDialog from lasp.lasp_avstream import AvStream -from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq -from lasp.lasp_gui_tools import Branding, ASCEEColors, warningdialog +from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq, umik +from lasp.lasp_gui_tools import Branding, warningdialog from PySide import QtGui 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.setFont(Branding.font()) - stream = AvStream(default_soundcard) - # stream = AvStream(roga_plugndaq) + # stream = AvStream(default_soundcard) + stream = AvStream(device) mw = RealTimeAPSDialog(None, stream) - mw.show() # Show the form - # Install exception hook to catch exceptions def excepthook(cls, exception, traceback): """ @@ -33,7 +48,8 @@ def main(): # Set custom exception hook that catches all exceptions sys.excepthook = excepthook stream.start() - app.exec_() # and execute the app + mw.show() # Show the window + app.exec_() # and start the event loop stream.stop() diff --git a/scripts/lasp_calibrate.py b/scripts/lasp_calibrate.py new file mode 100755 index 0000000..a2e7486 --- /dev/null +++ b/scripts/lasp_calibrate.py @@ -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 \ No newline at end of file diff --git a/scripts/lasp_record b/scripts/lasp_record index fe287bf..1a8d454 100755 --- a/scripts/lasp_record +++ b/scripts/lasp_record @@ -2,7 +2,7 @@ import argparse from lasp.lasp_record import Recording from lasp.lasp_avstream import AvStream - +from lasp.device.lasp_daqconfig import default_soundcard, roga_plugndaq, umik parser = argparse.ArgumentParser( 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]') parser.add_argument('--comment', '-c', type=str, 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() -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.start() stream.stop()