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>
37 lines
1.5 KiB
Python
37 lines
1.5 KiB
Python
"""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)
|