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.
91 lines
3.0 KiB
Go
91 lines
3.0 KiB
Go
package audio
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A silent second source must not silence the recording.
|
|
//
|
|
// From a real session: a Flex with "From Radio" on DAX RX and "Recording mic"
|
|
// on DAX Mic. In CW the mic path does not run, so DAX Mic delivered nothing —
|
|
// and because the mixer took min(len(A), len(B)) samples, nothing at all was
|
|
// recorded. The whole QSO ended in "recording was empty".
|
|
func TestMixerSurvivesASilentSource(t *testing.T) {
|
|
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
|
|
|
// The radio has been talking; the mic has not spoken for well over the
|
|
// grace period.
|
|
r.bufA = make([]int16, 800)
|
|
for i := range r.bufA {
|
|
r.bufA[i] = 1000
|
|
}
|
|
r.lastA = time.Now()
|
|
r.lastB = time.Now().Add(-5 * time.Second)
|
|
|
|
r.mixTick()
|
|
|
|
if len(r.acc) == 0 {
|
|
t.Fatal("nothing was recorded — a silent mic must not stop the radio being captured")
|
|
}
|
|
if r.acc[0] != 1000 {
|
|
t.Errorf("sample = %d, want 1000 — the live source must pass through unchanged", r.acc[0])
|
|
}
|
|
}
|
|
|
|
// While BOTH sources are alive the two are mixed, as before.
|
|
func TestMixerStillMixesBothSources(t *testing.T) {
|
|
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
|
r.bufA = []int16{100, 100, 100}
|
|
r.bufB = []int16{50, 50, 50}
|
|
now := time.Now()
|
|
r.lastA, r.lastB = now, now
|
|
|
|
r.mixTick()
|
|
|
|
if len(r.acc) != 3 {
|
|
t.Fatalf("recorded %d samples, want 3", len(r.acc))
|
|
}
|
|
if r.acc[0] != 150 {
|
|
t.Errorf("sample = %d, want 150 — both sources should be summed", r.acc[0])
|
|
}
|
|
}
|
|
|
|
// Just after capture starts, a source that has not delivered yet is NOT written
|
|
// off: the first samples take a moment to arrive, and declaring the mic dead at
|
|
// once would drop the opening of every recording.
|
|
func TestMixerWaitsBrieflyAtStartup(t *testing.T) {
|
|
r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate}
|
|
r.startedAt = time.Now()
|
|
r.bufA = []int16{100, 100}
|
|
|
|
r.mixTick()
|
|
|
|
if len(r.acc) != 0 {
|
|
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])
|
|
}
|
|
}
|