feat(audio): transmit audio for WSJT-X through a network Icom

An IC-705, IC-7610 or IC-7760 on Ethernet has no sound card on this PC:
its receive audio arrives on UDP 50003 and its transmit audio has to go
back the same way. OpsLog already did both — the monitor plays the
stream, the voice keyer and the talk button send into it.

WSJT-X could not. It drives the rig through the CAT OpsLog shares, so it
tunes and keys perfectly well, and then has nowhere to put its audio: it
needs a Windows endpoint and the radio is not one. A virtual cable
bridges that, which is what wfview asks of its users too — nothing short
of a signed kernel driver can present a sound card.

So: a "WSJT-X transmit audio" device in Settings ▸ Audio, shown only
when To Radio is the radio itself, since a rig with a USB codec needs
none of this and WSJT-X talks to that codec directly.

Almost no new machinery. StartTXAudioNetwork already pipes a chosen
capture device into the rig — it was written for the talk button — and
the missing piece was only WHEN. That is catShareRig.SetPTT: WSJT-X keys
through us, so we know. Audio starts after the carrier and stops before
it, because the other order transmits what is still buffered after the
program thinks the over is finished.

Armed by PTT rather than left running: a permanent stream would put
whatever the cable carries — desktop notifications, another program that
grabbed the same cable — on the air the moment anything else keyed the
rig. And stopped only if WE started it, since the talk button and the
voice keyer share the same route and a digital over ending must not cut
off a message being played by hand.

Receive needs no code: point Listening at a second cable and WSJT-X's
input at its other side. The hint in Settings says so.
This commit is contained in:
2026-09-11 09:09:35 +02:00
parent 97cc446c15
commit 60990276f5
8 changed files with 197 additions and 16 deletions
+45 -10
View File
@@ -190,14 +190,18 @@ const (
// Audio (Digital Voice Keyer + QSO recorder). Machine-local hardware, so
// global (not per-profile) like CAT/rotator. Device fields store the
// WASAPI endpoint id; the UI resolves it to a friendly name.
keyAudioFromRadio = "audio.from_radio" // capture: rig RX audio in
keyAudioToRadio = "audio.to_radio" // render: DVK plays into rig
keyAudioRecDevice = "audio.rec_device" // capture: your mic (record DVK msgs)
keyAudioListenDevice = "audio.listen_device" // render: local preview speakers
keyAudioQSORecord = "audio.qso_record" // "1" → auto-record every QSO
keyAudioQSODir = "audio.qso_dir" // folder for QSO recordings
keyAudioPreroll = "audio.preroll_seconds" // rolling-buffer pre-roll length
keyAudioTXGain = "audio.tx_gain" // voice-keyer playback level % (100 = as recorded)
keyAudioFromRadio = "audio.from_radio" // capture: rig RX audio in
keyAudioToRadio = "audio.to_radio" // render: DVK plays into rig
keyAudioRecDevice = "audio.rec_device" // capture: your mic (record DVK msgs)
keyAudioListenDevice = "audio.listen_device" // render: local preview speakers
// keyAudioDigiInput: capture device carrying a DIGITAL program's transmit
// audio — WSJT-X and the like — for a radio whose audio rides its network
// link. See app_digi_txaudio.go.
keyAudioDigiInput = "audio.digi_input"
keyAudioQSORecord = "audio.qso_record" // "1" → auto-record every QSO
keyAudioQSODir = "audio.qso_dir" // folder for QSO recordings
keyAudioPreroll = "audio.preroll_seconds" // rolling-buffer pre-roll length
keyAudioTXGain = "audio.tx_gain" // voice-keyer playback level % (100 = as recorded)
// Replaying a QSO recording needs its OWN level, and by a wide margin.
//
// A voice-keyer message is a microphone at speaking distance; a QSO
@@ -918,6 +922,10 @@ type App struct {
// pass. nil when nothing is being tracked.
satTrackMu sync.Mutex
satTrack *satTracker
// digiTX: WE started the digital transmit-audio route, so WE may stop it.
// The talk button and the voice keyer share that route, and unkeying a
// WSJT-X over must not cut off a message the operator is playing by hand.
digiTX atomic.Bool
cwMu sync.Mutex // guards the CW decoder lifecycle
cwStop chan struct{} // stops the CW decoder capture loop; nil when off
@@ -8856,6 +8864,11 @@ type AudioSettings struct {
FromRadio string `json:"from_radio"` // capture id: rig RX audio
ToRadio string `json:"to_radio"` // render id: into the rig
RecordingDevice string `json:"recording_device"` // capture id: your mic
// DigiInput is where WSJT-X (or any digital program) puts its transmit
// audio, when the radio takes audio over its network link and there is no
// sound card between the two. A virtual cable's OUTPUT side. Empty turns the
// whole path off. See app_digi_txaudio.go.
DigiInput string `json:"digi_input"`
ListeningDevice string `json:"listening_device"` // render id: preview
QSORecord bool `json:"qso_record"` // auto-record every QSO
QSODir string `json:"qso_dir"` // recordings folder
@@ -8926,7 +8939,7 @@ func (a *App) GetAudioSettings() (AudioSettings, error) {
return out, nil
}
m, err := a.settings.GetMany(a.ctx,
keyAudioFromRadio, keyAudioToRadio, keyAudioRecDevice, keyAudioListenDevice,
keyAudioFromRadio, keyAudioToRadio, keyAudioRecDevice, keyAudioListenDevice, keyAudioDigiInput,
keyAudioQSORecord, keyAudioQSODir, keyAudioPreroll, keyAudioPTTMethod, keyAudioPTTPort, keyAudioPTTData, keyAudioFormat,
keyAudioFromGain, keyAudioMicGain, keyAudioTXGain, keyAudioQSOPlayGain)
if err != nil {
@@ -8943,6 +8956,7 @@ func (a *App) GetAudioSettings() (AudioSettings, error) {
out.FromRadio = m[keyAudioFromRadio]
out.ToRadio = m[keyAudioToRadio]
out.RecordingDevice = m[keyAudioRecDevice]
out.DigiInput = m[keyAudioDigiInput]
out.ListeningDevice = m[keyAudioListenDevice]
out.QSORecord = m[keyAudioQSORecord] == "1"
out.QSODir = m[keyAudioQSODir]
@@ -9004,6 +9018,7 @@ func (a *App) SaveAudioSettings(s AudioSettings) error {
keyAudioFromRadio: s.FromRadio,
keyAudioToRadio: s.ToRadio,
keyAudioRecDevice: s.RecordingDevice,
keyAudioDigiInput: strings.TrimSpace(s.DigiInput),
keyAudioListenDevice: s.ListeningDevice,
keyAudioQSORecord: qr,
keyAudioQSODir: strings.TrimSpace(s.QSODir),
@@ -22108,7 +22123,27 @@ func (r catShareRig) RxFreq() int64 {
func (r catShareRig) SetFreq(hz int64) error { return r.a.cat.SetFrequency(hz) }
func (r catShareRig) SetMode(m string) error { return r.a.cat.SetMode(m) }
func (r catShareRig) SetPTT(on bool) error { return r.a.cat.SetPTT(on) }
// SetPTT keys the rig for a sharing client — and carries that client's
// transmit AUDIO with it when the radio takes audio over its network link.
//
// WSJT-X drives an Icom-over-Ethernet through this server: it has the CAT, so
// it can key and tune, but it has no sound card to reach a radio that has
// none either. The audio route below is the missing half — see
// app_digi_txaudio.go for why it hangs off PTT rather than being left running.
func (r catShareRig) SetPTT(on bool) error {
if !on {
// Audio down BEFORE the carrier: the other order transmits whatever is
// still in the buffer after the program thinks the over is finished.
r.a.stopDigiTXAudio()
return r.a.cat.SetPTT(false)
}
if err := r.a.cat.SetPTT(true); err != nil {
return err // no point streaming into a rig that is not transmitting
}
r.a.startDigiTXAudio()
return nil
}
// SetSplit passes the client's split request through to the radio, and passes
// the refusal back when the backend cannot do it. That refusal is the feature:
+114
View File
@@ -0,0 +1,114 @@
package main
// ── Transmit audio for a digital program, over the radio's own link ────────
//
// An IC-705, IC-7610 or IC-7760 reached over Ethernet has no sound card on this
// PC: its receive audio arrives on UDP 50003 and its transmit audio has to go
// back the same way. OpsLog already does both — the RX monitor plays the stream,
// the voice keyer and the talk button send into it.
//
// WSJT-X cannot. It drives the rig through the CAT OpsLog shares (rigctld), so
// it can tune and key perfectly well, and then has nowhere to put its audio: it
// needs a Windows audio endpoint and the radio is not one. A virtual cable
// bridges the gap — WSJT-X's output device is the cable, and OpsLog captures the
// cable's other side and streams it to the rig. That is what wfview asks of its
// users too; nothing short of a signed kernel driver can present a sound card,
// and OpsLog is neither signed nor a driver.
//
// The route is armed by PTT rather than left running. Streaming continuously
// would put whatever the cable carries — the desktop's notification sounds, an
// idle WSJT-X's silence, another program that grabbed the same cable — into a
// transmitter the moment anything else keyed it. PTT is also exactly the signal
// available: WSJT-X keys through us, so we know.
import (
"strings"
"hamlog/internal/applog"
"hamlog/internal/audio"
"hamlog/internal/cat"
)
// digiTXSender returns the radio's live-audio sender, or nil when this station
// is not set up for the path at all.
//
// Three things have to be true, and none of them is an error worth reporting on
// every over: a capture device is configured, the radio takes audio over its
// link, and its audio session is actually open.
func (a *App) digiTXSender() (dev string, send func([]byte) error) {
if a.audioMgr == nil || a.cat == nil {
return "", nil
}
cfg, err := a.GetAudioSettings()
if err != nil {
return "", nil
}
dev = strings.TrimSpace(cfg.DigiInput)
if dev == "" {
return "", nil
}
type sender interface {
TXAudioSender() (func([]byte) error, error)
}
if derr := a.cat.IcomDo(func(ic cat.IcomController) error {
p, ok := ic.(sender)
if !ok {
return nil // not a radio that takes audio over its link
}
fn, serr := p.TXAudioSender()
if serr != nil {
// The stream is not open — RX audio is switched off in Settings ▸ CAT.
// Said once per over at most, and it names the fix.
applog.Printf("digi audio: %v", serr)
return nil
}
send = fn
return nil
}); derr != nil {
return "", nil
}
if send == nil {
return "", nil
}
return dev, send
}
// startDigiTXAudio begins piping the configured capture device into the radio.
//
// Called after the rig is keyed, so a failure here leaves a transmitting radio
// with no audio rather than audio going into a receiver — and it is reported
// rather than silently dropped, because a station that transmits silence for a
// whole evening has no other way to find out.
func (a *App) startDigiTXAudio() {
dev, send := a.digiTXSender()
if send == nil {
return
}
if err := a.audioMgr.StartTXAudioNetwork(dev, send); err != nil {
applog.Printf("digi audio: %q could not be opened, the over will be silent: %v", dev, err)
return
}
a.digiTX.Store(true)
applog.Printf("digi audio: %q → the radio, for as long as the shared CAT holds PTT", dev)
}
// stopDigiTXAudio ends the route. Only when WE started it: the talk button and
// the voice keyer use the same one, and unkeying a digital over must not cut
// off a message the operator is playing by hand.
func (a *App) stopDigiTXAudio() {
if !a.digiTX.Swap(false) {
return
}
if a.audioMgr != nil {
a.audioMgr.StopTXAudio()
}
}
// ListDigiInputDevices is the dropdown behind the setting: the capture devices,
// which for this purpose means the virtual cable WSJT-X plays into.
//
// The radio itself is deliberately NOT offered. It is a source of receive audio,
// and picking it here would ask OpsLog to send the radio its own output.
func (a *App) ListDigiInputDevices() ([]audio.Device, error) {
return audio.ListInputDevices()
}
+4 -2
View File
@@ -2,10 +2,12 @@
{
"version": "0.27.25",
"en": [
"The relaunch after an update goes back to the helper that waited for OpsLog to close before starting the new version. Two rewrites tried to do without it and both left some operators with no window. Windows Defender may flag it — allow OpsLog in Defender if it does."
"The relaunch after an update goes back to the helper that waited for OpsLog to close before starting the new version. Two rewrites tried to do without it and both left some operators with no window. Windows Defender may flag it — allow OpsLog in Defender if it does.",
"WSJT-X can now transmit through an Icom reached over Ethernet. Set its output to a virtual audio cable and choose that cable under Settings ▸ Audio — OpsLog streams it to the radio while WSJT-X holds PTT through the shared CAT."
],
"fr": [
"La relance après une mise à jour revient à lassistant qui attendait la fermeture dOpsLog avant de démarrer la nouvelle version. Deux réécritures ont essayé de sen passer et laissaient certains opérateurs sans fenêtre. Windows Defender peut le signaler — dans ce cas, autorisez OpsLog dans Defender."
"La relance après une mise à jour revient à lassistant qui attendait la fermeture dOpsLog avant de démarrer la nouvelle version. Deux réécritures ont essayé de sen passer et laissaient certains opérateurs sans fenêtre. Windows Defender peut le signaler — dans ce cas, autorisez OpsLog dans Defender.",
"WSJT-X peut désormais émettre à travers un Icom joint en Ethernet. Réglez sa sortie sur un câble audio virtuel et choisissez ce câble dans Réglages ▸ Audio — OpsLog le diffuse vers la radio pendant que WSJT-X tient le PTT via le CAT partagé."
]
},
{
+20 -2
View File
@@ -1984,14 +1984,14 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
// ── Audio (DVK + QSO recorder) ──
type AudioSettings = {
from_radio: string; to_radio: string; recording_device: string; listening_device: string;
from_radio: string; to_radio: string; recording_device: string; listening_device: string; digi_input: string;
qso_record: boolean; qso_dir: string; preroll_seconds: number;
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<AudioSettings>({
from_radio: '', to_radio: '', recording_device: '', listening_device: '',
from_radio: '', to_radio: '', recording_device: '', listening_device: '', digi_input: '',
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,
});
@@ -7784,6 +7784,9 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
const NET_DEVICE = 'net:radio';
const soundCardsOnly = (devs: AudioDev[]) => devs.filter((d) => d.id !== NET_DEVICE);
const fromRadioIsNetwork = audioCfg.from_radio === NET_DEVICE;
// The radio takes transmit audio over its link, so a digital program can
// reach it through a cable OpsLog reads.
const toRadioIsNetwork = audioCfg.to_radio === NET_DEVICE;
const deviceSelect = (
field: keyof AudioSettings,
@@ -7829,7 +7832,22 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
{deviceSelect('recording_device', soundCardsOnly(audioInputs), t('aud.phRecMic'))}
<Label className="text-sm">{t('aud.listening')}</Label>
{deviceSelect('listening_device', soundCardsOnly(audioOutputs), t('aud.phListening'))}
{/* Only for a radio that carries its own audio, which is the only
case where a digital program has nowhere to put its transmit
audio: the rig has no sound card on this PC, so a virtual
cable stands in and OpsLog captures its other side. Hidden
otherwise a rig with a USB codec needs none of this, and
WSJT-X talks to that codec directly. */}
{toRadioIsNetwork && (
<>
<Label className="text-sm">{t('aud.digiInput')}</Label>
{deviceSelect('digi_input', soundCardsOnly(audioInputs), t('aud.phDigiInput'))}
</>
)}
</div>
{toRadioIsNetwork && (
<p className="text-[11px] text-muted-foreground max-w-2xl">{t('aud.digiInputHint')}</p>
)}
<p className="text-[11px] text-muted-foreground">
<strong>{t('aud.fromRadioShort')}</strong> {t('aud.explainFrom')}{' '}
<strong>{t('aud.toRadioShort')}</strong> {t('aud.explainTo')}
+6 -2
View File
@@ -577,7 +577,9 @@ const en: Dict = {
'clg2.allFiltered': '{n} spots received, none shown — your filters are hiding them all.', 'clg2.activeFilters': 'Active:', 'clg2.clearAllFilters': 'Clear every filter', 'clg2.fBandLock': 'band locked to the rig', 'clg2.fBands': 'bands {list}', 'clg2.fModeLock': 'mode locked to the rig', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'status chips', 'clg2.fHideWorked': 'hide worked', 'clg2.fLotwOnly': 'LoTW users only', 'clg2.fSpotterCont': 'spotter continent', 'clg2.fSource': 'one source node', 'clg2.fSearch': 'search “{q}”',
'clg2.c.time': 'Time', 'clg2.c.call': 'Call', 'clg2.c.status': 'Status', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Freq', 'clg2.c.band': 'Band', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Pfx', 'clg2.c.cqz': 'CQ Zone', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'ITU Zone', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Short path (°)', 'clg2.h.sp_deg': 'SP°', 'clg2.c.lp_deg': 'Long path (°)', 'clg2.h.lp_deg': 'LP°', 'clg2.c.country': 'Country', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Spotter locator', 'clg2.h.locator': 'Spotter loc', 'clg2.c.county': 'US County', 'clg2.tipNewCounty': 'NEW COUNTY — never worked', 'clg2.tipNewPfx': 'NEW PREFIX — this WPX prefix has never been worked', 'clg2.c.comment': 'Comment', 'clg2.c.received_at': 'Received at', 'clg2.h.received_at': 'Received UTC', 'clg2.c.raw': 'Raw', 'clg2.newDxcc': 'NEW DXCC', 'clg2.newBandMode': 'NEW B+M', 'clg2.newBand': 'NEW BAND', 'clg2.newMode': 'NEW MODE', 'clg2.newSlot': 'NEW SLOT', 'clg2.newCall': 'NEW CALL', 'clg2.wkdCall': 'WKD CALL', 'clg2.newCounty': 'NEW CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newState': 'New State', 'clg2.newGrid': 'NEW GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NEW GRID — this square has never been worked (grid heard in a CQ on the UDP link)', 'clg2.newPfx': "NEW PFX", 'clg2.newPota': 'NEW POTA', 'clg2.tipNewDxcc': 'NEW DXCC: {country}', 'clg2.tipWorkedCall': 'Already worked this call', 'clg2.tipNewBandMode': 'NEW BAND AND NEW MODE for this entity — neither has been worked with it', 'clg2.tipNewBand': 'NEW BAND for this entity', 'clg2.tipNewSlotBand': 'NEW SLOT (mode not yet worked on this band)', 'clg2.tipNewMode': 'NEW MODE (this mode never worked on this entity)', 'clg2.tipNewSlot': 'NEW SLOT (this band+mode not yet worked)', 'clg2.tipNewCall': 'NEW CALL — this callsign has never been worked on this band and mode (the entity has)', 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Geo', 'clg2.clearFiltersTitle': 'Clear all column filters', 'clg2.clearFilters': 'Clear filters', 'clg2.newSpots': '{n} new spots — click to resume', 'clg2.columns': 'Columns', 'clg2.pickerTitle': 'Cluster columns', 'clg2.pickerDesc': 'Pick the columns you want visible in the Cluster table.', 'clg2.allGroups': 'All groups:', 'clg2.all': 'all', 'clg2.none': 'none', 'clg2.resetDefaults': 'Reset to defaults', 'clg2.done': 'Done',
// Audio devices & voice keyer (Preferences → Audio devices).
'aud.refreshDevices': 'Refresh devices', 'aud.fromRadio': 'From Radio (RX in)', 'aud.toRadio': 'To Radio (TX out)', 'aud.recMic': 'Recording mic', 'aud.listening': 'Listening (preview)',
'aud.refreshDevices': 'Refresh devices',
'aud.digiInput': 'WSJT-X transmit audio', 'aud.phDigiInput': 'The virtual cable WSJT-X plays into',
'aud.digiInputHint': "Your radio has no sound card on this PC, so a digital program cannot reach it. Install a virtual audio cable, set WSJT-X's output to it, and choose the same cable's recording side here — OpsLog streams it to the radio while WSJT-X holds PTT through the shared CAT. For receive, point WSJT-X's input at a second cable and set Listening to it.", 'aud.fromRadio': 'From Radio (RX in)', 'aud.toRadio': 'To Radio (TX out)', 'aud.recMic': 'Recording mic', 'aud.listening': 'Listening (preview)',
'aud.phFromRadio': 'Rig audio output → soundcard input', 'aud.phToRadio': 'Soundcard output → rig mic/data in', 'aud.phRecMic': 'Your microphone (record voice-keyer messages)', 'aud.phListening': 'Local speakers for preview',
'aud.noneDefault': '— none / system default —', 'aud.defaultTag': '(default)',
'aud.fromRadioShort': 'From Radio', 'aud.toRadioShort': 'To Radio', 'aud.explainFrom': '= what you receive (used by the QSO recorder).', 'aud.explainTo': '= where voice-keyer messages are transmitted.',
@@ -1205,7 +1207,9 @@ const fr: Dict = {
'clg2.allFiltered': '{n} spots reçus, aucun affiché — vos filtres les masquent tous.', 'clg2.activeFilters': 'Actifs :', 'clg2.clearAllFilters': 'Effacer tous les filtres', 'clg2.fBandLock': 'bande verrouillée sur la radio', 'clg2.fBands': 'bandes {list}', 'clg2.fModeLock': 'mode verrouillé sur la radio', 'clg2.fModes': 'modes {list}', 'clg2.fStatus': 'pastilles de statut', 'clg2.fHideWorked': 'masquer les contactés', 'clg2.fLotwOnly': 'utilisateurs LoTW uniquement', 'clg2.fSpotterCont': 'continent du spotteur', 'clg2.fSource': 'un seul nœud source', 'clg2.fSearch': 'recherche « {q} »',
'clg2.c.time': 'Heure', 'clg2.c.call': 'Indicatif', 'clg2.c.status': 'Statut', 'clg2.c.pota': 'POTA', 'clg2.c.sota': 'SOTA', 'clg2.c.freq': 'Fréq', 'clg2.c.band': 'Bande', 'clg2.c.mode': 'Mode', 'clg2.c.pfx': 'Préf.', 'clg2.c.cqz': 'Zone CQ', 'clg2.h.cqz': 'CQZ', 'clg2.c.ituz': 'Zone ITU', 'clg2.h.ituz': 'ITU', 'clg2.c.distance_km': 'Distance (km)', 'clg2.h.distance_km': 'Dist km', 'clg2.c.sp_deg': 'Chemin court (°)', 'clg2.h.sp_deg': 'CC°', 'clg2.c.lp_deg': 'Chemin long (°)', 'clg2.h.lp_deg': 'CL°', 'clg2.c.country': 'Pays', 'clg2.c.continent': 'Continent', 'clg2.h.continent': 'Cont', 'clg2.c.spotter': 'Spotter', 'clg2.c.source': 'Source', 'clg2.c.locator': 'Locator du spotter', 'clg2.h.locator': 'Loc spotter', 'clg2.c.county': 'Comté US', 'clg2.tipNewCounty': 'NOUVEAU COMTÉ — jamais contacté', 'clg2.tipNewPfx': "NOUVEAU PRÉFIXE — ce préfixe WPX n'a jamais été contacté", 'clg2.c.comment': 'Commentaire', 'clg2.c.received_at': 'Reçu le', 'clg2.h.received_at': 'Reçu UTC', 'clg2.c.raw': 'Brut', 'clg2.newDxcc': 'NOUV DXCC', 'clg2.newBandMode': 'NOUV B+M', 'clg2.newBand': 'NOUV BANDE', 'clg2.newMode': 'NOUV MODE', 'clg2.newSlot': 'NOUV SLOT', 'clg2.newCall': 'CALL NEUF', 'clg2.wkdCall': 'DÉJÀ QSO', 'clg2.newCounty': 'NOUV CTY', 'clg2.newGridUnconf': 'GRID?', 'clg2.newState': 'Nouvel État', 'clg2.newGrid': 'NOUV GRID', 'clg2.c.grid': 'Grid', 'clg2.tipNewGrid': 'NOUVEAU GRID — ce carré n a jamais été contacté (grid entendu dans un CQ sur le lien UDP)', 'clg2.newPfx': "NOUVEAU PFX", 'clg2.newPota': 'NOUV POTA', 'clg2.tipNewDxcc': 'NOUVEAU DXCC : {country}', 'clg2.tipWorkedCall': 'Indicatif déjà contacté', 'clg2.tipNewBandMode': 'NOUVELLE BANDE ET NOUVEAU MODE pour cette entité — aucun des deux na été fait avec elle', 'clg2.tipNewBand': 'NOUVELLE BANDE pour cette entité', 'clg2.tipNewSlotBand': 'NOUVEAU SLOT (mode pas encore contacté sur cette bande)', 'clg2.tipNewMode': 'NOUVEAU MODE (ce mode jamais contacté sur cette entité)', 'clg2.tipNewSlot': 'NOUVEAU SLOT (cette bande+mode pas encore contactée)', 'clg2.tipNewCall': "CALL NEUF — cet indicatif n a jamais été contacté sur cette bande et ce mode (l entité, si)", 'clg2.tipPota': 'POTA — {name}', 'clg2.grpSpot': 'Spot', 'clg2.grpGeo': 'Géo', 'clg2.clearFiltersTitle': 'Effacer tous les filtres de colonne', 'clg2.clearFilters': 'Effacer les filtres', 'clg2.newSpots': '{n} nouveaux spots — cliquer pour reprendre', 'clg2.columns': 'Colonnes', 'clg2.pickerTitle': 'Colonnes du cluster', 'clg2.pickerDesc': 'Choisissez les colonnes à afficher dans le tableau du cluster.', 'clg2.allGroups': 'Tous les groupes :', 'clg2.all': 'tout', 'clg2.none': 'aucun', 'clg2.resetDefaults': 'Réinitialiser', 'clg2.done': 'Terminé',
// Périphériques audio et manipulateur vocal (Préférences → Périphériques audio).
'aud.refreshDevices': 'Actualiser les périphériques', 'aud.fromRadio': 'Depuis la radio (entrée RX)', 'aud.toRadio': 'Vers la radio (sortie TX)', 'aud.recMic': "Micro d'enregistrement", 'aud.listening': 'Écoute (pré-écoute)',
'aud.refreshDevices': 'Actualiser les périphériques',
'aud.digiInput': 'Audio d’émission WSJT-X', 'aud.phDigiInput': 'Le câble virtuel dans lequel WSJT-X émet',
'aud.digiInputHint': "Votre radio n'a pas de carte son sur ce PC, un logiciel numérique ne peut donc pas l'atteindre. Installez un câble audio virtuel, réglez la sortie de WSJT-X dessus, et choisissez ici le côté enregistrement du même câble — OpsLog le diffuse vers la radio pendant que WSJT-X tient le PTT via le CAT partagé. Pour la réception, pointez l'entrée de WSJT-X sur un second câble et réglez « Écoute » dessus.", 'aud.fromRadio': 'Depuis la radio (entrée RX)', 'aud.toRadio': 'Vers la radio (sortie TX)', 'aud.recMic': "Micro d'enregistrement", 'aud.listening': 'Écoute (pré-écoute)',
'aud.phFromRadio': 'Sortie audio du poste → entrée carte son', 'aud.phToRadio': 'Sortie carte son → entrée micro/data du poste', 'aud.phRecMic': 'Votre microphone (enregistrer les messages vocaux)', 'aud.phListening': 'Haut-parleurs locaux pour la pré-écoute',
'aud.noneDefault': '— aucun / défaut système —', 'aud.defaultTag': '(défaut)',
'aud.fromRadioShort': 'Depuis la radio', 'aud.toRadioShort': 'Vers la radio', 'aud.explainFrom': "= ce que vous recevez (utilisé par l'enregistreur de QSO).", 'aud.explainTo': '= où sont émis les messages du manipulateur vocal.',
+2
View File
@@ -834,6 +834,8 @@ export function ListCountries():Promise<Array<string>>;
export function ListDenkoviDevices():Promise<Array<string>>;
export function ListDigiInputDevices():Promise<Array<audio.Device>>;
export function ListOperatingTree():Promise<Array<operating.Station>>;
export function ListProfiles():Promise<Array<profile.Profile>>;
+4
View File
@@ -1598,6 +1598,10 @@ export function ListDenkoviDevices() {
return window['go']['main']['App']['ListDenkoviDevices']();
}
export function ListDigiInputDevices() {
return window['go']['main']['App']['ListDigiInputDevices']();
}
export function ListOperatingTree() {
return window['go']['main']['App']['ListOperatingTree']();
}
+2
View File
@@ -1920,6 +1920,7 @@ export namespace main {
from_radio: string;
to_radio: string;
recording_device: string;
digi_input: string;
listening_device: string;
qso_record: boolean;
qso_dir: string;
@@ -1942,6 +1943,7 @@ export namespace main {
this.from_radio = source["from_radio"];
this.to_radio = source["to_radio"];
this.recording_device = source["recording_device"];
this.digi_input = source["digi_input"];
this.listening_device = source["listening_device"];
this.qso_record = source["qso_record"];
this.qso_dir = source["qso_dir"];