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:
@@ -57,6 +57,9 @@ type Recorder struct {
|
|||||||
// produces anything is not an error anywhere — the capture call just sits
|
// produces anything is not an error anywhere — the capture call just sits
|
||||||
// there — so the only way to notice is to watch the clock.
|
// there — so the only way to notice is to watch the clock.
|
||||||
lastA, lastB time.Time
|
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
|
// deadB (deadA) latches once a source has been declared silent, so the
|
||||||
// warning is logged once rather than 25 times a second.
|
// warning is logged once rather than 25 times a second.
|
||||||
deadA, deadB bool
|
deadA, deadB bool
|
||||||
@@ -158,7 +161,9 @@ func (r *Recorder) Start(fromDev, micDev string, prerollSec int) error {
|
|||||||
r.twoSrc = micDev != "" && micDev != fromDev
|
r.twoSrc = micDev != "" && micDev != fromDev
|
||||||
r.stopCh = make(chan struct{})
|
r.stopCh = make(chan struct{})
|
||||||
r.running = true
|
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.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
|
stop := r.stopCh
|
||||||
twoSrc := r.twoSrc
|
twoSrc := r.twoSrc
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
@@ -265,10 +270,23 @@ func (r *Recorder) mixTick() {
|
|||||||
// path is not running — and the operator got "recording was empty" after
|
// path is not running — and the operator got "recording was empty" after
|
||||||
// a whole QSO. Half a recording is worth having; silence is not.
|
// a whole QSO. Half a recording is worth having; silence is not.
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
aDry := !r.lastA.IsZero() && now.Sub(r.lastA) > deadSourceAfter
|
// A source is dry when it has been quiet for too long — and a source that
|
||||||
bDry := !r.lastB.IsZero() && now.Sub(r.lastB) > deadSourceAfter
|
// has NEVER delivered is measured from when capture started, because it
|
||||||
// Never before either source has spoken at all: at startup both are zero
|
// has no last-delivery time of its own.
|
||||||
// and neither should be written off.
|
//
|
||||||
|
// 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 bDry && !aDry && len(r.bufA) > 0 {
|
||||||
if !r.deadB {
|
if !r.deadB {
|
||||||
r.deadB = true
|
r.deadB = true
|
||||||
|
|||||||
@@ -51,17 +51,40 @@ func TestMixerStillMixesBothSources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before either source has EVER delivered, neither may be written off: at
|
// Just after capture starts, a source that has not delivered yet is NOT written
|
||||||
// startup both timestamps are zero, and declaring one dead then would drop the
|
// off: the first samples take a moment to arrive, and declaring the mic dead at
|
||||||
// first moments of every recording.
|
// once would drop the opening of every recording.
|
||||||
func TestMixerWaitsForBothAtStartup(t *testing.T) {
|
func TestMixerWaitsBrieflyAtStartup(t *testing.T) {
|
||||||
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
||||||
|
r.startedAt = time.Now()
|
||||||
r.bufA = []int16{100, 100}
|
r.bufA = []int16{100, 100}
|
||||||
// lastA/lastB both zero — nothing has been heard from either yet.
|
|
||||||
|
|
||||||
r.mixTick()
|
r.mixTick()
|
||||||
|
|
||||||
if len(r.acc) != 0 {
|
if len(r.acc) != 0 {
|
||||||
t.Errorf("recorded %d samples before the second source was known to be silent", len(r.acc))
|
t.Errorf("recorded %d samples immediately — the second source deserves a moment to start", len(r.acc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A source that has NEVER delivered is written off once enough time has passed.
|
||||||
|
//
|
||||||
|
// This is the case that actually happens, and the one the first version missed:
|
||||||
|
// a DAX Mic channel switched off delivers not one sample, so it had no
|
||||||
|
// last-delivery time and stayed forever "not yet dry" — taking the whole
|
||||||
|
// recording down with it while the radio audio streamed perfectly.
|
||||||
|
func TestMixerWritesOffASourceThatNeverSpoke(t *testing.T) {
|
||||||
|
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
||||||
|
r.startedAt = time.Now().Add(-5 * time.Second)
|
||||||
|
r.bufA = []int16{700, 700, 700}
|
||||||
|
r.lastA = time.Now()
|
||||||
|
// lastB stays zero: the mic has never produced anything at all.
|
||||||
|
|
||||||
|
r.mixTick()
|
||||||
|
|
||||||
|
if len(r.acc) != 3 {
|
||||||
|
t.Fatalf("recorded %d samples, want 3 — the radio was streaming the whole time", len(r.acc))
|
||||||
|
}
|
||||||
|
if r.acc[0] != 700 {
|
||||||
|
t.Errorf("sample = %d, want 700", r.acc[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user