kopia lustrzana https://github.com/erdewit/HiFiScan
Add support for multiple measurements
rodzic
700e1ba9d0
commit
492f1ea476
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Ładowanie…
Reference in New Issue