feat: network audio into the QSO recorder, and a Yaesu that stays on its VFO
An Icom reached over the LAN streams its receive audio through the Icom
protocol, and Windows sees no sound card for it at all — the audio settings
could only offer the PC's own microphone, so "From radio" had nothing right to
point at and the QSO recorder had nothing to record. The recorder now accepts a
PUSHED source: the decoded stream goes to the speakers and to the recorder
alike, with no virtual cable to set up. Which source it uses follows the CAT
backend, and it is restarted only when that answer changes, so an ordinary
settings save never cuts a recording in half.
OmniRig no longer sends SetSimplexMode to a Yaesu when tuning. It is a silent
no-op on some — an FT-891 logged OK on every spot click while FreqA never
moved — and actively harmful on others. From an FT-2000 log, one QSY:
Vfo="AB"(0x80) Split=0x10000 (off) the operator's state
Vfo="BA"(0x100) Split=0x8000 (ON) after SetSimplexMode
The rig-agnostic "receive and transmit HERE, simplex" call turned split on and
moved reception to VFO B. OpsLog then displayed B — reading the radio correctly,
after having moved it itself. Icom is untouched: there the call is the
authoritative one and the direct write is unreliable.
Also:
- the NEW county badge shows in the entry form itself, inside the field,
where the operator is deciding whether to call.
- the basemap buttons clear the zoom controls; Light sat a few pixels from
the minus button and was being clicked by mistake.
- French cluster status: DÉJÀ CTC reads DÉJÀ QSO.
This commit is contained in:
+62
-18
@@ -168,7 +168,48 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
||||
twoSrc := r.twoSrc
|
||||
r.mu.Unlock()
|
||||
|
||||
// Capture goroutine(s) feed the per-source queues.
|
||||
// The radio side is either CAPTURED from a sound device or PUSHED in from
|
||||
// somewhere else. Over the network there is no sound device at all: an
|
||||
// IC-705 reached by LAN streams its receive audio over the Icom protocol,
|
||||
// and the operator's audio settings can only offer the PC's own microphone
|
||||
// and speakers. fromDev == PushedSource says "someone else will feed me",
|
||||
// and PushRX is how they do it.
|
||||
if fromDev == PushedSource {
|
||||
LogSink("recorder: radio audio is pushed in (network), not captured from a device")
|
||||
} else {
|
||||
r.startRadioCapture(fromDev, stop)
|
||||
}
|
||||
if twoSrc {
|
||||
r.startMicCapture(micDev, stop)
|
||||
}
|
||||
r.finishStart(fromDev, micDev, twoSrc, stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushedSource is the "device" name that means the radio audio arrives through
|
||||
// PushRX instead of a capture stream.
|
||||
const PushedSource = "\x00pushed"
|
||||
|
||||
// PushRX feeds one chunk of 16-bit mono PCM from a non-device source.
|
||||
//
|
||||
// Safe to call when nothing is recording — the samples are dropped, which is
|
||||
// what should happen: the network stream runs whenever the rig is connected,
|
||||
// and the recorder only wants it between BeginQSO and the save.
|
||||
func (r *Recorder) PushRX(pcm []byte) {
|
||||
r.mu.Lock()
|
||||
running := r.running
|
||||
r.mu.Unlock()
|
||||
if !running || len(pcm) == 0 {
|
||||
return
|
||||
}
|
||||
sm := bytesToInt16(pcm)
|
||||
r.srcMu.Lock()
|
||||
r.bufA = append(r.bufA, sm...)
|
||||
r.lastA = time.Now()
|
||||
r.srcMu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Recorder) startRadioCapture(fromDev string, stop chan struct{}) {
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
@@ -187,23 +228,27 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
||||
LogSink("recorder: capture from %q failed: %v", DeviceName(fromDev), err)
|
||||
}
|
||||
}()
|
||||
if twoSrc {
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
defer recoverGoroutine("recorder capture (mic)")
|
||||
if err := captureStream(micDev, stop, func(chunk []byte) {
|
||||
s := bytesToInt16(chunk)
|
||||
r.srcMu.Lock()
|
||||
r.bufB = append(r.bufB, s...)
|
||||
r.lastB = time.Now()
|
||||
r.srcMu.Unlock()
|
||||
}); err != nil {
|
||||
LogSink("recorder: capture from %q failed: %v", DeviceName(micDev), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) startMicCapture(micDev string, stop chan struct{}) {
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
defer recoverGoroutine("recorder capture (mic)")
|
||||
if err := captureStream(micDev, stop, func(chunk []byte) {
|
||||
s := bytesToInt16(chunk)
|
||||
r.srcMu.Lock()
|
||||
r.bufB = append(r.bufB, s...)
|
||||
r.lastB = time.Now()
|
||||
r.srcMu.Unlock()
|
||||
}); err != nil {
|
||||
LogSink("recorder: capture from %q failed: %v", DeviceName(micDev), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// finishStart logs what is being recorded and arms the silence watchdog.
|
||||
func (r *Recorder) finishStart(fromDev, micDev string, twoSrc bool, stop chan struct{}) {
|
||||
// Name the devices being recorded FROM, once, at the start.
|
||||
//
|
||||
// A station with two radios has two sets of endpoints, and the recorder will
|
||||
@@ -262,7 +307,6 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// mixTick drains the source queues, mixes what's available, and appends to the
|
||||
|
||||
@@ -500,10 +500,30 @@ func (o *OmniRig) SetFrequency(hz int64) error {
|
||||
// deliberately; a spot click is a request to change frequency, not to change
|
||||
// VFO. On SUB the direct FreqB write below does the whole job.
|
||||
onSubVFO := vfo == "B" || vfo == "BB" || vfo == "BA"
|
||||
// …and NEVER on a Yaesu.
|
||||
//
|
||||
// On Yaesu the call is useless at best: an FT-891 logged "SetSimplexMode OK"
|
||||
// on every spot click while FreqA never moved (see below — the direct property
|
||||
// write is what actually tunes these rigs). On an FT-2000 it is worse than
|
||||
// useless. From a log of one QSY, before and after the call:
|
||||
//
|
||||
// Vfo="AB"(0x80) Split=0x10000 (off) → the operator's state
|
||||
// Vfo="BA"(0x100) Split=0x8000 (ON) → after SetSimplexMode
|
||||
//
|
||||
// The rig-agnostic "receive and transmit HERE, simplex" method turned split ON
|
||||
// and moved reception to VFO B, which is exactly what the operator reported:
|
||||
// OpsLog showing B instead of A. OpsLog was not misreading the radio — it was
|
||||
// reading it correctly after having moved it itself.
|
||||
//
|
||||
// Restricted to Yaesu because that is where the evidence is: on Icom the call
|
||||
// is authoritative and the direct write is the unreliable one.
|
||||
isYaesu := isYaesuRig(rigType)
|
||||
simplexOK := false
|
||||
switch {
|
||||
case onSubVFO:
|
||||
debugLog.Printf("OmniRig.SetFrequency: on VFO %q — skipping SetSimplexMode so the rig stays on SUB", vfo)
|
||||
case isYaesu:
|
||||
debugLog.Printf("OmniRig.SetFrequency: %q is a Yaesu — skipping SetSimplexMode (a no-op on some, and on an FT-2000 it turns split on and jumps to VFO B); the direct write below does the work", rigType)
|
||||
default:
|
||||
if _, err := oleutil.CallMethod(o.rig, "SetSimplexMode", int32(hz32)); err == nil {
|
||||
simplexOK = true
|
||||
|
||||
@@ -125,3 +125,24 @@ func TestOmniRigWriteTarget(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetSimplexMode is skipped on Yaesu, and that is not a preference.
|
||||
//
|
||||
// On an FT-891 it returned OK on every spot click while the rig never moved. On
|
||||
// an FT-2000 it did something worse — a log of one QSY shows the state going
|
||||
// from Vfo="AB" split off to Vfo="BA" split ON, so the operator's reception
|
||||
// jumped to VFO B and OpsLog, reading the radio correctly, displayed B. The
|
||||
// direct FreqA/Freq write does the tuning on these rigs.
|
||||
func TestYaesuRigsAreRecognisedForTheSimplexSkip(t *testing.T) {
|
||||
for _, yes := range []string{"FT-2000", "FT-891", "FTDX10", "ftdx101", " FT-991A "} {
|
||||
if !isYaesuRig(yes) {
|
||||
t.Errorf("%q not recognised as a Yaesu — SetSimplexMode would still be called on it", yes)
|
||||
}
|
||||
}
|
||||
// Everything else keeps the call: on Icom it is the authoritative one.
|
||||
for _, no := range []string{"IC-7610", "TS-590", "K3", "", "Flex-6600"} {
|
||||
if isYaesuRig(no) {
|
||||
t.Errorf("%q wrongly treated as a Yaesu", no)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user