Flat-top window dynamic range is rather lacking for unsynchronized measurements

I was looking at the output of an audio oscillator I built (DDS) and noticed the skirts of the default flat-top window in the QA403 are well above the noise floor. Of course an external oscillator like this is not synchronized so the window skirt height and shape will matter.

The Hann window alas spreads out and obscures some close-in phase noise, as well as having scalloping error - flat-top windows are really what’s needed here.

Could a more performant flat-top window be added which has at least 120dB of dynamic range, perhaps HFT116D or HFT144D from https://holometer.fnal.gov/GH_FFT.pdf

Alternatively a way to define your own cosine-term windows would suffice and be more flexible.

Mark T

Hi @Mark,

I think what you are seeing is pretty normal. If you put the frequency precisely in the middle of a frequency bin, the skirt noise is obviously good. But as you drift outside the bin center, the skirt noise will rise.

As an experiment, try this:

  1. Do File->New Settings
  2. Set 0 dBV full scale input and go into single-ended loopback (short unused L- input)
  3. Add the measurement Sys:Resolution. This tell us the bin size. It will default to 11.72 Hz and is related to the size of the FFT. The real size of the bin is 48000 / 4096 = 11.7188, where 48k is the sample rate and 4096 is the FFT size
  4. Go into GEN1 and disable the “Round to Eliminate Leakage”
  5. Enter the frequency 996.098 Hz. This was computed by 85th bin * 11.7188 Hz = 996.098 . This puts us smack in the middle of the 85th bin. The plot is as follows:

Next, let’s move to the bin edge. Since the bin width is 11.7188 Hz, let’s set the frequency to (85 * 11.7188) + 11.7188/2 = 1001.69. This is the worse-case: A tone sitting right on the boundary of two bins.

And now the spectrum should appear as follows using Flat Top:

You can now click the different windows and see what you get. The Blackman Harris gives the plot below. This does give you the noise floor you want, but you have some close-in artifacts and a few dB of error. But it should allow you to discern the DDS performance.

Hann does even better on noise, getting around -140. And you can easily see the 2H. But the skirt is much wider:

Also, since it’s DDS and under your control, can you nudge the frequency a bit numerically to get it to center precisely in a bin using the math above? Once you nudge it into place, can the frequency hold?

Maybe you could use Flat top to check amplitude, and then switch to BH to look at close-in noise, and then switch to Hann to look at noise beyond 2H.

I don’t think you understand my issue, the oscillator is not synchronous to the QA403 as its a separate piece of kit, I cannot force it to be synchronous, it doesn’t sample at a multiple of 48kHz, its clock cannot be pulled into time, its a black box effectively.

There are flat-top windows with more dynamic range than the common ones (lower skirt breakout points) - read that link I provided and you’ll find all about them. Its the best work on using FFTs for measurement I know of, certainly the most comprehensive catalog of window functions and their many properties. I want to be able to pick the right tool for the job, ie low scalloping error and enough dynamic range to match the wonderful capabilities of the QA403 hardware.

BTW many flat-top windows in common use on various S.A.'s are undocumented and proprietry - is the QA flat-top window documented?

Thanks again for looking into this.

Pages 79 and 80 show the plots of the relevant flat-top windows I suggest, perhaps compare to the one on page 74 which is a reverse-engineered HP SA window to see the improvement in skirts.


Hi @MarkT,

I don’t think you understand my issue, the oscillator is not synchronous to the QA403 as its a separate piece of kit, I cannot force it to be synchronous, it doesn’t sample at a multiple of 48kHz, its clock cannot be pulled into time, its a black box effectively.

You mentioned you built the DDS, which suggested to me you could nudge the frequency a bit one way or the other. Sorry if I misunderstood.

What I was trying to illustrate is you can use the QA403 in loopback to explore the limits of the window functions. For example, if you got extremely lucky and your oscillator frequency fell in the exact middle of the bin, you’d see one response. But if you got really unlucky and your oscillator exactly straddled two bins, you’d see another response. And those two responses would set your limits. As the DDS oscillator drifted between the bins, you’d see the response morph from the best-case to the worst-case, and then back towards the best case. Those limits and how to calc the frequencies was what I was aiming to show above.

I developed the QA480 which had a discrete analog oscillator with THDN around -121, and yes, they are a challenge to measure. But, rather than try to find a single window that sort of meets your needs, a high-performance oscillator will probably need to rely on several windowing functions, each optimized for a different task. After all, if a kick-ass windowing function existed, there’d not be hundreds of windowing functions, right? And even measuring a PC DAC brings the same challenges as the PC clock is never in sync with the QA403 clock. There are a some threads on the forum about measuring the E70 Velvet which “driven” by the PC (so, async just like your oscillator). And Blackman-Harris will let you measure down to -124 dB THDN or so when using a differential notch.

As far as the origins, I think they are straight from Wiki. Nothing proprietary.

I simply want a window with flat-top and >=120dB dynamic range - I use the HFT144D window all the time in Python using scipy.signal for audio spectrum analysis as it gives accurate peak heights and doesn’t hide sidebands - its a great window for spectral analysis like this (not so good for noise obviously, but the Hann window is fine for that).

So you can get plots as clean as this (WAV file recorded via USB soundcard):

With a weaker flattop window (here the HPFT one), you get skirt issues like this:

Or with Hamming window the peak heights are higgledy-piggledy as well as poor skirts:

Hann window has better skirts but same scalloping issue.

The one in wikipedia seems to be from Matlab, IP status unclear. Matlab refers to a publication by Brüel & Kjær…

As far as I can tell only the GH_FTT paper presents any flat-top windows with greater than 90dB dynamic range.

Cosine summed flat-top windows are rather like type-2 chebshev filters in the frequency domain, giving maximally flat tops (no pass band ripple) and as brick-wall a characteristic as possible and limited stop-band ripple (extended skirts). The number of cosine terms is effectively the order of the filter.

Hi @MarkT, I took a look at this in more detail today. Here’s the QA403 in loopback with HFT144D weighting straddling two bins (worst case). The amplitude here normalized to zero (but it shows 5.23 dBr, that’s because the energy compensation hasn’t been done yet):

And here’s the same measurement with the tone adjusted to land precisely in the center of a single bin. Note the amplitude is the same and in fact the spectrum is the same.

And for comparison, here’s the two traces plotted together. There’s really no difference, which is indeed impressive!

And just in case the 1/2 bin frequency difference above isn’t clear, here’s a zoom:

Now, there are a never-ending collection of windows. Rather than implementing dozens and playing whack-a-mole as new requests come in, I’m thinking that a small text file could be authored that would have the code for the weighting. For example, for the HFT144D, the file would be named (for example) HFT144D.cs and would appear similar to this:

public static double[] ApplyWindow(this double[] timeSeries)
 // HTF144D
 double[] r = new double[timeSeries.Length];
 for (int i = 0; i < timeSeries.Length; i++)
    double z = 2 * Math.PI * (double)i / timeSeries.Length;
    double wj = 1.0 - 1.96760033 * Math.Cos(z) + 1.57983607 * Math.Cos(2 * z) - 0.81123644 * Math.Cos(3 * z) + 0.22583558 * Math.Cos(4 * z) - 0.02773848 * Math.Cos(5 * z) + 0.00090360 * Math.Cos(6 * z);

    r[i] = timeSeries[i] * wj;

 return r;

If you selected the USER windowing option(which would replace Hamming), you’d be asked to pick a *.cs file, that file would be compiled by the QA40x at run time, errors (if any flagged), and then execute. Alternately, the files could be DLLs, but that would require the installation of Visual Studio.

Then, someone that wanted 50 custom windows could have 50 *.cs files, and each would have whatever it took to make the window they wanted. Then they’d load any one of those 50 files as required by analysis needs.

Probably the ability to specify a USER windowing would be enabled via command line switch at first. And at that point, HAMMING would be replaced with USER.

What do you think?

Hi. It’s a great idea. In my opinion, however, rather than from a switch from the command line, it would be much more convenient to enable the “USER” window from a checkbox from the “Edit” “Setting” menu, and say the *.cs name of which file you are using in the SPECTRUM or GRAPH region.

Hi @Claudio, OK, this is underway. How about replace HAMMING with USER, add a context menu to USER. So, if you click USER this first time, then you are prompted to load a *.cs file. There will be a default HAMMING *.cs file to replace the HAMMING that has gone away. And then you can click USER windowing as needed and it will remember the last *.cs file you loaded. And you can right click in USER window button any time and load a different *.cs file. Hopefully this will be done by end of week with some other fixes.

Hi @Matt. I would say that this solution you propose is better than the one I was proposing to add a checkbox in the “Edit” “Settings” menu. I think it would also be useful to have it show somewhere which *.cs file is being loaded with the use of the “USER” window and thus ultimately always show which window is being used. Thanks

Hi @Claudio, noted, thanks!

Hi @MarkT, Release 1.197 is up that allows you to specify an arbitrary window function made up of sines and cosines. The Hamming window button was removed and replaced with a USER button. If you right click that user button, you can load others Windowing functions that you provide. A Hamming.cs window is provided to replace the Hamming button that was removed.

The source for the HFT144D window is pasted below. You can put this into a file named HFT144D.cs and the drop that into MyDocs/QuantAsylum/QA40x/UserWindows and then load it and it should give you the HFT144D response you are after.

public double GetAmplitudeCorrection()
	return 1.0;

public double GetEnergyCorrection()
	return 0.4693;

public double[] ApplyWindowToTimeSeries(double[] timeSeries)
	double[] r = new double[timeSeries.Length];
	for (int i = 0; i < timeSeries.Length; i++)
		double z = 2 * PI * (double)i / timeSeries.Length;
		double wj = 1.0 - 1.96760033 * Cos(z) + 1.57983607 * Cos(2 * z) - 0.81123644 * Cos(3 * z) + 0.22583558 * Cos(4 * z) - 0.02773848 * Cos(5 * z) + 0.00090360 * Cos(6 * z);

		r[i] = timeSeries[i] * wj;
	return r;

Hi. I just downloaded and started version 1.197. So far it is working fine. I feel that having added the ability to create your own windows is a great step forward in the flexibility of the software. However, now I need to study the “User Windows” document so I can write the code. Great job Matt, thank you

Thanks, @Claudio!

I think there’s a lot of opportunity to expand the compile-at-runtime mechanism for for user-installed signal processing at many points in the pipeline. For example, there’s a stock way to eliminate the DC offset, but perhaps it’d be nice for the user to write and use a high-pass IIR with a 20 Hz corner. Or a custom weighting file. And then, going beyond that, the ability to write automated tests. Lots of good stuff.

It would be simpler to just specify the windows by their cosine coefficients in a config file perhaps? Like in my Python code:
HPFT = [1.00000000, -1.91251094, 1.07917327, -0.183263088]
Matlab = [1.00000000, -1.93261717, 1.28613280, -0.387695306,0.03222656015]
HFT116D = [1.00000000, -1.95753750, 1.47807050, -0.63674310, 0.12283890, -0.00662880]
HFT144D = [1.00000000, -1.96760033, 1.57983607, -0.81123644, 0.22583558, -0.02773848, 0.00090360]
HFT169D = [1.00000000, -1.97441842, 1.65409888, -0.95788186, 0.33673420, -0.06364621, 0.00521942, -0.00010599]

But perhaps as simple csv format?

Got round to testing the coded version of user windows with HFT144D window on my oscillator today - works great!

Thanks for all the hard work implementing this - just have to mention the copyright sign still says 2023 on the 1.197 release.

Thanks again

1 Like

Hi @MarkT, if you cook up any more windows that are helpful to measuring a free running oscillator, please post them here as others might find them useful too.

Well 144dB of dynamic range should be enough to be going on with - not my windows, see the link in my postings above to GH_FFT.pdf for the source…