Files
OpsLog/app_tci_dvk.go
T
rouggy deeb654482 feat(tci): the voice keyer can send its messages through the radio
The radio now appears as a device in both audio lists — 'Radio (TCI
network audio)' — so the receive audio and the voice keyer can both take
the CAT link instead of a sound card. No virtual cable, no second card, no
Windows mixer between the recording and the air.

The transmit side reuses the exchange the tone probe established, with a
WAV in place of the sine: the radio asks, we answer with the next slice,
and it sets the pace. The message is converted once, up front, rather than
per frame — a voice message is a few hundred kilobytes, and resampling
inside a callback that has 21 ms to answer would put arithmetic on the
path where a late frame is a gap on the air.

The PTT is untouched by all this: the keyer keys before and unkeys after
exactly as it does with a sound card, so a transmission is bracketed by
the same code whichever way the audio travels. And the playback runs OFF
the CAT goroutine, since everything else about the rig goes through that
one place and a ten-second message would otherwise freeze the frequency
display and the antenna following for ten seconds.

Two refusals rather than silent failure. A radio that has gone away stops
being offered as a device at all, and a radio whose transmit audio source
is still the microphone is caught within a fifth of a second — a voice
keyer that transmits silence is worse than one that says why it will not.
2026-08-25 23:59:09 +02:00

61 lines
2.1 KiB
Go

package main
// The voice keyer, through the radio's own link.
//
// Selecting the radio as the "To radio" output makes the voice keyer hand its
// messages to the CAT backend instead of a sound card. Everything around it is
// unchanged — the same PTT before and after, the same gain, the same files —
// which is the point: the audio takes a different road, not a different route.
import (
"fmt"
"hamlog/internal/applog"
"hamlog/internal/audio"
"hamlog/internal/cat"
)
// tciTXPlayer hands one message to the radio.
//
// The controller is fetched on the CAT goroutine and the message is then played
// OFF it. Playing on it would hold that goroutine for the length of the
// message, and everything else about the rig — frequency, mode, PTT state —
// goes through the same place: a ten-second call would freeze the display and
// the antenna following for ten seconds. The TCI backend serialises its own
// writes, so this is safe to call from here.
func (a *App) tciTXPlayer(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error {
if a.cat == nil {
return fmt.Errorf("CAT not initialized")
}
type txPlayer interface {
PlayTXAudio(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error
}
var player txPlayer
err := a.cat.TCIAudioDo(func(t cat.TCIAudioController) error {
p, ok := t.(txPlayer)
if !ok {
return fmt.Errorf("this radio cannot take transmit audio over its CAT link")
}
player = p
return nil
})
if err != nil {
return err
}
return player.PlayTXAudio(pcm, rate, ch, bits, stop)
}
// installTCITXPlayer offers the radio as an audio output, or withdraws it.
//
// Withdrawing matters as much as offering: a radio that has gone away must stop
// being a device the voice keyer will happily "play" to, or a message goes
// nowhere and the operator hears their own PTT click and assumes it worked.
func (a *App) installTCITXPlayer(on bool) {
if !on {
audio.SetNetworkPlayer(nil)
return
}
audio.SetNetworkPlayer(a.tciTXPlayer)
applog.Printf("tci: the radio is available as an audio output — no virtual cable needed for the voice keyer")
}