Spectrum Analyzer
See which stations and signals are on the air around you with a radio and a single display.
Blocks used
Overview
A spectrum analyzer is the first thing most people open on a new radio, because it shows at a glance what is on the air. In this flowgraph, a Soapy SDR block receives 2 MHz of spectrum around 94.9 MHz, in the FM broadcast band, and a Spectrum Analyzer block draws it as a live trace above a scrolling waterfall. In the United States, FM stations sit 200 kHz apart between 88 and 108 MHz,[1] so the window can hold up to ten of them.
The file opens with no radio selected, and a note on the canvas walks through choosing one, reading the display, and fixing common problems. It is a good first step before the Simple FM Receiver, which demodulates one station and plays it. Running it needs an antenna for the FM broadcast band and a radio supported by SoapySDR, such as an RTL-SDR.
How it works
The radio shifts the band around the tuned frequency down to zero and delivers complex I/Q samples, pairs of numbers that tell frequencies above the tuned one apart from those below it.[2] At 2 MS/s, the analyzer sees from 1 MHz below the tuned frequency to 1 MHz above it.
For each batch, the Spectrum Analyzer tapers both ends with a window, which keeps a strong station from smearing across the plot.[3] It then takes a 2,048-point FFT, which splits the batch into frequency bins, and converts each bin to decibels. Each bin is just under 1 kHz wide, the sample rate divided by the FFT size.[3] The trace smooths the spectra over time, and the waterfall stacks them, averaging every four into one row of color.
The vertical scale is in dBFS, decibels relative to the largest signal the radio's converter can represent. The levels are relative, not calibrated power. The window lowers every peak, so even a full-scale tone would sit about 7.5 dB below zero.
What to look for
Each station shows up as a broad hump in the trace and a vertical stripe in the waterfall, where blue is quiet and red is strong. A steady stripe is a transmitter that never stops, such as a broadcast station, and a short dash is a burst. The newest row is at the top, and the 1,024 rows kept cover about four seconds.
A narrow spike exactly at the center that stays put when you retune is the radio's own DC offset, not a transmitter.[2] Tuning a few hundred kilohertz to one side moves a station away from it.
Turning on Max Hold adds a second trace that keeps the highest level seen in each bin, which catches signals that come and go. Scrolling over the plot zooms in, dragging pans, and right-clicking resets the view. On the Soapy SDR block, Buffer Loss should read 0.00%. Anything higher means samples were dropped and the display has gaps.
For a gentle introduction to I/Q samples, FFTs, and waterfalls, see the sources below.
Going further
A Python block can name the strongest signal in the window. Give it one input and no outputs, connect it to the Soapy SDR output, and paste the code below. Once per second it computes the same windowed spectrum as the analyzer, skips the few bins at the center where the DC spike sits, and prints the frequency and level of the highest peak. The level matches the analyzer for a steady tone and reads a few decibels above its smoothed trace for a station.
import time
import numpy as np
_LAST = 0.0
def compute(ctx):
global _LAST
if time.monotonic() - _LAST < 1.0:
return
_LAST = time.monotonic()
x = np.asarray(ctx.inputs[0])
n = x.shape[-1]
rate = ctx.input_attrs[0].get("sampleRate", 0.0)
center = ctx.input_attrs[0].get("frequency", 0.0)
spectrum = np.fft.fftshift(np.fft.fft(x * np.blackman(n), axis=-1), axes=-1)
power = np.mean(np.abs(spectrum) ** 2, axis=0)
power[n // 2 - 4 : n // 2 + 5] = 0.0
peak = int(np.argmax(power))
if power[peak] == 0.0:
print("Strongest signal: none")
return
frequency = center + (peak - n // 2) * rate / n
level = 10 * np.log10(power[peak] / n**2)
print(f"Strongest signal: {frequency / 1e6:.3f} MHz at {level:.1f} dBFS")
Raising Sample Rate widens the window. An RTL-SDR accepts up to 3.2 MS/s, but RTL-SDR Blog, which makes its own dongles, puts the highest rate that does not drop samples at 2.56 MS/s.[4] The Samples setting on the Soapy SDR block is also the FFT size. Doubling it halves the bin width, which sharpens the detail, and halves how often new rows arrive. Lineplot Averaging and Waterfall Averaging trade a quick response for a calmer picture.
To try the same analyzer with no radio, open the Signal Generator or First Steps. The Python QPSK Generator flowgraph feeds it from a Python block. The block catalog and the Python block reference cover the blocks used here.
References
References
-
U.S. Federal Communications Commission, "Numerical designation of FM broadcast channels," Code of Federal Regulations, Title 47, Sec. 73.201. ↩
-
M. Lichtman, "IQ sampling," PySDR: A Guide to SDR and DSP using Python. pysdr.org/content/sampling.html ↩ ↩2
-
M. Lichtman, "Frequency domain," PySDR: A Guide to SDR and DSP using Python. pysdr.org/content/frequency_domain.html ↩ ↩2
-
RTL-SDR Blog, "About RTL-SDR," RTL-SDR.com. rtl-sdr.com/about-rtl-sdr ↩