feat: added full support in USB (local) & ethernet (local or remote) of audio for Icom

This commit is contained in:
2026-07-09 11:30:06 +02:00
parent 1f5e5759cc
commit 521f8266cf
14 changed files with 882 additions and 22 deletions
+97 -2
View File
@@ -96,6 +96,7 @@ const (
keyCATIcomNetHost = "cat.icom.net.host" // Icom network remote: rig IP/hostname
keyCATIcomNetUser = "cat.icom.net.user" // Icom network: Network User1 ID
keyCATIcomNetPass = "cat.icom.net.pass" // Icom network: Network User1 password
keyCATIcomNetAudio = "cat.icom.net.audio" // Icom network: stream RX audio on 50003 (experimental)
keyCATTCIHost = "cat.tci.host" // TCI host (Expert Electronics SunSDR / ExpertSDR2)
keyCATTCIPort = "cat.tci.port" // TCI WebSocket port (default 40001)
keyCATTCISpots = "cat.tci.spots" // push cluster spots to the TCI panorama
@@ -279,6 +280,7 @@ type CATSettings struct {
IcomNetHost string `json:"icom_net_host"` // Icom network remote: rig IP/hostname (built-in LAN server)
IcomNetUser string `json:"icom_net_user"` // Icom network Network User1 ID
IcomNetPass string `json:"icom_net_pass"` // Icom network Network User1 password
IcomNetAudio bool `json:"icom_net_audio"` // Icom network: stream RX audio (50003) — experimental, needs on-rig verification
TCIHost string `json:"tci_host"` // TCI host (Expert Electronics SunSDR)
TCIPort int `json:"tci_port"` // TCI WebSocket port (default 40001)
TCISpots bool `json:"tci_spots"` // push cluster spots to the TCI panorama
@@ -4321,7 +4323,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if a.settings == nil {
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
if err != nil {
return CATSettings{}, err
}
@@ -4338,6 +4340,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
IcomNetHost: m[keyCATIcomNetHost],
IcomNetUser: m[keyCATIcomNetUser],
IcomNetPass: m[keyCATIcomNetPass],
IcomNetAudio: m[keyCATIcomNetAudio] == "1",
TCIHost: m[keyCATTCIHost],
TCIPort: 40001,
TCISpots: m[keyCATTCISpots] == "1",
@@ -4413,6 +4416,10 @@ func (a *App) SaveCATSettings(s CATSettings) error {
if s.TCISpots {
tciSpots = "1"
}
icomNetAudio := "0"
if s.IcomNetAudio {
icomNetAudio = "1"
}
if s.DigitalDefault == "" {
s.DigitalDefault = "FT8"
}
@@ -4429,6 +4436,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
keyCATIcomNetHost: strings.TrimSpace(s.IcomNetHost),
keyCATIcomNetUser: strings.TrimSpace(s.IcomNetUser),
keyCATIcomNetPass: s.IcomNetPass,
keyCATIcomNetAudio: icomNetAudio,
keyCATTCIHost: strings.TrimSpace(s.TCIHost),
keyCATTCIPort: strconv.Itoa(s.TCIPort),
keyCATTCISpots: tciSpots,
@@ -5877,6 +5885,72 @@ func (a *App) DVKStop() {
}
}
// AudioStartMonitor pipes live RX audio from the rig into your speakers so you
// hear the radio inside OpsLog. Source = the "From radio" capture device (for a
// USB-connected rig, its "USB Audio CODEC" input); sink = the "Listening"
// device. This is the USB half of the audio feature; the network 50003 stream
// will later Push into the same output path (see audio.Manager.PushMonitorAudio).
func (a *App) AudioStartMonitor() error {
if a.audioMgr == nil {
return fmt.Errorf("audio not initialized")
}
cfg, _ := a.GetAudioSettings()
if strings.TrimSpace(cfg.FromRadio) == "" {
return fmt.Errorf(`no "From radio" capture device set — pick the rig's USB Audio CODEC in Settings → Audio`)
}
applog.Printf("audio: RX monitor start (from=%q → listen=%q)", cfg.FromRadio, cfg.ListeningDevice)
return a.audioMgr.StartMonitor(cfg.FromRadio, cfg.ListeningDevice)
}
// AudioStopMonitor stops the RX monitor passthrough.
func (a *App) AudioStopMonitor() {
if a.audioMgr != nil {
a.audioMgr.StopMonitor()
applog.Printf("audio: RX monitor stopped")
}
}
// AudioMonitorActive reports whether the RX monitor is running (for the toggle).
func (a *App) AudioMonitorActive() bool {
return a.audioMgr != nil && a.audioMgr.MonitorActive()
}
// AudioStartTX keys PTT and pipes your live mic into the rig ("To Radio" device)
// so you can talk through the PC — the USB half of TX voice. PTT uses the
// configured method (CAT/RTS/DTR); if keying fails the audio route isn't started.
func (a *App) AudioStartTX() error {
if a.audioMgr == nil {
return fmt.Errorf("audio not initialized")
}
cfg, _ := a.GetAudioSettings()
if strings.TrimSpace(cfg.ToRadio) == "" {
return fmt.Errorf(`no "To radio" device set — pick the rig's USB Audio CODEC output in Settings → Audio`)
}
if err := a.pttKey(cfg); err != nil { // key first — no point streaming to a rig that isn't transmitting
return err
}
if err := a.audioMgr.StartTXAudio(cfg.RecordingDevice, cfg.ToRadio); err != nil {
a.pttUnkey()
return err
}
applog.Printf("audio: TX start (mic=%q → to-radio=%q, ptt=%q)", cfg.RecordingDevice, cfg.ToRadio, cfg.PTTMethod)
return nil
}
// AudioStopTX stops the TX passthrough and unkeys PTT.
func (a *App) AudioStopTX() {
if a.audioMgr != nil {
a.audioMgr.StopTXAudio()
}
a.pttUnkey()
applog.Printf("audio: TX stopped")
}
// AudioTXActive reports whether the TX passthrough is running (for the toggle).
func (a *App) AudioTXActive() bool {
return a.audioMgr != nil && a.audioMgr.TXAudioActive()
}
// GetLogFilePath returns where the diagnostic log file lives so the user
// can open it from the Settings UI. Empty when applog hasn't initialised.
func (a *App) GetLogFilePath() string {
@@ -8606,7 +8680,28 @@ func (a *App) reloadCAT() {
case "icom-net":
// Icom CI-V over the rig's built-in LAN server (remote, no RS-BA1 / Remote
// Utility). Reuses the whole IcomController over the network transport.
a.cat.Start(cat.NewIcomNet(s.IcomNetHost, s.IcomNetUser, s.IcomNetPass, s.IcomAddr, s.DigitalDefault))
var audioSink func([]byte)
if s.IcomNetAudio && a.audioMgr != nil {
// Experimental: stream RX audio over 50003. Start the render-only monitor
// sink (no USB capture) and feed each decoded payload into it, so remote
// RX audio plays through the "Listening" device. Decoding is via the PCM
// codec (Opus later). The 50003 payload OFFSET is still pending on-rig
// verification (see icomaudio.go) — hence experimental + opt-in.
acfg, _ := a.GetAudioSettings()
a.audioMgr.StopMonitor() // clear any prior monitor/sink so a re-save restarts cleanly
if err := a.audioMgr.StartMonitorSink(acfg.ListeningDevice); err != nil {
applog.Printf("icom-net audio: cannot start output sink: %v", err)
} else {
codec := audio.NewPCM16Codec()
audioSink = func(payload []byte) {
if pcm, err := codec.Decode(payload); err == nil {
a.audioMgr.PushMonitorAudio(pcm)
}
}
applog.Printf("icom-net audio: RX audio streaming ENABLED (experimental) → %q", acfg.ListeningDevice)
}
}
a.cat.Start(cat.NewIcomNet(s.IcomNetHost, s.IcomNetUser, s.IcomNetPass, s.IcomAddr, s.DigitalDefault, audioSink))
case "tci":
// Expert Electronics TCI (WebSocket) — SunSDR / ExpertSDR2, or any
// TCI-compatible server.