Subsystem Search

How Substation hears a transmission

By the end of this chapter, you know what Substation does between the receiver and a recording, and why a transmission turns a radio channel on.

The folder so far

The radio folder as Defined bands, and creating your own left it: Substation installed in an environment inside it, switched on, its defaults kept as defaults.yaml, your band in config.yaml, and the test signal made.

$ mkdir radio
$ cd radio
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install "substation==0.8.2"
…
Successfully installed … substation-…
$ substation --init
Created /home/keanu/radio/config.yaml
This is the full default configuration, fully commented. Edit it to suit
your hardware and bands, then start a scan, e.g. `substation --band amateur_2m`.
Any setting you delete falls back to the built-in default.
$ mv config.yaml defaults.yaml
bands:
  amateur_2m_simplex:
    type: NFM
    reception_class: general
    freq_start: 145.2e+6
    freq_end: 145.5625e+6
    channel_spacing: 12.5e+3
    sample_rate: 2.4e+6
# Makes test-signal.wav: eight seconds of the 2m band as a receiver hands it over,
# hiss with a transmission in the middle, for Substation to scan in place of a receiver.

import wave

import numpy

# The frequency the transmission is sent on, in hertz, and how many seconds it lasts.
frequency = 145_500_000
length = 3

# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000

# Two seconds of hiss first, while Substation measures it, and three after.
start = 2
seconds = start + length + 3

# The hiss is random, but the same every time the script runs.
noise = numpy.random.default_rng(1)

with wave.open("test-signal.wav", "wb") as file:

	# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
	file.setnchannels(2)
	file.setsampwidth(2)
	file.setframerate(rate)

	# A second at a time, so the script needs little memory.
	for second in range(seconds):

		# The moment of each IQ sample, in seconds.
		t = second + numpy.arange(rate) / rate

		# The receiver's own hiss.
		hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01

		# A buzz like a voice: overtones of a low note, each softer than the one below.
		voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))

		# Louder and softer six times a second, like syllables.
		loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))

		# FM: the sound swings the transmission's frequency to either side of its own.
		phase = 2 * numpy.pi * (frequency - centre) * t + loudness * voice

		# The transmission, only while it is on, added to the hiss.
		on = (t >= start) & (t < start + length)
		iq = hiss + numpy.where(on, 0.3 * numpy.exp(1j * phase), 0)

		# Written as pairs of 16-bit numbers, I then Q.
		pairs = numpy.stack([iq.real, iq.imag], axis=1)
		file.writeframes((pairs * 32767).astype("<i2").tobytes())
$ python make_signal.py

A slice at a time

Substation listens a slice at a time. It takes the IQ samples the receiver hands over in about a fifth of a second, a slice, and measures how strong the signal is in each of the band's radio channels. It measures each one eight times, in overlapping parts of the slice, and averages them, so that no single burst of hiss decides anything. The log gives both as a scan starts: Time slice: 200 ms, and Welch segments: 8, after the method it measures with.

The noise floor

Before it listens for transmissions, Substation measures the hiss, in the gaps between radio channels and either side of them, where nothing is sent. That level is the noise floor. For its first ten slices, about two seconds, it does nothing else, which also gives the receiver time to settle after it starts, and then says so: Noise floor warmup complete (6.8 dB). Detection enabled. That is why the test signal leaves two seconds of hiss before its transmission begins.

It goes on measuring the noise floor as it scans, slowly, so the floor follows the hiss as it changes, while a transmission, which comes and goes, stands out against it.

The threshold

A radio channel turns on when its signal rises far enough above the noise floor. How far is its signal-to-noise ratio, or SNR, in decibels, and it must pass the band's snr_threshold_db. Your band leaves it out, so it has Substation's default, 12 dB, where amateur_2m sets 4.5 dB.

To see the threshold at work, set it higher than the transmission rises, at 25 dB. Add it to your band:

bands:
  amateur_2m_simplex:
    type: NFM
    reception_class: general
    freq_start: 145.2e+6
    freq_end: 145.5625e+6
    channel_spacing: 12.5e+3
    sample_rate: 2.4e+6
    snr_threshold_db: 25
$ substation --band amateur_2m_simplex --iq-file test-signal.wav --center-freq 145000000 --start-time "2026-10-05 21:00:00"
… - INFO - Initialized scanner for band 'amateur_2m_simplex'
… - INFO - Frequency range: 145.20000 - 145.56250 MHz
… - INFO - Number of radio channels: 30
… - INFO - Center frequency: 145.38125 MHz
… - INFO - Required bandwidth: 0.38550 MHz (inc. 6.2kHz edge margin)
… - INFO - Sample rate: 2.400 MHz
… - INFO - SNR threshold: 25.0 dB ON / 22.0 dB OFF (hysteresis)
… - INFO - SDR Device: file (index 0)
… - INFO - Modulation: NFM
… - INFO - Recording: ENABLED (16000 Hz mono WAV to ./audio)
… - INFO - IQ file playback: test-signal.wav, center 145.000000 MHz, start 2026-10-05 21:00:00
… - INFO - Starting scan...
… - INFO - Setting up SDR device...
… - INFO - IQ file: test-signal.wav, 2.400 MHz, 19200000 frames (8s / 0.0h), center 145.000000 MHz
… - INFO - Using device center frequency: 145.000000 MHz (band midpoint: 145.381250 MHz)
… - INFO - SDR Gain: auto
… - INFO - SDR device configured successfully
… - INFO - Noise estimation regions: 31 gaps, 1630 bins total
… - INFO - FFT size: 65536 bins, frequency resolution: 36.6 Hz
… - INFO - Welch segments: 8, samples per slice: 524288
… - INFO - DC spike exclusion: 7 bins around center
… - INFO - Started async SDR streaming
… - INFO - Time slice: 200 ms (524288 samples)
… - INFO - Noise floor warmup complete (6.8 dB). Detection enabled.
… - INFO - IQ file playback complete
… - INFO - Cancelled async SDR streaming
… - INFO - SDR device closed

The transmission never rose 25 dB above the hiss, so radio channel 25 never turned on, and nothing was recorded. Now set the threshold as amateur_2m does:

bands:
  amateur_2m_simplex:
    type: NFM
    reception_class: general
    freq_start: 145.2e+6
    freq_end: 145.5625e+6
    channel_spacing: 12.5e+3
    sample_rate: 2.4e+6
    snr_threshold_db: 4.5
$ substation --band amateur_2m_simplex --iq-file test-signal.wav --center-freq 145000000 --start-time "2026-10-05 21:00:00"
… - INFO - Initialized scanner for band 'amateur_2m_simplex'
… - INFO - Frequency range: 145.20000 - 145.56250 MHz
… - INFO - Number of radio channels: 30
… - INFO - Center frequency: 145.38125 MHz
… - INFO - Required bandwidth: 0.38550 MHz (inc. 6.2kHz edge margin)
… - INFO - Sample rate: 2.400 MHz
… - INFO - SNR threshold: 4.5 dB ON / 1.5 dB OFF (hysteresis)
… - INFO - SDR Device: file (index 0)
… - INFO - Modulation: NFM
… - INFO - Recording: ENABLED (16000 Hz mono WAV to ./audio)
… - INFO - IQ file playback: test-signal.wav, center 145.000000 MHz, start 2026-10-05 21:00:00
… - INFO - Starting scan...
… - INFO - Setting up SDR device...
… - INFO - IQ file: test-signal.wav, 2.400 MHz, 19200000 frames (8s / 0.0h), center 145.000000 MHz
… - INFO - Using device center frequency: 145.000000 MHz (band midpoint: 145.381250 MHz)
… - INFO - SDR Gain: auto
… - INFO - SDR device configured successfully
… - INFO - Noise estimation regions: 31 gaps, 1630 bins total
… - INFO - FFT size: 65536 bins, frequency resolution: 36.6 Hz
… - INFO - Welch segments: 8, samples per slice: 524288
… - INFO - DC spike exclusion: 7 bins around center
… - INFO - Started async SDR streaming
… - INFO - Time slice: 200 ms (524288 samples)
… - INFO - Noise floor warmup complete (6.8 dB). Detection enabled.
… - INFO - Radio channel 25 ON (f = 145.50000 MHz, SNR = 15.3dB, recording: YES)
… - INFO - Radio channel 25 OFF (f = 145.50000 MHz, SNR = -0.2dB, recording: YES)
… - INFO - IQ file playback complete
… - INFO - Cancelled async SDR streaming
… - INFO - SDR device closed

This time radio channel 25 turned on, 15.3 dB above the hiss, as the transmission began. A lower threshold hears weaker transmissions, and turns on for hiss more often. If your recordings are mostly hiss, raise it a decibel or two at a time, and if you miss transmissions you can hear on another radio, lower it. Hiss strong in one radio channel alone is another matter, which raising it does not cure, as Recordings of nothing shows.

Turning off

A radio channel turns off at a lower level than it turned on: the band's snr_threshold_db less its hysteresis_db, 3 dB unless the band says otherwise, so that a signal hovering near the threshold does not turn it on and off over and over. The log gives both levels: SNR threshold: 4.5 dB ON / 1.5 dB OFF (hysteresis). With a threshold this low, a smaller hysteresis_db, such as 1.5 dB, suits weak signals.

Once a radio channel turns off, Substation goes on recording for half a second, its recording_hold_time_ms, so that a transmission dipping below the off level for a moment stays in one recording. That is why the recording runs on a little after the transmission ends.

The spike at the centre

A receiver hands over a spike at its own centre frequency, which radio calls the DC spike: something the receiver makes, not a signal, and it would make a radio channel there look busy all the time. So where a band has a radio channel at the centre, Substation tunes half a radio channel spacing away, and leaves the narrowest sliver around the spike out of its measurements. The first line of A first scan's log was that move: DC offset: shifted the centre frequency +6250 Hz to avoid masking a radio channel at 145.00000 MHz. Your band has no radio channel at 145 MHz, the test signal's centre, so its log has no such line.

Each modulation

A band's modulation says how its transmissions carry their sound, and Substation decodes each its own way:

Before a recording is written

A recording goes through a few stages on its way to the file, each with a setting in the configuration's recording section:

It is then written with its details: when it began, its frequency and its modulation.

Not everything that crosses the threshold is a transmission. The next chapter looks at how Substation tells hiss from a voice, and throws the hiss away.