Initial commit: bandsaunter, an RTL-SDR signal scanner
Sweeps any set of frequency ranges, records what it finds, and works out what kind of signal it was. - Frequency ranges entered by hand or picked from a 135-entry US band plan, including whole-band and all-CW sweeps that resolve the demodulator per segment. - Detection calibrated against the peak-hold detector's own noise statistics, so the threshold means real margin over static rather than over the floor. - A content gate: captures are kept only if they carry voice, decodable CW, or an identified digital keying scheme. Speech is recognised by a pitch track that drifts, which static cannot imitate. - Identification of NFM/WFM/AM/SSB, CW with Morse decoded to text, P25, DMR, NXDN, D-STAR, POCSAG, FLEX, ACARS, AIS, APRS, n-FSK and n-PSK. - Gapless streaming capture, with the signal path fast enough to keep up in real time, so recordings play back at the right speed. - Optional one-file-per-frequency recording with spoken timestamps, and speech-to-text transcription. - Menus and command line generated from one settings table, so neither can offer something the other cannot; settings persist in ~/.config. 367 tests, run against synthetic signals, a built-in receiver simulator, and real hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
db3e0c79b9
39 changed files with 13473 additions and 0 deletions
37
tests/morse_gen.py
Normal file
37
tests/morse_gen.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Generate correctly-timed Morse audio for testing (1/3/7 unit spacing)."""
|
||||
import numpy as np
|
||||
from bandsaunter.morse import encode_morse
|
||||
|
||||
|
||||
def morse_keying(msg: str, wpm: float, fs: float) -> np.ndarray:
|
||||
"""Key-down/key-up waveform with ITU timing: element 1, letter 3, word 7."""
|
||||
dot = 1.2 / wpm
|
||||
segs = [(0, 8)] # lead-in silence
|
||||
letters = encode_morse(msg).split(" ")
|
||||
for i, tok in enumerate(letters):
|
||||
if tok == "/":
|
||||
segs.append((0, 7)) # word gap
|
||||
continue
|
||||
if i > 0 and letters[i - 1] != "/":
|
||||
segs.append((0, 3)) # letter gap
|
||||
for j, el in enumerate(tok):
|
||||
if j > 0:
|
||||
segs.append((0, 1)) # element gap
|
||||
segs.append((1, 1 if el == "." else 3))
|
||||
segs.append((0, 8))
|
||||
return np.concatenate([np.full(max(1, int(u * dot * fs)), float(s))
|
||||
for s, u in segs])
|
||||
|
||||
|
||||
def morse_audio(msg: str, wpm: float, fs: float = 16000.0, snr_db: float = 20.0,
|
||||
tone: float = 700.0, seed: int = 0) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
env = morse_keying(msg, wpm, fs)
|
||||
# Soften the keying edges the way a real transmitter's shaping does.
|
||||
k = max(3, int(0.006 * fs))
|
||||
env = np.convolve(env, np.hanning(k), "same")
|
||||
env /= max(env.max(), 1e-9)
|
||||
t = np.arange(env.size) / fs
|
||||
x = env * np.sin(2 * np.pi * tone * t)
|
||||
n = np.sqrt(np.mean(x ** 2) / (10 ** (snr_db / 10.0)))
|
||||
return x + n * rng.standard_normal(x.size)
|
||||
Loading…
Add table
Add a link
Reference in a new issue