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

Offline modal analysis of a rendered impulse response or note: recover the resonant modes (frequency, T60, amplitude), track partials and measure inharmonicity, and resolve a single quasi-sinusoidal note's f0(t), decay rate sigma(t) and Q(t) cycle by cycle. More...

#include <cstddef>
#include <span>
#include <string>
#include <vector>
#include <pulp/signal/modal_spec.hpp>

Go to the source code of this file.

Classes

struct  pulp::test::audio::MeasuredMode
 One resonant mode recovered from a rendered impulse response. More...
struct  pulp::test::audio::ModeAnalysisOptions
 Options for analyze_modes. More...
struct  pulp::test::audio::ModeAnalysis
 Modes recovered from an impulse response, strongest first. More...
struct  pulp::test::audio::MeasuredPartial
 One tracked partial of a (quasi-)harmonic sound. More...
struct  pulp::test::audio::InharmonicityOptions
 Options for measure_inharmonicity. More...
struct  pulp::test::audio::InharmonicityResult
 Inharmonicity of a (quasi-)harmonic tone. More...
struct  pulp::test::audio::CycleObservation
 One cycle of a decaying quasi-sinusoid. More...
struct  pulp::test::audio::CycleTrackOptions
 Options for track_cycles. More...
struct  pulp::test::audio::CycleTrack
 Time-resolved track of one note. More...
struct  pulp::test::audio::Ar2Observation
 One short-window AR(2) pole fit. More...
struct  pulp::test::audio::Ar2TrackOptions
 Options for track_ar2. More...
struct  pulp::test::audio::Ar2Track
 AR(2) pole track — an independent cross-check on track_cycles. More...
struct  pulp::test::audio::ModalExtractionOptions
struct  pulp::test::audio::ModalExtractionCandidate
struct  pulp::test::audio::ModalExtractionResult

Namespaces

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

Enumerations

enum class  pulp::test::audio::ModalExtractionError {
  pulp::test::audio::none , pulp::test::audio::empty_input , pulp::test::audio::input_too_short , pulp::test::audio::silent_input ,
  pulp::test::audio::non_finite_input , pulp::test::audio::invalid_sample_rate , pulp::test::audio::invalid_options , pulp::test::audio::no_modes
}

Functions

ModeAnalysis pulp::test::audio::analyze_modes (std::span< const float > ir, double sample_rate, const ModeAnalysisOptions &options={})
 Recover the modes present in ir, a rendered impulse response (mono).
MeasuredMode pulp::test::audio::measure_mode (std::span< const float > ir, double sample_rate, double guess_hz, const ModeAnalysisOptions &options={})
 Measure one mode whose frequency is already known approximately.
std::string pulp::test::audio::summarize (const ModeAnalysis &analysis)
 One-line human-readable summary of a mode analysis, for INFO() in a test.
std::string pulp::test::audio::summarize (const MeasuredMode &mode)
InharmonicityResult pulp::test::audio::measure_inharmonicity (std::span< const float > signal, double sample_rate, double f0_guess, const InharmonicityOptions &options={})
 Track the partials of signal starting from f0_guess and measure the inharmonicity.
std::string pulp::test::audio::summarize (const InharmonicityResult &result)
CycleTrack pulp::test::audio::track_cycles (std::span< const float > signal, double sample_rate, const CycleTrackOptions &options={})
 Track f0(t), sigma(t) and Q(t) of a decaying quasi-sinusoidal note, cycle by cycle.
Ar2Track pulp::test::audio::track_ar2 (std::span< const float > signal, double sample_rate, const Ar2TrackOptions &options={})
std::string pulp::test::audio::summarize (const CycleTrack &track)
std::string pulp::test::audio::summarize (const Ar2Track &track)
double pulp::test::audio::windowed_magnitude (std::span< const float > x, std::size_t start, std::size_t length, double freq_hz, double sample_rate)
 Hann-windowed DFT magnitude of x[start .
ModalExtractionResult pulp::test::audio::compile_modal_spec (std::span< const float > impulse, double sample_rate, const ModalExtractionOptions &options={})
 Compile a bounded, mono impulse response into the existing ModalSpec data model.

Detailed Description

Offline modal analysis of a rendered impulse response or note: recover the resonant modes (frequency, T60, amplitude), track partials and measure inharmonicity, and resolve a single quasi-sinusoidal note's f0(t), decay rate sigma(t) and Q(t) cycle by cycle.

This is the measurement side of physical modelling. A resonator is specified as modes; this header reads the modes back out of what the resonator actually emitted, so "did it do what I asked" is a number rather than a spectrogram someone squints at.

Shipped offline/tool layer only — every entry point analyzes a buffer that has already left the audio thread. Nothing here is realtime-safe, and this target must not be pulled into an audio processor merely because it is installed as part of the SDK.

Layering (test/support/README.md): this sits at the metrics layer. It takes plain buffers and returns facts; it never renders and never asserts. It depends only on pulp::audio-analysis (for the discovery FFT) and the standard library — no scenario or contract knowledge.

── Analyzer determinism contract ───────────────────────────────────────── Every measurement below is deterministic for identical input on a given platform: no randomness, no clocks, no threading, no platform math beyond IEEE-754 double. Mode discovery runs one Hann-windowed FFT (via magnitude_spectrum_curve); everything after it is an exact-frequency Hann-windowed DFT probe plus least-squares fits, which are FFT-backend independent. Discovery only nominates candidate frequencies — the reported numbers all come from the refinement stage — so a backend that shifts a peak by a fraction of a bin cannot move a result outside its tolerance.

── Stated limits (read these before trusting a number) ───────────────────

  • Every result carries a message. Read it. A MeasuredMode with low confidence is a nomination, not a measurement.
  • Modes must be resolvable. Two modes closer than roughly one main lobe of the discovery window (~4 * sample_rate / fft_length for Hann) merge into one peak and are reported as one mode at a pulled frequency. ModeAnalysisOptions::min_separation_hz is the guard; widen fft_length to separate closer pairs.
  • T60 is a slope fit over a dB window, extrapolated — never a chase to −60 dB. A detector that hunts for the −60 dB crossing latches onto the noise floor (or onto a neighbouring mode's tail) and reports a confident, wrong, and typically flat number. The fit window (fit_start_db..fit_end_db below the mode's own onset level) stays in the region where the mode dominates. A mode whose fit window does not fit inside the buffer is reported with confidence == 0 and a message saying so, not silently truncated.
  • T60 assumes exponential decay. A mode that beats against a close neighbour, or one whose decay rate is time-varying (the TR-808 kick's feedback loop is exactly this), does not have a single T60. The fit's R^2 lands in confidence; a low value means "this is not one exponential", not "the analyzer failed".
  • Amplitude is back-extrapolated to t = 0 from the windowed magnitude using the fitted decay, so it is only as good as the fit. It is calibrated to read the amplitude of the mode's contribution to the impulse response at n = 0 — for pulp::signal::ModalBankT that is exactly ModalMode::gain. The probe is a one-sided DFT, so a mode whose frequency is within ~2 bins of DC leaks its own negative- frequency image into its magnitude and reads high; widen envelope_window for very low modes.
  • Attack Q is ill-defined in principle, not merely hard: a window shorter than one period of the attack frequency contains no cycle to measure a decay over. track_cycles therefore never reports a sigma for the attack cycle; it reports the cycle's frequency and flags it directional_only. Treat that frequency as a direction, never a value.
  • sigma(t) has a runway. The sliding fit needs sigma_half_window cycles on each side, so the first and last few cycles carry sigma_valid == false. On a fast glide most of the interesting motion can be over before sigma becomes valid — check f0_span_percent over the valid region before concluding anything about how sigma behaved "through the sigh".
  • track_cycles assumes one dominant component. It is a zero-crossing tracker; a signal with two comparable partials will produce crossings that belong to neither. Check CycleTrack::monocomponent_confidence before believing it.

── Calibration ─────────────────────────────────────────────────────────── The estimators here are calibrated against synthetic signals with known ground truth in test/test_modal_analysis.cpp, including the control that makes the sigma/Q results usable: a constant-sigma signal carrying a prescribed frequency glide must recover sigma dead flat. An estimator that invents decay variation from a frequency glide would manufacture exactly the state-dependent-Q result one hopes to find. Any change to this file must keep that test green.