Multi-FM Receiver

Capture a wide slice of the FM band with one radio and pull two stations out of it at the same time.

Download
Blocks 7
Category Receivers
Version 1.0.0
License MIT
Updated Sep 28, 2026

Blocks used

Soapy SDR
Filter
Slice
FM Demodulator
Audio
Spectrum Analyzer
Note

Overview

A broadcast radio hears one station at a time, but a software-defined radio can capture a piece of the band wide enough to hold several. In the United States the FM band is divided into 200 kHz channels,[1] so a 2 MHz capture spans ten of them. The Simple FM Receiver captures 240 kHz, enough for one station.

This flowgraph tunes the radio to 96.9 MHz at 2 MS/s and cuts two 200 kHz channels out of that capture at once, one at 97.3 MHz and one at 96.5 MHz. Each comes out at 200 kS/s, as if a separate radio had been tuned to it. The first station plays through the speakers, and each channel has its own spectrum analyzer next to a wide analyzer fed directly by the radio. Running it needs an antenna for the FM broadcast band, two stations within the same 2 MHz window, speakers or headphones, and a radio supported by SoapySDR, such as an RTL-SDR.

How it works

Soapy SDRFilterSlice (head 0)FM DemodulatorAudioSlice (head 1)Spectrum Analyzer

The radio delivers eight batches of 8,000 samples per update, 32 ms of signal. The Filter block gives each head, one per station, its own bandpass filter. Each is a 51-tap Blackman-windowed sinc with its half-amplitude points at ±100 kHz,[2] shifted to the head's offset of +400 kHz or −400-400 kHz.

The filtering runs in the frequency domain. The block pads each batch to 8,050 samples, takes one FFT, and multiplies it by each head's filter spectrum. It then rotates each head's product so the station sits at 0 Hz and adds its ten 805-bin segments together. Folding the spectrum this way is the frequency-domain equivalent of keeping every tenth sample, so one 805-point inverse FFT per head filters, tunes, and decimates at once. Overlap-add joins consecutive batches by adding the tail each one leaves over.[3] The forward FFT is shared by every head, which keeps an extra station cheap. Borgerding describes the same combination of a shared transform, mixing, and frequency-domain decimation for overlap-save.[4]

The output holds batches, heads, and 800 samples per batch at 200 kS/s. Each Slice keeps one head. Head 0 feeds the FM Demodulator and the Audio block, both set to 200 kS/s, and the Audio block resamples it to 48 kS/s. Demodulation works as in the Simple FM Receiver.

What to look for

The large analyzer shows the full 2 MHz around 96.9 MHz, and each station appears as a hump about 200 kHz wide. The two small analyzers each show one head, 97.3 MHz from head 0 and 96.5 MHz from head 1. Their frequency axes stay centered on 96.9 MHz, because the Filter passes on the new sample rate but not the new center frequency, so read each one as a window around its own station. A head centered on its station shows one hump in the middle, and a head that misses shows it off center, or only noise.

For a kernel of M+1M + 1 taps, the transition from passband to stopband is about 4/M4/M of the sample rate wide,[2] which is 160 kHz for these 51 taps. Attenuation starts about 20 kHz from the head's center and is complete only about 180 kHz out, so the outer part of a station's own channel is already turned down, and part of a strong neighbor 200 kHz away gets through and folds into the head at the lower rate. Raising Taps to 101 halves the transition to 80 kHz.

Changing the upper Slice from index 0 to index 1 switches the audio to 96.5 MHz.

For a deeper treatment of windowed-sinc filters and fast convolution filter banks, see the sources below.

Going further

To check each head's tuning, give a Python block one input and no outputs, connect it to the Filter output, and paste the code below. It averages 31 updates, about one second, and prints each head's average power in dB relative to full scale and its station's offset from the head's center, positive when the station sits above it. Program audio averages out over a second, so the mean phase step is the carrier's offset. An empty head reads low power and a meaningless offset.

PYTHON
import numpy as np

UPDATES = 31

_FREQ = 0.0
_POWER = 0.0
_CALLS = 0


def compute(ctx):
    global _FREQ, _POWER, _CALLS
    x = np.asarray(ctx.inputs[0])
    fs = float(ctx.input_attrs[0].get("sampleRate", 200e3))
    step = np.angle(x[..., 1:] * np.conj(x[..., :-1]))
    _FREQ = _FREQ + step.mean(axis=(0, 2)) * fs / (2 * np.pi)
    _POWER = _POWER + np.mean(np.abs(x) ** 2, axis=(0, 2))
    _CALLS += 1
    if _CALLS % UPDATES == 0:
        power = 10 * np.log10(np.maximum(_POWER / UPDATES, 1e-20))
        for head, (f, p) in enumerate(zip(_FREQ / UPDATES, power)):
            print(f"Head {head}: {p:6.1f} dB, offset {f / 1e3:+6.2f} kHz")
        _FREQ, _POWER = 0.0, 0.0

Frequency and Center can change freely. A third station needs Heads set to 3, a third offset in Center, and a third Slice for index 2 feeding its own analyzer. Each offset has to keep the station's whole 200 kHz channel inside the capture, so within ±0.9 MHz of the tuned frequency. The offsets do not have to be symmetric.

The Filter's Sample Rate must match the radio's. Its Bandwidth sets the output rate, so the FM Demodulator and Audio sample rates must follow it. The block decimates by the sample rate divided by the bandwidth, 10 here, and only when that ratio is a whole number and divides both the number of taps minus one and the radio's batch length. The radio's 8,000 samples per batch pass that test where its default of 8,192 would not, and Taps can be 41, 51, 61, or 101 but not 55. When a rule breaks, the block warns in the console and skips the decimation and the shift, so each head stays at 2 MS/s with its station at its offset, which no longer matches the FM Demodulator and Audio blocks.

The Overlap-Add-Fold flowgraph lays out the Filter's steps as separate blocks. 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. S. W. Smith, "Windowed-sinc filters," in The Scientist and Engineer's Guide to Digital Signal Processing. California Technical Publishing, 1997, ch. 16. dspguide.com/ch16.htm ↩ ↩2

  3. S. W. Smith, "FFT convolution," in The Scientist and Engineer's Guide to Digital Signal Processing. California Technical Publishing, 1997, ch. 18. dspguide.com/ch18.htm ↩

  4. M. Borgerding, "Turning overlap-save into a multiband mixing, downsampling filter bank," IEEE Signal Process. Mag., vol. 23, no. 2, pp. 158-161, Mar. 2006, doi:10.1109/MSP.2006.1598092. ↩

CyberEther

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

Star on GitHub

© 2026 CyberEther. All rights reserved.

The final frontier!