Skip to content

Commit

Permalink
Pitch tracker has a minimum amplitude threshold instead of SNR
Browse files Browse the repository at this point in the history
Using SNR (as formulated) didn't work great because, surprise, it turns out
that the dumb approximation of mean level for noise level is correlated with
signal power.

Amplitude is simple and works as desired in practice, although it probably
requires tuning to any given environment for best results -.-
  • Loading branch information
celeste-sinead authored and tlecomte committed May 7, 2024
1 parent 2e86ca8 commit 4fed2d5
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 29 deletions.
33 changes: 17 additions & 16 deletions friture/pitch_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from PyQt5.QtQuick import QQuickWindow
from PyQt5.QtQuickWidgets import QQuickWidget
from PyQt5.QtQml import QQmlComponent, QQmlEngine
from typing import Optional
from typing import Any, no_type_check, Optional

from friture.audiobackend import SAMPLING_RATE
from friture.audioproc import audioproc
Expand All @@ -37,8 +37,8 @@
DEFAULT_MAX_FREQ,
DEFAULT_DURATION,
DEFAULT_FFT_SIZE,
DEFAULT_MIN_SNR,
PitchTrackerSettingsDialog
DEFAULT_MIN_DB,
PitchTrackerSettingsDialog,
)
from friture.plotting.coordinateTransform import CoordinateTransform
import friture.plotting.frequency_scales as fscales
Expand Down Expand Up @@ -127,15 +127,17 @@ def __init__(self, parent, engine):

self.min_freq = DEFAULT_MIN_FREQ
self.max_freq = DEFAULT_MAX_FREQ
self._pitch_tracker_data.vertical_axis.setRange(
self._pitch_tracker_data.vertical_axis.setRange( # type: ignore
self.min_freq, self.max_freq)
self._pitch_tracker_data.vertical_axis.setScale(fscales.Octave)
self._pitch_tracker_data.vertical_axis.setScale( # type: ignore
fscales.Octave)
self.vertical_transform = CoordinateTransform(
self.min_freq, self.max_freq, 1, 0, 0)
self.vertical_transform.setScale(fscales.Octave)

self.duration = DEFAULT_DURATION
self._pitch_tracker_data.horizontal_axis.setRange(-self.duration, 0.)
self._pitch_tracker_data.horizontal_axis.setRange( # type: ignore
-self.duration, 0.)

self.settings_dialog = PitchTrackerSettingsDialog(self)

Expand Down Expand Up @@ -185,8 +187,8 @@ def set_duration(self, value):
self.duration = value
self._pitch_tracker_data.horizontal_axis.setRange(-self.duration, 0.)

def set_min_snr(self, value: float):
self.tracker.min_snr = value
def set_min_db(self, value: float):
self.tracker.min_db = value

# slot
def settings_called(self, checked):
Expand Down Expand Up @@ -222,14 +224,14 @@ def pitch(self, pitch: float):
self._pitch = pitch
self.pitch_changed.emit(pitch)

@pyqtProperty(str, notify=pitch_changed)
@pyqtProperty(str, notify=pitch_changed) # type: ignore
def pitch_unit(self) -> str:
if self._pitch >= 1000.0:
return "kHz"
else:
return "Hz"

@pyqtProperty(str, notify=pitch_changed)
@pyqtProperty(str, notify=pitch_changed) # type: ignore
def note(self) -> str:
if not self._pitch or np.isnan(self._pitch):
return '--'
Expand All @@ -244,12 +246,12 @@ def __init__(
fft_size: int = DEFAULT_FFT_SIZE,
overlap: float = 0.75,
sample_rate: int = SAMPLING_RATE,
min_snr: float = DEFAULT_MIN_SNR,
min_db: float = DEFAULT_MIN_DB,
):
self.fft_size = fft_size
self.overlap = overlap
self.sample_rate = sample_rate
self.min_snr = min_snr
self.min_db = min_db

self.input_buf = input_buf
self.input_buf.grow_if_needed(fft_size)
Expand Down Expand Up @@ -307,11 +309,10 @@ def estimate_pitch(self, frame: np.ndarray) -> Optional[float]:
# try to take the log of zero.
return None

# Compute SNR for the detected pitch; if it's too low presume it's
# Compute dB for the detected fundamental; if it's too low presume it's
# a false detection and return no result.
variance = np.mean(spectrum ** 2)
snr = 10 * np.log10((spectrum[pitch_idx] ** 2) / variance)
if snr < self.min_snr:
db = 10 * np.log10(spectrum[pitch_idx] ** 2 / self.fft_size ** 2)
if db < self.min_db:
return None
else:
return self.proc.freq[pitch_idx]
27 changes: 14 additions & 13 deletions friture/pitch_tracker_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DEFAULT_MIN_FREQ = 80
DEFAULT_MAX_FREQ = 1000
DEFAULT_DURATION = 30
DEFAULT_MIN_SNR = 3.0
DEFAULT_MIN_DB = -70.0
DEFAULT_FFT_SIZE = 16384

class PitchTrackerSettingsDialog(QtWidgets.QDialog):
Expand Down Expand Up @@ -64,23 +64,23 @@ def __init__(self, parent):
self.duration.valueChanged.connect(self.parent().set_duration)
self.form_layout.addRow("Duration:", self.duration)

self.min_snr = QtWidgets.QDoubleSpinBox(self)
self.min_snr.setMinimum(0)
self.min_snr.setMaximum(50)
self.min_snr.setSingleStep(1)
self.min_snr.setValue(DEFAULT_MIN_SNR)
self.min_snr.setSuffix(" dB")
self.min_snr.setObjectName("min_snr")
self.min_snr.valueChanged.connect(self.parent().set_min_snr)
self.form_layout.addRow("Min SNR:", self.min_snr)
self.min_db = QtWidgets.QDoubleSpinBox(self)
self.min_db.setMinimum(-100)
self.min_db.setMaximum(0)
self.min_db.setSingleStep(1)
self.min_db.setValue(DEFAULT_MIN_DB)
self.min_db.setSuffix(" dB")
self.min_db.setObjectName("min_db")
self.min_db.valueChanged.connect(self.parent().set_min_db) # type: ignore
self.form_layout.addRow("Min Amplitude:", self.min_db)

self.setLayout(self.form_layout)

def save_state(self, settings):
settings.setValue("min_freq", self.min_freq.value())
settings.setValue("max_freq", self.max_freq.value())
settings.setValue("duration", self.duration.value())
settings.setValue("min_snr", self.min_snr.value())
settings.setValue("min_db", self.min_db.value())

def restore_state(self, settings):
self.min_freq.setValue(
Expand All @@ -89,5 +89,6 @@ def restore_state(self, settings):
settings.value("max_freq", DEFAULT_MAX_FREQ, type=int))
self.duration.setValue(
settings.value("duration", DEFAULT_DURATION, type=int))
self.min_snr.setValue(
settings.value("min_snr", DEFAULT_MIN_SNR, type=float))
self.min_db.setValue(
settings.value("min_db", DEFAULT_MIN_DB, type=float))

0 comments on commit 4fed2d5

Please sign in to comment.