pitch.rs

crates/veilvoice-core/src/pitch.rs

veilvoice-core · 286 lines · read the source here · or on GitHub

Monophonic fundamental-frequency tracker (decimated YIN).

Accent neutralisation needs to know the speaker's current f0 so the intonation contour can be replaced with a canonical one (see crate::accent).

Two constraints shape this implementation:

  • The STFT frame is too short to resolve f0 directly. At the default 1024-point FFT / 48 kHz the bin spacing is ~47 Hz, so a spectral peak-pick cannot tell 100 Hz from 140 Hz. This tracker therefore works in the time domain over its own rolling history, which may be longer than one STFT frame without adding any output latency, because the window still ends at the current frame, so it stays causal.
  • It must be cheap enough for an audio callback. The signal is decimated to ~8 kHz first (pitch lives in the low harmonics), which cuts the difference-function cost by the square of the decimation factor. At the default settings it costs on the order of 8 M flops/s, well under 1 % of one core, and allocates nothing after construction.

The algorithm is YIN's cumulative mean normalised difference function (de Cheveigné & Kawahara, 2002) with parabolic interpolation, minus the optimisations that only matter for offline accuracy.

In plain words

This works out how high or low somebody is speaking, moment by moment.

It is needed for the accent work: to replace the rise and fall of somebody's voice with a flatter, more ordinary pattern, you first have to know what the rise and fall currently is.

It is built to be quick rather than perfect, because it has to keep up with a live conversation. When it is not sure, it says so instead of guessing, and the accent work simply leaves that moment alone.

WHAT THIS FILE CONTAINS

286 lines defining 4 functions (3 public), 2 types and 6 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct PitchEstimate line 53 · One f0 measurement.
  • struct PitchTracker line 63 · Rolling, allocation-free f0 tracker.

What happens when it runs. These are the ways in: public, and nothing else in this file calls them, so they are what an outside caller reaches first.

  • PitchTracker::new line 81 · Build a tracker for input at sample_rate hertz.
  • PitchTracker::push line 103 · Feed new input samples (anti-aliased and decimated internally).
  • PitchTracker::estimate line 123 · Estimate f0 over the newest history.
    reaches parabolic

WHAT CALLS WHAT

PitchTracker::new line 81 PitchTracker::push line 103 PitchTracker::estimate line 123 PitchTracker::parabolic line 185 entry: a way in: public, and nothing in this file calls it helper: private to this file dashed: a call that goes back up, or across a wrapped rank The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
    n_new(["PitchTracker::new<br/>line 81"])
    n_push(["PitchTracker::push<br/>line 103"])
    n_estimate(["PitchTracker::estimate<br/>line 123"])
    n_parabolic["PitchTracker::parabolic<br/>line 185"]
    n_estimate --> n_parabolic
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/pitch.rs#L81" "open the source"
    click n_push href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/pitch.rs#L103" "open the source"
    click n_estimate href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/pitch.rs#L123" "open the source"
    click n_parabolic href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-core/src/pitch.rs#L185" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_new,n_push,n_estimate entry
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_parabolic helper

This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.

ITEMS

ItemLineDocumentation
F0_MIN_HZ const38Lowest fundamental the tracker will report, in hertz.
F0_MAX_HZ const40Highest fundamental the tracker will report, in hertz.
DECIMATED_HZ const42Target sample rate after decimation, in hertz.
WINDOW const45Analysis window length in decimated samples (~40 ms at 8 kHz, at least two periods of the lowest supported f0).
YIN_THRESHOLD const47d'(tau) below this counts as a confident voiced period.
SILENCE_RMS const49Frames quieter than this (RMS) are treated as unvoiced regardless.
PitchEstimate pub struct53One f0 measurement.
PitchTracker pub struct63Rolling, allocation-free f0 tracker.
PitchTracker::new pub fn81Build a tracker for input at sample_rate hertz.
PitchTracker::push pub fn103Feed new input samples (anti-aliased and decimated internally).
PitchTracker::estimate pub fn123Estimate f0 over the newest history.
PitchTracker::parabolic fn185Sub-sample refinement of the minimum at tau by fitting a parabola through its two neighbours.