Files
OpsLog/app_digi_txaudio.go
T
rouggy 60990276f5 feat(audio): transmit audio for WSJT-X through a network Icom
An IC-705, IC-7610 or IC-7760 on Ethernet has no sound card on this PC:
its receive audio arrives on UDP 50003 and its transmit audio has to go
back the same way. OpsLog already did both — the monitor plays the
stream, the voice keyer and the talk button send into it.

WSJT-X could not. It drives the rig through the CAT OpsLog shares, so it
tunes and keys perfectly well, and then has nowhere to put its audio: it
needs a Windows endpoint and the radio is not one. A virtual cable
bridges that, which is what wfview asks of its users too — nothing short
of a signed kernel driver can present a sound card.

So: a "WSJT-X transmit audio" device in Settings ▸ Audio, shown only
when To Radio is the radio itself, since a rig with a USB codec needs
none of this and WSJT-X talks to that codec directly.

Almost no new machinery. StartTXAudioNetwork already pipes a chosen
capture device into the rig — it was written for the talk button — and
the missing piece was only WHEN. That is catShareRig.SetPTT: WSJT-X keys
through us, so we know. Audio starts after the carrier and stops before
it, because the other order transmits what is still buffered after the
program thinks the over is finished.

Armed by PTT rather than left running: a permanent stream would put
whatever the cable carries — desktop notifications, another program that
grabbed the same cable — on the air the moment anything else keyed the
rig. And stopped only if WE started it, since the talk button and the
voice keyer share the same route and a digital over ending must not cut
off a message being played by hand.

Receive needs no code: point Listening at a second cable and WSJT-X's
input at its other side. The hint in Settings says so.
2026-09-11 09:09:35 +02:00

115 lines
4.0 KiB
Go

package main
// ── Transmit audio for a digital program, over the radio's own link ────────
//
// An IC-705, IC-7610 or IC-7760 reached over Ethernet has no sound card on this
// PC: its receive audio arrives on UDP 50003 and its transmit audio has to go
// back the same way. OpsLog already does both — the RX monitor plays the stream,
// the voice keyer and the talk button send into it.
//
// WSJT-X cannot. It drives the rig through the CAT OpsLog shares (rigctld), so
// it can tune and key perfectly well, and then has nowhere to put its audio: it
// needs a Windows audio endpoint and the radio is not one. A virtual cable
// bridges the gap — WSJT-X's output device is the cable, and OpsLog captures the
// cable's other side and streams it to the rig. That is what wfview asks of its
// users too; nothing short of a signed kernel driver can present a sound card,
// and OpsLog is neither signed nor a driver.
//
// The route is armed by PTT rather than left running. Streaming continuously
// would put whatever the cable carries — the desktop's notification sounds, an
// idle WSJT-X's silence, another program that grabbed the same cable — into a
// transmitter the moment anything else keyed it. PTT is also exactly the signal
// available: WSJT-X keys through us, so we know.
import (
"strings"
"hamlog/internal/applog"
"hamlog/internal/audio"
"hamlog/internal/cat"
)
// digiTXSender returns the radio's live-audio sender, or nil when this station
// is not set up for the path at all.
//
// Three things have to be true, and none of them is an error worth reporting on
// every over: a capture device is configured, the radio takes audio over its
// link, and its audio session is actually open.
func (a *App) digiTXSender() (dev string, send func([]byte) error) {
if a.audioMgr == nil || a.cat == nil {
return "", nil
}
cfg, err := a.GetAudioSettings()
if err != nil {
return "", nil
}
dev = strings.TrimSpace(cfg.DigiInput)
if dev == "" {
return "", nil
}
type sender interface {
TXAudioSender() (func([]byte) error, error)
}
if derr := a.cat.IcomDo(func(ic cat.IcomController) error {
p, ok := ic.(sender)
if !ok {
return nil // not a radio that takes audio over its link
}
fn, serr := p.TXAudioSender()
if serr != nil {
// The stream is not open — RX audio is switched off in Settings ▸ CAT.
// Said once per over at most, and it names the fix.
applog.Printf("digi audio: %v", serr)
return nil
}
send = fn
return nil
}); derr != nil {
return "", nil
}
if send == nil {
return "", nil
}
return dev, send
}
// startDigiTXAudio begins piping the configured capture device into the radio.
//
// Called after the rig is keyed, so a failure here leaves a transmitting radio
// with no audio rather than audio going into a receiver — and it is reported
// rather than silently dropped, because a station that transmits silence for a
// whole evening has no other way to find out.
func (a *App) startDigiTXAudio() {
dev, send := a.digiTXSender()
if send == nil {
return
}
if err := a.audioMgr.StartTXAudioNetwork(dev, send); err != nil {
applog.Printf("digi audio: %q could not be opened, the over will be silent: %v", dev, err)
return
}
a.digiTX.Store(true)
applog.Printf("digi audio: %q → the radio, for as long as the shared CAT holds PTT", dev)
}
// stopDigiTXAudio ends the route. Only when WE started it: the talk button and
// the voice keyer use the same one, and unkeying a digital over must not cut
// off a message the operator is playing by hand.
func (a *App) stopDigiTXAudio() {
if !a.digiTX.Swap(false) {
return
}
if a.audioMgr != nil {
a.audioMgr.StopTXAudio()
}
}
// ListDigiInputDevices is the dropdown behind the setting: the capture devices,
// which for this purpose means the virtual cable WSJT-X plays into.
//
// The radio itself is deliberately NOT offered. It is a source of receive audio,
// and picking it here would ask OpsLog to send the radio its own output.
func (a *App) ListDigiInputDevices() ([]audio.Device, error) {
return audio.ListInputDevices()
}