From 30e6f7239d4319e8712f1c39661f00008e1f8ee3 Mon Sep 17 00:00:00 2001 From: Ewald de Wit Date: Fri, 30 Sep 2022 11:44:46 +0200 Subject: [PATCH] Add loading and saving of measurements to file --- hifiscan/__init__.py | 4 +-- hifiscan/analyzer.py | 28 ++++++++++----- hifiscan/app.py | 84 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 90 insertions(+), 26 deletions(-) diff --git a/hifiscan/__init__.py b/hifiscan/__init__.py index 50759c6..ee864ed 100644 --- a/hifiscan/__init__.py +++ b/hifiscan/__init__.py @@ -1,7 +1,7 @@ """'Optimize the frequency response spectrum of an audio system""" from hifiscan.analyzer import ( - Analyzer, XY, geom_chirp, linear_chirp, transform_causality, resample, - smooth, taper, tone, window) + Analyzer, XY, geom_chirp, linear_chirp, resample, smooth, taper, tone, + transform_causality, window) from hifiscan.audio import Audio from hifiscan.io_ import Sound, read_correction, read_wav, write_wav diff --git a/hifiscan/analyzer.py b/hifiscan/analyzer.py index 8d1ff18..7f90b42 100644 --- a/hifiscan/analyzer.py +++ b/hifiscan/analyzer.py @@ -4,9 +4,14 @@ from functools import lru_cache from typing import NamedTuple, Optional, Tuple import numpy as np -from numba import njit from numpy.fft import fft, ifft, irfft, rfft +try: + from numba import njit +except ImportError: + def njit(f): + return f + from hifiscan.io_ import Correction @@ -39,15 +44,19 @@ class Analyzer: MAX_DELAY_SECS = 0.1 TIMEOUT_SECS = 1.0 + CACHED_METHODS = [ + 'X', 'Y', 'calcH', 'H', 'H2', 'h', 'h_inv', 'spectrum', + 'frequency', 'calibration', 'target'] chirp: np.ndarray x: np.ndarray y: np.ndarray + sumH: np.ndarray + numMeasurements: int rate: int fmin: float fmax: float time: float - numMeasurements: int def __init__( self, f0: int, f1: int, secs: float, rate: int, ampl: float, @@ -63,18 +72,21 @@ class Analyzer: self.fmin = min(f0, f1) self.fmax = max(f0, f1) self.time = 0 + self.sumH = np.zeros(self.X().size) self.numMeasurements = 0 self._calibration = calibration self._target = target - self._sumH = np.zeros(self.X().size) + + def __getstate__(self): + return {k: v for k, v in self.__dict__.items() + if k not in self.CACHED_METHODS} def setCaching(self): """ 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']: + for name in self.CACHED_METHODS: unbound = getattr(Analyzer, name) bound = types.MethodType(unbound, self) setattr(self, name, lru_cache(bound)) @@ -83,7 +95,7 @@ class 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.sumH = self.sumH + analyzer.sumH self.numMeasurements += analyzer.numMeasurements self.setCaching() @@ -109,7 +121,7 @@ class Analyzer: if idx >= 0: self.y = np.array(recording[idx:idx + self.x.size]) self.numMeasurements += 1 - self._sumH += self.calcH() + self.sumH += self.calcH() self.setCaching() return True return False @@ -181,7 +193,7 @@ class Analyzer: Transfer function H averaged over all measurements. """ freq = self.frequency() - H = self._sumH / (self.numMeasurements or 1) + H = self.sumH / (self.numMeasurements or 1) return XY(freq, H) def H2(self, smoothing: float) -> XY: diff --git a/hifiscan/app.py b/hifiscan/app.py index fcadf71..85f2398 100644 --- a/hifiscan/app.py +++ b/hifiscan/app.py @@ -3,6 +3,7 @@ import copy import datetime as dt import logging import os +import pickle import signal import sys from pathlib import Path @@ -19,6 +20,16 @@ class App(qt.QMainWindow): def __init__(self): super().__init__() + + self.paused = False + self.analyzer = None + self.refAnalyzer = None + self.calibration = None + self.target = None + self.saveDir = Path.home() + self.loop = asyncio.get_event_loop_policy().get_event_loop() + self.task = self.loop.create_task(wrap_coro(self.analyze())) + self.setWindowTitle('HiFi Scan') topWidget = qt.QWidget() self.setCentralWidget(topWidget) @@ -31,16 +42,6 @@ class App(qt.QMainWindow): self.stack.currentChanged.connect(self.plot) vbox.addWidget(self.stack) vbox.addWidget(self.createSharedControls()) - - self.paused = False - self.analyzer = None - self.refAnalyzer = None - self.calibration = None - self.target = None - self.saveDir = Path.home() - self.loop = asyncio.get_event_loop_policy().get_event_loop() - self.task = self.loop.create_task(wrap_coro(self.analyze())) - self.resize(1800, 900) self.show() @@ -126,8 +127,8 @@ class App(qt.QMainWindow): self.targetSimPlot.clear() def screenshot(self): - timestamp = dt.datetime.now().strftime('%Y%m%d_%H%M%S') - name = f'hifiscan_{timestamp}.png' + timestamp = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + name = f'HiFiScan {timestamp}.png' filename, _ = qt.QFileDialog.getSaveFileName( self, 'Save screenshot', str(self.saveDir / name), 'PNG (*.png)') if filename: @@ -393,17 +394,21 @@ class App(qt.QMainWindow): self.refAnalyzer.addMeasurements(self.analyzer) else: self.refAnalyzer = copy.copy(self.analyzer) - measurementsLabel.setText( - f'Measurements: {self.refAnalyzer.numMeasurements}') + setMeasurementsText() self.plot() def clearButtonClicked(): self.refAnalyzer = None self.refSpectrumPlot.clear() - measurementsLabel.setText('Measurements: ') + setMeasurementsText() self.plot() - measurementsLabel = qt.QLabel('Measurements: ') + def setMeasurementsText(): + num = self.refAnalyzer.numMeasurements if self.refAnalyzer else 0 + measurementsLabel.setText(f'Measurements: {num if num else ""}') + + measurementsLabel = qt.QLabel('') + setMeasurementsText() storeButton = qt.QPushButton('Store') storeButton.clicked.connect(storeButtonClicked) @@ -415,6 +420,52 @@ class App(qt.QMainWindow): clearButton.setShortcut('C') clearButton.setToolTip('') + def load(): + path, _ = qt.QFileDialog.getOpenFileName( + self, 'Load measurements', str(self.saveDir)) + if path: + with open(path, 'rb') as f: + self.refAnalyzer = pickle.load(f) + setMeasurementsText() + self.plot() + + def loadAdd(): + path, _ = qt.QFileDialog.getOpenFileName( + self, 'Load and Add measurements', str(self.saveDir)) + if path: + with open(path, 'rb') as f: + analyzer: hifi.Analyzer = pickle.load(f) + if analyzer and analyzer.isCompatible(self.refAnalyzer): + self.refAnalyzer.addMeasurements(analyzer) + else: + self.refAnalyzer = analyzer + setMeasurementsText() + self.plot() + + def save(): + analyzer = self.refAnalyzer or self.analyzer + timestamp = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + name = f'Measurements: {analyzer.numMeasurements}, {timestamp}' + path, _ = qt.QFileDialog.getSaveFileName( + self, 'Save measurements', + str(self.saveDir / name)) + if path: + self.saveDir = Path(path).parent + with open(path, 'wb') as f: + pickle.dump(analyzer, f) + self.plot() + + def filePressed(): + fileMenu.popup(fileButton.mapToGlobal(qtcore.QPoint(0, 0))) + + fileMenu = qt.QMenu() + fileMenu.addAction('Load', load) + fileMenu.addAction('Load and Add', loadAdd) + fileMenu.addAction('Save', save) + + fileButton = qt.QPushButton('File...') + fileButton.clicked.connect(filePressed) + screenshotButton = qt.QPushButton('Screenshot') screenshotButton.clicked.connect(self.screenshot) @@ -442,6 +493,7 @@ class App(qt.QMainWindow): hbox.addWidget(measurementsLabel) hbox.addWidget(storeButton) hbox.addWidget(clearButton) + hbox.addWidget(fileButton) hbox.addStretch(1) hbox.addWidget(screenshotButton) hbox.addSpacing(32)