Wideband Sweep

Hop a radio across the whole FM band and stitch the slices into one live spectrum.

Download
Blocks 3
Category Basics
Revision 1
License MIT

Blocks used

SDR Sweep
Spectrum Analyzer
Note

Overview

A radio sees only a few megahertz of spectrum at a time, yet in the United States the FM broadcast band alone runs from 88 to 108 MHz.[1] To show all of it, a radio has to hop, tuning, capturing a short slice, and retuning until the band is covered. In this flowgraph, an SDR Sweep block hops a radio across those 20 MHz and joins the slices into one spectrum, and a Spectrum Analyzer block draws it as a trace above a waterfall.

The file opens with no radio selected, and a note on the canvas explains how to pick one and read the block's readouts. Running it needs an antenna for the FM broadcast band and a supported radio that tunes it, such as an RTL-SDR.

How it works

The numbers below assume an RTL-SDR, which the block captures at 2.4 MS/s. To reach the 10 kHz Resolution, each capture is 240 samples long, because an FFT bin's width is the sample rate divided by the number of samples.[2] Each capture gets a window and an FFT.

The block keeps only the middle three quarters of each capture's spectrum, much like the cropping that the rtl_power scanner recommends,[3] and drops a few bins at the center, where the radio's DC spike sits.[4] Each hop then moves 860 kHz, so most frequencies are seen by two hops, which are blended with weights that fade toward the edges. Covering the band takes 23 hops and fills 2,000 bins.

An inverse FFT turns the stitched spectrum back into 2,000 complex samples, labeled as a 20 MS/s signal. The Spectrum Analyzer reads them like samples from any radio, skips its own window because every hop already had one, and draws the whole band.

Each hop is captured at a different moment, so the output is a picture of the band, not a recording, and it cannot be demodulated. Automatic Gain is off, so every hop uses the same gain and levels from different hops can be compared.

What to look for

Each station shows up as a hump in the trace and a vertical stripe in the waterfall, on the 200 kHz channel grid used in the United States.[1] The waterfall adds one row per sweep, newest at the top, and keeps 512 of them. The trace averages about four sweeps, and setting Spectrum Averaging to 1 shows each sweep on its own. The vertical scale runs from −110 to −20 dBFS, decibels below the largest signal the radio can represent.

Unlike the Spectrum Analyzer example, the middle of the plot has no DC spike, because the bins near each hop's center come from its neighbors. A faint copy of a strong station 860 kHz above it means the radio had not settled after retuning. Settling Time 0 uses the radio's own default, 5 ms on an RTL-SDR, so set it to about 10 ms, which usually clears the copy.

On the SDR Sweep block, Sweep Speed shows the band covered per second and the complete sweeps per second. Buffer Loss should stay at 0%.

For a gentle introduction to FFTs, bins, and windows, see the sources below.

Going further

A Python block can list the strongest signals in the band. Give it one input and no outputs, connect it to the SDR Sweep output, and paste the code below. Every two seconds it takes the FFT of the latest sweep and prints the five highest peaks at least 200 kHz apart. Its levels match a single sweep on the analyzer and read up to a few decibels above the averaged trace.

PYTHON
import time

import numpy as np

_LAST = 0.0


def compute(ctx):
    global _LAST
    if time.monotonic() - _LAST < 2.0:
        return
    _LAST = time.monotonic()
    x = np.asarray(ctx.inputs[0]).reshape(-1)
    n = x.size
    span = float(ctx.input_attrs[0].get("sampleRate", 0.0))
    center = float(ctx.input_attrs[0].get("frequency", 0.0))
    if n == 0 or span <= 0.0:
        return
    level = 20 * np.log10(np.abs(np.fft.fftshift(np.fft.fft(x))) / n + 1e-12)
    frequency = center + (np.arange(n) - n // 2) * span / n
    found = []
    for _ in range(5):
        peak = int(np.argmax(level))
        if not np.isfinite(level[peak]):
            break
        found.append(f"{frequency[peak] / 1e6:.2f} MHz {level[peak]:.1f} dBFS")
        level[np.abs(frequency - frequency[peak]) < 200e3] = -np.inf
    print("Strongest: " + ", ".join(found))

Start Frequency and End Frequency move the sweep to any band the radio can tune. A wider span needs more hops and takes longer, and from 400 to 470 MHz an RTL-SDR needs 81 hops for 7,000 bins. Lowering Resolution lengthens each capture for finer detail, and Dwell averages several captures per hop for a calmer noise floor. A HackRF with recent firmware can sweep in its firmware at 20 MS/s, and the block switches to that mode on its own when the settings allow.

The block catalog and the Python block reference cover the blocks used here.

References

References

  1. U.S. Federal Communications Commission, "Numerical designation of FM broadcast channels," Code of Federal Regulations, Title 47, Sec. 73.201. ↩ ↩2

  2. M. Lichtman, "Frequency domain," PySDR: A Guide to SDR and DSP using Python. pysdr.org/content/frequency_domain.html ↩

  3. Osmocom, "rtl_power.c," rtl-sdr, GitHub. ↩

  4. M. Lichtman, "IQ sampling," PySDR: A Guide to SDR and DSP using Python. pysdr.org/content/sampling.html ↩

CyberEther

High-performance GPU-accelerated signal processing and visualization framework that runs anywhere.

Star on GitHub

© 2026 CyberEther. All rights reserved.

The final frontier!