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.fftandnumpy.hanning. numpy.org
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
