feat(icom): phase 5 — the voice keyer plays to the radio over the LAN
The audio session opens its TX side alongside RX (txenable in the conninfo, 16 kHz both ways), the stream gains a paced sender — 320 samples every 20 ms, the frame mirroring what the rig itself sends on this socket, IDs swapped — and the network Icom takes the same NetworkPlayer slot a TCI radio does: pick 'Radio (network audio)' as the To-radio device and the voice keyer needs no cable and no virtual sound card. PTT brackets the message through the same code as every other audio path.
This commit is contained in:
@@ -64,4 +64,4 @@ func NetworkPlayerReady() bool { return networkPlayer() != nil }
|
||||
// comes back with. Named, because "the device could not be opened" would send
|
||||
// an operator hunting through Windows sound settings for a device that never
|
||||
// existed.
|
||||
var errNoNetworkRadio = errors.New("no radio is connected to take the audio — check the CAT link (the radio output only works with a TCI radio)")
|
||||
var errNoNetworkRadio = errors.New("no radio is connected to take the audio — check the CAT link (a TCI radio, or a network Icom with RX audio enabled)")
|
||||
|
||||
@@ -22,6 +22,7 @@ package cat
|
||||
// audio stream is opt-in and entirely separate from control/CI-V.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -236,3 +237,58 @@ func (a *icomAudio) sendRetransmitReq() {
|
||||
_, _ = a.conn.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
// PlayTX sends one already-decoded message out over the audio session, paced
|
||||
// at the stream's own 20 ms / 320-sample cadence, and returns when the last
|
||||
// packet has gone or stop is closed.
|
||||
//
|
||||
// The frame mirrors what the rig itself sends on this socket byte for byte —
|
||||
// ident 0x81 0x01 (LPCM mono 16-bit), a big-endian send sequence at 0x12, the
|
||||
// payload length at 0x14, PCM from 0x18 — with the IDs swapped for direction.
|
||||
// PTT is the caller's business, exactly as on the TCI and sound-card paths.
|
||||
func (a *icomAudio) PlayTX(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error {
|
||||
mono := decodeToMono(pcm, ch, bits)
|
||||
if len(mono) == 0 {
|
||||
return fmt.Errorf("the message is empty")
|
||||
}
|
||||
if rate > 0 && rate != 16000 {
|
||||
mono = resampleLinear(mono, rate, 16000)
|
||||
}
|
||||
const frame = 320 // samples per packet: 20 ms at 16 kHz, the rig's own cadence
|
||||
tick := time.NewTicker(20 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
var outerSeq uint16 = 1
|
||||
var sendSeq uint16
|
||||
for pos := 0; pos < len(mono); pos += frame {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
case <-a.done:
|
||||
return fmt.Errorf("the audio stream closed mid-message")
|
||||
case <-tick.C:
|
||||
}
|
||||
pkt := make([]byte, 0x18+frame*2)
|
||||
icnLE.PutUint32(pkt[0:], uint32(len(pkt)))
|
||||
icnLE.PutUint16(pkt[6:], outerSeq)
|
||||
icnLE.PutUint32(pkt[8:], a.aID)
|
||||
icnLE.PutUint32(pkt[12:], a.aRemote)
|
||||
pkt[0x10], pkt[0x11] = 0x81, 0x01
|
||||
icnBE.PutUint16(pkt[0x12:], sendSeq)
|
||||
icnBE.PutUint32(pkt[0x14:], frame*2)
|
||||
for i := 0; i < frame && pos+i < len(mono); i++ {
|
||||
v := mono[pos+i] * 32767
|
||||
if v > 32767 {
|
||||
v = 32767
|
||||
} else if v < -32768 {
|
||||
v = -32768
|
||||
}
|
||||
icnLE.PutUint16(pkt[0x18+i*2:], uint16(int16(v)))
|
||||
}
|
||||
if _, err := a.conn.Write(pkt); err != nil {
|
||||
return fmt.Errorf("sending audio to the rig: %w", err)
|
||||
}
|
||||
outerSeq++
|
||||
sendSeq++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-2
@@ -940,7 +940,11 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
||||
copy(b[0x40:0x60], []byte("IC-7610"))
|
||||
copy(b[0x60:0x70], icnPasscode(user))
|
||||
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
||||
b[0x71] = 0x00 // txenable (Phase 5)
|
||||
// TX rides the same audio session: whenever the operator wants RX audio the
|
||||
// TX side is opened with it, so the voice keyer can play to the radio with
|
||||
// no cable. Costs nothing when unused — no packets flow until a message is
|
||||
// actually played.
|
||||
b[0x71] = rxEnable // txenable
|
||||
// rxcodec 0x04 = LPCM, ONE channel, 16-BIT — settled by experiment on a
|
||||
// real IC-7760, one wrong guess at a time: 0x10 produced 1280-byte payloads
|
||||
// of interleaved 16-bit stereo, 0x02 produced 320-byte payloads of 8-bit
|
||||
@@ -949,7 +953,7 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
||||
b[0x72] = 0x04 // rxcodec
|
||||
b[0x73] = 0x04 // txcodec
|
||||
icnBE.PutUint32(b[0x74:], 16000)
|
||||
icnBE.PutUint32(b[0x78:], 8000)
|
||||
icnBE.PutUint32(b[0x78:], 16000) // TX sample rate — the same 16 kHz everything else here runs at
|
||||
icnBE.PutUint32(b[0x7c:], uint32(civPort))
|
||||
icnBE.PutUint32(b[0x80:], uint32(audioPort))
|
||||
icnBE.PutUint32(b[0x84:], 100)
|
||||
@@ -957,6 +961,17 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
||||
return b
|
||||
}
|
||||
|
||||
// PlayTXAudio plays one voice-keyer message through the 50003 audio session.
|
||||
// On the transport because the transport owns the session; the IcomSerial
|
||||
// controller forwards here after a type assertion, exactly as the TCI path
|
||||
// reaches its radio.
|
||||
func (n *icomNet) PlayTXAudio(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error {
|
||||
if n.audio == nil {
|
||||
return fmt.Errorf("the radio's audio stream is not open — enable RX audio in Settings → CAT first")
|
||||
}
|
||||
return n.audio.PlayTX(pcm, rate, ch, bits, stop)
|
||||
}
|
||||
|
||||
func icnOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
|
||||
b := make([]byte, 0x16)
|
||||
icnLE.PutUint32(b[0:], 0x16)
|
||||
|
||||
@@ -2047,3 +2047,20 @@ func agcValue(name string) byte {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// PlayTXAudio hands a voice-keyer message to the network audio session, when
|
||||
// this rig is reached over one. A USB-connected Icom takes its audio through
|
||||
// its own sound card instead, and says so.
|
||||
func (b *IcomSerial) PlayTXAudio(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error {
|
||||
// port is written once per Connect, on the CAT goroutine this is fetched
|
||||
// from (IcomDo); at worst a reconnect leaves this one connection stale, and
|
||||
// a stale transport fails fast on its closed done channel.
|
||||
port := b.port
|
||||
type txPlayer interface {
|
||||
PlayTXAudio(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error
|
||||
}
|
||||
if p, ok := port.(txPlayer); ok {
|
||||
return p.PlayTXAudio(pcm, rate, ch, bits, stop)
|
||||
}
|
||||
return fmt.Errorf("this rig takes transmit audio through its USB sound card, not the CAT link")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user