feat: a level control for the voice keyer, and name every CAT backend in its PTT list

The messages went to the radio exactly as recorded. Nothing in OpsLog could
raise them, so a microphone captured quietly drove the rig quietly and the only
remedies were the radio's own USB input menu or the Windows mixer — which is
where the operator was heading. There is now a level from 10 to 400 %, applied
with clamping (a wrap would turn loud speech into noise on the air), and Play
previews at that same level so the adjustment is made against what will actually
be transmitted.

The PTT method list also named only OmniRig, Flex, Icom and TCI, so choosing a
native Yaesu left "CAT" with no backend beside it — which reads as "there is no
CAT PTT for my radio" and sends the operator to RTS on a COM port that has
nothing to do with the rig. Same list-needing-every-member shape as three
earlier bugs in this feature. The TestPTT log line had the same rot: it said
"CAT via OmniRig" whatever backend was running.
This commit is contained in:
2026-07-29 14:29:47 +02:00
parent 8da3a27803
commit 67a03c3ddc
6 changed files with 84 additions and 11 deletions
+30 -1
View File
@@ -104,11 +104,27 @@ func (m *Manager) IsPlaying() bool {
// Play renders a WAV file to deviceID. Any current playback is stopped first.
// Returns immediately; playback runs in the background.
func (m *Manager) Play(deviceID, path string) error {
// Play sends a recorded message to a device, amplified by gainPct (100 = as
// recorded).
//
// The gain exists because nothing else could raise the level: the message went
// out exactly as captured, so a mic recorded quietly drove the rig quietly and
// the operator had no control anywhere in OpsLog — only the radio's own USB
// input level, buried in its menus, and the Windows mixer.
func (m *Manager) Play(deviceID, path string, gainPct int) error {
pcm, rate, ch, bits, err := readWAV(path)
if err != nil {
return err
}
if gainPct > 0 && gainPct != 100 && bits == 16 {
g := float64(gainPct) / 100
// In place: the buffer is this call's own copy of the file.
for i := 0; i+1 < len(pcm); i += 2 {
v := int16(uint16(pcm[i]) | uint16(pcm[i+1])<<8)
v = scalePCM(v, g)
pcm[i], pcm[i+1] = byte(uint16(v)), byte(uint16(v)>>8)
}
}
m.StopPlayback()
stop := make(chan struct{})
m.mu.Lock()
@@ -265,3 +281,16 @@ func (m *Manager) TXAudioActive() bool {
defer m.mu.Unlock()
return m.txStop != nil
}
// scalePCM applies a gain to one sample, clamping rather than wrapping — an
// overflow that wraps turns loud speech into a burst of noise on the air.
func scalePCM(s int16, g float64) int16 {
v := float64(s) * g
if v > 32767 {
return 32767
}
if v < -32768 {
return -32768
}
return int16(v)
}