fix: write off an audio source that never delivers anything

The operator said it from the start: in CW there is no sidetone, only reception.
My previous fix did not cover it — it wrote off a source that STOPPED, but a
source that never starts has no last-delivery time, so it stayed forever "not
yet dry" and held the mixer at min(A,B)=0. Three empty recordings while the
radio audio streamed perfectly.

A source with no delivery is now measured from when capture started. Tested both
ways: nothing is written off in the first moments, and a source that has never
spoken is written off once the grace period has passed.
This commit is contained in:
2026-07-31 00:15:11 +02:00
parent b25efabab8
commit 311479c52f
2 changed files with 51 additions and 10 deletions
+22 -4
View File
@@ -57,6 +57,9 @@ type Recorder struct {
// produces anything is not an error anywhere — the capture call just sits
// there — so the only way to notice is to watch the clock.
lastA, lastB time.Time
// startedAt is when capture began — the reference for a source that has not
// delivered anything at all yet.
startedAt time.Time
// deadB (deadA) latches once a source has been declared silent, so the
// warning is logged once rather than 25 times a second.
deadA, deadB bool
@@ -158,7 +161,9 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
r.twoSrc = micDev != "" && micDev != fromDev
r.stopCh = make(chan struct{})
r.running = true
r.startedAt = time.Now()
r.ring, r.acc, r.active, r.paused, r.bufA, r.bufB = nil, nil, false, false, nil, nil
r.lastA, r.lastB, r.deadA, r.deadB = time.Time{}, time.Time{}, false, false
stop := r.stopCh
twoSrc := r.twoSrc
r.mu.Unlock()
@@ -265,10 +270,23 @@ func (r *Recorder) mixTick() {
// path is not running — and the operator got "recording was empty" after
// a whole QSO. Half a recording is worth having; silence is not.
now := time.Now()
aDry := !r.lastA.IsZero() && now.Sub(r.lastA) > deadSourceAfter
bDry := !r.lastB.IsZero() && now.Sub(r.lastB) > deadSourceAfter
// Never before either source has spoken at all: at startup both are zero
// and neither should be written off.
// A source is dry when it has been quiet for too long — and a source that
// has NEVER delivered is measured from when capture started, because it
// has no last-delivery time of its own.
//
// The first version required a source to have spoken at least once. That
// covers a device that stops, but not the one that actually happens: a
// DAX Mic channel that is simply switched off never delivers a single
// sample, so it stayed "not yet dry" forever and took the whole recording
// down with it. Three empty CW recordings, with the radio audio streaming
// perfectly the entire time.
dry := func(last time.Time) bool {
if last.IsZero() {
return now.Sub(r.startedAt) > deadSourceAfter
}
return now.Sub(last) > deadSourceAfter
}
aDry, bDry := dry(r.lastA), dry(r.lastB)
if bDry && !aDry && len(r.bufA) > 0 {
if !r.deadB {
r.deadB = true