Signal Processing

Signal processing is the set of techniques for analyzing, modifying and extracting information from signals. A signal is any function carrying information about a phenomenon, usually of time or space: continuous-time \(x(t)\), discrete-time \(x[n]\). Speech and music are signals, so are images as two-dimensional intensity patterns, ECG and EEG traces, radio and Wi-Fi transmissions, and the output of an accelerometer or a temperature sensor.

The goals are filtering (remove noise, keep the useful part), compression (MP3, JPEG), feature extraction (detect patterns, as in speech recognition), transformation (move to a domain where the question is easier), and restoration (denoising, deblurring).

The techniques divide by domain. Time-domain methods manipulate the sequence directly — a moving average filter is the simplest. Frequency-domain methods use the Fourier transform to work on frequency content. Time-frequency methods, such as the short-time Fourier transform and the wavelet transform, handle signals whose frequency content changes as they go. The transforms behind them are the Fourier transform and its discrete and fast forms, the Laplace transform for continuous-time system analysis, the Z-transform for discrete-time systems, and the wavelet transform for local time-frequency analysis.

Almost all of this now happens digitally, in four steps: sampling (analog to discrete, where the Nyquist–Shannon theorem applies), quantization (continuous values to discrete ones), processing, and reconstruction where needed. The first step is where the irreversible decisions are made, and the theorem named in passing there deserves to be seen working.

Two limits that no later stage can undo

import numpy as np

fs = 50.0
dur = 4.0
t = np.arange(0, dur, 1 / fs)
fine = np.arange(0, dur, 1 / 2000.0)
"""Sampling at 50 Hz. The Nyquist-Shannon theorem says a signal is recoverable
   only if it contains nothing at or above half the sampling rate - 25 Hz."""

print(f"  sampled at {fs:.0f} Hz, so the Nyquist frequency is {fs / 2:.0f} Hz")
print(f"{'true frequency':>16}{'indistinguishable from':>26}{'phase':>12}"
      f"{'max difference':>17}")
for f in (5.0, 20.0, 30.0, 45.0, 55.0):
    folded = ((f + fs / 2) % fs) - fs / 2
    alias = abs(folded)
    a = np.sin(2 * np.pi * f * t)
    b = np.sin(2 * np.pi * alias * t) * (1 if folded >= 0 else -1)
    print(f"{f:>14.0f} Hz{alias:>22.0f} Hz{('same' if folded >= 0 else 'inverted'):>12}"
          f"{np.abs(a - b).max():>17.2e}")
print("  55 Hz produces exactly the samples of a 5 Hz tone, and 30 Hz those of")
print("  an inverted 20 Hz tone. Nothing")
print("  downstream can separate them, because there is nothing to separate -")
print("  the information was destroyed at the moment of sampling, and no")
print("  filter applied afterwards can recover it")

print(f"\n  which is what the anti-aliasing filter in front of a converter is for.")
print("  It has to remove the high frequencies before sampling, not after")

n = 256
fs2 = 256.0
tt = np.arange(n) / fs2
print(f"\n  and the transform reports only the frequencies it has bins for.")
print(f"  {n} samples at {fs2:.0f} Hz gives bins every {fs2 / n:.1f} Hz:")
print(f"{'true frequency':>16}{'largest bin':>14}{'its share of power':>21}"
      f"{'bins above 1%':>16}")
for f in (8.0, 8.5, 10.0, 10.25):
    x = np.sin(2 * np.pi * f * tt)
    sp = np.abs(np.fft.rfft(x)) ** 2
    freqs = np.fft.rfftfreq(n, 1 / fs2)
    share = sp.max() / sp.sum()
    print(f"{f:>14.2f} Hz{freqs[sp.argmax()]:>12.1f} Hz{share:>21.4f}"
          f"{int(np.sum(sp > 0.01 * sp.sum())):>16}")
print("  a tone sitting exactly on a bin is one clean line. Move it half a bin")
print("  and its power spreads over seven of them - the transform is")
print("  describing a signal it assumes repeats every window, and half a cycle")
print("  short of that assumption looks like a discontinuity")

print(f"\n  a window function trades the smear for a wider line:")
print(f"{'frequency':>12}{'window':>12}{'largest bin share':>20}{'bins above 1%':>16}")
for f in (8.5, 10.25):
    for wname, w in (("none", np.ones(n)), ("Hann", np.hanning(n))):
        x = np.sin(2 * np.pi * f * tt) * w
        sp = np.abs(np.fft.rfft(x)) ** 2
        print(f"{f:>10.2f} Hz{wname:>12}{sp.max() / sp.sum():>20.4f}"
              f"{int(np.sum(sp > 0.01 * sp.sum())):>16}")
print("  neither column is the true answer. The signal has one frequency and")
print("  the transform has finitely many bins, so every reading is a choice")
print("  about how the mismatch is spread")
#   sampled at 50 Hz, so the Nyquist frequency is 25 Hz
#   true frequency    indistinguishable from       phase   max difference
#              5 Hz                     5 Hz        same         0.00e+00
#             20 Hz                    20 Hz        same         0.00e+00
#             30 Hz                    20 Hz    inverted         1.69e-13
#             45 Hz                     5 Hz    inverted         9.46e-14
#             55 Hz                     5 Hz        same         1.75e-13
#   55 Hz produces exactly the samples of a 5 Hz tone, and 30 Hz those of
#   an inverted 20 Hz tone. Nothing
#   downstream can separate them, because there is nothing to separate -
#   the information was destroyed at the moment of sampling, and no
#   filter applied afterwards can recover it
#
#   which is what the anti-aliasing filter in front of a converter is for.
#   It has to remove the high frequencies before sampling, not after
#
#   and the transform reports only the frequencies it has bins for.
#   256 samples at 256 Hz gives bins every 1.0 Hz:
#   true frequency   largest bin   its share of power   bins above 1%
#           8.00 Hz         8.0 Hz               1.0000               1
#           8.50 Hz         8.0 Hz               0.4287               7
#          10.00 Hz        10.0 Hz               1.0000               1
#          10.25 Hz        10.0 Hz               0.8085               4
#   a tone sitting exactly on a bin is one clean line. Move it half a bin
#   and its power spreads over seven of them - the transform is
#   describing a signal it assumes repeats every window, and half a cycle
#   short of that assumption looks like a discontinuity
#
#   a window function trades the smear for a wider line:
#    frequency      window   largest bin share   bins above 1%
#       8.50 Hz        none              0.4287               7
#       8.50 Hz        Hann              0.4798               4
#      10.25 Hz        none              0.8085               4
#      10.25 Hz        Hann              0.6128               3
#   neither column is the true answer. The signal has one frequency and
#   the transform has finitely many bins, so every reading is a choice
#   about how the mismatch is spread

What the two blocks mean in practice

Aliasing is not distortion in the ordinary sense. A 55 Hz tone sampled at 50 Hz does not produce a degraded 55 Hz tone; it produces exactly the samples a 5 Hz tone would produce, to floating-point precision. There is no residue to recover and no filter that can help, because after sampling there is no difference between the two cases to detect. This is why the anti-aliasing filter sits in front of the converter, in analog, and why downsampling a digital signal requires low-pass filtering first rather than afterward.

The same reasoning applies outside audio hardware. A sensor read every minute cannot see anything that varies faster than every two minutes, and a metric aggregated hourly will report a spike pattern that repeats every 90 minutes as something quite different. Choosing a sampling rate is choosing which phenomena remain observable at all.

Spectral leakage is the milder but more frequently misread effect. A transform of \(N\) samples has \(N/2 + 1\) frequency bins, and a tone that does not sit exactly on one is expressed as a combination of those that do. The result looks like energy at frequencies the signal never contained. A window function such as the Hann window tapers the ends of the segment and confines the spread, at the price of a broader main peak — neither reading is the true one, because the mismatch between a continuous frequency and a finite set of bins has to go somewhere.

Where it meets data science

Much of machine learning on sequential or high-dimensional data is signal processing under another name. Speech models are fed MFCCs, which are a transform of the spectrum. Convolutional networks learn filters and apply them by convolution, which is what a filter bank does by hand. ECG and EEG classification runs on time-frequency features. Describing deep networks as learned signal-processing pipelines is a fair reading of the architecture, provided the difference is kept in view: a designed filter has a stated response and a learned one does not.

The applications are long-standing — speech recognition and noise cancellation, edge detection and medical imaging, modulation and error correction, radar and sonar tracking, smoothing noisy sensor readings in IoT deployments. What carries across all of them is the discipline of the two blocks above: know the sampling rate before trusting a frequency, and know the window length before trusting a spectrum.

References

  • NumPy documentation, numpy.fft and numpy.hanning. numpy.org

Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.