Tapering

Initialize NeuroAnalyzer
using FFTW
using NeuroAnalyzer

Tapering multiplies a signal by a window function before spectral analysis. It is one of the most common preprocessing steps in EEG frequency analysis and has a direct impact on the quality of power spectra and spectrograms.

Why taper?

Any real EEG epoch has a finite length. When you compute its power spectrum via the Fourier transform, the algorithm implicitly assumes the signal repeats periodically. Unless the signal happens to complete an exact whole number of cycles inside the epoch, the values at the two endpoints will be different, creating an artificial sharp discontinuity. That discontinuity smears power from the true frequency into neighboring frequencies - this is called spectral leakage.

Spectral leakage: a phenomenon in which signal at a specific frequency is leaked to other frequencies within the spectrum of the data:

  • this occurs when periodic signals fit within the measurement interval (such an epoch) a non-integer number of times and can be reduced by applying some form of tapering to the data
  • tapering reduces spectral leakage but also smooths the frequency spectrum, reducing the frequency resolution of the data

A taper is a smooth window that gently fades the signal to zero at both ends, removing the discontinuity before the Fourier transform sees it.

Trade-offs

Tapering always involves a trade-off between two spectral properties:

Property Without taper (rectangular window) With taper (e.g. Hann)
Spectral leakage High - power spills into neighboring bins Low - power stays near the true frequency
Amplitude accuracy High - signal is unmodified Lower - taper attenuates the signal
Frequency resolution Highest possible for the epoch length Slightly reduced (broader spectral peaks)

Rule of thumb

Taper whenever the shape or presence of spectral peaks matters — which is most of the time. Skip tapering only when you need the most accurate peak amplitude measurement and spectral leakage is not a concern.

Quick example with a sine wave

The effect of tapering is easiest to understand with a simple sine wave whose frequency does not fit an exact number of cycles into the epoch.

# sampling rate (Hz)
fs = 1000
# 2-second time vector
t = collect(0:1/fs:2)
# 10 Hz sine wave, amplitude 2 units
s = generate_sine(10, t, 2)

NeuroAnalyzer.plot(t,
                   s,
                   ylabel="Amplitude [AU]")

Apply a Hann taper and plot the result:

# generate a Hann window
w = generate_window(:hann, length(s))
# multiply signal by window
sw = s .* w

NeuroAnalyzer.plot(t,
                   sw,
                   ylabel="amplitude [AU]")

You will see the tapered signal smoothly reach zero at both ends. Plotting the power spectra of s and sw side by side shows the leakage reduction: the untapered spectrum has broad skirts around the 10 Hz peak; the tapered spectrum has a much cleaner, tighter peak.

Available window functions

NeuroAnalyzer provides a set of built-in windows via generate_window(). Each has different leakage and resolution characteristics:

Window Key characteristic Typical use
:hann Tapers fully to zero; good leakage suppression with moderate resolution loss General-purpose default for EEG spectral analysis
:bh (Blackman-Harris) Very low leakage; wider main lobe than Hann When leakage suppression is the priority
:bn (Blackman-Nutall) Refined version of the Blackman window When leakage suppression is the priority
:flat Flat-top Excellent amplitude accuracy for spectral analysis
:exp Symmetric exponential (left half ↑, right half ↓) Baseline comparison only
:triangle Symmetric triangle (left half ↑, right half ↓) Used to reduce the effects of spectral leakage
:bohman The product of a triangular window and a single cycle of a cosine Very low sidelobes, which helps significantly reduce spectral leakage
:hamming Does not fully taper to zero; slightly better amplitude accuracy than Hann When a small amount of leakage is acceptable
:kaiser Adjustable trade-off via a shape parameter When you need explicit control over leakage vs. resolution
:gaussian Can be tuned; becomes narrow if forced to zero at edges Specific signal processing applications
:cosine Gentle half-cosine fade at edges Mild leakage reduction
:rectangular No tapering at all Baseline comparison only

†: These window functions are available via DSP.jl.

To see the full current list accepted by generate_window(): ?generate_window

To choose a window for your analysis, plot the windows and their frequency responses side by side:

windows=[:hann, :bh, :bn, :flat, :exp, :triangle, :bohman]
fs = 1000
n = 1000
for w in windows
    win = generate_window(w, n)
    println("$w: peak sidelobe ≈ 
            $(round(minimum(20 .* log10.(abs.(fft(win) ./ n))), digits=1)) dB")
end
hann: peak sidelobe ≈ 
            -350.1 dB
bh: peak sidelobe ≈ 
            -355.0 dB
bn: peak sidelobe ≈ 
            -350.1 dB
flat: peak sidelobe ≈ 
            -351.4 dB
exp: peak sidelobe ≈ 
            -368.3 dB
triangle: peak sidelobe ≈ 
            -108.0 dB
bohman: peak sidelobe ≈ 
            -401.9 dB

Lower (more negative) peak sidelobe values indicate better leakage suppression. The Blackman window typically achieves around −58 dB, Hann around −31 dB, and a rectangular window only around −13 dB.

Applying a taper to an EEG object

Use taper() to apply a window to all channels of a NeuroAnalyzer.NEURO object. The taper is applied epoch-by-epoch, so epoch_len(eeg) gives the correct window length.

taper(eeg,
      ch="all",
      t=generate_window(:hann, epoch_len(eeg)))

To apply in-place (modifies the original object):

taper!(eeg,
       ch="all",
       t=generate_window(:hann, epoch_len(eeg)))

Channel selection

Use ch="eeg" to taper only EEG signal channels, leaving ECG, EOG and reference channels unchanged. Use ch="all" to taper every channel.

Using windows from DSP.jl

If you need a window type not available in NeuroAnalyzer, you can pass any vector of the same length as the epoch, including windows from DSP.jl:

taper(eeg,
      ch="all",
      t=DSP.hanning(epoch_len(eeg)))

Tapering in the context of a full pipeline

Tapering is applied after epoching and artifact rejection, and immediately before spectral analysis. It should not be applied to continuous (un-epoched) data intended for further editing, since the amplitude attenuation at epoch edges will corrupt subsequent processing steps.

Typical order of operations:

NeuroAnalyzer.filter!(eeg,
                      ch="eeg",
                      fprototype=:butterworth,
                      ftype=:bp,
                      cutoff=(1, 40),
                      order=4)
reference_avg!(eeg)
eeg_ep=epoch(eeg, ep_len=2)
# ... reject bad epochs ...
taper!(eeg_ep, ch="eeg", t=generate_window(:hann, epoch_len(eeg_ep)))
psd_result=psd(eeg_ep, ch="eeg")

Do not taper before filtering

Tapering before filtering attenuates edge regions that the filter needs for accurate transient response. Always filter first, then epoch, then taper.