Ep4Stats::rmsDbfs() computes the RMS about zero rather than about the sample mean, so any converter DC offset is carried into the result at full weight. That inflates the reported RMS, and since the crest factor is derived as peak − RMS, it deflates adcCrestDb.
The code
MetisProtocol.h, struct Ep4Stats, carries no sum member — only samples, peakAbs, sumSquares, clippedSamples — so the mean is not tracked and cannot be removed downstream:
struct Ep4Stats {
int samples = 0;
int peakAbs = 0; // 0..kEp4FullScale
double sumSquares = 0.0; // of raw codes, so rms shares peak's scale
int clippedSamples = 0;
MetisProtocol.cpp accumulates the raw code:
s.sumSquares += static_cast<double>(code) * static_cast<double>(code);
and Ep4Stats::rmsDbfs() divides by the sample count directly:
const double rms = std::sqrt(sumSquares / static_cast<double>(samples));
return 20.0 * std::log10(rms / static_cast<double>(kEp4FullScale));
For a signal with mean m and standard deviation σ, that yields √(m² + σ²) rather than σ.
Why it matters
Both adcCrestDb and bandscopeHeadroom() consume it. Crest factor is one of the few surfaces that distinguishes broadband noise from a discrete carrier — Gaussian noise over 2048 samples gives 11–12 dB, a sinusoid gives ~3 dB — and a DC pedestal pushes the reading toward the sinusoid end regardless of what the RF is actually doing.
This is not hypothetical; it cost us a diagnosis. Reading adcCrestDb = 3.71 dB on a 50 Ω dummy load, we concluded the input was a near-sinusoidal carrier at roughly −36 dBm. It was not. Solving peak = m + 3.4σ against rms = √(m² + σ²) shows a DC pedestal with σ/m = 0.163 fits that crest exactly as well as a sinusoid with σ/A = 0.025. The statistic cannot choose between them, and we spent a run's worth of effort on a characterisation the number could not support.
Suggested fix
Add a double sum to Ep4Stats, accumulate s.sum += code, merge it alongside sumSquares, and compute the variance form:
const double mean = sum / samples;
const double var = std::max(0.0, sumSquares / samples - mean * mean);
const double rms = std::sqrt(var);
merge() stays a plain addition for sum, exactly as it already is for sumSquares, so block-from-packets merging is unaffected.
Two things worth deciding rather than assuming, which is why this is an issue and not a PR:
- Whether
peakAbs should also become mean-referred. Leaving it absolute while RMS becomes AC-coupled changes the meaning of their difference. Probably both should be AC-referred for the crest to mean what its name says, but that is a call about what adcCrestDb is for.
- Whether the DC itself is worth surfacing. If there is a real converter offset, a reported
adcDcDbfs would be more useful than silently removing it — and would have answered our question directly.
Happy to send the patch once someone states a preference on (1).
Provenance
Found while investigating an HL2 receive noise floor on a terminated antenna port. Code read on origin/main; the struct, the accumulation site and rmsDbfs() were each checked rather than inferred. No claim here rests on the hardware measurement — the defect is visible in the source alone.
🤖 Generated with Claude Code
Ep4Stats::rmsDbfs()computes the RMS about zero rather than about the sample mean, so any converter DC offset is carried into the result at full weight. That inflates the reported RMS, and since the crest factor is derived as peak − RMS, it deflatesadcCrestDb.The code
MetisProtocol.h,struct Ep4Stats, carries nosummember — onlysamples,peakAbs,sumSquares,clippedSamples— so the mean is not tracked and cannot be removed downstream:MetisProtocol.cppaccumulates the raw code:and
Ep4Stats::rmsDbfs()divides by the sample count directly:For a signal with mean
mand standard deviationσ, that yields√(m² + σ²)rather thanσ.Why it matters
Both
adcCrestDbandbandscopeHeadroom()consume it. Crest factor is one of the few surfaces that distinguishes broadband noise from a discrete carrier — Gaussian noise over 2048 samples gives 11–12 dB, a sinusoid gives ~3 dB — and a DC pedestal pushes the reading toward the sinusoid end regardless of what the RF is actually doing.This is not hypothetical; it cost us a diagnosis. Reading
adcCrestDb= 3.71 dB on a 50 Ω dummy load, we concluded the input was a near-sinusoidal carrier at roughly −36 dBm. It was not. Solvingpeak = m + 3.4σagainstrms = √(m² + σ²)shows a DC pedestal withσ/m = 0.163fits that crest exactly as well as a sinusoid withσ/A = 0.025. The statistic cannot choose between them, and we spent a run's worth of effort on a characterisation the number could not support.Suggested fix
Add a
double sumtoEp4Stats, accumulates.sum += code, merge it alongsidesumSquares, and compute the variance form:merge()stays a plain addition forsum, exactly as it already is forsumSquares, so block-from-packets merging is unaffected.Two things worth deciding rather than assuming, which is why this is an issue and not a PR:
peakAbsshould also become mean-referred. Leaving it absolute while RMS becomes AC-coupled changes the meaning of their difference. Probably both should be AC-referred for the crest to mean what its name says, but that is a call about whatadcCrestDbis for.adcDcDbfswould be more useful than silently removing it — and would have answered our question directly.Happy to send the patch once someone states a preference on (1).
Provenance
Found while investigating an HL2 receive noise floor on a terminated antenna port. Code read on
origin/main; the struct, the accumulation site andrmsDbfs()were each checked rather than inferred. No claim here rests on the hardware measurement — the defect is visible in the source alone.🤖 Generated with Claude Code