1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
#!/usr/bin/env python3
"""MAX25 signal sniffer — analyze Bell 202 AFSK on ALSA capture or PCM file."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
LIB = ROOT / "stacks" / "crdop" / "lib"
sys.path.insert(0, str(LIB))
from acoustic_engine import AcousticEngine # noqa: E402
from sound_proxy import SoundConfig, SoundProxy # noqa: E402
def read_wav_pcm(path: Path) -> bytes:
import wave
with wave.open(str(path), "rb") as wf:
if wf.getsampwidth() != 2 or wf.getnchannels() != 1:
raise SystemExit("WAV must be mono S16_LE")
return wf.readframes(wf.getnframes())
def main() -> int:
ap = argparse.ArgumentParser(description="MAX25 acoustic signal sniffer (Bell 202 1200 bd)")
ap.add_argument("-D", "--device", default="", help="ALSA capture device (hw: or plughw:)")
ap.add_argument("-r", "--rate", type=int, default=48000)
ap.add_argument("-f", "--file", type=Path, help="Read mono S16 WAV instead of ALSA")
ap.add_argument("-t", "--seconds", type=float, default=2.0, help="ALSA capture duration")
ap.add_argument("--loopback", action="store_true", help="Internal DSP loopback (no audio hw)")
ap.add_argument("--mark", action="store_true", help="Play mark calibration tone (1200 Hz)")
ap.add_argument("--space", action="store_true", help="Play space calibration tone (2200 Hz)")
args = ap.parse_args()
cfg = SoundConfig(capture=args.device or "default", sample_rate=args.rate)
eng = AcousticEngine(sample_rate=args.rate, sound=cfg)
if args.loopback:
rep = eng.loopback_self_test()
print(f"loopback samples={rep.samples} symbols={rep.symbols} transitions={rep.transitions}")
print(f"mark={rep.mark_ratio:.2%} space={rep.space_ratio:.2%}")
for line in rep.decode_lines:
print(line)
return 0 if rep.decode_lines else 1
if args.mark:
eng.play_mark_calibration(args.seconds)
print(f"played mark tone {args.seconds}s")
return 0
if args.space:
eng.play_space_calibration(args.seconds)
print(f"played space tone {args.seconds}s")
return 0
if args.file:
pcm = read_wav_pcm(args.file)
else:
if not args.device:
print("error: -D hw:... or --file or --loopback required", file=sys.stderr)
return 2
proxy = SoundProxy(cfg)
proxy.start_capture()
nbytes = int(args.rate * args.seconds) * 2
pcm = proxy.read_capture(nbytes)
proxy.close()
rep = eng.analyze_pcm(pcm)
print(f"sniff samples={rep.samples} symbols={rep.symbols} transitions={rep.transitions}")
print(f"mark={rep.mark_ratio:.2%} space={rep.space_ratio:.2%} frames={len(rep.frames)}")
for line in rep.decode_lines:
print(line)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|