Add support for multiple measurements

pull/14/head
Ewald de Wit 2022-09-20 18:09:28 +02:00
rodzic 700e1ba9d0
commit 492f1ea476
3 zmienionych plików z 103 dodań i 43 usunięć

Wyświetl plik

@ -95,8 +95,12 @@ and then "Load"):
.. image:: images/Convolver.png .. image:: images/Convolver.png
We go back to the spectrum measurement and set the uncorrected We go back to the spectrum measurement and store the uncorrected
spectrum as reference (to compare with later measurements). spectrum with the "Store" button (to compare with later measurements).
More measurements can be stored as well, for example where the microphone
is placed in different locatations, The total average of the stored
measurements is shown in orange
Measuring the equalized system gives this: Measuring the equalized system gives this:
.. image:: images/laptop-flattened-spectrum.png .. image:: images/laptop-flattened-spectrum.png

Wyświetl plik

@ -1,4 +1,5 @@
import array import array
import types
from functools import lru_cache from functools import lru_cache
from typing import List, NamedTuple, Optional, Tuple from typing import List, NamedTuple, Optional, Tuple
@ -44,10 +45,10 @@ class Analyzer:
x: np.ndarray x: np.ndarray
y: np.ndarray y: np.ndarray
rate: int rate: int
secs: float
fmin: float fmin: float
fmax: float fmax: float
time: float time: float
numMeasurements: int
def __init__( def __init__(
self, f0: int, f1: int, secs: float, rate: int, ampl: float, self, f0: int, f1: int, secs: float, rate: int, ampl: float,
@ -58,18 +59,41 @@ class Analyzer:
self.chirp, self.chirp,
np.zeros(int(self.MAX_DELAY_SECS * rate)) np.zeros(int(self.MAX_DELAY_SECS * rate))
]) ])
self.secs = self.x.size / rate self.y = np.zeros(self.x.size)
self.rate = rate self.rate = rate
self.fmin = min(f0, f1) self.fmin = min(f0, f1)
self.fmax = max(f0, f1) self.fmax = max(f0, f1)
self.time = 0 self.time = 0
self.numMeasurements = 0
self._calibration = calibration self._calibration = calibration
self._target = target self._target = target
self._sumH = np.zeros(self.X().size)
# Cache the methods in a way that allows garbage collection of self. def setCaching(self):
for meth in ['X', 'Y', 'H', 'H2', 'h', 'h_inv', 'spectrum', """
Cache the main methods in a way that allows garbage collection of self.
Calling this method again will in effect clear the previous caching.
"""
for name in ['X', 'Y', 'calcH', 'H', 'H2', 'h', 'h_inv', 'spectrum',
'frequency', 'calibration', 'target']: 'frequency', 'calibration', 'target']:
setattr(self, meth, lru_cache(getattr(self, meth))) unbound = getattr(Analyzer, name)
bound = types.MethodType(unbound, self)
setattr(self, name, lru_cache(bound))
def addMeasurements(self, analyzer):
"""Add measurements from other analyzer to this one."""
if not self.isCompatible(analyzer):
raise ValueError('Incompatible analyzers')
self._sumH = self._sumH + analyzer._sumH
self.numMeasurements += analyzer.numMeasurements
self.setCaching()
def isCompatible(self, analyzer):
"""
See if other analyzer is compatible for adding measurement to this one.
"""
return isinstance(analyzer, Analyzer) and np.array_equal(
analyzer.x, self.x)
def findMatch(self, recording: array.array) -> bool: def findMatch(self, recording: array.array) -> bool:
""" """
@ -84,15 +108,19 @@ class Analyzer:
corr = np.fft.ifft(X * Y).real corr = np.fft.ifft(X * Y).real
idx = int(corr.argmax()) - self.x.size + 1 idx = int(corr.argmax()) - self.x.size + 1
if idx >= 0: if idx >= 0:
self.y = np.array(recording[idx:idx + self.x.size], 'f') self.y = np.array(recording[idx:idx + self.x.size])
self.numMeasurements += 1
self._sumH += self.calcH()
self.setCaching()
return True return True
return False return False
def timedOut(self) -> bool: def timedOut(self) -> bool:
"""See if time to find a match has exceeded the timeout limit.""" """See if time to find a match has exceeded the timeout limit."""
return self.time > self.secs + self.TIMEOUT_SECS return self.time > self.x.size / self.rate + self.TIMEOUT_SECS
def frequency(self) -> np.ndarray: def frequency(self) -> np.ndarray:
"""Frequency array, from 0 to the Nyquist frequency."""
return np.linspace(0, self.rate // 2, self.X().size) return np.linspace(0, self.rate // 2, self.X().size)
def freqRange(self, size: int = 0) -> slice: def freqRange(self, size: int = 0) -> slice:
@ -107,9 +135,11 @@ class Analyzer:
return slice(i0, i1 + 1) return slice(i0, i1 + 1)
def calibration(self) -> Optional[np.ndarray]: def calibration(self) -> Optional[np.ndarray]:
"""Interpolated calibration curve."""
return self.interpolateCorrection(self._calibration) return self.interpolateCorrection(self._calibration)
def target(self) -> Optional[np.ndarray]: def target(self) -> Optional[np.ndarray]:
"""Interpolated target curve."""
return self.interpolateCorrection(self._target) return self.interpolateCorrection(self._target)
def interpolateCorrection(self, corr: Correction) -> Optional[np.ndarray]: def interpolateCorrection(self, corr: Correction) -> Optional[np.ndarray]:
@ -134,10 +164,9 @@ class Analyzer:
def Y(self) -> np.ndarray: def Y(self) -> np.ndarray:
return np.fft.rfft(self.y) return np.fft.rfft(self.y)
def H(self) -> XY: def calcH(self) -> np.ndarray:
""" """
Calculate complex-valued transfer function H in the Calculate transfer function H of the last measurement.
frequency domain.
""" """
X = self.X() X = self.X()
Y = self.Y() Y = self.Y()
@ -145,13 +174,20 @@ class Analyzer:
H = Y * np.conj(X) / (np.abs(X) ** 2 + 1e-3) H = Y * np.conj(X) / (np.abs(X) ** 2 + 1e-3)
if self._calibration: if self._calibration:
H *= 10 ** (-self.calibration() / 20) H *= 10 ** (-self.calibration() / 20)
H = np.abs(H)
return H
def H(self) -> XY:
"""
Transfer function H averaged over all measurements.
"""
freq = self.frequency() freq = self.frequency()
H = self._sumH / (self.numMeasurements or 1)
return XY(freq, H) return XY(freq, H)
def H2(self, smoothing: float): def H2(self, smoothing: float) -> XY:
"""Calculate smoothed squared transfer function |H|^2.""" """Calculate smoothed squared transfer function |H|^2."""
freq, H = self.H() freq, H = self.H()
H = np.abs(H)
r = self.freqRange() r = self.freqRange()
H2 = np.empty_like(H) H2 = np.empty_like(H)
# Perform smoothing on the squared amplitude. # Perform smoothing on the squared amplitude.

Wyświetl plik

@ -1,4 +1,5 @@
import asyncio import asyncio
import copy
import datetime as dt import datetime as dt
import logging import logging
import os import os
@ -68,9 +69,6 @@ class App(qt.QMainWindow):
if analyzer.timedOut(): if analyzer.timedOut():
break break
def setPaused(self):
self.paused = not self.paused
def plot(self, *_): def plot(self, *_):
if self.stack.currentIndex() == 0: if self.stack.currentIndex() == 0:
self.plotSpectrum() self.plotSpectrum()
@ -92,7 +90,7 @@ class App(qt.QMainWindow):
self.refSpectrumPlot.setData(*spectrum) self.refSpectrumPlot.setData(*spectrum)
def plotIR(self): def plotIR(self):
if self.refAnalyzer and self.useRefBox.isChecked(): if self.refAnalyzer and self.useBox.currentIndex() == 0:
analyzer = self.refAnalyzer analyzer = self.refAnalyzer
else: else:
analyzer = self.analyzer analyzer = self.analyzer
@ -131,7 +129,7 @@ class App(qt.QMainWindow):
self.saveDir = Path(filename).parent self.saveDir = Path(filename).parent
def saveIR(self): def saveIR(self):
if self.refAnalyzer and self.useRefBox.isChecked(): if self.refAnalyzer and self.useBox.currentIndex() == 0:
analyzer = self.refAnalyzer analyzer = self.refAnalyzer
else: else:
analyzer = self.analyzer analyzer = self.analyzer
@ -151,16 +149,6 @@ class App(qt.QMainWindow):
hifi.write_wav(filename, analyzer.rate, irInv) hifi.write_wav(filename, analyzer.rate, irInv)
self.saveDir = Path(filename).parent self.saveDir = Path(filename).parent
def setReference(self, withRef: bool):
if withRef:
if self.analyzer:
self.refAnalyzer = self.analyzer
self.plot()
else:
self.refAnalyzer = None
self.refSpectrumPlot.clear()
self.spectrumPlotWidget.repaint()
def run(self): def run(self):
"""Run both the Qt and asyncio event loops.""" """Run both the Qt and asyncio event loops."""
@ -210,8 +198,6 @@ class App(qt.QMainWindow):
self.spectrumSmoothing = pg.SpinBox( self.spectrumSmoothing = pg.SpinBox(
value=15, step=1, bounds=[0, 30]) value=15, step=1, bounds=[0, 30])
self.spectrumSmoothing.sigValueChanging.connect(self.plot) self.spectrumSmoothing.sigValueChanging.connect(self.plot)
refBox = qt.QCheckBox('Reference')
refBox.stateChanged.connect(self.setReference)
hbox = qt.QHBoxLayout() hbox = qt.QHBoxLayout()
hbox.addStretch(1) hbox.addStretch(1)
@ -229,8 +215,6 @@ class App(qt.QMainWindow):
hbox.addSpacing(32) hbox.addSpacing(32)
hbox.addWidget(qt.QLabel('Smoothing: ')) hbox.addWidget(qt.QLabel('Smoothing: '))
hbox.addWidget(self.spectrumSmoothing) hbox.addWidget(self.spectrumSmoothing)
hbox.addSpacing(32)
hbox.addWidget(refBox)
hbox.addStretch(1) hbox.addStretch(1)
vbox.addLayout(hbox) vbox.addLayout(hbox)
@ -287,8 +271,11 @@ class App(qt.QMainWindow):
value=15, step=1, bounds=[0, 30]) value=15, step=1, bounds=[0, 30])
self.irSmoothing.sigValueChanging.connect(self.plot) self.irSmoothing.sigValueChanging.connect(self.plot)
self.kaiserBeta.sigValueChanging.connect(self.plot) self.kaiserBeta.sigValueChanging.connect(self.plot)
self.useRefBox = qt.QCheckBox('Use reference')
self.useRefBox.stateChanged.connect(self.plot) self.useBox = qt.QComboBox()
self.useBox.addItems(['Stored measurements', 'Last measurement'])
self.useBox.currentIndexChanged.connect(self.plot)
exportButton = qt.QPushButton('Export as WAV') exportButton = qt.QPushButton('Export as WAV')
exportButton.setShortcut('E') exportButton.setShortcut('E')
exportButton.setToolTip('<Key E>') exportButton.setToolTip('<Key E>')
@ -308,10 +295,10 @@ class App(qt.QMainWindow):
hbox.addWidget(qt.QLabel('Smoothing: ')) hbox.addWidget(qt.QLabel('Smoothing: '))
hbox.addWidget(self.irSmoothing) hbox.addWidget(self.irSmoothing)
hbox.addSpacing(32) hbox.addSpacing(32)
hbox.addWidget(self.useRefBox) hbox.addWidget(qt.QLabel('Use: '))
hbox.addSpacing(32) hbox.addWidget(self.useBox)
hbox.addWidget(exportButton)
hbox.addStretch(1) hbox.addStretch(1)
hbox.addWidget(exportButton)
vbox.addLayout(hbox) vbox.addLayout(hbox)
return topWidget return topWidget
@ -379,20 +366,49 @@ class App(qt.QMainWindow):
correctionsButton = qt.QPushButton('Corrections...') correctionsButton = qt.QPushButton('Corrections...')
correctionsButton.pressed.connect(correctionsPressed) correctionsButton.pressed.connect(correctionsPressed)
def storeButtonClicked():
if self.analyzer:
if self.analyzer.isCompatible(self.refAnalyzer):
self.refAnalyzer.addMeasurements(self.analyzer)
else:
self.refAnalyzer = copy.copy(self.analyzer)
measurementsLabel.setText(
f'Measurements: {self.refAnalyzer.numMeasurements}')
self.plot()
def clearButtonClicked():
self.refAnalyzer = None
self.refSpectrumPlot.clear()
measurementsLabel.setText('Measurements: ')
self.plot()
measurementsLabel = qt.QLabel('Measurements: ')
storeButton = qt.QPushButton('Store')
storeButton.clicked.connect(storeButtonClicked)
storeButton.setShortcut('S')
storeButton.setToolTip('<Key S>')
clearButton = qt.QPushButton('Clear')
clearButton.clicked.connect(clearButtonClicked)
clearButton.setShortcut('C')
clearButton.setToolTip('<Key C>')
screenshotButton = qt.QPushButton('Screenshot') screenshotButton = qt.QPushButton('Screenshot')
screenshotButton.setShortcut('S')
screenshotButton.setToolTip('<Key S>')
screenshotButton.clicked.connect(self.screenshot) screenshotButton.clicked.connect(self.screenshot)
def setPaused():
self.paused = not self.paused
pauseButton = qt.QPushButton('Pause') pauseButton = qt.QPushButton('Pause')
pauseButton.setShortcut('Space') pauseButton.setShortcut('Space')
pauseButton.setToolTip('<Space>') pauseButton.setToolTip('<Space>')
pauseButton.setFocusPolicy(qtcore.Qt.FocusPolicy.NoFocus) pauseButton.setFocusPolicy(qtcore.Qt.FocusPolicy.NoFocus)
pauseButton.clicked.connect(self.setPaused) pauseButton.clicked.connect(setPaused)
exitButton = qt.QPushButton('Exit') exitButton = qt.QPushButton('Exit')
exitButton.setShortcut('Esc') exitButton.setShortcut('Ctrl+Q')
exitButton.setToolTip('<Esc>') exitButton.setToolTip('Ctrl+Q')
exitButton.clicked.connect(self.close) exitButton.clicked.connect(self.close)
hbox = qt.QHBoxLayout() hbox = qt.QHBoxLayout()
@ -402,6 +418,10 @@ class App(qt.QMainWindow):
hbox.addSpacing(64) hbox.addSpacing(64)
hbox.addWidget(correctionsButton) hbox.addWidget(correctionsButton)
hbox.addStretch(1) hbox.addStretch(1)
hbox.addWidget(measurementsLabel)
hbox.addWidget(storeButton)
hbox.addWidget(clearButton)
hbox.addStretch(1)
hbox.addWidget(screenshotButton) hbox.addWidget(screenshotButton)
hbox.addSpacing(32) hbox.addSpacing(32)
hbox.addWidget(pauseButton) hbox.addWidget(pauseButton)