feat(icom): live microphone over the network — the last remote brick

The talk button learns the network road the voice keyer already took: when
To-radio is the radio itself, the microphone is captured and re-framed into
the rig's 320-sample packets, the capture callback serving as the clock — the
mic delivers in real time, so no ring and no pacer. Counters and the frame
remainder live on the audio stream, so a talk session spans calls. PTT keys
before and releases after, through the same code as the USB path.

With this, a network Icom is a complete remote station over three UDP ports:
RX audio to the headset, live voice and recorded messages back, CW through
the rig's keyer, CAT for everything else.
This commit is contained in:
2026-08-29 21:51:37 +02:00
parent 08bc401681
commit e8b5444fc2
6 changed files with 127 additions and 4 deletions
+23
View File
@@ -356,6 +356,29 @@ func (m *Manager) StartTXAudio(micDev, toRadioDev string) error {
return nil
}
// StartTXAudioNetwork pipes the live microphone into a SEND function instead
// of a render device — the talk button when the radio is reached over its own
// link. No ring and no pacing goroutine: the microphone delivers in real time,
// and the sender re-frames to the rig's cadence, so the capture callback IS
// the clock.
func (m *Manager) StartTXAudioNetwork(micDev string, send func([]byte) error) error {
m.mu.Lock()
if m.txStop != nil {
m.mu.Unlock()
return fmt.Errorf("TX audio already running")
}
stop := make(chan struct{})
m.txStop = stop
m.mu.Unlock()
go func() {
if err := captureStream(micDev, stop, func(chunk []byte) { _ = send(chunk) }); err != nil {
LogSink("audio: network TX capture from %q failed: %v", DeviceName(micDev), err)
}
}()
m.notify()
return nil
}
// StopTXAudio stops the TX mic→rig passthrough.
func (m *Manager) StopTXAudio() {
m.mu.Lock()