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
+22 -1
View File
@@ -58,7 +58,7 @@ import {
GetFolderSync, SaveFolderSync, PickFolderSyncFolder, GetFolderSyncStatus, SyncFolderNow,
GetRelayAuto, SaveRelayAuto, GetStationDevices,
GetAwardDefs, GetTrackedAwards, SaveTrackedAwards,
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax, StartTCIAudio, StopTCIAudio, GetTCIAudioStatus,
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax, StartTCIAudio, StopTCIAudio, GetTCIAudioStatus, RecordTCIAudio,
} from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -1947,6 +1947,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
// that asks the backend twice a second for a stream nobody started is work
// done for nothing.
const [tciAudio, setTciAudio] = useState<any>({ running: false, sample_rate: 0, frames: 0, peak_db: -99 });
const [tciRecBusy, setTciRecBusy] = useState(false);
const [tciRecPath, setTciRecPath] = useState('');
useEffect(() => {
if (!tciAudio.running) return;
const id = window.setInterval(() => { GetTCIAudioStatus().then(setTciAudio).catch(() => {}); }, 500);
@@ -6577,6 +6579,25 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</span>
)}
</div>
{/* The test that actually settles it. Frames arriving 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 file the operator can PLAY says all three at once. */}
{tciAudio.running && (
<div className="flex items-center gap-3 flex-wrap">
<Button variant="outline" size="sm" className="h-8" disabled={tciRecBusy}
onClick={() => {
setTciRecBusy(true); setTciRecPath('');
RecordTCIAudio(10)
.then((p: string) => setTciRecPath(p))
.catch((e: any) => setTciAudio((s: any) => ({ ...s, last_err: String(e?.message ?? e) })))
.finally(() => setTciRecBusy(false));
}}>
{tciRecBusy ? t('aud.tciRecBusy') : t('aud.tciRec')}
</Button>
{!!tciRecPath && <span className="text-[11px] font-mono text-muted-foreground truncate">{tciRecPath}</span>}
</div>
)}
{!!tciAudio.last_err && <p className="text-[11px] text-danger">{tciAudio.last_err}</p>}
<p className="text-[11px] text-muted-foreground">{t('aud.tciHint')}</p>
</div>