Skip to content

A spectrum without its uncertainty is half a measurement. This page covers the Welch spectral estimators of phonometry.signals that report, next to the spectrum itself, the statistical quality of the estimate following Bendat & Piersol, Random Data: Analysis and Measurement Procedures (4th ed., 2010): the power spectral density and cross-spectral density with the effective number of averages, the normalized random error and chi-square confidence intervals; the coherent output spectrum that splits a measured output into the part linearly explained by the input and the noise remainder, with the spectral signal-to-noise ratio; a fractional-octave smoother with a constant-power kernel; and colored-noise generators with an exact power-law slope for exercising all of the above. A Thomson multitaper estimator (Percival & Walden, 1993) completes the family for records too short to segment. Every error formula is a closed form from the sources, verified by seeded Monte Carlo in the test suite.

1. Power spectral density with its statistical error

Section titled “1. Power spectral density with its statistical error”

power_spectral_density estimates the one-sided autospectral density by Welch’s method: the record is split into tapered (Hann by default), 50 %-overlapped segments whose periodograms are averaged. No detrending is applied, so absolute calibration is preserved: a signal in pascals yields Pa²/Hz. Two scalings are available: 'density' (units²/Hz, integrates to the signal power) and 'spectrum' (units², reads the power of discrete tones directly).

The price of preserving the calibration is that any DC offset or slow drift stays in the record and leaks. The offset lands in the DC bin, and the Hann taper’s sidelobes spread a fraction of it over the first few bins, so an uncorrected offset appears as a spurious low-frequency rise that no amount of averaging removes. When the lowest bins matter, subtract the mean or high-pass the record explicitly before the call — that is a change to the signal, not a setting on the estimator — and note that the same offset is invisible in a fractional-octave display, because no band reaches DC. The same condition returns in Data qualification, whose level-crossing and peak statistics are written for a zero-mean process.

The estimator is a fixed pipeline: the window sets the resolution bandwidth, and the effective averages follow from the record length (the raw segment count) together with the window and overlap. Once those are fixed every quality figure follows from the main design choice, the segment length. The diagram traces it with the numbers of this page’s example.

Block diagram of the Welch PSD pipeline: a 20 second pink noise record at 48 kilohertz is split into 50 percent overlapped segments of 4096 samples giving 467 segments and an 11.7 hertz bin spacing, each segment is Hann tapered for a resolution bandwidth of 17.6 hertz, the one-sided squared FFT periodograms are averaged into 442 effective averages, and the result is Gxx of f with a chi-square confidence interval, a random error of 1 over the square root of n d equal to 4.8 percent and about 885 degrees of freedom; a final note states the trade-off that longer segments buy resolution but spend averagesBlock diagram of the Welch PSD pipeline: a 20 second pink noise record at 48 kilohertz is split into 50 percent overlapped segments of 4096 samples giving 467 segments and an 11.7 hertz bin spacing, each segment is Hann tapered for a resolution bandwidth of 17.6 hertz, the one-sided squared FFT periodograms are averaged into 442 effective averages, and the result is Gxx of f with a chi-square confidence interval, a random error of 1 over the square root of n d equal to 4.8 percent and about 885 degrees of freedom; a final note states the trade-off that longer segments buy resolution but spend averages

Averaging independent segments gives the estimate chi-square degrees of freedom (Eq. 8.162), from which everything else follows:

With overlapped, tapered segments the averages are correlated, so the result reports both the raw segment count (n_segments) and the effective number of independent averages (n_averages), computed with the window-correlation formula of Welch (1967) that Bendat & Piersol reference in Section 11.5.2.2; for a Hann taper at 50 % overlap it is roughly 0.95 of the raw count. The random error and the confidence interval use the effective value. At DC, and at Nyquist for an even segment length, the one-sided spectrum has a single real Fourier component, so those bins carry half the degrees of freedom and a correspondingly wider interval.

from phonometry import power_spectral_density
# record: one channel of a calibrated capture, in pascals (see the Calibration
# guide below); in dBFS work it is the raw [-1, 1] array and the result is
# FS^2/Hz. fs: its sample rate in Hz.
res = power_spectral_density(record, fs) # Hann, 50 % overlap, 95 % CI
print(res.n_averages, res.random_error) # nd and 1/sqrt(nd)
print(res.ci_lower[10], res.psd[10], res.ci_upper[10])
res.plot() # PSD in dB with the CI band

The resolution bias is the other half of the error budget: a finite analysis bandwidth (reported as resolution_bandwidth, the effective noise bandwidth of the taper) smooths sharp spectral features, always in the direction of reduced dynamic range (Eq. 8.139). For a resonance peak of half-power bandwidth , the first-order normalized bias is the closed form of Eq. 8.141, exposed as resolution_bias_error:

from phonometry import resolution_bias_error
eps_b = resolution_bias_error(res.resolution_bandwidth, 25.0) # Br = 25 Hz peak

Narrow (long segments) suppresses the bias but leaves fewer averages and a larger random error; the two requirements on segment length pull in opposite directions, which is exactly the trade-off the reported numbers make visible.

Choosing the segment length, and then the record length

Section titled “Choosing the segment length, and then the record length”

The bias formula is more useful read backwards. Keeping a resonance peak’s bias under 1 dB means , hence ; keeping it under a tenth of a decibel means . The familiar rule of thumb — three or four analysis bandwidths across the half-power width of the narrowest feature that matters — is exactly this formula, and now it has a number attached to it.

Work forward from the measurement rather than from a habitual nperseg. A mode at 100 Hz with has Hz. Holding its bias under 2 % (0.09 dB) needs Hz, and for the Hann taper , so at 48 kHz that is nperseg , i.e. segments of at least 3.1 s. Asking for a 10 % random error then needs effective averages, which at 50 % overlap means a record of about 150 s:

target_bias = 0.02 # 2 % on the peak, about 0.09 dB
b_r, q = 100.0 / 50.0, 100 # a 100 Hz mode with Q = 50
b_e = b_r * (3 * target_bias) ** 0.5 # the analysis bandwidth it allows
nperseg = 1.5 * 48000 / b_e # Hann: Be = 1.5 fs / nperseg
record_s = q * 0.5 * nperseg / 48000 # nd = 100 averages at 50 % overlap
print(round(b_e, 2), int(nperseg), round(record_s)) # 0.49 146969 153
Left: the Welch estimate of a 25 hertz wide resonance at 1 kilohertz computed with segment lengths of 512, 1024, 4096 and 16384 samples; the short segments clip and broaden the peak while the long ones resolve it. Right: the resolution bias and the random error in decibels against segment length on a logarithmic axis, the bias falling steeply and the random error rising slowly, crossing near 7400 samples, with the measured peak deficit following the biasLeft: the Welch estimate of a 25 hertz wide resonance at 1 kilohertz computed with segment lengths of 512, 1024, 4096 and 16384 samples; the short segments clip and broaden the peak while the long ones resolve it. Right: the resolution bias and the random error in decibels against segment length on a logarithmic axis, the bias falling steeply and the random error rising slowly, crossing near 7400 samples, with the measured peak deficit following the bias

The same 25 Hz-wide resonance seen at four segment lengths. At nperseg = 512 the analysis bandwidth is 140.6 Hz, five times the resonance width, and the peak reads about 8 dB low and far too broad; by 4096 the bandwidth is 17.6 Hz and the peak is essentially resolved. The right panel puts the two errors on one axis: the bias falls as and the random error rises as , and they are equal near nperseg = 7400 for this resonance — which is the segment length to choose if there is no reason to favour one error over the other.

The two decisions are independent: the segment length is set by the sharpest feature that matters, the record length by the precision you want. Lengthening the segments without lengthening the record trades one error for the other and buys nothing. And 150 s of record is 150 s over which the process has to stay stationary — which is a claim, not an assumption, and the one Data qualification decides. When the record simply cannot be that long, the multitaper estimator of section 7 is the escape.

power_spectral_density scales whatever it is given and applies no calibration of its own, so the sensitivity factor goes on the record, before the estimate:

calibration_factor = 1.002 # Pa per digital unit, from the calibrator
pressure = calibration_factor * record
psd = power_spectral_density(pressure, fs) # now in Pa^2/Hz

The ordinate is then dB re (20 µPa)²/Hz, and with scaling="spectrum" the same call reads a discrete tone’s level directly as dB. To get from there to the band level an acoustic report contains, integrate the density over the band — sum the bins and multiply by the bin spacing — and divide by ; fractional_octave_smoothing in section 4 is a display smoother and not a substitute for that integration, as that section spells out. The factor itself comes from Calibration and dBFS.

Welch power spectral density of pink noise in dB per Hz over 20 Hz to 20 kHz, with the 95 percent chi-square confidence band shaded around the estimate, the 1/3-octave smoothed curve on top and the exact -3.01 dB per octave power law as a dashed reference lineWelch power spectral density of pink noise in dB per Hz over 20 Hz to 20 kHz, with the 95 percent chi-square confidence band shaded around the estimate, the 1/3-octave smoothed curve on top and the exact -3.01 dB per octave power law as a dashed reference line

What 20 s of record buys, read off the figure. 467 segments of 4096 samples give 442 equivalent averages, a normalized random error of 0.047 and a 95 % chi-square band 0.81 dB wide — so the ragged estimate is never more than about 0.8 dB from the smoothed curve anywhere in the decade, and the shaded band, not the ripple, is the honest statement of what is known. Both curves fit −3.007 dB/octave against the exact −3.01 dB/octave of pink noise. Smoothing narrows nothing: it redraws the same estimate, and the interval below it still applies.

Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from phonometry import (
fractional_octave_smoothing,
noise_signal,
power_spectral_density,
)
fs = 48000.0
x = noise_signal(fs, 20.0, color="pink", seed=11)
res = power_spectral_density(x, fs, nperseg=4096)
band = (res.frequencies >= 20.0) & (res.frequencies <= 20000.0)
freqs = res.frequencies[band]
smooth = fractional_octave_smoothing(res.frequencies, res.psd, 3.0)[band]
fig, ax = plt.subplots(figsize=(10, 6))
ax.fill_between(freqs, 10 * np.log10(res.ci_lower[band]),
10 * np.log10(res.ci_upper[band]), alpha=0.3,
label="95 % chi-square confidence interval")
ax.semilogx(freqs, 10 * np.log10(res.psd[band]), lw=1.0,
label="Welch PSD estimate")
ax.semilogx(freqs, 10 * np.log10(smooth), lw=2.2,
label="1/3-octave smoothed")
ax.set_xlabel("Frequency [Hz]")
ax.set_ylabel("PSD [dB re 1/Hz]")
ax.legend()
plt.show()

cross_spectral_density estimates the complex between two channels with the same Welch core, and reports the ordinary coherence together with the Bendat & Piersol random errors of the magnitude and phase (Eqs. 9.33 and 9.52, with the measured coherence in place of the unknown true value, as the book recommends for measured data):

Both shrink as the coherence approaches one: a strongly coherent pair needs far fewer averages for the same confidence. The phase is unwrapped, so its slope against frequency is the group delay ; for a pure delay path the phase is linear and that slope reads the propagation delay directly.

from phonometry import cross_spectral_density
res = cross_spectral_density(x, y, fs)
print(res.magnitude_random_error[100], res.phase_std[100]) # bin 100 errors
res.plot() # magnitude, phase with ±sigma band, coherence
Two panels for the cross-spectral density of a two-sensor path with a 2 millisecond delay: the Welch magnitude estimate fluctuating around a flat level, and below it the unwrapped cross-spectrum phase falling as a straight line on a logarithmic frequency axis, lying exactly on the dashed minus two pi f tau reference with a narrow one-sigma band around itTwo panels for the cross-spectral density of a two-sensor path with a 2 millisecond delay: the Welch magnitude estimate fluctuating around a flat level, and below it the unwrapped cross-spectrum phase falling as a straight line on a logarithmic frequency axis, lying exactly on the dashed minus two pi f tau reference with a narrow one-sigma band around it

The cross-spectral density of a 2 ms delay path: the unwrapped phase is exactly the line , so its slope reads the propagation delay directly, and the ±1 s.d. band of Eq. 9.52 quantifies how far to trust it per frequency.

Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from phonometry import cross_spectral_density, noise_signal
fs = 8000.0
tau = 0.002 # 2 ms = 16 samples
delay = int(tau * fs)
x = noise_signal(fs, 8.0, seed=8)
noise = noise_signal(fs, 8.0, rms=0.3, seed=9)
y = 0.9 * np.concatenate([np.zeros(delay), x[:-delay]]) + noise
res = cross_spectral_density(x, y, fs)
# One line — magnitude, phase with its ±sigma band and coherence:
res.plot()
plt.show()
# By hand, from the fields the result carries:
band = (res.frequencies >= 20) & (res.frequencies <= 3500)
freqs = res.frequencies[band]
fig, (ax_m, ax_p) = plt.subplots(2, 1, sharex=True)
ax_m.semilogx(freqs, 10 * np.log10(res.magnitude[band]),
label="|Gxy| (Welch estimate)")
ax_m.set_ylabel("Magnitude [dB]")
ax_p.semilogx(freqs, res.phase[band], label="Unwrapped phase")
ax_p.fill_between(freqs, res.phase[band] - res.phase_std[band],
res.phase[band] + res.phase_std[band], alpha=0.25,
label="±1 s.d. (Eq. 9.52)")
ax_p.semilogx(freqs, -2 * np.pi * freqs * tau, "r--",
label="slope -2·pi·f·tau")
ax_p.set(xlabel="Frequency [Hz]", ylabel="Phase [rad]")
for ax in (ax_m, ax_p):
ax.legend()
plt.show()

3. Coherent output spectrum and spectral SNR

Section titled “3. Coherent output spectrum and spectral SNR”

In the single-input/single-output model the measured output autospectrum splits exactly into the part linearly explained by the input and the uncorrelated remainder (Eqs. 9.55–9.57):

coherent_output_spectrum returns all three spectra, the spectral signal-to-noise ratio (linear and in dB) and the random error of the coherent output estimate (Eq. 9.73), plus the first-order propagation of the coherence error through the SNR:

For additive uncorrelated output noise of known level the coherence has the closed form , which makes the whole chain verifiable with a synthetic signal:

import numpy as np
from phonometry import coherent_output_spectrum, noise_signal
fs = 48000.0
x = noise_signal(fs, 8.0, color="white", seed=1)
noise = noise_signal(fs, 8.0, color="white", rms=0.5, seed=2)
y = 0.8 * x + noise # SNR = 0.64/0.25 at every frequency
res = coherent_output_spectrum(x, y, fs)
print(np.median(res.coherence)) # -> SNR/(1+SNR) = 0.719
print(np.median(res.snr_db)) # -> 10·lg(2.56) = 4.1 dB
res.plot() # Gyy, Gvv, Gnn and the SNR panel
Two panels for the coherent output spectrum of a white noise system with additive noise: the measured output spectrum with the coherent part about 1.5 decibels below it and the uncorrelated noise floor about 6 decibels lower still, and below them the spectral signal-to-noise ratio fluctuating around the dashed closed-form line at 4.1 decibelsTwo panels for the coherent output spectrum of a white noise system with additive noise: the measured output spectrum with the coherent part about 1.5 decibels below it and the uncorrelated noise floor about 6 decibels lower still, and below them the spectral signal-to-noise ratio fluctuating around the dashed closed-form line at 4.1 decibels

The exact split of the snippet’s model: explains the output except for the flat noise remainder , and the spectral SNR scatters around its closed form dB at every frequency.

Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from phonometry import coherent_output_spectrum, noise_signal
fs = 48000.0
x = noise_signal(fs, 8.0, color="white", seed=1)
noise = noise_signal(fs, 8.0, color="white", rms=0.5, seed=2)
y = 0.8 * x + noise # SNR = 0.64/0.25 per band
res = coherent_output_spectrum(x, y, fs, nperseg=2048)
# One line — the three spectra and the SNR panel:
res.plot()
plt.show()
# By hand, from the fields the result carries:
band = (res.frequencies >= 20) & (res.frequencies <= 20000)
freqs = res.frequencies[band]
fig, (ax_g, ax_s) = plt.subplots(2, 1, sharex=True)
for values, style, label in ((res.output_psd, "-", "Gyy (measured)"),
(res.coherent_psd, "--", "Gvv (coherent)"),
(res.noise_psd, ":", "Gnn (noise)")):
ax_g.semilogx(freqs, 10 * np.log10(values[band]), style, label=label)
ax_g.set_ylabel("Spectral density [dB re 1/Hz]")
ax_s.semilogx(freqs, res.snr_db[band], label="Spectral SNR [dB]")
ax_s.axhline(10 * np.log10(0.64 / 0.25), color="r", linestyle="--",
label="closed form 4.1 dB")
ax_s.set(xlabel="Frequency [Hz]", ylabel="SNR [dB]")
for ax in (ax_g, ax_s):
ax.legend()
plt.show()

The coherence_bias field reports the small positive bias of the coherence estimate, (Eq. 9.75), negligible once reaches a few hundred, and another reason to average generously before trusting a low coherence.

Sections 2 and 3 both start from a pair of records, and everything they report is only as good as the pair. Two halves: how to acquire it, and how to read a coherence that comes back below one.

Acquisition. Use one interface with simultaneously sampled inputs, or measure the inter-channel skew and remove it. A converter that sequences its channels puts a fixed skew straight into the cross-spectrum phase and therefore into every group delay read off it: one sample of skew at 48 kHz is 20.8 µs, which is 15° of phase at 2 kHz and a flat 20.8 µs error on any delay estimate. Set both gains before the run and do not touch them, because the coherent output split of section 3 assumes the two channels share a fixed scale. Validate the pair once with a zero-baseline run: feed both channels the same signal — the generator split into both inputs, or the two microphones side by side — and confirm that the measured delay is a small fraction of a sample and the coherence is unity across the band. That one run separates a chain problem from a physics problem for every measurement afterwards.

Reading a coherence below one. It is not automatically an SNR problem, and Bendat & Piersol name five causes with different fixes:

What you seeThe causeThe fix
Coherence flat across the band, consistent with uncorrelated noise at either sensor — the modelled case of section 3average more, raise the level, get closer
Coherence falling progressively with frequency, worse as the record is segmented shortera bulk propagation delay comparable with the segment lengthalign the records first, or raise nperseg well above the delay — see Correlation and delay
Sharp dips exactly at the resonancesresolution bias: the analysis bandwidth smears the peak differently in the two channelslengthen the segment; the formula of section 1 says by how much
Low at the harmonics of a strong tone while broadband coherence stays highnon-linearity, a rattle or a clipped channelfix the path, or work below the level at which it appears
Low over a band with no obvious structurea second, uncorrelated source is also driving the outputthe MISO page: one input cannot explain two sources

Two closing cautions. The coherence estimate is biased high by , so a low coherence read from a handful of averages is even lower than it looks. And read the other way, a coherence near one is the acceptance criterion that makes the whole estimate reportable: quote it band by band beside the transfer function, because the bands where it falls are exactly the bands where the numbers above it mean nothing.

fractional_octave_smoothing averages a spectrum over a rectangular window of constant relative width: 1/n octave, around each frequency. This is the constant-percentage resolution bandwidth that Bendat & Piersol recommend for the spectra of resonant systems (Section 8.5.3), and the de facto standard for presenting loudspeaker and room responses. The average is always computed on power (amplitudes are squared first, dB levels converted and back), so band power is conserved rather than amplitude, and a flat spectrum passes through exactly unchanged.

The three domain= values name what the ordinate you hand it is, because the averaging is always done on power and the conversion back has to know where it started: a density or a power spectrum ("power", the default), a linear magnitude such as a frequency-response ("amplitude", squared first), or a curve already in decibels ("db", converted and converted back).

from phonometry import fractional_octave_smoothing
freqs = res.frequencies # from the section 1 estimate
smooth_psd = fractional_octave_smoothing(freqs, res.psd, 3.0)
magnitude = np.sqrt(res.psd) # any linear |H| would do here
smooth_mag = fractional_octave_smoothing(freqs, magnitude, 6.0,
domain="amplitude") # an FRF |H|
levels = 10.0 * np.log10(res.psd) # any dB curve would do here
smooth_db = fractional_octave_smoothing(freqs, levels, 3.0, domain="db")

A single spectral line with PSD ordinate (units²/Hz) in a bin of width smooths to the closed-form level over one kernel width, the oracle pinned in the tests.

Smoothing changes the resolution of the estimate, never its units. The output of fractional_octave_smoothing is a density in units²/Hz, exactly as the input was — flat white noise smooths to the same flat density — whereas the one-third-octave band levels of that same white noise rise 3 dB per band. Overlay a smoothed PSD on a filter-bank spectrum without converting and the two disagree by 20 to 30 dB, and neither is wrong.

The conversion is one term. A fractional-octave band of centre frequency has bandwidth , that is in one third of an octave and in a full octave, so

which at 1 kHz in one-third octave adds dB to the density level, and 3 dB more for every octave upward. Summing the PSD bins across the band and multiplying by the bin spacing is the exact form; the expression above is its flat-within-the-band approximation.

Which presentation belongs where: the smoothed density for comparing loudspeaker and room magnitude responses, where the eye wants a curve at constant relative resolution; the band level for anything that faces a criterion, or that has to agree with octave_filter and OctaveFilterBank — see Levels.

noise_signal produces Gaussian noise whose PSD follows exactly in expectation: seeded white noise is shaped in the frequency domain by the exact magnitude response bin by bin (a zero-phase filter applied circularly), so a measured slope deviates from the power law only by the random error of the spectral estimate, not by the piecewise or few-pole pink approximations whose slope ripples by fractions of a dB. The record is zero-mean and rescaled to the requested RMS exactly, and the same seed reproduces the same record bit for bit.

colorPSD slope
white00 dB/octave
pink-1-3.01 dB/octave
red (Brownian)-2-6.02 dB/octave
blue+1+3.01 dB/octave
violet+2+6.02 dB/octave
from phonometry import noise_signal
pink = noise_signal(48000, 10.0, color="pink", seed=7) # deterministic
white = noise_signal(48000, 10.0, color="white", rms=0.5, seed=7)

Measured over three decades (20 Hz – 20 kHz) with the estimator of section 1, the regression slope of each color lands within a few thousandths of a dB/octave of the exact value; the conformance suite pins the pink slope at -3.0116 against the exact -3.0103.

Those slopes are density slopes, and they are not what a fractional-octave display of the same signal shows. A band’s width grows in proportion to its centre frequency, so a band level is the density plus with : every colour reads 3 dB per octave higher in a band display than its density slope suggests. White noise, whose density is flat, therefore rises 3 dB per octave through an octave-band filter bank, and pink noise, at −3 dB/octave, comes out flat — which is why pink is the reference stimulus for room and loudspeaker work, and why an octave-band spectrum of white noise sloping upward is not a bug. Red noise falls 3 dB per octave in a band display and blue rises 6. The same +3 dB per octave applies to any density-versus-band comparison in this library; section 4 above gives the exact conversion, and Levels is where the band side lives.

Welch spectral densities of the five noise colours over 20 hertz to 20 kilohertz, each normalised to its own 1 kilohertz level so the five straight lines fan out from a single point: violet rising at 6 dB per octave, blue at 3, white flat, pink falling at 3 and red at 6, each with its exact power law dashed underneath and the measured regression slope in the legendWelch spectral densities of the five noise colours over 20 hertz to 20 kilohertz, each normalised to its own 1 kilohertz level so the five straight lines fan out from a single point: violet rising at 6 dB per octave, blue at 3, white flat, pink falling at 3 and red at 6, each with its exact power law dashed underneath and the measured regression slope in the legend

The five generators over three decades, each normalised to its own 1 kHz level so the slopes fan out from one point, with the exact power law dashed underneath. The measured regression slopes land within four thousandths of a decibel per octave of the exact values, and the residual is the random error of the estimator, not of the generator: the same seed reproduces the record bit for bit.

Every estimator on this page accepts any window scipy.signal.get_window knows, but the choice is a quantified trade-off, not a preference. window_metrics computes the figures of merit Harris (1978) tabulated, for any taper and length, sampled DFT-even exactly as the Welch estimators apply it:

  • ENBW (equivalent noise bandwidth, in bins): how much wider than one bin the effective analysis bandwidth is. This is the same number the PSD result reports as resolution_bandwidth (ENBW·fs/nperseg in Hz), and it enters directly in the tone/noise trade: a broadband noise floor read from a windowed spectrum sits dB above the true density.
  • Coherent gain: the DC gain a bin-centered tone is scaled by.
  • Scalloping loss: the worst-case attenuation of a tone that falls midway between two bins (3.92 dB for rectangular, 1.42 dB for Hann).
  • Worst-case processing loss: scalloping plus , the worst-case reduction in output SNR for tone detection in white noise.
  • Highest sidelobe and main-lobe -3 dB width: leakage floor versus resolution.
from phonometry import window_metrics
m = window_metrics("hann", 2048)
print(m.enbw_bins) # 1.5, exactly
print(m.scalloping_loss_db) # 1.42 dB
print(m.highest_sidelobe_db) # -31.5 dB
m.plot() # window + spectrum with metrics marked

The closed forms anchor the tests: ENBW is exactly 1 for rectangular, 3/2 for Hann, 1987/1458 for Hamming and 1523/882 for Blackman (DFT-even sampling), and the rectangular scalloping loss is , the Dirichlet kernel evaluated half a bin off center.

Spectra of the rectangular, Hann, Hamming and Blackman windows over 16 DFT bins, showing the trade-off between main-lobe width and sidelobe level, with each window's equivalent noise bandwidth and highest sidelobe level in the legendSpectra of the rectangular, Hann, Hamming and Blackman windows over 16 DFT bins, showing the trade-off between main-lobe width and sidelobe level, with each window's equivalent noise bandwidth and highest sidelobe level in the legend

The trade in one picture, and it costs 44.8 dB of leakage floor to buy 0.73 of a bin. Going from rectangular to Blackman drops the highest sidelobe from −13.3 dB to −58.1 dB while the ENBW widens from 1.000 to 1.727 bins and the −3 dB main lobe from 0.886 to 1.644 bins. Hann sits where the module’s default sits: −31.5 dB for 1.500 bins. Read the two axes together — a narrow main lobe separates two tones of similar level, a low sidelobe floor finds a weak tone beside a strong one, and no window does both.

Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from phonometry import window_metrics
n, oversample = 1024, 256
fig, ax = plt.subplots(figsize=(10, 6.2))
for name in ("boxcar", "hann", "hamming", "blackman"):
res = window_metrics(name, n)
spectrum = np.abs(np.fft.rfft(res.taps, n=n * oversample))
level = 20.0 * np.log10(spectrum / spectrum[0])
bins = np.arange(level.size) / oversample
shown = bins <= 16.0
ax.plot(bins[shown], level[shown],
label=(f"{name}: ENBW {res.enbw_bins:.2f} bins, "
f"sidelobe {res.highest_sidelobe_db:.1f} dB"))
ax.set_xlim(0.0, 16.0)
ax.set_ylim(-100.0, 5.0)
ax.set_xlabel("Frequency offset [DFT bins]")
ax.set_ylabel("Level re main lobe [dB]")
ax.legend(loc="upper right")
plt.tight_layout()
plt.show()

The Hann default of this module is the balanced choice: fast sidelobe falloff (-18 dB/octave) protects noise spectra from leakage, ENBW 1.5 costs only 1.76 dB against the rectangular window, and its overlap correlation at 50 % keeps nearly all segment information in the effective average count. Reach for a lower-sidelobe taper (Blackman, Kaiser with a high beta) when a weak tone must be found next to a strong one, and accept the wider main lobe; reach for the rectangular window only for self-windowing records (transients that decay inside the segment) or bin-centered synthesis.

7. Multitaper estimation for short records

Section titled “7. Multitaper estimation for short records”

Welch’s method buys stability with record length: each independent segment adds two degrees of freedom, so a record that only fits a couple of segments leaves an estimate barely better than a periodogram. multitaper_psd implements the alternative of Thomson (1982), as developed by Percival & Walden (1993, Chapter 7): the whole record is multiplied by orthogonal discrete prolate spheroidal (Slepian) tapers - the sequences that concentrate the most spectral-window energy inside a chosen design band - and the resulting eigenspectra are averaged:

Because the tapers are orthogonal the eigenspectra are nearly uncorrelated, so the average carries about chi-square degrees of freedom from a single record - the same statistical machinery as the Welch result (random error, chi-square confidence interval), without segmenting. The design parameter is the dimensionless duration-times-half-bandwidth product (default 4), and for a record of samples at it fixes the half-bandwidth in hertz. is the resolution of the estimate (reported as resolution_bandwidth), and only the tapers below the Shannon number keep their spectral-window energy inside the design band - their concentrations are reported as eigenvalues, and the default taper count is , all tapers with near-unity concentration. Larger admits more tapers (lower variance) at the cost of resolution.

from phonometry import multitaper_psd
res = multitaper_psd(record, fs) # p = NW = 4, K = 7, adaptive
print(res.degrees_of_freedom.mean()) # ~2K from one record
print(res.eigenvalues) # taper concentrations
res.plot() # density with the CI band

By default the eigenspectra are combined with Thomson’s adaptive weights (P&W Eqs. 368a/370a, iterated to convergence). Each taper’s weight at each frequency balances the local spectrum against the broad-band leakage the taper could carry:

so the leakier high-order tapers are downweighted exactly where the spectrum is locally weak, and nothing is lost where it is locally white (for white noise the weights are uniform). The price is bookkept honestly: the equivalent degrees of freedom become frequency dependent, with (P&W Eq. 370b), and the confidence interval widens wherever leakage protection spent them. adaptive=False selects the plain eigenvalue-weighted average instead.

Calibration matches the Welch estimators exactly: no detrending, 'density' integrates to the signal power, and 'spectrum' reads at the peak of a sinusoid of amplitude (a tone’s power in 'density' scaling spreads over the band). The Slepian tapers themselves come from scipy.signal.windows.dpss; their concentrations reproduce the quadruple-precision table of Percival & Walden (Table 382) to machine precision, which is the anchor oracle of the test suite.

Left: the multitaper spectral density of a 171 millisecond pink noise record with its 95 percent confidence band, against a Welch estimate with only 6.7 effective averages and a visibly wider band, a single-taper estimate as a jagged grey line and the exact minus 3.01 dB per octave law dashed. Right: a 60 dB tone over a pink floor, where the Hann-windowed Welch estimate leaves a wider skirt around the tone than the adaptive multitaper, and the equivalent degrees of freedom drop from about nine to five where the adaptive weights spend themLeft: the multitaper spectral density of a 171 millisecond pink noise record with its 95 percent confidence band, against a Welch estimate with only 6.7 effective averages and a visibly wider band, a single-taper estimate as a jagged grey line and the exact minus 3.01 dB per octave law dashed. Right: a 60 dB tone over a pink floor, where the Hann-windowed Welch estimate leaves a wider skirt around the tone than the adaptive multitaper, and the equivalent degrees of freedom drop from about nine to five where the adaptive weights spend them

Left, the case the text claims multitaper wins: a 171 ms record. Welch with a 2048-sample segment fits only effective averages into it, so its confidence band is visibly the wider of the two and its curve the rougher, while the 7-taper adaptive estimate carries 13.7 equivalent degrees of freedom from the same record. Right, the other reason to reach for it: a 60 dB tone over a pink floor. The Hann taper leaves a broader skirt around the tone than the adaptive weights do, and the price is printed on the right-hand axis — the equivalent degrees of freedom fall wherever the weights bought that protection, which is exactly what widens the confidence interval there.

Show the code for this figure
import matplotlib.pyplot as plt
import numpy as np
from phonometry import multitaper_psd, noise_signal
fs = 48000.0
x = noise_signal(fs, 8192 / fs, color="pink", seed=11) # 171 ms record
single = multitaper_psd(x, fs, n_tapers=1, adaptive=False)
res = multitaper_psd(x, fs) # NW = 4, K = 7
band = (res.frequencies >= 20.0) & (res.frequencies <= 20000.0)
freqs = res.frequencies[band]
fig, ax = plt.subplots(figsize=(10, 6))
ax.semilogx(freqs, 10 * np.log10(single.psd[band]), color="gray",
alpha=0.45, lw=0.7, label="Single Slepian taper (K = 1)")
ax.fill_between(freqs, 10 * np.log10(res.ci_lower[band]),
10 * np.log10(res.ci_upper[band]), alpha=0.3,
label="95 % chi-square confidence interval")
ax.semilogx(freqs, 10 * np.log10(res.psd[band]), lw=1.2,
label="Multitaper estimate (K = 7, adaptive)")
ax.set_xlabel("Frequency [Hz]")
ax.set_ylabel("PSD [dB re 1/Hz]")
ax.legend()
plt.show()

Reach for multitaper_psd when the record is too short to segment (room impulse response tails, transient captures, single machine cycles) or when a high-dynamic-range spectrum needs leakage protection that a Hann-windowed Welch average cannot give; stay with power_spectral_density for long records, where segment averaging is cheaper than full-length FFTs and the two estimators agree.

Consistency with the frequency-response and intensity estimators

Section titled “Consistency with the frequency-response and intensity estimators”

The frequency-response estimators form the same cross-spectra into a transfer function — , unbiased when the noise is on the output, and , unbiased when it is on the input — and the two-microphone sound intensity probe forms the imaginary part of . Those estimators, transfer_function and coherence, the intensity probe and these estimators all share one Welch core (same taper, overlap policy and detrend-off calibration), so a PSD, a coherence and an computed with the same segment length are mutually consistent bin by bin. The same cross-spectral matrix underlies multiple and partial coherence, which extends the ordinary coherence to several correlated inputs and one output.

  • Covered

    Bendat & Piersol’s Welch estimators (Random Data, 4th ed., 2010, Sections 5.2, 8.5, 9.1-9.2 and 11.5): power_spectral_density and cross_spectral_density with the effective number of averages, chi-square confidence intervals and the resolution-bias error; the coherent output spectrum and spectral SNR; the Harris (1978) window figures of merit of window_metrics; and the Thomson (1982) multitaper estimator of multitaper_psd, following Percival & Walden’s Chapters 7-8. fractional_octave_smoothing and noise_signal complete the toolbox.

  • Not covered

    The frequency-response estimators transfer_function and coherence, and the sound-intensity probe, share this page’s Welch core but are documented on the electroacoustics and sound intensity pages, not here. So is multiple and partial coherence, on the MISO coherence page. This page implements Bendat & Piersol’s textbook estimators, not a certification standard, so it carries no clause numbers or compliance limits to check against.

  • Bendat, J. S., & Piersol, A. G. (2010). Random data: Analysis and measurement procedures (4th ed.). Wiley. https://doi.org/10.1002/9781118032428Sections 5.2 and 8.5 (autospectra and their random/bias errors, chi-square intervals), 9.1-9.2 (cross-spectra, coherent output spectrum and their errors) and 11.5 (Welch processing, tapering and overlap). ISBN 978-0-470-24877-5.
  • Harris, F. J. (1978). On the use of windows for harmonic analysis with the discrete Fourier transform. Proceedings of the IEEE, 66(1), 51-83. https://doi.org/10.1109/PROC.1978.10837The window figures of merit (Table 1): equivalent noise bandwidth, coherent gain, scalloping loss, worst-case processing loss, highest sidelobe level and main-lobe width, computed by window_metrics for any scipy taper.
  • Percival, D. B., & Walden, A. T. (1993). Spectral analysis for physical applications: Multitaper and conventional univariate techniques. Cambridge University Press. https://doi.org/10.1017/CBO9780511622762Chapter 7 (multitaper estimation: eigenspectra, adaptive weighting, equivalent degrees of freedom) and Chapter 8 (computing the Slepian sequences); the Table 382 eigenvalues anchor the taper oracle in the test suite. ISBN 978-0-521-43541-3.
  • Thomson, D. J. (1982). Spectrum estimation and harmonic analysis. Proceedings of the IEEE, 70(9), 1055-1096. https://doi.org/10.1109/PROC.1982.12433The multitaper method: Slepian tapers, eigenspectra and the adaptive weights implemented by multitaper_psd.
  • Welch, P. D. (1967). The use of fast Fourier transform for the estimation of power spectra: A method based on time averaging over short, modified periodograms. IEEE Transactions on Audio and Electroacoustics, 15(2), 70-73. https://doi.org/10.1109/TAU.1967.1161901The overlapped-segment variance formula behind the effective number of averages (Bendat & Piersol Section 11.5.2.2, Ref. 11).