diff --git a/app.go b/app.go index c282165..983d6c4 100644 --- a/app.go +++ b/app.go @@ -195,6 +195,7 @@ const ( keyAudioQSOPlayGain = "audio.qso_play_gain" // QSO-recording playback level % keyAudioPTTMethod = "audio.ptt_method" // "none" (VOX) | "rts" | "dtr" keyAudioPTTPort = "audio.ptt_port" // COM port for serial PTT + keyAudioPTTData = "audio.ptt_data" // keyer audio arrives on the rig DATA/USB input keyAudioFormat = "audio.qso_format" // "wav" | "mp3" keyAudioFromGain = "audio.from_gain" // From Radio (RX) mix level, percent keyAudioMicGain = "audio.mic_gain" // mic mix level, percent @@ -8529,11 +8530,15 @@ type AudioSettings struct { PrerollSeconds int `json:"preroll_seconds"` // rolling pre-roll (default 8) PTTMethod string `json:"ptt_method"` // "none" (VOX) | "rts" | "dtr" PTTPort string `json:"ptt_port"` // COM port for serial PTT - Format string `json:"format"` // "wav" | "mp3" - FromGain int `json:"from_gain"` // From Radio (RX) mix level %, default 100 - MicGain int `json:"mic_gain"` // mic mix level %, default 100 - TXGain int `json:"tx_gain"` // voice-keyer playback level %, default 100 - QSOPlayGain int `json:"qso_play_gain"` // QSO-recording playback level %, default 100 + // PTTData: the keyer's audio reaches the radio on its DATA/USB input, not + // the microphone socket. CAT keying only — it changes which transmit + // command is sent (a Kenwood TS-590 takes TX1 instead of TX). + PTTData bool `json:"ptt_data"` + Format string `json:"format"` // "wav" | "mp3" + FromGain int `json:"from_gain"` // From Radio (RX) mix level %, default 100 + MicGain int `json:"mic_gain"` // mic mix level %, default 100 + TXGain int `json:"tx_gain"` // voice-keyer playback level %, default 100 + QSOPlayGain int `json:"qso_play_gain"` // QSO-recording playback level %, default 100 } // ListAudioInputDevices / ListAudioOutputDevices enumerate WASAPI endpoints @@ -8590,7 +8595,7 @@ func (a *App) GetAudioSettings() (AudioSettings, error) { } m, err := a.settings.GetMany(a.ctx, keyAudioFromRadio, keyAudioToRadio, keyAudioRecDevice, keyAudioListenDevice, - keyAudioQSORecord, keyAudioQSODir, keyAudioPreroll, keyAudioPTTMethod, keyAudioPTTPort, keyAudioFormat, + keyAudioQSORecord, keyAudioQSODir, keyAudioPreroll, keyAudioPTTMethod, keyAudioPTTPort, keyAudioPTTData, keyAudioFormat, keyAudioFromGain, keyAudioMicGain, keyAudioTXGain, keyAudioQSOPlayGain) if err != nil { return out, err @@ -8602,6 +8607,7 @@ func (a *App) GetAudioSettings() (AudioSettings, error) { out.PTTMethod = v } out.PTTPort = m[keyAudioPTTPort] + out.PTTData = m[keyAudioPTTData] == "1" out.FromRadio = m[keyAudioFromRadio] out.ToRadio = m[keyAudioToRadio] out.RecordingDevice = m[keyAudioRecDevice] @@ -8672,6 +8678,7 @@ func (a *App) SaveAudioSettings(s AudioSettings) error { keyAudioPreroll: strconv.Itoa(s.PrerollSeconds), keyAudioPTTMethod: pttMethod, keyAudioPTTPort: strings.TrimSpace(s.PTTPort), + keyAudioPTTData: boolStr(s.PTTData), keyAudioFormat: format, keyAudioFromGain: strconv.Itoa(s.FromGain), keyAudioMicGain: strconv.Itoa(s.MicGain), @@ -10599,7 +10606,7 @@ func (a *App) pttKey(cfg AudioSettings) error { if a.cat == nil { return fmt.Errorf("CAT not initialized") } - if err := a.cat.SetPTT(true); err != nil { + if err := a.cat.SetPTTSource(true, cfg.PTTData); err != nil { applog.Printf("ptt: CAT SetPTT failed: %v", err) return err } diff --git a/changelog.json b/changelog.json index bbcf159..e43d32a 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.27.9", "date": "", "en": [ - "FT decodes warn when the decoding application announces a band the radio is not on — the signature of a lost CAT link, where it repeats the last frequency it knew and every decode after that carries a stale band. Nothing downstream could tell, so NEW BAND was being judged against a band the operator had left. OpsLog says it rather than deciding: a second receiver on another band is a real setup, and it costs that one only a line to read past." + "FT decodes warn when the decoding application announces a band the radio is not on — the signature of a lost CAT link, where it repeats the last frequency it knew and every decode after that carries a stale band. Nothing downstream could tell, so NEW BAND was being judged against a band the operator had left. OpsLog says it rather than deciding: a second receiver on another band is a real setup, and it costs that one only a line to read past.", + "Voice keyer with CAT keying: an option saying the keyer’s audio arrives on the radio’s DATA / USB input rather than the microphone socket. A Kenwood TS-590 has two transmit commands — TX opens the front mic, TX1 the rear ACC2/USB — so a keyer playing through the rig’s own sound card was transmitting dead air while the radio listened to a microphone nobody was speaking into. Rigs with a single PTT are unaffected, and the Test PTT button exercises the same path." ], "fr": [ - "Les FT decodes signalent quand le logiciel de décodage annonce une bande sur laquelle la radio n’est pas — la signature d’une liaison CAT perdue, où il répète la dernière fréquence connue et où tous les décodages suivants portent une bande périmée. Rien en aval ne pouvait s’en apercevoir : NOUVELLE BANDE était donc jugé sur une bande quittée. OpsLog le dit sans décider à votre place : un second récepteur sur une autre bande est une configuration légitime, et il ne lui en coûte qu’une ligne à ignorer." + "Les FT decodes signalent quand le logiciel de décodage annonce une bande sur laquelle la radio n’est pas — la signature d’une liaison CAT perdue, où il répète la dernière fréquence connue et où tous les décodages suivants portent une bande périmée. Rien en aval ne pouvait s’en apercevoir : NOUVELLE BANDE était donc jugé sur une bande quittée. OpsLog le dit sans décider à votre place : un second récepteur sur une autre bande est une configuration légitime, et il ne lui en coûte qu’une ligne à ignorer.", + "Voice keyer avec PTT CAT : une option indiquant que l’audio du keyer arrive sur l’entrée DATA / USB de la radio et non sur la prise micro. Un Kenwood TS-590 a deux commandes d’émission — TX ouvre le micro de face avant, TX1 l’ACC2/USB — si bien qu’un keyer jouant par la carte son du poste émettait dans le vide pendant que la radio écoutait un micro devant lequel personne ne parlait. Les postes à PTT unique ne sont pas concernés, et le bouton Test PTT emprunte le même chemin." ] }, { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index de3c725..8ba48d8 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1702,13 +1702,13 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged type AudioSettings = { from_radio: string; to_radio: string; recording_device: string; listening_device: string; qso_record: boolean; qso_dir: string; preroll_seconds: number; - ptt_method: 'none' | 'cat' | 'rts' | 'dtr'; ptt_port: string; format: 'wav' | 'mp3'; + ptt_method: 'none' | 'cat' | 'rts' | 'dtr'; ptt_port: string; ptt_data?: boolean; format: 'wav' | 'mp3'; from_gain: number; mic_gain: number; tx_gain: number; qso_play_gain: number; }; type AudioDev = { id: string; name: string; default: boolean }; const [audioCfg, setAudioCfg] = useState({ from_radio: '', to_radio: '', recording_device: '', listening_device: '', - qso_record: false, qso_dir: '', preroll_seconds: 8, ptt_method: 'none', ptt_port: '', format: 'wav', + qso_record: false, qso_dir: '', preroll_seconds: 8, ptt_method: 'none', ptt_port: '', ptt_data: false, format: 'wav', from_gain: 100, mic_gain: 100, tx_gain: 100, qso_play_gain: 100, }); const [audioInputs, setAudioInputs] = useState([]); @@ -7251,6 +7251,16 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged )} + {audioCfg.ptt_method === 'cat' && ( + <> + + + + )} {(audioCfg.ptt_method === 'rts' || audioCfg.ptt_method === 'dtr') && ( <> diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 779feec..4a3fb62 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -535,7 +535,7 @@ const en: Dict = { 'aud.preroll': 'Pre-roll (seconds)', 'aud.format': 'File format', 'aud.wav': 'WAV (lossless, larger)', 'aud.mp3': 'MP3 (compressed, small)', 'aud.fromLevel': 'From Radio level', 'aud.txLevel': 'Voice keyer level', 'aud.txLevelHint': 'Level of the recorded messages sent to the radio. Raise it if your voice keyer is much quieter than your microphone; Play previews at this same level. If the radio transmits almost nothing, its modulation source is still the front microphone: on an FTDX10 set MENU → SSB MOD SOURCE to REAR (the USB input).', 'aud.micLevel': 'Mic level', 'aud.qsoPlayLevel': 'QSO playback level', 'aud.levelHint': 'If your voice is louder than the station, lower Mic level.', 'aud.autoSend': 'Auto-send the recording to the station by e-mail when I log a QSO', - 'aud.dvkTitle': 'Voice keyer messages (F1–F12)', 'aud.deleteMsg': 'Delete this message', 'aud.pttMethod': 'PTT method', 'aud.pttNone': 'None (VOX)', 'aud.pttCat': 'CAT (the radio link)', 'aud.pttRts': 'Serial RTS', 'aud.pttDtr': 'Serial DTR', + 'aud.dvkTitle': 'Voice keyer messages (F1–F12)', 'aud.deleteMsg': 'Delete this message', 'aud.pttData': 'The keyer’s audio arrives on the radio’s DATA / USB input', 'aud.pttDataHint': 'Changes which transmit command is sent: a Kenwood TS-590 takes TX1 (ACC2/USB) instead of TX (front microphone). Without it the radio transmits while listening to a microphone nobody is speaking into. Rigs with a single PTT ignore this.', 'aud.pttMethod': 'PTT method', 'aud.pttNone': 'None (VOX)', 'aud.pttCat': 'CAT (the radio link)', 'aud.pttRts': 'Serial RTS', 'aud.pttDtr': 'Serial DTR', 'aud.testPtt': 'Test PTT', 'aud.pttPort': 'PTT COM port', 'aud.pickPort': 'Pick a COM port', 'aud.selectPort': '— select —', 'aud.refresh': 'Refresh', 'aud.msgPlaceholder': 'Message {n} label (CQ, report, 73…)', 'aud.holdRec': '● Hold to rec', 'aud.recordingNow': '● Recording…', 'aud.play': '▶ Play', 'aud.stop': '■ Stop', 'aud.errPttTest': 'PTT test: ', 'aud.errRecord': 'Record: ', 'aud.errSave': 'Save: ', 'aud.errPlay': 'Play: ', @@ -1037,7 +1037,7 @@ const fr: Dict = { 'aud.preroll': 'Pré-enregistrement (secondes)', 'aud.format': 'Format de fichier', 'aud.wav': 'WAV (sans perte, plus volumineux)', 'aud.mp3': 'MP3 (compressé, léger)', 'aud.fromLevel': 'Niveau depuis la radio', 'aud.txLevel': 'Niveau du voice keyer', 'aud.txLevelHint': "Niveau des messages enregistrés envoyés à la radio. Augmentez-le si votre voice keyer est bien plus faible que votre micro ; Lire fait entendre ce même niveau. Si la radio n'émet presque rien, c'est que sa source de modulation est restée le micro de façade : sur un FTDX10, réglez MENU → SSB MOD SOURCE sur REAR (l'entrée USB).", 'aud.micLevel': 'Niveau micro', 'aud.qsoPlayLevel': 'Niveau de relecture QSO', 'aud.levelHint': 'Si votre voix est plus forte que la station, baissez le niveau micro.', 'aud.autoSend': "Envoyer automatiquement l'enregistrement à la station par e-mail lorsque j'enregistre un QSO", - 'aud.dvkTitle': 'Messages du manipulateur vocal (F1–F12)', 'aud.deleteMsg': 'Supprimer ce message', 'aud.pttMethod': 'Méthode PTT', 'aud.pttNone': 'Aucune (VOX)', 'aud.pttCat': 'CAT (la liaison radio)', 'aud.pttRts': 'RTS série', 'aud.pttDtr': 'DTR série', + 'aud.dvkTitle': 'Messages du manipulateur vocal (F1–F12)', 'aud.deleteMsg': 'Supprimer ce message', 'aud.pttData': 'L’audio du keyer arrive sur l’entrée DATA / USB de la radio', 'aud.pttDataHint': 'Change la commande d’émission envoyée : un Kenwood TS-590 attend TX1 (ACC2/USB) au lieu de TX (micro de face avant). Sans cela la radio émet en écoutant un micro devant lequel personne ne parle. Les postes à PTT unique l’ignorent.', 'aud.pttMethod': 'Méthode PTT', 'aud.pttNone': 'Aucune (VOX)', 'aud.pttCat': 'CAT (la liaison radio)', 'aud.pttRts': 'RTS série', 'aud.pttDtr': 'DTR série', 'aud.testPtt': 'Tester le PTT', 'aud.pttPort': 'Port COM du PTT', 'aud.pickPort': 'Choisir un port COM', 'aud.selectPort': '— choisir —', 'aud.refresh': 'Actualiser', 'aud.msgPlaceholder': 'Libellé du message {n} (CQ, report, 73…)', 'aud.holdRec': '● Maintenir pour enreg.', 'aud.recordingNow': '● Enregistrement…', 'aud.play': '▶ Lire', 'aud.stop': '■ Arrêter', 'aud.errPttTest': 'Test PTT : ', 'aud.errRecord': 'Enregistrement : ', 'aud.errSave': 'Sauvegarde : ', 'aud.errPlay': 'Lecture : ', diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index c555e88..9943a23 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1924,6 +1924,7 @@ export namespace main { preroll_seconds: number; ptt_method: string; ptt_port: string; + ptt_data: boolean; format: string; from_gain: number; mic_gain: number; @@ -1945,6 +1946,7 @@ export namespace main { this.preroll_seconds = source["preroll_seconds"]; this.ptt_method = source["ptt_method"]; this.ptt_port = source["ptt_port"]; + this.ptt_data = source["ptt_data"]; this.format = source["format"]; this.from_gain = source["from_gain"]; this.mic_gain = source["mic_gain"]; diff --git a/internal/cat/cat.go b/internal/cat/cat.go index eea5b70..8b1a8b9 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -256,6 +256,33 @@ func (m *Manager) SetPTT(on bool) error { return m.exec(func(b Backend) error { return b.SetPTT(on) }) } +// dataPTTSetter is implemented by a backend that can key the DATA input rather +// than the microphone. A Kenwood TS-590 has two transmit commands and takes its +// audio from a different socket for each: TX (or TX0) opens the front mic, TX1 +// the rear ACC2/USB. Send the wrong one and the radio transmits in silence, +// because the audio arriving on USB is simply not the input it is listening to. +type dataPTTSetter interface { + SetPTTData(on bool) error +} + +// SetPTTSource keys the transmitter, saying WHERE the audio is coming from. +// +// data=true means "the audio reaches the radio on its data/USB input" — what a +// voice keyer playing through the rig's own sound card needs. A backend that +// draws no distinction (every rig where one PTT is all there is) falls back to +// the ordinary key, so nothing changes for it. +func (m *Manager) SetPTTSource(on, data bool) error { + if !data { + return m.SetPTT(on) + } + return m.exec(func(b Backend) error { + if d, ok := b.(dataPTTSetter); ok { + return d.SetPTTData(on) + } + return b.SetPTT(on) + }) +} + // splitSetter is implemented by the backends that can arm split AND place the // transmit frequency. Both together: arming without setting the dial transmits // on whatever the transmit VFO happened to hold, which is worse than refusing. diff --git a/internal/cat/kenwood.go b/internal/cat/kenwood.go index d6f7d1b..069168a 100644 --- a/internal/cat/kenwood.go +++ b/internal/cat/kenwood.go @@ -632,6 +632,28 @@ func (k *Kenwood) SetPTT(on bool) error { return k.write("RX;") } +// SetPTTData keys the transmitter on the DATA input: TX1 on a TS-590, which is +// ACC2/USB rather than the front microphone. The radio's own manual is explicit +// that the parameter chooses the input — "0: SEND (normal transmission using +// the MIC input), 1: DATA SEND (ACC2/USB input)" — so a voice keyer playing +// into the rig's USB codec has to say TX1 or it transmits dead air while the +// radio listens to a microphone nobody is speaking into. +// +// Unkeying is the same RX either way; there is no data-flavoured stop. +func (k *Kenwood) SetPTTData(on bool) error { + k.mu.Lock() + defer k.mu.Unlock() + if k.port == nil { + return fmt.Errorf("kenwood: not connected") + } + k.tx = on + if on { + k.txAt = time.Now() + return k.write("TX1;") + } + return k.write("RX;") +} + func (k *Kenwood) write(cmd string) error { if k.port == nil { return fmt.Errorf("kenwood: not connected")