• Welcome to ASR. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

The W&F Analyzer Project, Uberrimus

JP

Major Contributor
Forum Donor
Joined
Jul 4, 2018
Messages
3,288
Likes
3,538
Location
Brookfield, CT
This has been on my personal to-do list for quite some time. I don't work on turntables much anymore, but when I did, they were primarily direct drives, and I used the motor frequency generator (FG) signal to ascertain speed stability. It's not too difficult to learn how to eyeball zero-crossing behavior on a scope, but it's not easy to convey that to someone else.

Resilient software options for W&F analysis are typically expensive, or old and therefore dated. They're also not designed to measure carriers in the 50–150 Hz range where motor FG/tach signals typically are. Even packages with adaptive filters like MultiInstrument don't deal with edge artifacts, as those don't matter with 3–3.15 kHz signals.

So, this Python prototype was born:

Auto-tuned adaptive prefilter. The bandpass prefilter centers itself on whatever carrier it finds. Below ~500 Hz, bandwidth scales as a fixed percentage of carrier (±30%) — wide enough to pass all W&F sidebands without cutting into the fundamental. Above 500 Hz, absolute bandwidth caps at ±150 Hz — still generous for worst-case W&F sidebands (Carson's rule gives ~±80 Hz) but tight enough to reject broadband noise that would corrupt zero-crossing timing.

Adaptive edge trimming. Bandpass prefilters corrupt the first and last few zero crossings — the filter needs time to settle. The analyzer uses a two-stage approach: first, a time-based trim removes crossings within the prefilter's settling time (2/bandwidth seconds) from each end, guaranteeing the worst transient samples are gone regardless of carrier rate. Then a MAD-based (median absolute deviation) scan extends the trim if any of the first/last 20 crossings still exceed 5× MAD. The two trims combine by taking whichever is larger. This scales automatically — at 3 kHz the time-based trim catches the transient that would otherwise slip past the amplitude check (because per-crossing error is small at high crossing rates), while at 100 Hz the amplitude check catches edge artifacts that stick out well beyond the settling window.

AES6-2008 weighting and metrics. Weighted peak is 2σ (95th percentile), not max. RMS per AES6/IEC 60386. Wow and flutter are band-separated — wow is weighted content below 6 Hz, flutter is the remainder. Drift (0.05–0.5 Hz) is reported independently.

Motor harmonic identification. Optional — if you provide slot and pole counts, the spectrum plot auto-identifies peaks as rotation harmonics, electrical frequency, slot passing, or torque ripple, each labeled with per-bin RMS amplitude. Tells you whether your dominant W&F source is mechanical, electrical, or something else.

Polar speed plot. Instantaneous frequency per revolution on a polar axis, 0.1%/div radial scale. Two revolutions overlaid to show repeatability. Rotation-synchronous components show a consistent shape; random flutter shows as spread between traces.

5-second minimum capture. Validated stable for weighted W&F (peak, RMS, wow, flutter) down to 5-second recordings. Drift needs 20s+ but the W&F numbers don't move.

It's usable now as a standalone Python script; however, the next phase is to turn it into an SPA (Single Page Application) that runs in the web browser, similar to sjplot.com/online, but with far more interactivity.

Initial spec is attached. More to come.



TT101-1_analysis.png
gae-wf-norm_20s_analysis.png
 

Attachments

Last edited:
wf_core_aes6_compliance.png


wf_core — Pipeline Specifications & Limitations

Quick Reference

Maximum Reliable Modulation Depth by Carrier Frequency

Carrier (Hz)Max DepthLimiting Factor
5010%Outlier rejection threshold
10010%Outlier rejection threshold
20010%Outlier rejection threshold
50010%Outlier rejection threshold
100010%Outlier rejection threshold
150010%Both limits converge
20007.5%Prefilter bandwidth cap
30005%Prefilter bandwidth cap
50003%Prefilter bandwidth cap

For carriers above 1500 Hz, max depth = 150 / f_carrier.

Practical note: Real-world W&F on any transport in usable condition is well under 1%. These limits are only relevant for extreme calibration signals or severely damaged mechanisms.

Weighting Filter Accuracy (Topology B, AES6-2008)

Mod Freq (Hz)AES6 Spec (dB)Filter Actual (dB)Gain ErrorWeighted Bias
0.2-30.6-30.2+4.5%+4.5% on weighted metrics
0.8-6.0-5.4+7.1%+7.1% on weighted metrics
4.00.00.00.0%Reference - no bias
20.0-5.9-5.3+0.6 dBVaries by carrier (see below)

All points within AES6-2008 Table 1 tolerances. Overall PTP error: 0.681 dB at fs = 1000 Hz. 17/17 spec points pass.

Unweighted Bandpass Edge Attenuation

Mod Freq (Hz)PositionAttenuationEffect
0.2At bp_lo cutoff-6 dB (~-55%)sosfiltfilt doubles 4th-order Butterworth
0.5Just above bp_loModerate rolloffTransition band
1-200Passband interior< 1 dBFlat
Near bp_hiApproaching upper cutoffRolloffCarrier-dependent (bp_hi = min(0.4*carrier, 200))

Signal 07 (20 Hz Modulation) — Carrier-Dependent Behavior

Carrier (Hz)bp_hi (Hz)20 Hz vs bp_hiWeighted ErrorCause
5020At cutoff-83%20 Hz completely attenuated by bandpass
1004050% of bp_hi-16%Filter rolloff + SRC interpolation artifact
2008025% of bp_hi+3%Passband - normal accuracy
100020010% of bp_hi+7%Passband - weighting filter gain offset
300020010% of bp_hi+8%Passband - weighting filter gain offset




Detailed Analysis

1. Prefilter Bandwidth Cap


What: A 2nd-order Butterworth bandpass filter is applied to the raw audio before zero-crossing detection. It is centered on the detected carrier frequency with bandwidth = 45% of carrier, capped at 150 Hz for carriers above 500 Hz.

Why it matters: FM modulation sweeps the instantaneous frequency across a range of f_carrier +/- f_carrier * mod_depth. If the sweep exceeds the prefilter passband, the negative (or positive) excursions of the modulation are attenuated. The zero-crossing detector then misses crossings during those attenuated portions, producing a distorted, shorter deviation signal.

How it fails: At 3 kHz carrier with 10% modulation, the FM sweep spans 2700-3300 Hz. The prefilter passband is 3138-3438 Hz (centered on 3288 Hz detected carrier, +/-150 Hz). The bottom 437 Hz of the sweep falls outside the passband. Roughly half the zero crossings are lost, the output sample rate drops to 54% of nominal, and the resulting deviation waveform is severely distorted (+35% raw peak error, significant harmonic content at 8 Hz and 12 Hz).

The limit: For carriers <= 500 Hz, the bandwidth scales at 45% of carrier — no cap applies, and the prefilter accommodates up to 45% modulation depth. For carriers above 500 Hz, the 150 Hz cap limits maximum depth to 150 / f_carrier. The crossover where the cap becomes the binding constraint (tighter than the 10% outlier threshold) is at 1500 Hz.

Observed in calibration:

CarrierSignal 05 (9.97%)Output RateRate/NominalPrefilter Clips?
50 Hz-6.2% wtd peak42 Hz84%No - outlier rejection only
100 Hz-1.6%96 Hz96%No
200 Hz-2.1%176 Hz88%No
1000 Hz-1.8%880 Hz88%Yes - 42 Hz clipped (mild)
3000 Hz-12.2%1624 Hz54%Yes - 437 Hz clipped (severe)


2. Outlier Rejection Threshold

What: After zero-crossing frequency extraction, samples deviating more than a threshold from the median are rejected. The threshold is min(8 * MAD, 10% * f_median). For any modulation depth above ~2%, the 10% cap is the binding constraint.

Why it matters: At exactly 10% modulation depth, the peaks of the FM sine just touch the rejection boundary. Due to discrete sampling and slight asymmetries in the zero-crossing estimates, some peak samples are rejected. This clips the deviation waveform peaks and reduces the measured amplitude.

How it fails: The effect is proportional to how close the modulation depth is to 10%. At 9.97% depth (signal 05), the outlier rejection clips 4-16% of crossings depending on carrier. At 1% depth (signal 04), zero crossings are rejected.

Observed in calibration — outlier rejection counts for signal 05:

CarrierCrossings Rejected% Rejected
50 Hz24016.0%
100 Hz1204.0%
200 Hz72012.0%
1000 Hz360012.0%
3000 Hz1200.2%

Note: 3 kHz shows minimal outlier rejection because the prefilter has already removed the extreme crossings before the outlier stage sees them.


3. Weighting Filter Gain Accuracy

What: The AES6-2008 weighting filter (Topology B optimized) has gain errors at specific frequencies that are inherent to the filter design. The filter was optimized for minimum overall PTP error across all 17 AES6 Table 1 spec points, which means individual frequencies trade accuracy for better global fit.

Why it matters: The gain error at a given modulation frequency produces a proportional bias in all weighted metrics. This is systematic and consistent — it does not vary by carrier or signal level.

Error decomposition for signal 06 (0.8 Hz, expected 0.0499% wtd peak):

CarrierRaw Dev ErrorFilter Gain ErrorPredicted TotalActual TotalResidual
50 Hz-0.2%+7.3%+7.1%+7.0%-0.04%
100 Hz-0.1%+7.1%+7.0%+7.0%+0.00%
200 Hz-0.1%+7.1%+7.0%+7.0%-0.01%
1000 Hz-0.1%+7.1%+6.9%+7.0%+0.00%
3000 Hz-0.2%+7.1%+6.9%+6.6%-0.28%

The error is almost entirely explained by the filter gain at 0.8 Hz. Residuals under 0.3%.

Error decomposition for signal 08 (0.2 Hz, expected 0.003% wtd peak):

CarrierRaw Dev ErrorFilter Gain ErrorPredicted TotalActual TotalResidual
50 Hz-1.7%+4.6%+2.9%+3.0%+0.2%
100 Hz-1.7%+4.5%+2.8%+2.8%+0.0%
200 Hz-1.7%+4.5%+2.8%+2.7%-0.1%
1000 Hz-1.7%+4.5%+2.8%+2.7%-0.1%
3000 Hz-1.7%+4.5%+2.7%+10.3%+7.6%

The 3 kHz residual of +7.6% on signal 08 is from isolated high-amplitude samples in the raw deviation at 3 kHz (raw peak 0.111% vs 0.100% at other carriers), amplified through the weighting filter. This is a deviation extraction artifact at 3 kHz, not a filter or SRC issue.


4. Unweighted Bandpass at Low Modulation Frequencies

What: The unweighted signal path uses a 4th-order Butterworth bandpass from 0.2 Hz to min(0.4 * carrier, 200) Hz, applied via sosfiltfilt (zero-phase, forward-backward filtering). The effective filter order at the cutoffs is doubled to 8th-order equivalent.

Why it matters: Any modulation at or near the 0.2 Hz lower cutoff is attenuated by approximately 6 dB (50%) because sosfiltfilt squares the frequency response. This is not a bug — it is the expected behavior of a zero-phase filter at its cutoff frequency.

Observed: Signal 08 (0.2 Hz modulation) shows -55% unweighted RMS at ALL carriers, regardless of SRC. The error is identical from 50 Hz to 3 kHz, confirming the bandpass filter as the sole cause.

Upper cutoff effects: At low carriers, the upper cutoff bp_hi = 0.4 * carrier can fall close to the modulation frequency. Signal 07 (20 Hz) at 50 Hz carrier has bp_hi = 20 Hz — the modulation is exactly at the cutoff, producing -83% attenuation.


5. SRC (Sample Rate Conversion)

What: All signals are resampled to 1 kHz before metric computation using scipy.signal.resample_poly with odd-extension boundary padding (filtfilt-style).

Verified transparent: Comprehensive A/B testing (native rate vs. post-SRC metrics) shows SRC-induced weighted metric errors under 0.1% for all signals except signal 07 at low carriers (see below). SRC is not a significant error source.

CarrierSRC DirectionRatioAvg |dWtdPeak|Max |dWtdPeak|
50 HzUP20:114.75%*117%*
100 HzUP10:11.56%*11.6%*
200 HzUP5:10.37%2.6%*
1000 Hz--1:10.00%0.03%
3000 HzDOWN1:30.02%0.05%

*Signal 07 (20 Hz) only. All other signals show < 0.5% SRC error at all carriers.

Signal 07 SRC artifact: At 50 Hz carrier, the 20 Hz modulation is at 40% of native Nyquist (25 Hz). Upsampling 20:1 introduces interpolation artifacts at frequencies close to the native Nyquist. This is an inherent limitation of polyphase interpolation — it cannot create information above the original Nyquist. The artifact scales inversely with the ratio of Nyquist to modulation frequency.

 
Last edited:
Can it interpret a 3150hz wow&flutter track in *.wav format?
 
Initial scaffolding and the js/py bridge is working well. ~60s 3kHz file processes in about 371ms. It takes longer to hand the data back to the front-end than it does to process it. That's a good problem to have.

Screenshot 2026-03-16 at 3.18.50 PM.png



Code:
Navigated to http://localhost:5173/
Loading numpy, openblas, scipy
[WF] Array conversion (5968875 samples): 0 ms
[WF] Carrier estimate: 12 ms
[WF] Prefilter: 109 ms
[WF] Zero crossings (186235 found): 93 ms
[WF] Edge trim + outlier rejection + despike: 24 ms
[WF] Uniform interpolation (186197 points): 25 ms
[WF] AES6 metrics: 58 ms
[WF] Spectrum: 50 ms
 
Curious what result you get with this file (for comparisons with previous apps).

 
While I can imagine the 1.8 s cycle pattern I see a more evident pattetn repeat after 10 revolutions (0 and ≈18 s).
 
The metrics align with what you expected?
Quite close to the previous app; 2sigma unw ±0.0885 and w ±0.0426. Multitone gave ±0.081 (unw). So yes the compute, but slightly lower with your app.
 
Can it handle a noisy file? The 45 RPM Denon run at 33 RPM could not be handled well with the previous app.

 
It handles it better than the previous app (2sigma =0.3268 vs your app 0.1250 which is more similar to multitone 0.113). Great!
 
Have this 0.47-0.5 Hz and 0.98-1.0 Hz peaks which are different from the rotational 0.55 and 1.11 peaks With both test records. Bearing issue?
 
Last edited:
  • Like
Reactions: JP
Back
Top Bottom