using Gadfly
using ColorSchemes
using Statistics
using Printf
using FFTW
set_default_plot_size(20cm, 15cm)
style(background_color=colorant"white");

An astute reader may have noticed something odd when reading the first post on power measurement. The FFT size used was 10,000. Anyone who has learned about the FFT algorithm knows that algorithm is drastically faster when an FFT size that’s a power of 2 is used (eg 2^10 == 1024).

So why use an FFT size of 10,000? Well, there’s a source of loss that wasn’t accounted for in that post and under certain conditions this loss is 0 dB. This loss is called scalloping loss and scalloping loss is zero when the tone in the FFT (10 kHz in this case) falls on an FFT bin boundary.

When computing the frequency domain representation of a signal, the representation is broken up into bins. The sampling rate in the first post was 100 kHz and there were 10,000 bins meaning that each bin represents 10 Hz of bandwidth.

When the tone frequency falls on that 10 Hz bonudary (eg 40,500 Hz), there’s no scalloping loss. However, when the tone frequency doesn’t fall on a boundary (eg 40,509 Hz) the signal is affected.

Scalloping loss comes from the FFT window that’s being used. In this case no window is explicitly applied, which really means that a rectangualar window is being used (since all the samples before and after the signal are 0).

In this post, we’ll examine scalloping loss and how you can compensate for it. First, we’ll setup a signal that’s identical to the one used in the first post. Then, we’ll take 3 different FFTs of that signal and see the affect of scalloping. Next, we develop a method for compensating for scalloping loss. Finally, we plot scalloping loss directly and then the output of the signal with scalloping loss compensated for.

Step 1 - Signal Setup

# Signal setup
duration = 1                        # Signal durations [s]
fs = 100e3                          # Sampling rate, [Hz]
f = 10e3                            # Tone frequency [Hz]
t = 1/fs:1/fs:duration              # Signal time vector [s]
x = exp.(im * 2π * f * t)           # Signal with no noise
noise = randn(ComplexF64, size(x))  # Noise
xₙ = x .+ noise                     # xₙ, signal with noise 
;

Plot the first 100 samples of the signal

n = 100
p_1 = plot(Guide.title("x (complex valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))
p_2 = plot(Guide.title("xₙ (complex valued signal)"), Guide.ylabel("Amplitude",  orientation=:vertical), Guide.xlabel("Time [s]"))

push!(p_1, layer(x=t[1:n], y=real(x[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_1, layer(x=t[1:n], y=imag(x[1:n]), Geom.line, color=[colorant"red"]))
push!(p_2, layer(x=t[1:n], y=real(xₙ[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=t[1:n], y=imag(xₙ[1:n]), Geom.line, color=[colorant"red"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 -12.0 -11.5 -11.0 -10.5 -10.0 -9.5 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 -20 -10 0 10 20 -12.0 -11.5 -11.0 -10.5 -10.0 -9.5 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 Amplitude xₙ (complex valued signal) Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 -4 -2 0 2 4 -3.0 -2.9 -2.8 -2.7 -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2.0 -1.9 -1.8 -1.7 -1.6 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 Amplitude x (complex valued signal)

Step 2 - Three different FFTs of the same signal

In this section we see three different FFTs of the same signal. The only difference is the FFT Size (10000, 8192 and 4096).

The top plot shows the FFT output over the whole bandwidth and the bottom plot shows a zoom-in of the signal peak.

fft_size = 10000
X = (abs.(fft(xₙ[1:fft_size]))).^2 ./ fs ./ fft_size / (fft_size/length(xₙ))
peak_power_10000 = findmax(10 .* log10.(X))[1]
freqs = FFTW.fftfreq(fft_size, fs)

p_1 = plot()
push!(p_1, Guide.title("Frequency Response w/ FFT Size = $fft_size"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_1, Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), ymin=-80, ymax=10))
push!(p_1, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))

p_2 = plot()
push!(p_2, Guide.title("Zoomed-In Frequency Response = $fft_size"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_2, Coord.cartesian(xmin=8, xmax=12, ymin=-5, ymax=5))
push!(p_2, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=[-50, 50], y=[0, 0], Geom.line, color=[colorant"red"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Frequency [kHz] 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 0 5 10 15 20 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 8.2 8.4 8.6 8.8 9.0 9.2 9.4 9.6 9.8 10.0 10.2 10.4 10.6 10.8 11.0 11.2 11.4 11.6 11.8 12.0 12.2 12.4 12.6 12.8 13.0 13.2 13.4 13.6 13.8 14.0 14.2 14.4 14.6 14.8 15.0 15.2 15.4 15.6 15.8 16.0 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -20 -15 -10 -5 0 5 10 15 20 -15 -14 -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -20 -10 0 10 20 -15.0 -14.5 -14.0 -13.5 -13.0 -12.5 -12.0 -11.5 -11.0 -10.5 -10.0 -9.5 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 Power Density [dB/Hz] Zoomed-In Frequency Response = 10000 Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 -200 -100 0 100 -170 -165 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Power Density [dB/Hz] Frequency Response w/ FFT Size = 10000
fft_size = 8192
X = (abs.(fft(xₙ[1:fft_size]))).^2 ./ fs ./ fft_size / (fft_size/length(xₙ))
peak_power_8192 = findmax(10 .* log10.(X))[1];
freqs = FFTW.fftfreq(fft_size, fs)

p_1 = plot()
push!(p_1, Guide.title("Frequency Response w/ FFT Size = 8192"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_1, Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), ymin=-80, ymax=10))
push!(p_1, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))

p_2 = plot()
push!(p_2, Guide.title("Zoomed-In Frequency Response w/ FFT Size = 8192"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_2, Coord.cartesian(xmin=8, xmax=12, ymin=-10, ymax=10))
push!(p_2, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=[-50, 50], y=[0, 0], Geom.line, color=[colorant"red"]))


stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Frequency [kHz] 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 0 5 10 15 20 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 8.2 8.4 8.6 8.8 9.0 9.2 9.4 9.6 9.8 10.0 10.2 10.4 10.6 10.8 11.0 11.2 11.4 11.6 11.8 12.0 12.2 12.4 12.6 12.8 13.0 13.2 13.4 13.6 13.8 14.0 14.2 14.4 14.6 14.8 15.0 15.2 15.4 15.6 15.8 16.0 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 -30 -28 -26 -24 -22 -20 -18 -16 -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 -40 -20 0 20 40 -30 -29 -28 -27 -26 -25 -24 -23 -22 -21 -20 -19 -18 -17 -16 -15 -14 -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Power Density [dB/Hz] Zoomed-In Frequency Response w/ FFT Size = 8192 Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 -200 -100 0 100 -170 -165 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Power Density [dB/Hz] Frequency Response w/ FFT Size = 8192
fft_size = 4096
X = (abs.(fft(xₙ[1:fft_size]))).^2 ./ fs ./ fft_size / (fft_size/length(xₙ))
freqs = FFTW.fftfreq(fft_size, fs)

p_1 = plot()
push!(p_1, Guide.title("Frequency Response w/ FFT Size = $fft_size"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_1, Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), ymin=-80, ymax=10))
push!(p_1, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))

p_2 = plot()
push!(p_2, Guide.title("Zoomed-In Frequency Response w/ FFT Size = $fft_size"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(p_2, Coord.cartesian(xmin=8, xmax=12, ymin=-10, ymax=10))
push!(p_2, layer(x=freqs ./ 1e3, y=10 .* log10.(X), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=[-50, 50], y=[0, 0], Geom.line, color=[colorant"red"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Frequency [kHz] 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0 15.5 16.0 0 5 10 15 20 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 8.2 8.4 8.6 8.8 9.0 9.2 9.4 9.6 9.8 10.0 10.2 10.4 10.6 10.8 11.0 11.2 11.4 11.6 11.8 12.0 12.2 12.4 12.6 12.8 13.0 13.2 13.4 13.6 13.8 14.0 14.2 14.4 14.6 14.8 15.0 15.2 15.4 15.6 15.8 16.0 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 -30 -28 -26 -24 -22 -20 -18 -16 -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 -40 -20 0 20 40 -30 -29 -28 -27 -26 -25 -24 -23 -22 -21 -20 -19 -18 -17 -16 -15 -14 -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Power Density [dB/Hz] Zoomed-In Frequency Response w/ FFT Size = 4096 Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 -200 -100 0 100 -170 -165 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Power Density [dB/Hz] Frequency Response w/ FFT Size = 4096

These results show that as we change the FFT Size the signal power drops from 0 dB, to -0.5 dB to -2.5 dB.

Here we can directly see the affects of scalloping. We’re only changing the FFT Size and the power of the signal is decreasing, that doesn’t make sense!

@printf "Peak Power for FFT Size of 10000 = %2.3f dB\n" peak_power_10000
@printf "Peak Power for FFT Size of 8192 = %2.3f dB\n" peak_power_8192
@printf "Peak Power for FFT Size of 4096 = %2.3f dB\n" peak_power_4096
;
Peak Power for FFT Size of 10000 = 0.041 dB
Peak Power for FFT Size of 8192 = -0.552 dB
Peak Power for FFT Size of 4096 = -2.543 dB

Step 3 - Compensate for Scalloping Loss

Here we write a function to compensate for scalloping loss. The frequency domain representation of the rectangular window in a sinc() function. We apply that sinc function at the correct frequency to get the scalloping loss that we need to compensate for.

Note that scalloping loss that we compensate for matches the loss in step 2 for each FFT Size.

function sinc(x::Float64)
    sin(x)/x;
end

function compute_scalloping_loss(Fs::Float64, fft_size::Int64, f::Float64)
    res_bw = Fs/fft_size
    a = π/(res_bw) * mod((f - (res_bw/2)), res_bw) - π/2
    if a == 0
        # Avoid divide by 0 in sinc(x)
        b = 0  
    else
        b = 10 * log10(sinc(a) .^ 2)
    end
    b
end
;

sloss_10000 = compute_scalloping_loss(fs, 10000, f)
sloss_8192 = compute_scalloping_loss(fs, 8192, f)
sloss_4096 = compute_scalloping_loss(fs, 4096, f)

@printf "Scalloping loss for FFT Size = 10000 is %2.3f [dB]\n"  sloss_10000
@printf "Scalloping loss for FFT Size = 8192 is %2.3f [dB]\n"  sloss_8192
@printf "Scalloping loss for FFT Size = 4096 is %2.3f [dB]\n"  sloss_4096
Scalloping loss for FFT Size = 10000 is 0.000 [dB]
Scalloping loss for FFT Size = 8192 is -0.579 [dB]
Scalloping loss for FFT Size = 4096 is -2.420 [dB]

Step 4 - Plot Scalloping Loss and Compensating For It

In this last step a tone is swept from 9980 Hz to 10020 Hz in 0.08 Hz increments. The power of the tone is recorded for each increment. The power of the tone is then plotted as a function of frequency.

l = 500
X_comps = zeros(l)
scalloping_loss = zeros(l)
N = 10000
freq = zeros(l)

for (ii, kk) in enumerate(range(-20, 20, length=l))
    F = f + kk                              # Tone frequency 
    x = exp.(im * 2π * F .* t)              # Signal, a tone at frequency F
    X = fft(x[1:N])                         # FFT of the signal, FFT Size is 10k
    X_comp = abs.(X).^2 / N / fs / (N/fs)   # Signal, compensated for FFT Size, sampling rate and FFT bin size
    
    s_loss = compute_scalloping_loss(fs, N, F)  # Scalloping loss
    scalloping_loss[ii] = s_loss
    
    X_comps[ii] = 10*log10(findmax(abs.(X_comp))[1]) - s_loss  # X, compensated for all of the above and scalloping loss
    freq[ii] = F
end

Here we plot the scalloping loss as a function of frequency. Notice that when the FFT size divides the tone frequency evenly (eg 9990 Hz, 10000 Hz, etc) there is no scalloping loss.

plot(x=freq, 
     y=scalloping_loss, 
     Geom.line, 
     Guide.xlabel("Frequency [Hz]"), 
     Guide.ylabel("Scalloping Loss [dB]"),
     Guide.title("Scalloping Loss as a function of Frequency for FFT Size = $N"))
Frequency [Hz] 9930 9940 9950 9960 9970 9980 9990 10000 10010 10020 10030 10040 10050 10060 10070 9940 9945 9950 9955 9960 9965 9970 9975 9980 9985 9990 9995 10000 10005 10010 10015 10020 10025 10030 10035 10040 10045 10050 10055 10060 9900 9950 10000 10050 10100 9940 9942 9944 9946 9948 9950 9952 9954 9956 9958 9960 9962 9964 9966 9968 9970 9972 9974 9976 9978 9980 9982 9984 9986 9988 9990 9992 9994 9996 9998 10000 10002 10004 10006 10008 10010 10012 10014 10016 10018 10020 10022 10024 10026 10028 10030 10032 10034 10036 10038 10040 10042 10044 10046 10048 10050 10052 10054 10056 10058 10060 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 -10 -5 0 5 -8.0 -7.8 -7.6 -7.4 -7.2 -7.0 -6.8 -6.6 -6.4 -6.2 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 Scalloping Loss [dB] Scalloping Loss as a function of Frequency for FFT Size = 10000

This is also a good place to discuss wherer the name “scalloped” comes from. It turns out that curtains and other textiles with this shape are referred to as “scalloped”.

A scalloped curtain: scalloped_drapes.jpeg

In the last plot of this post, the power of the tone is shown as the tone is swept from 9980 Hz to 10020 Hz. All parameters are compensated for in this plot:

  • FFT Size (10k)
  • Sampling rate (100 kHz)
  • FFT Bin Size
  • Scalloping loss

With all of these items factored in, the power of the tone stays at a constant 0 dB/Hz for all frequencies examined.

plot(x=freq, 
     y=X_norms,
     Geom.line,  
     Coord.cartesian(ymin=-1, ymax=1),
     Guide.xlabel("Frequency [Hz]"), 
     Guide.ylabel("Peak Power [dB/Hz]"),
     Guide.title("Peak Power of Swept Tone after Compensating for Scalloping Loss"))
Frequency [Hz] 9930 9940 9950 9960 9970 9980 9990 10000 10010 10020 10030 10040 10050 10060 10070 9940 9945 9950 9955 9960 9965 9970 9975 9980 9985 9990 9995 10000 10005 10010 10015 10020 10025 10030 10035 10040 10045 10050 10055 10060 9900 9950 10000 10050 10100 9940 9942 9944 9946 9948 9950 9952 9954 9956 9958 9960 9962 9964 9966 9968 9970 9972 9974 9976 9978 9980 9982 9984 9986 9988 9990 9992 9994 9996 9998 10000 10002 10004 10006 10008 10010 10012 10014 10016 10018 10020 10022 10024 10026 10028 10030 10032 10034 10036 10038 10040 10042 10044 10046 10048 10050 10052 10054 10056 10058 10060 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 -4 -2 0 2 4 -3.0 -2.9 -2.8 -2.7 -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2.0 -1.9 -1.8 -1.7 -1.6 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 Peak Power [dB] Peak Power of Swept Tone after Compensating for Scalloping Loss