feat(tci): read the radio's declared format, and record a test WAV

The SunSDR announces its own stream at connect —
audio_stream_sample_type:float32 and audio_stream_channels:2 — and both
were being logged as unhandled while the code worked the format out from
frame arithmetic. The declaration is better evidence and arrives before
the first frame; the arithmetic stays as the check on it. The channel
count now drives the mix-down instead of an assumed stereo.

Adds a ten-second test recording, written as a WAV beside the QSO
recordings. Counting frames proves a socket is delivering bytes; it says
nothing about whether those bytes are the receiver's audio, at the right
rate, in the right order. A stream decoded with the width wrong or the
samples misaligned counts exactly as well as a correct one and sounds
like a fan — so the test is a file the operator can play, the same way
the CW decoder was settled on the air rather than on a spectrogram.

The file is written at the rate the RADIO reported, not a constant: a
recording at the wrong rate plays at the wrong speed, which is the one
fault that would be blamed on the decoding.
This commit is contained in:
2026-08-25 08:25:29 +02:00
parent 9b8168370f
commit 01a23ccb77
7 changed files with 247 additions and 6 deletions
+10
View File
@@ -406,6 +406,16 @@ func (t *TCI) handle(msg string) {
switch strings.ToLower(name) {
case "device":
t.device = strings.TrimSpace(args)
// The radio ANNOUNCES its audio format at connect —
// "audio_stream_sample_type:float32" and "audio_stream_channels:2" — which
// is better evidence than anything derived from a frame, and it arrives
// before the first frame does. Both were being logged as unhandled.
case "audio_stream_sample_type":
t.audio.declaredType = strings.TrimSpace(args)
case "audio_stream_channels":
if n, err := strconv.Atoi(strings.TrimSpace(args)); err == nil && n > 0 && n <= 8 {
t.audio.declaredChans = n
}
case "ready", "start":
t.ready = true
case "stop":