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() }