Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::test::audio Namespace Reference

Classes

struct  CheckResult
 Outcome of one signal assertion. More...
struct  AnalyzeOptions
 Options for analyze(). More...
struct  ChannelMetrics
 Per-channel facts about a rendered buffer. More...
struct  BufferMetrics
 Whole-buffer facts. channels.size() == channel count. More...
struct  FrequencyEstimate
 Result of estimate_frequency(). More...
struct  ResponsePoint
 One point on a response curve: frequency and magnitude in dB. More...
struct  ResponseCurve
 Magnitude/frequency-response curve. More...
struct  ResponseOptions
 Options for the response analyzer. Lengths must be powers of two. More...
struct  PhasePoint
 One point on a phase/group-delay curve. More...
struct  PhaseCurve
 Phase / group-delay curve of a transfer function. More...
struct  GroupDelayOptions
 Options for the phase/group-delay analyzer. Lengths must be powers of two. More...
struct  Harmonic
 One harmonic's contribution to a distortion measurement. More...
struct  ThdResult
 Total harmonic distortion result. More...
struct  ThdOptions
 Options for the THD analyzer. More...
struct  ToneFit
 Least-squares fit of a single sinusoid at a known frequency. More...
struct  AliasComponent
 One expected component of the ideal series, and what was measured where it landed. More...
struct  AliasOptions
 Options for measure_aliasing. More...
struct  AliasReport
 Three-way classification of an oscillator's output into harmonic / alias / noise, with the worst in-band alias called out. More...
struct  ModulationSidebandOptions
 Options for measure_modulation_sidebands. More...
struct  ModulationSidebandReport
 Where a steady tone's energy went after a periodic modulation was applied to it. More...
struct  LatencyEvidence
 One processor's latency evidence: the facts, the measurement, the verdict. More...
struct  DelayedNullOptions
 Knobs for measure_delayed_passthrough. More...
struct  MarkerOffsetOptions
 Knobs for measure_marker_offset. More...
struct  MeasuredMode
 One resonant mode recovered from a rendered impulse response. More...
struct  ModeAnalysisOptions
 Options for analyze_modes. More...
struct  ModeAnalysis
 Modes recovered from an impulse response, strongest first. More...
struct  MeasuredPartial
 One tracked partial of a (quasi-)harmonic sound. More...
struct  InharmonicityOptions
 Options for measure_inharmonicity. More...
struct  InharmonicityResult
 Inharmonicity of a (quasi-)harmonic tone. More...
struct  CycleObservation
 One cycle of a decaying quasi-sinusoid. More...
struct  CycleTrackOptions
 Options for track_cycles. More...
struct  CycleTrack
 Time-resolved track of one note. More...
struct  Ar2Observation
 One short-window AR(2) pole fit. More...
struct  Ar2TrackOptions
 Options for track_ar2. More...
struct  Ar2Track
 AR(2) pole track — an independent cross-check on track_cycles. More...
struct  ModalExtractionOptions
struct  ModalExtractionCandidate
struct  ModalExtractionResult
struct  PitchEstimate
 A single fundamental-frequency estimate. More...
struct  PitchOptions
 Options for estimate_pitch. More...
struct  PitchTrackPoint
 One point on an f0(t) trajectory: the frame's center time and its estimate. More...
struct  PitchTrackOptions
 Options for track_pitch. More...
struct  PitchTrack
 An f0(t) trajectory extracted from a render — the demodulator output the drift/jitter statistics consume. More...

Enumerations

enum class  Window {
  rectangular , hann , hamming , blackman ,
  flat_top , kaiser
}
 Analysis window. More...
enum class  ComponentClass { harmonic , alias }
 How measure_aliasing classified one expected component of the ideal harmonic series h · f0. More...
enum class  LatencyReportStatus { available , unsupported , query_failed , invalid }
 Whether a latency report could be read at all, and whether it was sane. More...
enum class  LatencyReportObservation { stable , changed , unobservable }
 Whether the report held still for the duration of the render. More...
enum class  LatencyObservationMode { processor_flag_and_poll , per_block_poll , none }
 How the report was watched while the render ran. More...
enum class  LatencyPolicy { none , delayed_passthrough_null , marker_offset }
 The probe used to turn audio into a delay measurement. More...
enum class  LatencyMeasurementStatus { match , mismatch , not_measurable , not_requested }
 What the probe concluded. More...
enum class  LatencyContractOutcome { satisfied , violated , inconclusive , not_requested }
 The gating verdict. More...
enum class  ModalExtractionError {
  none , empty_input , input_too_short , silent_input ,
  non_finite_input , invalid_sample_rate , invalid_options , no_modes
}

Functions

std::string metrics_to_json (const BufferMetrics &metrics, std::string_view scenario)
 Serialize metrics to a JSON string.
std::filesystem::path write_metrics_artifact (const BufferMetrics &metrics, std::string_view scenario)
 Write metrics_to_json() to <temp>/pulp-audio-metrics/<sanitized-scenario>.json (overwriting any previous run's artifact for the same scenario) and return the path.
CheckResult assert_no_nan_inf (const BufferMetrics &metrics)
 Fails if any channel contains NaN or ±Inf samples.
CheckResult assert_not_clipped (const BufferMetrics &metrics, double ceiling_dbfs=-0.1)
 Fails if any channel's peak reaches ceiling_dbfs or any sample counted as clipped during analysis.
CheckResult assert_silent (const BufferMetrics &metrics, double threshold_dbfs=-90.0)
 Fails if any channel's RMS is at or above threshold_dbfs.
CheckResult assert_not_silent (const BufferMetrics &metrics, double min_rms_dbfs=-60.0)
 Fails if every channel's RMS is below min_rms_dbfs.
CheckResult assert_peak_between (const BufferMetrics &metrics, double min_dbfs, double max_dbfs)
 Fails unless every channel's peak lies within [min_dbfs, max_dbfs].
CheckResult assert_rms_between (const BufferMetrics &metrics, double min_dbfs, double max_dbfs)
 Fails unless every channel's RMS lies within [min_dbfs, max_dbfs].
CheckResult assert_frequency_near (std::span< const float > samples, double sample_rate, double expected_hz, double tolerance_cents=5.0)
 Fails unless the zero-crossing frequency estimate is within tolerance_cents of expected_hz.
CheckResult assert_null_near (const pulp::audio::BufferView< const float > &a, const pulp::audio::BufferView< const float > &b, double tolerance_dbfs=-120.0)
 Null test: fails if the per-sample residual |a−b| peak is at or above tolerance_dbfs.
CheckResult assert_null_near (const pulp::audio::Buffer< float > &a, const pulp::audio::Buffer< float > &b, double tolerance_dbfs=-120.0)
 Null test: fails if the per-sample residual |a−b| peak is at or above tolerance_dbfs.
CheckResult assert_channels_independent (const pulp::audio::BufferView< const float > &buffer)
 Fails if any two channels are sample-identical (within 1e-9), which usually means a channel-routing bug duplicated one channel.
CheckResult assert_channels_independent (const pulp::audio::Buffer< float > &buffer)
 Fails if any two channels are sample-identical (within 1e-9), which usually means a channel-routing bug duplicated one channel.
std::string response_curve_to_json (const ResponseCurve &curve, std::string_view scenario)
 Serialize a magnitude/response curve to a JSON string.
std::string thd_to_json (const ThdResult &thd, std::string_view scenario)
 Serialize a THD result to a JSON string (including the harmonic breakdown).
std::string phase_curve_to_json (const PhaseCurve &curve, std::string_view scenario)
 Serialize a phase/group-delay curve to a JSON string.
std::filesystem::path write_response_artifact (const ResponseCurve &curve, std::string_view scenario)
 Write response_curve_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.response.json and return the path (overwriting any previous run's artifact for the same scenario).
std::filesystem::path write_thd_artifact (const ThdResult &thd, std::string_view scenario)
 Write thd_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.thd.json and return path.
std::filesystem::path write_phase_artifact (const PhaseCurve &curve, std::string_view scenario)
 Write phase_curve_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.groupdelay.json and return the path.
double to_dbfs (double linear)
 Linear amplitude (1.0 == full scale) to dBFS, clamped to kSilenceFloorDb.
double from_dbfs (double dbfs)
 dBFS to linear amplitude.
BufferMetrics analyze (const pulp::audio::BufferView< const float > &buffer, double sample_rate, const AnalyzeOptions &options={})
 Analyze a const view. Pure arithmetic; safe for any buffer size.
BufferMetrics analyze (const pulp::audio::Buffer< float > &buffer, double sample_rate, const AnalyzeOptions &options={})
 Convenience overload for an owning Buffer.
FrequencyEstimate estimate_frequency (std::span< const float > samples, double sample_rate)
 Estimate the dominant frequency of one channel.
std::string summarize (const BufferMetrics &metrics, const FrequencyEstimate &frequency={})
 Compact, agent-readable signal summary.
std::string window_name (Window w)
 Human-readable window name for artifacts/messages ("rectangular", "hann", "hamming", "blackman", "flat_top", "kaiser").
double window_coherent_gain (Window w, int n, double kaiser_beta=kDefaultKaiserBeta)
 Coherent gain (a.k.a.
std::vector< double > window_coefficients (Window w, int n, double kaiser_beta=kDefaultKaiserBeta)
 Coefficients of w for an n-sample analysis segment.
ResponseCurve response_relative_to_input (const pulp::audio::BufferView< const float > &input, const pulp::audio::BufferView< const float > &output, double sample_rate, std::span< const double > checkpoints_hz, const ResponseOptions &options={})
 Compute the magnitude response of output relative to input, one channel.
ResponseCurve magnitude_spectrum_curve (const pulp::audio::BufferView< const float > &signal, double sample_rate, std::span< const double > checkpoints_hz, const ResponseOptions &options={})
 The signal's OWN magnitude spectrum, one channel, in dB relative to its own peak bin (loudest frequency = 0 dB, everything else negative).
PhaseCurve measure_group_delay (const pulp::audio::BufferView< const float > &input, const pulp::audio::BufferView< const float > &output, double sample_rate, std::span< const double > checkpoints_hz, const GroupDelayOptions &options={})
 Phase and group delay of output relative to input, one channel.
ThdResult measure_thd (const pulp::audio::BufferView< const float > &signal, double fundamental_hz, double sample_rate, const ThdOptions &options={})
 Measure THD / THD+N of signal (one channel of an already-rendered buffer) at fundamental_hz.
ToneFit fit_tone (std::span< const double > samples, double cycles_per_sample)
 Fit one sinusoid at cycles_per_sample (frequency ÷ sample rate; 0.5 is Nyquist) to samples by least squares.
double tone_residual_db (std::span< const double > samples, double cycles_per_sample)
 Energy of samples NOT explained by a tone at cycles_per_sample, in dB relative to the fitted tone's own energy: 10·log10(residual / fitted).
double tone_gain_db (std::span< const double > samples, double cycles_per_sample, double input_amplitude)
 Gain of a tone at cycles_per_sample relative to input_amplitude, in dB: 20·log10(fitted_amplitude / input_amplitude).
double fold_frequency (double frequency_hz, double sample_rate)
 Where a component at frequency_hz LANDS once sampled at sample_rate.
AliasReport measure_aliasing (const pulp::audio::BufferView< const float > &signal, double fundamental_hz, double sample_rate, const AliasOptions &options={})
 Classify signal (one channel of an already-rendered buffer) against the ideal harmonic series of fundamental_hz, and report the worst in-band alias.
ModulationSidebandReport measure_modulation_sidebands (const pulp::audio::BufferView< const float > &signal, double carrier_hz, double modulation_hz, double sample_rate, const ModulationSidebandOptions &options={})
 Classify signal (one channel of an already-rendered buffer) as a tone at carrier_hz modulated at modulation_hz, and report the energy at the modulation's own sidebands against the energy anywhere else in the band.
LatencyEvidence measure_delayed_passthrough (const pulp::audio::Buffer< float > &input, const pulp::audio::Buffer< float > &output, std::optional< int > reported_samples, const DelayedNullOptions &options={})
 Sweep the delay D that best explains output as a copy of input, and compare it against reported_samples.
LatencyEvidence measure_marker_offset (const pulp::audio::Buffer< float > &input, const pulp::audio::Buffer< float > &output, std::optional< int > reported_samples, const MarkerOffsetOptions &options={})
 Measure delay as output_onset - input_marker - intrinsic_response_offset and compare it against reported_samples.
LatencyEvidence & apply_report_observation (LatencyEvidence &evidence)
 Fold a report-observation fact into already-measured evidence.
LatencyEvidence & apply_expected_samples (LatencyEvidence &evidence, int expected, std::int64_t tolerance_samples=0)
 Additionally require the latency to equal expected, pinning the value rather than only its self-consistency.
const char * to_string (LatencyReportStatus status)
const char * to_string (LatencyReportObservation observation)
const char * to_string (LatencyObservationMode mode)
const char * to_string (LatencyPolicy policy)
const char * to_string (LatencyMeasurementStatus status)
const char * to_string (LatencyContractOutcome outcome)
std::string latency_evidence_to_json (const LatencyEvidence &evidence)
 Serialize evidence to a JSON object string.
std::string latency_evidence_summary (const LatencyEvidence &evidence)
 One-line human summary, suitable for a test failure message.
ModeAnalysis 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 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 summarize (const ModeAnalysis &analysis)
 One-line human-readable summary of a mode analysis, for INFO() in a test.
std::string summarize (const MeasuredMode &mode)
InharmonicityResult 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 summarize (const InharmonicityResult &result)
CycleTrack 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 track_ar2 (std::span< const float > signal, double sample_rate, const Ar2TrackOptions &options={})
std::string summarize (const CycleTrack &track)
std::string summarize (const Ar2Track &track)
double 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 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.
double cents_between (double hz, double reference_hz)
 Musical interval between two frequencies in cents: 1200·log2(hz / reference).
PitchEstimate 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 estimate_pitch (std::span< const float > samples, double sample_rate, const PitchOptions &options={})
 Span overload — analyzes a single already-extracted channel.
PitchTrack track_pitch (std::span< const float > samples, double sample_rate, const PitchTrackOptions &options={})
 Slide a window across samples, estimating the fundamental at each hop.

Variables

constexpr std::int64_t kMetricsArtifactSchemaVersion = 1
 Current artifact schema version.
constexpr std::int64_t kDoctorCurveSchemaVersion = 1
 Current Doctor-curve schema version.
constexpr double kSilenceFloorDb = -200.0
 Floor used when converting a zero/negative linear level to dBFS, so summaries and messages stay finite.
constexpr double kDefaultKaiserBeta = 14.0
 Default Kaiser β.
constexpr int kLatencyEvidenceSchemaVersion = 1
 Schema version of the emitted object. Bump on any breaking field change.

Enumeration Type Documentation

◆ ComponentClass

How measure_aliasing classified one expected component of the ideal harmonic series h · f0.

Enumerator
harmonic 

h · f0 is below Nyquist: it lands on the f0 grid, where it belongs.

A legitimate part of the waveform, however loud.

alias 

h · f0 is above Nyquist: it CANNOT exist at its own frequency, so whatever energy sits at its fold-back site is aliasing — the artifact an anti-aliased oscillator exists to suppress.

◆ LatencyContractOutcome

The gating verdict.

Enumerator
satisfied 

Proven: the report matches the audio.

violated 

Disproven: they disagree.

inconclusive 

A measurement was requested but could not be drawn.

not_requested 

Neutral — nothing was claimed, so nothing failed.

◆ LatencyMeasurementStatus

What the probe concluded.

Enumerator
match 

Measured delay agrees with the report, within tolerance.

mismatch 

Measured delay disagrees with the report.

not_measurable 

The response cannot pin a delay down. See reason.

not_requested 

No measurement was asked for.

◆ LatencyObservationMode

How the report was watched while the render ran.

Enumerator
processor_flag_and_poll 

Direct Processor: poll + the latency-changed flag.

per_block_poll 

Hosted slot: poll between blocks (no flag surface).

none 

Not watched.

◆ LatencyPolicy

The probe used to turn audio into a delay measurement.

Enumerator
none 
delayed_passthrough_null 

Compare the output against the input delayed by D, sweeping D.

Requires the processor to be in a declared identity / bypass / fully-dry mode: the output must actually BE a delayed copy of the input. This is the strongest policy — it checks every sample, not just an onset.

marker_offset 

The author declares a unique input marker and any intrinsic response offset (e.g.

leading silence in a known IR). Measures output_marker - input_marker - intrinsic_offset.

◆ LatencyReportObservation

Whether the report held still for the duration of the render.

Enumerator
stable 

Same value at every observation point.

changed 

The value moved, or a change was flagged, mid-render.

unobservable 

The surface offers no way to watch it.

◆ LatencyReportStatus

Whether a latency report could be read at all, and whether it was sane.

Enumerator
available 

A value was read. It may legitimately be zero.

unsupported 

This format/host cannot expose latency. NOT a zero.

query_failed 

The query was attempted and errored.

invalid 

A value was read but is nonsensical (e.g. negative).

◆ ModalExtractionError

Enumerator
none 
empty_input 
input_too_short 
silent_input 
non_finite_input 
invalid_sample_rate 
invalid_options 
no_modes 

◆ Window

enum class pulp::test::audio::Window
strong

Analysis window.

The leakage policy each implies is part of the Analyzer Determinism Contract and is recorded in the curve artifact.

Choosing one is a dynamic-range decision, not a style preference. A window's first side lobe is the floor below which a small component sitting near a loud one cannot be seen: the analyzer reports its OWN leakage skirt instead of the signal. Measuring a −100 dB component next to a 0 dB fundamental through Hann (−31 dB side lobes) measures Hann.

First-side-lobe figures below are MEASURED by test_audio_doctor.cpp's side-lobe test (48 kHz, 16384-point FFT, half-bin-offset tone), not quoted from a table:

Window 1st side lobe Falloff Usable floor near a loud tone
rectangular −14 dB −6 dB/oct bin-coherent tones only
hann −31 dB −18 dB/oct ~−60 dBc
hamming −41 dB −6 dB/oct ~−45 dBc
blackman −57 dB −18 dB/oct ~−90 dBc
flat_top −93 dB ~flat ~−93 dBc (does NOT improve)
kaiser tunable by β steep −124 dBc at β = 14

Only kaiser reaches a −100 dBc gate. That is a measured result, not a preference: at 16 bins from a 0 dB fundamental a −100 dBc tone reads −82 dB through hann, −91 dB through blackman, and −96 dB through flat_top — every one of those is the window's own leakage, not the signal (the test proves it by re-measuring with the small tone REMOVED and getting the same number).

Note the falloff column outranks the first-side-lobe column with distance: hann's first side lobe is 62 dB worse than flat_top's, but hann falls at −18 dB/octave while flat_top is flat, so 64 bins out hann's floor (−117 dB) is already better than flat_top's (−101 dB). flat_top is for amplitude accuracy on a tone it CAN resolve, never for floor depth.

Coefficient math is NOT duplicated here: every window delegates to pulp::signal::WindowFunction (core/signal/windowing.hpp), the source of truth. See window_coefficients for the periodic/normalization contract.

Enumerator
rectangular 

Rectangular (no taper).

Zero scalloping/leakage ONLY when every tone of interest is bin-coherent (an integer number of cycles in the FFT length). Use for THD on a coherent tone; the harmonics then land on exact bins with no spectral spreading.

hann 

Periodic Hann (0.5 - 0.5*cos(2π n / N)).

−31 dB side lobes, wide main lobe; tolerant of non-coherent tones at the cost of bin resolution. Use for broadband response curves where the stimulus is not guaranteed bin-coherent. NOT usable below roughly −60 dBc near a loud tone — reach for blackman/flat_top/kaiser there.

hamming 

Periodic Hamming.

−41 dB first side lobe but a slow −6 dB/octave falloff, so its far skirt stays HIGHER than Hann's despite the better first lobe. Rarely the right pick here; prefer blackman or kaiser when the floor is what matters.

blackman 

Periodic Blackman.

−57 dB first side lobe with an −18 dB/octave falloff, so the skirt collapses quickly with distance from the tone. Good to roughly −90 dBc. NOT sufficient for a −100 dBc gate: at 16 bins from a loud tone its leakage is still ≈ −89 dB and a −100 dB component reads ≈ −91 dB. Reach for kaiser there.

flat_top 

Periodic flat-top (5-term).

A flat-topped main lobe makes a tone's measured amplitude accurate to ~0.01 dB regardless of where it falls between bins — that is what this window is for. Its side lobes are ≈ −93 dB with an essentially FLAT falloff, so unlike hann/blackman its floor barely improves with distance (still ≈ −101 dB 64 bins out, by which point hann is at −117 dB). Its ~10-bin main lobe also swallows anything closer than ~6 bins. Use for amplitude accuracy, never for floor depth — it is not adequate for a −100 dBc gate.

kaiser 

Periodic Kaiser, side-lobe level dialed by β (kaiser_beta on the options structs, default kDefaultKaiserBeta).

The only window here that reaches a −100 dBc gate — see kDefaultKaiserBeta. Trades main-lobe width for floor depth as β rises.

Function Documentation

◆ analyze() [1/2]

BufferMetrics pulp::test::audio::analyze ( const pulp::audio::Buffer< float > & buffer,
double sample_rate,
const AnalyzeOptions & options = {} )

Convenience overload for an owning Buffer.

◆ analyze() [2/2]

BufferMetrics pulp::test::audio::analyze ( const pulp::audio::BufferView< const float > & buffer,
double sample_rate,
const AnalyzeOptions & options = {} )

Analyze a const view. Pure arithmetic; safe for any buffer size.

◆ analyze_modes()

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).

Discovery nominates spectral peaks; each is then refined on a cents grid and given a T60 from a log-envelope slope fit over the stated dB window. Returns modes sorted by descending amplitude. A mode that could not be fit still appears — with confidence == 0 and a note — because "there is a peak at 830 Hz that does not decay exponentially" is a finding, not a failure.

◆ apply_expected_samples()

LatencyEvidence & pulp::test::audio::apply_expected_samples ( LatencyEvidence & evidence,
int expected,
std::int64_t tolerance_samples = 0 )

Additionally require the latency to equal expected, pinning the value rather than only its self-consistency.

Violates a passing result whose reported latency is not the intended one. Call after a measurement.

Use it to regression-pin a known-good latency: it is the only thing that catches a processor whose true delay AND report both moved together.

◆ apply_report_observation()

LatencyEvidence & pulp::test::audio::apply_report_observation ( LatencyEvidence & evidence)

Fold a report-observation fact into already-measured evidence.

A processor whose latency moved mid-render has no single "the" latency, so a fixed-latency proof cannot stand even if the measured and reported values happen to agree at the end — including when the value moves away and comes back. Call this after a measurement to demote such a result to inconclusive.

◆ assert_channels_independent() [1/2]

CheckResult pulp::test::audio::assert_channels_independent ( const pulp::audio::Buffer< float > & buffer)

Fails if any two channels are sample-identical (within 1e-9), which usually means a channel-routing bug duplicated one channel.

◆ assert_channels_independent() [2/2]

CheckResult pulp::test::audio::assert_channels_independent ( const pulp::audio::BufferView< const float > & buffer)

Fails if any two channels are sample-identical (within 1e-9), which usually means a channel-routing bug duplicated one channel.

◆ assert_frequency_near()

CheckResult pulp::test::audio::assert_frequency_near ( std::span< const float > samples,
double sample_rate,
double expected_hz,
double tolerance_cents = 5.0 )

Fails unless the zero-crossing frequency estimate is within tolerance_cents of expected_hz.

Estimator limits are documented on estimate_frequency; this also fails when no estimate is possible.

◆ assert_no_nan_inf()

CheckResult pulp::test::audio::assert_no_nan_inf ( const BufferMetrics & metrics)

Fails if any channel contains NaN or ±Inf samples.

◆ assert_not_clipped()

CheckResult pulp::test::audio::assert_not_clipped ( const BufferMetrics & metrics,
double ceiling_dbfs = -0.1 )

Fails if any channel's peak reaches ceiling_dbfs or any sample counted as clipped during analysis.

◆ assert_not_silent()

CheckResult pulp::test::audio::assert_not_silent ( const BufferMetrics & metrics,
double min_rms_dbfs = -60.0 )

Fails if every channel's RMS is below min_rms_dbfs.

◆ assert_null_near() [1/2]

CheckResult pulp::test::audio::assert_null_near ( const pulp::audio::Buffer< float > & a,
const pulp::audio::Buffer< float > & b,
double tolerance_dbfs = -120.0 )

Null test: fails if the per-sample residual |a−b| peak is at or above tolerance_dbfs.

Buffers must have identical shape. Convenience overload over owning buffers.

◆ assert_null_near() [2/2]

CheckResult pulp::test::audio::assert_null_near ( const pulp::audio::BufferView< const float > & a,
const pulp::audio::BufferView< const float > & b,
double tolerance_dbfs = -120.0 )

Null test: fails if the per-sample residual |a−b| peak is at or above tolerance_dbfs.

Buffers must have identical shape.

◆ assert_peak_between()

CheckResult pulp::test::audio::assert_peak_between ( const BufferMetrics & metrics,
double min_dbfs,
double max_dbfs )

Fails unless every channel's peak lies within [min_dbfs, max_dbfs].

◆ assert_rms_between()

CheckResult pulp::test::audio::assert_rms_between ( const BufferMetrics & metrics,
double min_dbfs,
double max_dbfs )

Fails unless every channel's RMS lies within [min_dbfs, max_dbfs].

◆ assert_silent()

CheckResult pulp::test::audio::assert_silent ( const BufferMetrics & metrics,
double threshold_dbfs = -90.0 )

Fails if any channel's RMS is at or above threshold_dbfs.

◆ cents_between()

double pulp::test::audio::cents_between ( double hz,
double reference_hz )

Musical interval between two frequencies in cents: 1200·log2(hz / reference).

Positive when hz is above reference. Returns kSilenceFloorDb-style sentinel behavior is NOT used here — both arguments must be > 0 (throws otherwise), because a cents value is only meaningful between two real pitches.

◆ compile_modal_spec()

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.

All storage and FFT work are offline/control-thread operations. Candidates are ranked by amplitude * confidence, ties by frequency, while the emitted spec is always frequency sorted. The candidate ledger retains accepted and rejected evidence for an auditable result.

◆ estimate_frequency()

FrequencyEstimate pulp::test::audio::estimate_frequency ( std::span< const float > samples,
double sample_rate )

Estimate the dominant frequency of one channel.

Estimator (declared per the Analyzer Determinism Contract): counts positive-going zero crossings with linear interpolation between the bracketing samples, then averages the crossing-to-crossing periods. Confidence is 1 − (period standard deviation / mean period), clamped to [0, 1]. Suited to near-periodic, single-pitch signals (test tones, oscillator output). Not a pitch detector for harmonically dense or noisy material — use the offline FFT analyzers for that. Needs at least three crossings; returns {0.0, 0.0} otherwise.

◆ estimate_pitch() [1/2]

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.

Accurate for harmonically-dense signals (saw, pulse, additive stacks) where estimate_frequency's zero-crossing method fails. Fails closed: throws std::invalid_argument for a non-positive sample rate or an analysis window that does not lie within the buffer; returns voiced = false (never a fabricated frequency) for silence or non-tonal content.

◆ estimate_pitch() [2/2]

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.

◆ fit_tone()

ToneFit pulp::test::audio::fit_tone ( std::span< const double > samples,
double cycles_per_sample )

Fit one sinusoid at cycles_per_sample (frequency ÷ sample rate; 0.5 is Nyquist) to samples by least squares.

Limits, all load-bearing:

  • The frequency must be known. This does not search. Feed it a frequency that is not present and it returns the amplitude of whatever the signal happens to correlate with there — near zero for unrelated content, but NOT a detection.
  • Degenerate at DC and at exactly Nyquist. At cycles_per_sample = 0 the sin basis vanishes; at exactly 0.5 it vanishes again (sin(π·n) ≡ 0) and only the cos basis (±1 alternating) carries information, so amplitude and phase cannot be separated. Throws for both.
  • Not a multi-tone separator. Two tones closer than roughly 1 / segment-duration are not orthogonal over the segment, so fitting one absorbs part of the other. Use fit_tones for a known set.

◆ fold_frequency()

double pulp::test::audio::fold_frequency ( double frequency_hz,
double sample_rate )

Where a component at frequency_hz LANDS once sampled at sample_rate.

Sampling maps the whole frequency axis onto [0, sample_rate/2] by wrapping at sample_rate and mirroring about Nyquist. This folds repeatedly, not once: at 48 kHz, 80 kHz (1.67·fs) wraps to 32 kHz and then mirrors to 16 kHz. Sign is ignored (a real signal's spectrum is symmetric).

Exact-boundary landings (0 and Nyquist) are returned as-is; they are degenerate to MEASURE (see fit_tone), not to compute.

◆ from_dbfs()

double pulp::test::audio::from_dbfs ( double dbfs)

dBFS to linear amplitude.

◆ latency_evidence_summary()

std::string pulp::test::audio::latency_evidence_summary ( const LatencyEvidence & evidence)

One-line human summary, suitable for a test failure message.

e.g. "reported 512, measured 448 (delta -64) — VIOLATED".

◆ latency_evidence_to_json()

std::string pulp::test::audio::latency_evidence_to_json ( const LatencyEvidence & evidence)

Serialize evidence to a JSON object string.

◆ magnitude_spectrum_curve()

ResponseCurve pulp::test::audio::magnitude_spectrum_curve ( const pulp::audio::BufferView< const float > & signal,
double sample_rate,
std::span< const double > checkpoints_hz,
const ResponseOptions & options = {} )

The signal's OWN magnitude spectrum, one channel, in dB relative to its own peak bin (loudest frequency = 0 dB, everything else negative).

Unlike response_relative_to_input this is NOT a transfer response — there is no reference stimulus — so it answers "what frequencies are present and how loud relative to the strongest" for an arbitrary captured file. DC (bin 0) is removed before analysis and excluded from the peak so a DC offset can't dominate the normalization. Use a leakage-tolerant window (Window::hann) for arbitrary, non-bin-coherent file content. Peak-normalization makes the result FFT-backend-stable (a ratio). analyzer is set to "magnitude_spectrum".

◆ measure_aliasing()

AliasReport pulp::test::audio::measure_aliasing ( const pulp::audio::BufferView< const float > & signal,
double fundamental_hz,
double sample_rate,
const AliasOptions & options = {} )

Classify signal (one channel of an already-rendered buffer) against the ideal harmonic series of fundamental_hz, and report the worst in-band alias.

The caller is responsible for the signal being a steady tone at fundamental_hz over the analysis segment; use analysis_offset to skip settling. Throws std::invalid_argument on a non-positive rate/frequency, a fundamental at or above Nyquist, a band qualifier outside (0, Nyquist], a series whose harmonics all sit below Nyquist (no alias site to measure — see AliasOptions::num_harmonics), a segment too short to fit the requested series, or a signal with no energy at the fundamental.

◆ measure_delayed_passthrough()

LatencyEvidence pulp::test::audio::measure_delayed_passthrough ( const pulp::audio::Buffer< float > & input,
const pulp::audio::Buffer< float > & output,
std::optional< int > reported_samples,
const DelayedNullOptions & options = {} )

Sweep the delay D that best explains output as a copy of input, and compare it against reported_samples.

The processor must be in a declared identity / bypass / fully-dry mode, so that its output genuinely is its input, delayed. Every sample is checked, so a one-sample misreport fails.

Returns evidence with the policy, measurement, and verdict filled in. report_status / reported_samples / observation fields are the caller's to supply — this function does not touch a processor. When reported_samples is null the delay is still measured and reported, but the outcome is inconclusive (there is nothing to check it against).

◆ measure_group_delay()

PhaseCurve pulp::test::audio::measure_group_delay ( const pulp::audio::BufferView< const float > & input,
const pulp::audio::BufferView< const float > & output,
double sample_rate,
std::span< const double > checkpoints_hz,
const GroupDelayOptions & options = {} )

Phase and group delay of output relative to input, one channel.

── Estimator ────────────────────────────────────────────────────────────── Group delay is the derivative τ(ω) = -dφ/dω, and differentiating a noisy, wrapped phase curve by finite differences is both fragile (it aliases whenever the true phase advances more than π between bins) and noisy (a difference amplifies the bin-to-bin error). This analyzer instead uses the Fourier/ramped-signal identity, which computes the derivative analytically rather than numerically:

τ(ω) = Re{ X_r(ω) · conj(X(ω)) } / |X(ω)|²   where X_r = FFT(n·x[n])

(from dX/dω = -j·FFT(n·x[n])). It is evaluated per bin on the reference and the output separately and subtracted — τ_H = τ_output − τ_input — because phase subtracts in a ratio and the derivative is linear. Three properties follow, and they are why this estimator was chosen:

  • No unwrapping is involved. Group delay is exact per bin and cannot be corrupted by an unwrapping mistake elsewhere in the curve.
  • No frequency step to choose. There is no finite-difference stride trading resolution against noise; the value at each bin is the true derivative of the segment's phase there.
  • Exact, not approximate, for any signal that lies entirely inside the rectangular analysis window. A truncated (still-ringing) IR is the one real error source: the estimator faithfully reports the group delay of the truncated signal. Budget fft_length so the response has decayed into the noise floor within it. Expected tolerance for a well-contained IR: within ~0.01 samples of truth for a pure delay or FIR, and dominated by IR truncation otherwise. The asserted tolerances in the tests state what was actually observed.

── Phase unwrapping ─────────────────────────────────────────────────────── phase_rad is reported separately and IS unwrapped, by the standard cumulative-offset method: walk the bins low to high and add the multiple of 2π that keeps each successive raw atan2 difference inside (−π, π]. This is exact only while the true phase advances less than π per bin, i.e. while group_delay < fft_length / 2 samples (the phase slope is −τ·2π/N per bin). Beyond that the phase aliases and unwrapping silently tracks the wrong branch — budget fft_length accordingly.

Unwrapping also cannot recover the branch across a magnitude null or a gap in the reference spectrum: there is no phase to read in between, so the accumulated offset carries an unresolved 2πk ambiguity past it. That is a precondition on the WALK, not on the destination bin, so defined — which asks only about a bin's own energy — cannot express it. phase_defined is the gate that does: it is true only for a bin the unwrap reached over an unbroken run of defined bins from DC, and phase_rad is NaN everywhere else, including at bins that are themselves perfectly well defined. So a band-limited or gapped reference yields real group delay at every bin it excites and phase only up to the first gap — which is the honest split. group_delay_samples does not inherit the ambiguity at all; it never consults the unwrapped phase.

The aliasing bound above (group_delay < fft_length / 2) is NOT gated: it is a budgeting duty on the caller, and per bin the analyzer cannot tell an aliased phase from a true one. It is stated, not enforced. The gap ambiguity is gated because it IS exactly testable.

── Stopband contract ────────────────────────────────────────────────────── Where the transfer magnitude is at the noise floor there is no signal whose phase could be measured, and atan2 of numerical noise yields a plausible- looking number that means nothing. Every bin whose magnitude is more than options.magnitude_floor_db below the curve's peak is therefore reported defined = false with NaN group delay, and NaN phase both there and at every bin above it that the unwrap could only reach through it. A bin whose reference (input) spectrum is itself negligible is undefined for the same reason.

That relative gate is backstopped by an absolute one, because the two fail in different places: the relative gate is measured against the curve's own peak, so a processor whose output is entirely silent has a peak that IS the numerical floor, putting every bin at 0 dB relative to it. Such a curve would report a flat, fully-defined, zero-delay response for a processor that emitted nothing. Bins whose absolute transfer magnitude is at the numerical floor are therefore undefined regardless of the relative reading, and a silent output yields a curve with no defined bins at all — which is the honest answer. The analyzer reports no group delay it did not measure.

Computed in double precision (Fft64) throughout: the estimator's numerator is a difference of products that can cancel heavily near a null, and the double path is also the backend-portable one.

◆ measure_inharmonicity()

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.

The fundamental is refined first, then each partial is searched around the stiff-string prediction using the B estimated from the partials found so far, so a stiff string's high partials are still found after they have drifted well sharp of n * f0.

◆ measure_marker_offset()

LatencyEvidence pulp::test::audio::measure_marker_offset ( const pulp::audio::Buffer< float > & input,
const pulp::audio::Buffer< float > & output,
std::optional< int > reported_samples,
const MarkerOffsetOptions & options = {} )

Measure delay as output_onset - input_marker - intrinsic_response_offset and compare it against reported_samples.

Weaker than the delayed-null policy — it checks one onset rather than every sample — but it works for processors that reshape the signal (a convolver, a filter) where a null comparison is meaningless. It refuses to measure when the input marker is not unique or the output has no detectable onset.

◆ measure_mode()

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.

Skips discovery entirely — use this when a test prescribed the mode and wants to read it back (the calibration path), or when a peak is known to be buried under a louder neighbour that discovery would nominate instead.

◆ measure_modulation_sidebands()

ModulationSidebandReport pulp::test::audio::measure_modulation_sidebands ( const pulp::audio::BufferView< const float > & signal,
double carrier_hz,
double modulation_hz,
double sample_rate,
const ModulationSidebandOptions & options = {} )

Classify signal (one channel of an already-rendered buffer) as a tone at carrier_hz modulated at modulation_hz, and report the energy at the modulation's own sidebands against the energy anywhere else in the band.

A modulation_hz of zero claims no sidebands, so every bin but the carrier's is residual: the control a modulated run is compared to.

The caller is responsible for the signal being steady over the analysis segment. Throws std::invalid_argument on a non-positive rate or carrier, a carrier at or above Nyquist, a negative modulation frequency, a band outside (0, Nyquist], or a segment that does not fit the signal.

◆ measure_thd()

ThdResult pulp::test::audio::measure_thd ( const pulp::audio::BufferView< const float > & signal,
double fundamental_hz,
double sample_rate,
const ThdOptions & options = {} )

Measure THD / THD+N of signal (one channel of an already-rendered buffer) at fundamental_hz.

The caller is responsible for ensuring the signal is a steady tone at that frequency over the analysis segment; coherent is computed from fundamental_hz, fft_length, and sample_rate and recorded for the reader to judge the tolerance.

Fails closed rather than fabricating a pass: throws std::invalid_argument for a fundamental at or above Nyquist (it would clamp to the Nyquist bin and read thd = 0), for a buffer that does not cover the analysis segment (see the determinism contract above), and for a signal with no energy at the fundamental bin — silence would otherwise measure thd = 0, letting a dead processor pass a distortion gate.

◆ metrics_to_json()

std::string pulp::test::audio::metrics_to_json ( const BufferMetrics & metrics,
std::string_view scenario )

Serialize metrics to a JSON string.

scenario is recorded verbatim as provenance (which test scenario produced this buffer).

◆ phase_curve_to_json()

std::string pulp::test::audio::phase_curve_to_json ( const PhaseCurve & curve,
std::string_view scenario )

Serialize a phase/group-delay curve to a JSON string.

A point carries the two gates PhaseCurve defines — "defined" and the stricter "phase_defined" — and omits the field each one gates when it is false: group_delay_samples / group_delay_seconds under defined, phase_rad under phase_defined. Their in-memory value is NaN, which JSON cannot represent, and writing a placeholder would hand a reader a number the analyzer never measured. A stopband point omits all three; a defined point above a gap in the reference spectrum carries the delay pair and omits phase_rad. Read the matching gate before reaching for a field — or just test presence, which says the same thing.

The magnitude key is magnitude_db_rel_peak, not the .response.json artifact's magnitude_db, because the two are different quantities: this one is normalized to the phase curve's own peak transfer magnitude, while .response.json's is the absolute out/in transfer ratio. A processor with 12 dB of passband gain reports +12 in the sibling artifact and 0 here.

◆ response_curve_to_json()

std::string pulp::test::audio::response_curve_to_json ( const ResponseCurve & curve,
std::string_view scenario )

Serialize a magnitude/response curve to a JSON string.

scenario is recorded verbatim as provenance.

◆ response_relative_to_input()

ResponseCurve pulp::test::audio::response_relative_to_input ( const pulp::audio::BufferView< const float > & input,
const pulp::audio::BufferView< const float > & output,
double sample_rate,
std::span< const double > checkpoints_hz,
const ResponseOptions & options = {} )

Compute the magnitude response of output relative to input, one channel.

input and output are already-rendered buffers (e.g. an impulse and the processor's impulse response, or a decoded reference and processed WAV). The output spectrum is divided by the input spectrum bin-by-bin, giving a true transfer magnitude in dB. Both segments use the same window so its shaping cancels in the ratio. No DC removal (an impulse's flat spectrum is the reference). checkpoints_hz are read from the nearest full-resolution bin.

◆ summarize() [1/6]

std::string pulp::test::audio::summarize ( const Ar2Track & track)

◆ summarize() [2/6]

std::string pulp::test::audio::summarize ( const BufferMetrics & metrics,
const FrequencyEstimate & frequency = {} )

Compact, agent-readable signal summary.

Multi-line text; stable field ordering for diffing.

◆ summarize() [3/6]

std::string pulp::test::audio::summarize ( const CycleTrack & track)

◆ summarize() [4/6]

std::string pulp::test::audio::summarize ( const InharmonicityResult & result)

◆ summarize() [5/6]

std::string pulp::test::audio::summarize ( const MeasuredMode & mode)

◆ summarize() [6/6]

std::string pulp::test::audio::summarize ( const ModeAnalysis & analysis)

One-line human-readable summary of a mode analysis, for INFO() in a test.

◆ thd_to_json()

std::string pulp::test::audio::thd_to_json ( const ThdResult & thd,
std::string_view scenario )

Serialize a THD result to a JSON string (including the harmonic breakdown).

◆ to_dbfs()

double pulp::test::audio::to_dbfs ( double linear)

Linear amplitude (1.0 == full scale) to dBFS, clamped to kSilenceFloorDb.

◆ to_string() [1/6]

const char * pulp::test::audio::to_string ( LatencyContractOutcome outcome)

◆ to_string() [2/6]

const char * pulp::test::audio::to_string ( LatencyMeasurementStatus status)

◆ to_string() [3/6]

const char * pulp::test::audio::to_string ( LatencyObservationMode mode)

◆ to_string() [4/6]

const char * pulp::test::audio::to_string ( LatencyPolicy policy)

◆ to_string() [5/6]

const char * pulp::test::audio::to_string ( LatencyReportObservation observation)

◆ to_string() [6/6]

const char * pulp::test::audio::to_string ( LatencyReportStatus status)

◆ tone_gain_db()

double pulp::test::audio::tone_gain_db ( std::span< const double > samples,
double cycles_per_sample,
double input_amplitude )

Gain of a tone at cycles_per_sample relative to input_amplitude, in dB: 20·log10(fitted_amplitude / input_amplitude).

The passband-flatness counterpart to tone_residual_db — same fit, different readout. Throws if input_amplitude is not > 0.

◆ tone_residual_db()

double pulp::test::audio::tone_residual_db ( std::span< const double > samples,
double cycles_per_sample )

Energy of samples NOT explained by a tone at cycles_per_sample, in dB relative to the fitted tone's own energy: 10·log10(residual / fitted).

This is the single-tone purity primitive. Because it subtracts the tone rather than windowing around it, it has no leakage skirt to fight — it resolves residuals below −100 dB on a signal whose tone is NOT bin-coherent and whose length is not a power of two, neither of which any FFT analyzer here can offer.

What it measures: EVERYTHING that is not the tone, lumped together — harmonics, aliases, noise, and hum are one number. It answers "how pure is this?", never "what is the impurity?". For that, measure_aliasing fits the whole expected grid and reports the offenders separately.

◆ track_ar2()

Ar2Track pulp::test::audio::track_ar2 ( std::span< const float > signal,
double sample_rate,
const Ar2TrackOptions & options = {} )

◆ track_cycles()

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.

Method: positive-going zero crossings with sub-sample linear interpolation give per-cycle periods (f0) and per-cycle peak envelopes; sigma is the negative slope of a sliding least-squares fit of the log envelope over 2 * sigma_half_window + 1 cycles; Q = pi * f0 / sigma.

This resolves the question a single T60 cannot: whether a resonator's decay rate co-varies with its own instantaneous state within one note. The calibration test proves it does not invent that co-variation out of a frequency glide.

◆ track_pitch()

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.

Throws std::invalid_argument for a non-positive rate, window, or hop, or a window longer than the signal. Unvoiced frames are kept in points (with voiced = false) so the caller sees where the estimator refused; the voiced_* accessors filter them out for the statistics stage.

◆ window_coefficients()

std::vector< double > pulp::test::audio::window_coefficients ( Window w,
int n,
double kaiser_beta = kDefaultKaiserBeta )

Coefficients of w for an n-sample analysis segment.

Two properties define this function, and both are load-bearing:

Periodic (DFT-even), not symmetric. Coefficient i uses denominator n, not n - 1, so the window wraps exactly the way the FFT assumes the segment does. pulp::signal::WindowFunction::generate is symmetric, so this generates n + 1 points and drops the last — that yields the periodic form exactly, with no coefficient math duplicated here.

Coherent-gain normalized. Coefficients are divided by window_coherent_gain(w), so every window has a DC gain of exactly 1 and a tone's measured magnitude is the same through any of them. Without this a window swap would silently move the 0 dB reference (flat-top attenuates by ~13 dB relative to rectangular), which would make a "−100 dB" claim meaningless. For rectangular the divisor is 1.0 and for hann it is 0.5 — both exact powers of two — so those two paths are bit-for-bit identical to the un-normalized coefficients they replaced, and every dB/ratio the analyzers report is unchanged.

◆ window_coherent_gain()

double pulp::test::audio::window_coherent_gain ( Window w,
int n,
double kaiser_beta = kDefaultKaiserBeta )

Coherent gain (a.k.a.

DC gain) of w over an n-sample segment: the mean of its coefficients, i.e. the factor by which the window scales a tone's peak-bin magnitude. This is the number window_coefficients divides out — see its normalization contract.

For rectangular (1.0) and hann (0.5) this is the exact analytic constant — an exact power of two, which is what preserves their bit-exact results. Every other window measures the true mean of its length-n coefficients (which is why this takes n), so the DC gain lands on exactly 1 regardless of how core/signal rounds its coefficient literals.

◆ window_name()

std::string pulp::test::audio::window_name ( Window w)

Human-readable window name for artifacts/messages ("rectangular", "hann", "hamming", "blackman", "flat_top", "kaiser").

◆ windowed_magnitude()

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 .

. start+length) evaluated at exactly freq_hz — the measurement primitive the refinement and envelope stages are built on. Evaluating at an exact frequency rather than a bin centre is what removes the FFT's scalloping error from every number this header reports. length is clamped to the available samples.

◆ write_metrics_artifact()

std::filesystem::path pulp::test::audio::write_metrics_artifact ( const BufferMetrics & metrics,
std::string_view scenario )

Write metrics_to_json() to <temp>/pulp-audio-metrics/<sanitized-scenario>.json (overwriting any previous run's artifact for the same scenario) and return the path.

Scenario names are sanitized to [A-Za-z0-9._-] for the filename only. Returns an empty path if the temp dir is unavailable or the open/write fails — callers should treat an empty path as "no artifact written" rather than reporting a phantom successful write.

◆ write_phase_artifact()

std::filesystem::path pulp::test::audio::write_phase_artifact ( const PhaseCurve & curve,
std::string_view scenario )

Write phase_curve_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.groupdelay.json and return the path.

◆ write_response_artifact()

std::filesystem::path pulp::test::audio::write_response_artifact ( const ResponseCurve & curve,
std::string_view scenario )

Write response_curve_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.response.json and return the path (overwriting any previous run's artifact for the same scenario).

◆ write_thd_artifact()

std::filesystem::path pulp::test::audio::write_thd_artifact ( const ThdResult & thd,
std::string_view scenario )

Write thd_to_json to <temp>/pulp-audio-doctor/<sanitized-scenario>.thd.json and return path.

Variable Documentation

◆ kDefaultKaiserBeta

double pulp::test::audio::kDefaultKaiserBeta = 14.0
inlineconstexpr

Default Kaiser β.

Chosen from measurement, not a formula. At 48 kHz / 16384 points against a 0 dB half-bin-offset fundamental (test_audio_doctor.cpp), β = 14 measures a leakage floor of −124 dB or lower at ≥ 12 bins from the tone and reads a −100 dBc component to within 0.4 dB. That clears the harness's "detection floor ≤ −110 dB" acceptance bar with ~14 dB of margin.

Why not lower: β = 12 measures only ≈ −102 dB immediately outside the main lobe — right at a −100 dBc gate, with no margin. Why not core/signal's generic β = 3 fallback: ≈ −36 dB, useless here. Raise β for a deeper floor and a wider main lobe; lower it for sharper resolution of close-in components you do not need −100 dB of range on.

That floor holds in the tone's NEIGHBORHOOD, not at the bottom of the spectrum: removing the mean of a non-integer-cycle tone leaves a DC-removal pedestal that no window touches — measured on the same fixture, bins 1–3 read ≈ −66 / −75 / −92 dB through kaiser β = 14. A component landing within a few bins of DC is pedestal-limited through ANY window; measure it with the projection path instead (measure_aliasing fits the constant jointly and reads a −100 dBc component exactly even under a 0.5 DC offset).

◆ kDoctorCurveSchemaVersion

std::int64_t pulp::test::audio::kDoctorCurveSchemaVersion = 1
inlineconstexpr

Current Doctor-curve schema version.

Bump when a field changes meaning or is removed; purely additive fields do not require a bump.

◆ kLatencyEvidenceSchemaVersion

int pulp::test::audio::kLatencyEvidenceSchemaVersion = 1
inlineconstexpr

Schema version of the emitted object. Bump on any breaking field change.

◆ kMetricsArtifactSchemaVersion

std::int64_t pulp::test::audio::kMetricsArtifactSchemaVersion = 1
inlineconstexpr

Current artifact schema version.

Bump when fields change meaning or are removed; purely additive fields do not require a bump.

◆ kSilenceFloorDb

double pulp::test::audio::kSilenceFloorDb = -200.0
inlineconstexpr

Floor used when converting a zero/negative linear level to dBFS, so summaries and messages stay finite.

-200 dB is far below any audible or asserted threshold.