Skip to content

Commit 4aee161

Browse files
author
NathaU
committed
Added support of microphone calibration files
1 parent 64b4856 commit 4aee161

12 files changed

+110
-16
lines changed

friture/analyzer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from friture.level_view_model import LevelViewModel
4646
from friture.level_data import LevelData
4747
from friture.levels import Levels_Widget
48+
from friture.octavespectrum import OctaveSpectrum_Widget
4849
from friture.store import GetStore, Store
4950
from friture.scope_data import Scope_Data
5051
from friture.axis import Axis
@@ -112,6 +113,9 @@ def __init__(self):
112113
qmlRegisterType(ColorBar, 'Friture', 1, 0, 'ColorBar')
113114
qmlRegisterType(Tick, 'Friture', 1, 0, 'Tick')
114115
qmlRegisterSingletonType(Store, 'Friture', 1, 0, 'Store', lambda engine, script_engine: GetStore())
116+
117+
self.original_calibration = [0, 0]
118+
self.original_calibration_freqs = [0, 20000]
115119

116120
# Setup the user interface
117121
self.ui = Ui_MainWindow()
@@ -312,6 +316,14 @@ def timer_toggle(self):
312316
self.playback_widget.start_recording()
313317
AudioBackend().restart()
314318
self.dockmanager.restart()
319+
320+
def recalculate_calibration(self):
321+
for dock in self.dockmanager.docks:
322+
if not dock.audiowidget is None:
323+
if hasattr(dock.audiowidget, "proc"):
324+
dock.audiowidget.proc.recalculate_calibration()
325+
elif type(dock.audiowidget) == OctaveSpectrum_Widget:
326+
dock.audiowidget.filters.recalculate_calibration()
315327

316328

317329
def qt_message_handler(mode, context, message):

friture/audioproc.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919

2020
import logging
2121

22-
from numpy import linspace, log10, cos, arange, pi
22+
from numpy import linspace, log10, cos, arange, pi, interp
2323
from numpy.fft import rfft
2424
from friture.audiobackend import SAMPLING_RATE
2525

2626

2727
class audioproc():
2828

29-
def __init__(self):
29+
def __init__(self, parent):
30+
self.parent_widget = parent
3031
self.logger = logging.getLogger(__name__)
3132

3233
self.freq = linspace(0, SAMPLING_RATE / 2, 10)
@@ -39,6 +40,10 @@ def __init__(self):
3940

4041
self.fft_size = 10
4142

43+
self.calibration = [0 for i in self.freq]
44+
45+
self.recalculate_calibration()
46+
4247
def analyzelive(self, samples):
4348
# FFT for a linear transformation in frequency scale
4449
fft = rfft(samples * self.window)
@@ -95,6 +100,17 @@ def update_freq_cache(self):
95100
self.B = 0.17 + 20. * log10(Rb + eps)
96101
self.A = 2.0 + 20. * log10(Ra + eps)
97102

103+
self.recalculate_calibration()
104+
105+
def recalculate_calibration(self):
106+
try:
107+
if hasattr(self.parent_widget.parent(), "dockmanager"):
108+
self.calibration = interp(self.freq, self.parent_widget.parent().dockmanager.parent().original_calibration_freqs, self.parent_widget.parent().dockmanager.parent().original_calibration)
109+
else:
110+
self.calibration = interp(self.freq, self.parent_widget.parent().original_calibration_freqs, self.parent_widget.parent().original_calibration)
111+
except:
112+
self.calibration = [0 for i in self.freq]
113+
98114
# above is done a FFT of the signal. This is ok for linear frequency scale, but
99115
# not satisfying for logarithmic scale, which is much more adapted to voice or music
100116
# analysis

friture/levels.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __init__(self, parent, engine):
7878
self.calibration = 0
7979

8080
# initialize the class instance that will do the fft
81-
self.proc = audioproc()
81+
self.proc = audioproc(self)
8282

8383
# time = SMOOTH_DISPLAY_TIMER_PERIOD_MS/1000. #DISPLAY
8484
# time = 0.025 #IMPULSE setting for a sound level meter
@@ -161,8 +161,6 @@ def handle_new_data(self, floatdata):
161161
value_rms = pyx_exp_smoothed_value(self.kernel, self.alpha, y2 ** 2, self.old_rms_2)
162162
self.old_rms_2 = value_rms
163163

164-
value_spl = value_rms + 94.0 + self.calibration
165-
166164
self.level_view_model.level_data_2.level_rms = 10. * np.log10(value_rms + 0. * 1e-80)
167165
self.level_view_model.level_data_2.level_spl = self.level_view_model.level_data_2.level_rms + 94.0 + self.calibration
168166
self.level_view_model.level_data_2.level_max = 20. * np.log10(self.old_max_2 + 0. * 1e-80)

friture/longlevels.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def __init__(self, parent, engine):
143143
self.settings_dialog = LongLevels_Settings_Dialog(self)
144144

145145
# initialize the class instance that will do the fft
146-
self.proc = audioproc()
146+
self.proc = audioproc(self)
147147

148148
self.level = None # 1e-30
149149
self.level_rms = -200.

friture/octavefilters.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# You should have received a copy of the GNU General Public License
1818
# along with Friture. If not, see <http://www.gnu.org/licenses/>.
1919

20-
from numpy import log10, array, where
20+
from numpy import log10, array, where, interp
2121

2222
from friture.filter import (octave_filter_bank_decimation, octave_frequencies,
2323
octave_filter_bank_decimation_filtic, NOCTAVE)
@@ -26,12 +26,16 @@
2626

2727
class Octave_Filters():
2828

29-
def __init__(self, bandsperoctave):
29+
def __init__(self, parent, bandsperoctave):
3030
[self.bdec, self.adec] = generated_filters.PARAMS['dec']
3131

3232
self.bdec = array(self.bdec)
3333
self.adec = array(self.adec)
3434

35+
self.calibration = []
36+
37+
self.parent_widget = parent
38+
3539
self.setbandsperoctave(bandsperoctave)
3640

3741
def filter(self, floatdata):
@@ -48,12 +52,21 @@ def get_decs(self):
4852

4953
return decs
5054

55+
def recalculate_calibration(self):
56+
try:
57+
58+
self.calibration = interp(self.fi, self.parent_widget.parent().dockmanager.parent().original_calibration_freqs, self.parent_widget.parent().dockmanager.parent().original_calibration)
59+
except:
60+
self.calibration = [0 for i in self.fi]
61+
5162
def setbandsperoctave(self, bandsperoctave):
5263
self.bandsperoctave = bandsperoctave
5364
self.nbands = NOCTAVE * self.bandsperoctave
5465
self.fi, self.flow, self.fhigh = octave_frequencies(self.nbands, self.bandsperoctave)
5566
[self.boct, self.aoct, fi, flow, fhigh] = generated_filters.PARAMS['%d' % bandsperoctave]
5667

68+
self.recalculate_calibration()
69+
5770
self.boct = [array(f) for f in self.boct]
5871
self.aoct = [array(f) for f in self.aoct]
5972

friture/octavespectrum.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(self, parent, engine):
6161
self.PlotZoneSpect.setspecrange(self.spec_min, self.spec_max)
6262
self.PlotZoneSpect.setweighting(self.weighting)
6363

64-
self.filters = Octave_Filters(DEFAULT_BANDSPEROCTAVE)
64+
self.filters = Octave_Filters(self, DEFAULT_BANDSPEROCTAVE)
6565
self.dispbuffers = [0] * DEFAULT_BANDSPEROCTAVE * NOCTAVE
6666

6767
# set kernel and parameters for the smoothing filter
@@ -118,7 +118,7 @@ def handle_new_data(self, floatdata):
118118
w = self.filters.C
119119

120120
epsilon = 1e-30
121-
db_spectrogram = 10 * log10(sp + epsilon) + w
121+
db_spectrogram = 10 * log10(sp + epsilon) + w + self.filters.calibration
122122
self.PlotZoneSpect.setdata(self.filters.flow, self.filters.fhigh, self.filters.f_nominal, db_spectrogram)
123123

124124
# method

friture/pitch_tracker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def __init__(
265265
self.out_buf = RingBuffer()
266266
self.out_offset = self.out_buf.offset
267267

268-
self.proc = audioproc()
268+
self.proc = audioproc(self)
269269
self.proc.set_fftsize(self.fft_size)
270270

271271
def set_input_buffer(self, new_buf: RingBuffer) -> None:

friture/settings.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from PyQt5.QtCore import pyqtSignal, pyqtProperty
2525
from friture.audiobackend import AudioBackend
2626
from friture.ui_settings import Ui_Settings_Dialog
27+
import csv
2728

2829
no_input_device_title = "No audio input device found"
2930

@@ -82,6 +83,9 @@ def __init__(self, parent):
8283
self.checkbox_showPlayback.stateChanged.connect(self.show_playback_checkbox_changed)
8384
self.spinBox_historyLength.editingFinished.connect(self.history_length_edit_finished)
8485

86+
self.fileBox_compensation.clicked.connect(self.browse_compensation_file)
87+
self.clearButton.clicked.connect(self.compensation_file_cleared)
88+
8589
@pyqtProperty(bool, notify=show_playback_changed) # type: ignore
8690
def show_playback(self) -> bool:
8791
return bool(self.checkbox_showPlayback.checkState())
@@ -178,6 +182,37 @@ def show_playback_checkbox_changed(self, state: int) -> None:
178182
# slot
179183
def history_length_edit_finished(self) -> None:
180184
self.history_length_changed.emit(self.spinBox_historyLength.value())
185+
186+
def browse_compensation_file(self):
187+
file_name = QtWidgets.QFileDialog.getOpenFileName(self, "Open compensation file", "", "Compensation files (*.*)")[0]
188+
self.fileBox_compensation.setText(file_name)
189+
self.load_compensation_file()
190+
191+
def load_compensation_file(self):
192+
file_name = self.fileBox_compensation.text()
193+
if file_name:
194+
try:
195+
with open(file_name, newline='') as csvfile:
196+
csvreader = csv.reader(csvfile)
197+
self.parent().original_calibration = []
198+
self.parent().original_calibration_freqs = []
199+
for row in csvreader:
200+
if row[0][0].isdigit():
201+
dict = row[0].split()
202+
self.parent().original_calibration.append(float(dict[1]))
203+
self.parent().original_calibration_freqs.append(float(dict[0]))
204+
205+
except Exception as e:
206+
self.logger.error(f"Failed to load compensation file: {e}")
207+
else:
208+
self.parent().original_calibration = [0, 0]
209+
self.parent().original_calibration_freqs = [0, 20000]
210+
211+
self.parent().recalculate_calibration()
212+
213+
def compensation_file_cleared(self):
214+
self.fileBox_compensation.setText("")
215+
self.load_compensation_file()
181216

182217
# method
183218
def saveState(self, settings):
@@ -189,13 +224,17 @@ def saveState(self, settings):
189224
settings.setValue("duoInput", self.inputTypeButtonGroup.checkedId())
190225
settings.setValue("showPlayback", self.checkbox_showPlayback.checkState())
191226
settings.setValue("historyLength", self.spinBox_historyLength.value())
227+
settings.setValue("compensationFile", self.fileBox_compensation.text())
192228

193229
# method
194230
def restoreState(self, settings):
195231
device_name = settings.value("deviceName", "")
196232
device_index = self.comboBox_inputDevice.findText(device_name)
197233
# change the device only if it exists in the device list
198234
if device_index >= 0:
235+
compensation_file = settings.value("compensationFile", "")
236+
self.fileBox_compensation.setText(compensation_file)
237+
self.load_compensation_file()
199238
self.comboBox_inputDevice.setCurrentIndex(device_index)
200239
channel = settings.value("firstChannel", 0, type=int)
201240
self.comboBox_firstChannel.setCurrentIndex(channel)

friture/spectrogram.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(self, parent, engine):
5858
self.audiobuffer = None
5959

6060
# initialize the class instance that will do the fft
61-
self.proc = audioproc()
61+
self.proc = audioproc(self)
6262

6363
self.frequency_resampler = Frequency_Resampler()
6464
self.screen_resampler = Online_Linear_2D_resampler()
@@ -159,6 +159,7 @@ def handle_new_data(self, floatdata: ndarray) -> None:
159159

160160
w = tile(self.w, (1, realizable))
161161
norm_spectrogram = self.scale_spectrogram(self.log_spectrogram(spn) + w)
162+
#norm_spectrogram = self.scale_spectrogram(self.log_spectrogram(spn) + w + self.proc.calibration) # This produces lags
162163

163164
self.screen_resampler.set_height(self.PlotZoneImage.spectrogram_screen_height())
164165
screen_rate_frac = Fraction(self.PlotZoneImage.spectrogram_screen_width(), int(self.timerange_s * 1000))

friture/spectrum.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(self, parent, engine):
5353
self.gridLayout.addWidget(self.PlotZoneSpect, 0, 0, 1, 1)
5454

5555
# initialize the class instance that will do the fft
56-
self.proc = audioproc()
56+
self.proc = audioproc(self)
5757

5858
self.maxfreq = DEFAULT_MAXFREQ
5959
self.proc.set_maxfreq(self.maxfreq)
@@ -159,7 +159,7 @@ def handle_new_data(self, floatdata):
159159
if self.dual_channels and floatdata.shape[0] > 1:
160160
dB_spectrogram = self.log_spectrogram(sp2) - self.log_spectrogram(sp1)
161161
else:
162-
dB_spectrogram = self.log_spectrogram(sp1) + self.w
162+
dB_spectrogram = self.log_spectrogram(sp1) + self.w + self.proc.calibration
163163

164164
# the log operation and the weighting could be deffered
165165
# to the post-weedening !

friture/spl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(self, parent, engine):
5353
self.gridLayout.addWidget(self.PlotZoneSpect, 0, 0, 1, 1)
5454

5555
# initialize the class instance that will do the fft
56-
self.proc = audioproc()
56+
self.proc = audioproc(self)
5757

5858
self.maxfreq = DEFAULT_MAXFREQ
5959
self.proc.set_maxfreq(self.maxfreq)
@@ -160,7 +160,7 @@ def handle_new_data(self, floatdata):
160160
if self.dual_channels and floatdata.shape[0] > 1:
161161
dB_spectrogram = self.log_spectrogram(sp2) - self.log_spectrogram(sp1)
162162
else:
163-
dB_spectrogram = self.log_spectrogram(sp1) + self.w
163+
dB_spectrogram = self.log_spectrogram(sp1) + self.w + self.proc.calibration
164164

165165
# the log operation and the weighting could be deffered
166166
# to the post-weedening !

friture/ui_settings.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ def setupUi(self, Settings_Dialog):
3030
self.comboBox_inputDevice = QtWidgets.QComboBox(self.inputGroup)
3131
self.comboBox_inputDevice.setObjectName("comboBox_inputDevice")
3232
self.verticalLayout_6.addWidget(self.comboBox_inputDevice)
33+
self.label_inputType_3 = QtWidgets.QLabel(self.inputGroup)
34+
self.label_inputType_3.setObjectName("label_inputType_3")
35+
self.verticalLayout_6.addWidget(self.label_inputType_3)
36+
37+
# Add horizontal layout for QLineEdit and buttons
38+
self.horizontalLayout_file = QtWidgets.QHBoxLayout()
39+
self.fileBox_compensation = QtWidgets.QPushButton("Browse", self.inputGroup)
40+
self.fileBox_compensation.setObjectName("fileButton")
41+
self.horizontalLayout_file.addWidget(self.fileBox_compensation)
42+
self.clearButton = QtWidgets.QPushButton("Clear", self.inputGroup)
43+
self.clearButton.setObjectName("clearButton")
44+
self.horizontalLayout_file.addWidget(self.clearButton)
45+
self.verticalLayout_6.addLayout(self.horizontalLayout_file)
46+
3347
self.horizontalLayout = QtWidgets.QHBoxLayout()
3448
self.horizontalLayout.setObjectName("horizontalLayout")
3549
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
@@ -108,6 +122,7 @@ def retranslateUi(self, Settings_Dialog):
108122
Settings_Dialog.setWindowTitle(_translate("Settings_Dialog", "Settings"))
109123
self.inputGroup.setTitle(_translate("Settings_Dialog", "Input"))
110124
self.label_inputType_2.setText(_translate("Settings_Dialog", "Select the input device :"))
125+
self.label_inputType_3.setText(_translate("Settings_Dialog", "Select the compensation file:"))
111126
self.label_inputType.setText(_translate("Settings_Dialog", "Select the type of input :"))
112127
self.radioButton_single.setText(_translate("Settings_Dialog", "Single channel"))
113128
self.radioButton_duo.setText(_translate("Settings_Dialog", "Two channels"))

0 commit comments

Comments
 (0)