Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pitch_track.hpp File Reference

Accurate fundamental-frequency estimation for harmonically-dense, bandlimited oscillator output, plus an f0(t) trajectory extractor over a render. More...

#include "audio_metrics.hpp"
#include "audio_spectrum.hpp"
#include <pulp/audio/buffer.hpp>
#include <span>
#include <vector>

Go to the source code of this file.

Classes

struct  pulp::test::audio::PitchEstimate
 A single fundamental-frequency estimate. More...
struct  pulp::test::audio::PitchOptions
 Options for estimate_pitch. More...
struct  pulp::test::audio::PitchTrackPoint
 One point on an f0(t) trajectory: the frame's center time and its estimate. More...
struct  pulp::test::audio::PitchTrackOptions
 Options for track_pitch. More...
struct  pulp::test::audio::PitchTrack
 An f0(t) trajectory extracted from a render — the demodulator output the drift/jitter statistics consume. More...

Namespaces

namespace  pulp
namespace  pulp::test
namespace  pulp::test::audio

Functions

double pulp::test::audio::cents_between (double hz, double reference_hz)
 Musical interval between two frequencies in cents: 1200·log2(hz / reference).
PitchEstimate pulp::test::audio::estimate_pitch (const pulp::audio::BufferView< const float > &signal, double sample_rate, const PitchOptions &options={})
 Estimate the fundamental of one channel of an already-rendered buffer.
PitchEstimate pulp::test::audio::estimate_pitch (std::span< const float > samples, double sample_rate, const PitchOptions &options={})
 Span overload — analyzes a single already-extracted channel.
PitchTrack pulp::test::audio::track_pitch (std::span< const float > samples, double sample_rate, const PitchTrackOptions &options={})
 Slide a window across samples, estimating the fundamental at each hop.

Detailed Description

Accurate fundamental-frequency estimation for harmonically-dense, bandlimited oscillator output, plus an f0(t) trajectory extractor over a render.

Why this exists: audio_metrics.hpp's estimate_frequency is a positive-going zero-crossing detector, and its own contract disclaims harmonically-dense material. A saw, pulse, or any real oscillator IS dense — intra-period wiggle and Gibbs ripple add spurious crossings, so zero-crossing returns a harmonic multiple or noise. The oscillator suite cannot gate VCO drift/jitter or a DCO pitch-quantization error without a fundamental estimate accurate to a fraction of a cent; that is what estimate_pitch provides, and track_pitch turns it into the f0(t) demodulator the drift/jitter statistics (Allan deviation, Theil-Sen slope — owned by the Quality Lab, not here) consume.

Method (two shipped primitives, no new FFT):

  1. Coarse: the loudest non-DC bin of magnitude_spectrum_curve (audio_spectrum.hpp), refined to sub-bin by a parabolic (quadratic) fit over the peak and its two neighbours in the log-magnitude domain.
  2. Refine: golden-section search that MAXIMISES the shipped fit_tone's fitted energy over a one-bin bracket around the coarse peak. Because fit_tone projects a known frequency out of the whole segment by least squares, it has no leakage skirt and no bin quantisation — the fitted energy peaks exactly at the true fundamental, and the search resolves it to well under a cent on a clean tone (proven in the tests).

Fail-closed discipline (mirrors the rest of the harness): silence or content with no clear fundamental is REFUSED (voiced = false), never handed a confident wrong number. Confidence is the fraction of segment energy the fundamental and its harmonics explain — high for a tone or a saw, low for noise, and gated below PitchOptions::min_confidence. The estimator also recovers the true fundamental when a HARMONIC is the loudest partial (a rolled-off pulse whose 2nd, 3rd, … or higher partial dominates, or a faint fundamental beneath a loud harmonic): it ranks the coarse peak against its subharmonics (down to coarse/8) and adopts the lowest that both explains the segment and carries real energy at its own fundamental — the tooth measured against a leakage-aware floor, so a short analysis window's spectral leakage is not mistaken for a subharmonic partial.

Two honest limitations: • A genuine MISSING-FUNDAMENTAL signal — energy only at 2·f0 and 3·f0 with nothing at f0 — has no tone at f0 to lock onto, so the estimate stays on the loudest present partial (2·f0), an octave above the perceived pitch. This is reported as the frequency actually present, not a fabricated f0. • The subharmonic descent stops at coarse/8, so a signal whose loudest partial is the 9th-or-higher harmonic while the fundamental is faint (rare — most oscillators roll off monotonically) can report a lower harmonic rather than the true f0. Both cases return a frequency genuinely present in the signal, never a fabricated one.

Determinism: pure arithmetic over the supplied buffer (the FFT peak plus a deterministic golden-section search). Identical input yields identical output on a given platform. Test/tool tier only — never linked into a runtime build.