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.
68 lines
2.7 KiB
Go
68 lines
2.7 KiB
Go
package audio
|
|
|
|
// Playing a message through the RADIO instead of a sound card.
|
|
//
|
|
// A SunSDR takes its transmit audio over TCI, on the same socket as the
|
|
// commands, so the voice keyer can hand it the message directly: no virtual
|
|
// cable, no second sound card, no Windows mixer between the recording and the
|
|
// air. To everything above, that radio is simply another output device.
|
|
//
|
|
// The device it presents itself as is a name rather than a WASAPI endpoint id,
|
|
// which is why Play checks for it before opening anything: there is no endpoint
|
|
// to open, and asking Windows for one produces a confusing error about a device
|
|
// that does not exist rather than the truth, which is that nothing is connected
|
|
// to the radio.
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// NetworkDeviceID is the id the radio-over-network output carries in the
|
|
// settings and in the device lists. A fixed string, not a Windows endpoint id:
|
|
// it is chosen by us and must survive a radio being switched off and on.
|
|
const NetworkDeviceID = "net:radio"
|
|
|
|
// NetworkPlayer sends already-decoded PCM to the radio, returning when the
|
|
// message has been played or when stop is closed.
|
|
//
|
|
// It carries the same arguments as the sound-card path so that Play can hand
|
|
// over whatever it read, and the radio can decide what converting it needs —
|
|
// the sample rate a WAV was recorded at is not the radio's business until the
|
|
// moment it has to be resampled.
|
|
type NetworkPlayer func(pcm []byte, rate, ch, bits int, stop <-chan struct{}) error
|
|
|
|
var (
|
|
netMu sync.RWMutex
|
|
netPlayer NetworkPlayer
|
|
)
|
|
|
|
// SetNetworkPlayer installs (or clears, with nil) the radio's transmit path.
|
|
//
|
|
// Package-level rather than per-Manager: there is one radio, the CAT backend
|
|
// owns it, and a Manager that happened to be built before the radio connected
|
|
// would otherwise be permanently unable to reach it.
|
|
func SetNetworkPlayer(fn NetworkPlayer) {
|
|
netMu.Lock()
|
|
netPlayer = fn
|
|
netMu.Unlock()
|
|
}
|
|
|
|
// networkPlayer returns the installed player, or nil.
|
|
func networkPlayer() NetworkPlayer {
|
|
netMu.RLock()
|
|
defer netMu.RUnlock()
|
|
return netPlayer
|
|
}
|
|
|
|
// NetworkPlayerReady says whether a radio is currently able to take transmit
|
|
// audio, so the settings panel can offer the option honestly rather than
|
|
// listing a device that would fail when used.
|
|
func NetworkPlayerReady() bool { return networkPlayer() != nil }
|
|
|
|
// errNoNetworkRadio is what a message played to a radio that is not there
|
|
// 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 (a TCI radio, or a network Icom with RX audio enabled)")
|