refactor(tci): remove the test bench now that choosing the device is the setup

The TCI section of the audio settings was an investigation: open the
stream, read what arrives, record ten seconds to listen to, key a tone.
It answered every question it was built for — the frame layout, the
sample width, that the radio asks rather than follows a clock, that the
transmit audio source decides — and confirmed 80 W on real hardware.

None of that belongs in front of an operator now. The radio is simply one
of the devices in the two dropdowns, and choosing it IS the configuration:
one control, in the place where the question is already being asked.
Keeping the tick box beside it would have been two switches for one
decision, with the second one where nobody looks.

Gone with it: the stream/record/probe bindings, the audio.tci_rx setting,
and twenty-four translation keys. The plumbing they proved out stays and
now carries the voice keyer.
This commit is contained in:
2026-08-26 00:13:04 +02:00
parent ec3e60e47b
commit ceb88d2e29
10 changed files with 24 additions and 601 deletions
+9 -84
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, RecordTCIAudio, GetTCIRecordAudio, SetTCIRecordAudio, ProbeTCITransmit,
GetBandOpenSettings, SaveBandOpenSettings, GetGridScopeSettings, SaveGridScopeSettings, GetPSKReporterStatus, GetChaseNewGrids, SetChaseNewGrids, GetChaseNew, SetChaseNew, GetGridCacheStatus, GetLinkedAmps, SetLinkedAmps, GetSpotTTLMinutes, SetSpotTTLMinutes, GetSpotMax, SetSpotMax,
} from '../../wailsjs/go/main/App';
import type { profile as profileModels } from '../../wailsjs/go/models';
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
@@ -2038,18 +2038,10 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
// TCI receive-audio test bench. Polled only while the stream is open: a panel
// 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 [tciTXBusy, setTciTXBusy] = useState(false);
// Whether the QSO recorder takes its audio from the radio's own stream.
const [tciRec, setTciRec] = useState(false);
useEffect(() => { GetTCIRecordAudio().then((v: boolean) => setTciRec(!!v)).catch(() => {}); }, []);
const [tciRecPath, setTciRecPath] = useState('');
useEffect(() => {
if (!tciAudio.running) return;
const id = window.setInterval(() => { GetTCIAudioStatus().then(setTciAudio).catch(() => {}); }, 500);
return () => window.clearInterval(id);
}, [tciAudio.running]);
const [gridStat, setGridStat] = useState<any>(null);
const [pskrStatus, setPskrStatus] = useState<any>(null);
const saveBandOpen = async (next: any) => {
@@ -6762,78 +6754,11 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
<strong>{t('aud.toRadioShort')}</strong> {t('aud.explainTo')}
</p>
{/* TCI receive audio EXPERIMENTAL, and the panel says so.
A SunSDR already carries its receive audio on the WebSocket that
carries its commands, so none of the devices above need to exist
for it: no virtual cable, no second sound card. This is the test
bench for that path it opens the stream and reports what really
arrives, because "the stream is open" and "audio is arriving" are
different claims and only the second one is worth anything. */}
<div className="rounded-md border border-border p-3 space-y-2">
<div className="text-xs font-medium">{t('aud.tciTitle')}</div>
<div className="flex items-center gap-3 flex-wrap">
<Button variant={tciAudio.running ? 'default' : 'outline'} size="sm" className="h-8"
onClick={() => {
const p = tciAudio.running ? StopTCIAudio() : StartTCIAudio(0, 48000);
p.then(() => GetTCIAudioStatus().then(setTciAudio))
.catch((e: any) => setTciAudio((s: any) => ({ ...s, last_err: String(e?.message ?? e) })));
}}>
{tciAudio.running ? t('aud.tciStop') : t('aud.tciStart')}
</Button>
{tciAudio.running && (
<span className="text-[11px] font-mono text-muted-foreground">
{tciAudio.sample_rate || 0} Hz · {tciAudio.frames || 0} frames ·{' '}
{tciAudio.peak_db > -90 ? tciAudio.peak_db.toFixed(1) + ' dBFS' : t('aud.tciSilent')}
</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>
)}
{/* The transmit experiment. It KEYS THE RADIO, which is why it is
worded as a warning and not as another test button: a first pass
on real hardware showed the radio sends nothing extra while the
operator keys it by hand, so the only way to learn what it wants
is to key it from here and push a tone. */}
<div className="rounded border border-caution-border bg-caution-muted p-2 space-y-1.5">
<Button variant="outline" size="sm" className="h-8" disabled={tciTXBusy}
onClick={() => {
setTciTXBusy(true);
ProbeTCITransmit(5)
.catch((e: any) => setTciAudio((s: any) => ({ ...s, last_err: String(e?.message ?? e) })))
.finally(() => setTciTXBusy(false));
}}>
{tciTXBusy ? t('aud.tciTXBusy') : t('aud.tciTX')}
</Button>
<p className="text-[11px] text-caution-muted-foreground">{t('aud.tciTXWarn')}</p>
</div>
{!!tciAudio.last_err && <p className="text-[11px] text-danger">{tciAudio.last_err}</p>}
<label className="flex items-start gap-2 text-xs cursor-pointer">
<Checkbox className="mt-0.5" checked={tciRec}
onCheckedChange={(c) => { setTciRec(!!c); SetTCIRecordAudio(!!c).catch(() => {}); }} />
<span>
{t('aud.tciRecord')}
<span className="block text-muted-foreground">{t('aud.tciRecordHint')}</span>
</span>
</label>
<p className="text-[11px] text-muted-foreground">{t('aud.tciHint')}</p>
</div>
{/* The radio is one of the devices above when it can carry its own
audio see ListAudioInputDevices. What used to be here was a test
bench: open the stream, record ten seconds, key a tone. It settled
how TCI works and has no business in front of an operator now that
choosing the device is the whole of the setup. */}
<div className="flex items-center gap-3">
<Button
variant={monitorOn ? 'default' : 'outline'}