Skip to content

Commit 17ac4fc

Browse files
author
NathaU
committed
Added support of microphone calibration files
1 parent 64b4856 commit 17ac4fc

File tree

11 files changed

+88
-13
lines changed

11 files changed

+88
-13
lines changed

friture/analyzer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ def __init__(self):
112112
qmlRegisterType(ColorBar, 'Friture', 1, 0, 'ColorBar')
113113
qmlRegisterType(Tick, 'Friture', 1, 0, 'Tick')
114114
qmlRegisterSingletonType(Store, 'Friture', 1, 0, 'Store', lambda engine, script_engine: GetStore())
115+
116+
self.original_calibration = [0, 0]
117+
self.original_calibration_freqs = [0, 20000]
115118

116119
# Setup the user interface
117120
self.ui = Ui_MainWindow()
@@ -312,6 +315,11 @@ def timer_toggle(self):
312315
self.playback_widget.start_recording()
313316
AudioBackend().restart()
314317
self.dockmanager.restart()
318+
319+
def recalculate_calibration(self):
320+
for dock in self.dockmanager.docks:
321+
if not dock.audiowidget is None and dock.audiowidget.proc is not None:
322+
dock.audiowidget.proc.recalculate_calibration()
315323

316324

317325
def qt_message_handler(mode, context, message):

friture/audioproc.py

Lines changed: 15 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,14 @@ 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+
if hasattr(self.parent_widget.parent(), "dockmanager"):
107+
self.calibration = interp(self.freq, self.parent_widget.parent().dockmanager.parent().original_calibration_freqs, self.parent_widget.parent().dockmanager.parent().original_calibration)
108+
else:
109+
self.calibration = interp(self.freq, self.parent_widget.parent().original_calibration_freqs, self.parent_widget.parent().original_calibration)
110+
98111
# above is done a FFT of the signal. This is ok for linear frequency scale, but
99112
# not satisfying for logarithmic scale, which is much more adapted to voice or music
100113
# 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/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)