package offline import ( "math" "testing" "time" "github.com/jan/fm-rds-tx/internal/dsp" "github.com/jan/fm-rds-tx/internal/watermark" ) func TestWatermarkE2EFloat32(t *testing.T) { if testing.Short() { t.Skip("skipping long watermark E2E float32 test in -short mode") } const key = "test-key-e2e-f32" const duration = 45 * time.Second gen := newWatermarkE2EGenerator(t, key) frame := gen.GenerateFrame(duration) nSamples := len(frame.Samples) compositeRate := frame.SampleRateHz t.Logf("Generated %d samples @ %.0f Hz", nSamples, compositeRate) t.Run("float32_storage", func(t *testing.T) { composite := extractCompositeFrame(frame) payload, _, ok := decodeWatermarkFromComposite(t, composite, compositeRate) if !ok { t.Fatal("decode failed after float32 composite storage") } if !watermark.KeyMatchesPayload(key, payload) { t.Fatalf("payload mismatch after float32 storage: %x", payload) } }) t.Run("fm_mod_demod", func(t *testing.T) { maxDev := 75000.0 phases := make([]float64, nSamples) phaseInc := 2 * math.Pi * maxDev / compositeRate phase := 0.0 for i, s := range frame.Samples { phase += float64(s.I) * phaseInc phases[i] = phase } demod := make([]float64, nSamples) for i := 1; i < nSamples; i++ { dp := phases[i] - phases[i-1] demod[i] = dp / phaseInc } demod[0] = demod[1] payload, _, ok := decodeWatermarkFromComposite(t, demod, compositeRate) if !ok { t.Fatal("decode failed after FM mod/demod") } if !watermark.KeyMatchesPayload(key, payload) { t.Fatalf("payload mismatch after FM mod/demod: %x", payload) } }) t.Run("fm_upsample_downsample", func(t *testing.T) { maxDev := 75000.0 deviceRate := 2280000.0 upsampler := dsp.NewFMUpsampler(compositeRate, deviceRate, maxDev) upFrame := upsampler.Process(frame) t.Logf("Upsampled: %d IQ samples @ %.0f Hz", len(upFrame.Samples), upFrame.SampleRateHz) nUp := len(upFrame.Samples) demod := make([]float64, nUp) prevPhase := 0.0 for i, s := range upFrame.Samples { p := math.Atan2(float64(s.Q), float64(s.I)) dp := p - prevPhase for dp > math.Pi { dp -= 2 * math.Pi } for dp < -math.Pi { dp += 2 * math.Pi } demod[i] = dp * deviceRate / (2 * math.Pi * maxDev) prevPhase = p } ratio := int(deviceRate / compositeRate) nDown := nUp / ratio downsampled := make([]float64, nDown) for i := 0; i < nDown; i++ { sum := 0.0 for j := 0; j < ratio; j++ { idx := i*ratio + j if idx < nUp { sum += demod[idx] } } downsampled[i] = sum / float64(ratio) } payload, _, ok := decodeWatermarkFromComposite(t, downsampled, compositeRate) if !ok { t.Fatal("decode failed after FM upsample/downsample path") } if !watermark.KeyMatchesPayload(key, payload) { t.Fatalf("payload mismatch after FM upsample/downsample path: %x", payload) } }) }