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]) } } // Nothing captured during the grace period is thrown away. // // The drift guard exists for two sound cards running on their own clocks. A // STARVED source is not drift, and treating it as such discarded the radio // audio a second at a time while the mixer was still waiting for the mic — so // the opening of every CW recording was lost although it had been captured. func TestGracePeriodKeepsWhatWasCaptured(t *testing.T) { r := &Recorder{twoSrc: true, gainA: 1, gainB: 1, running: true, active: true, prerollSamples: sampleRate} r.startedAt = time.Now() // Three seconds of radio audio arrive while the mic says nothing at all. r.bufA = make([]int16, 3*sampleRate) for i := range r.bufA { r.bufA[i] = 500 } r.lastA = time.Now() r.mixTick() // still inside the grace period: nothing is written yet… if len(r.acc) != 0 { t.Fatalf("wrote %d samples before the mic was written off", len(r.acc)) } if len(r.bufA) != 3*sampleRate { t.Fatalf("buffered audio was trimmed to %d samples — it must be kept, not dropped", len(r.bufA)) } // …and once the mic is written off, everything captured comes through. r.startedAt = time.Now().Add(-5 * time.Second) r.mixTick() if len(r.acc) != 3*sampleRate { t.Errorf("recorded %d samples, want the full %d captured during the wait", len(r.acc), 3*sampleRate) } }