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"
substation --init
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"
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"
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:
- NFM, narrow FM, on the 2m band, CB and PMR446: it decodes the voice and keeps only its range, 300 to 3,400 Hz. It listens too for a tone below that range, CTCSS or DCS, which many radios send with their voice, and writes any it hears into the recording. That listening has been little tested with real radios, so treat a tone it reports as a guide.
- AM, on the airband: it follows the signal's strength, which is how AM carries a voice, through a level control that rises at once and falls slowly, so that strong and weak stations come out alike.
- USB and LSB, upper and lower sideband, on the HF amateur bands: it picks out the one side of the frequency the voice is carried on.
- TETRA, a digital system: Substation detects its transmissions, but cannot decode or record them.
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:
- noise reduction, which takes the hiss heard in the quietest moments of its start out of the whole recording (
noise_reduction_enabled); - a fade in and out, a few milliseconds long, so that it neither begins nor ends with a click (
fade_in_msandfade_out_ms); - a soft limiter, which rounds off the loudest peaks rather than let them clip (
soft_limit_drive); - and, if you switch it on, trimming of the clicks a transmitter makes as it starts and stops sending, which suits the airband (
trim_carrier_transients).
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.