From ca3be9f2587fab9d814c4ce6392e2ec71ea6db05 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 22 Mar 2026 21:15:24 +0100 Subject: [PATCH] recorder: add stereo pilot lock test --- internal/recorder/streamer_test.go | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 internal/recorder/streamer_test.go diff --git a/internal/recorder/streamer_test.go b/internal/recorder/streamer_test.go new file mode 100644 index 0000000..67ad9c9 --- /dev/null +++ b/internal/recorder/streamer_test.go @@ -0,0 +1,77 @@ +package recorder + +import ( + "math" + "testing" +) + +func TestStereoDecodeStatefulPilotLock(t *testing.T) { + const ( + sampleRate = 192000 + blockSize = 4096 + blocks = 10 + pilotAmp = 0.1 + toneL = 440.0 + toneR = 880.0 + ) + + sess := &streamSession{} + var out []float32 + locked := false + + for b := 0; b < blocks; b++ { + mono := make([]float32, blockSize) + base := b * blockSize + for i := 0; i < blockSize; i++ { + t := float64(base+i) / float64(sampleRate) + l := math.Sin(2 * math.Pi * toneL * t) + r := math.Sin(2 * math.Pi * toneR * t) + lpr := 0.5 * (l + r) + lmr := 0.5 * (l - r) + composite := lpr + lmr*math.Cos(2*math.Pi*38000*t) + pilotAmp*math.Sin(2*math.Pi*19000*t) + mono[i] = float32(composite) + } + out, locked = sess.stereoDecodeStateful(mono, sampleRate) + } + + if !locked { + t.Fatalf("expected pilot lock after warmup blocks") + } + if len(out) != blockSize*2 { + t.Fatalf("unexpected output size: got %d, want %d", len(out), blockSize*2) + } + + left := make([]float32, blockSize) + right := make([]float32, blockSize) + for i := 0; i < blockSize; i++ { + left[i] = out[i*2] + right[i] = out[i*2+1] + } + + magL440 := toneMagnitude(left, toneL, sampleRate) + magL880 := toneMagnitude(left, toneR, sampleRate) + magR440 := toneMagnitude(right, toneL, sampleRate) + magR880 := toneMagnitude(right, toneR, sampleRate) + + if magL440 < 0.05 || magR880 < 0.05 { + t.Fatalf("decoded tones too weak: L440=%.3f R880=%.3f", magL440, magR880) + } + leftIsL := magL440 >= magL880*1.3 && magR880 >= magR440*1.3 + rightIsL := magL880 >= magL440*1.3 && magR440 >= magR880*1.3 + if !leftIsL && !rightIsL { + t.Fatalf( + "channels not cleanly separated: L440=%.3f L880=%.3f R440=%.3f R880=%.3f", + magL440, magL880, magR440, magR880, + ) + } +} + +func toneMagnitude(x []float32, freq float64, sampleRate int) float64 { + var iSum, qSum float64 + for n, v := range x { + angle := 2 * math.Pi * freq * float64(n) / float64(sampleRate) + iSum += float64(v) * math.Cos(angle) + qSum += float64(v) * math.Sin(angle) + } + return math.Hypot(iSum, qSum) / float64(len(x)) +}