// cmd/wmtest — Ferrite watermark self-test tool. // // Generates a mono WAV file containing only the spread-spectrum watermark // signal (silence + watermark, scaled up for visibility). Run wmdecode on // the output to verify embedder and decoder work without FM transmission. // // Usage: // // wmtest --key --output test.wav --duration 30s // wmdecode test.wav package main import ( "encoding/binary" "flag" "fmt" "math" "os" "time" "github.com/jan/fm-rds-tx/internal/watermark" ) func main() { key := flag.String("key", "free", "License key to embed") output := flag.String("output", "wmtest.wav", "Output WAV file") duration := flag.Duration("duration", 60*time.Second, "Duration (min 45s for 2 full frames)") flag.Parse() const compRate = watermark.CompositeRate // 228000 const recRate = watermark.RecordingRate // 48000 nSamples := int(duration.Seconds() * float64(recRate)) chipRate := watermark.ChipRate frameSeconds := float64(128 * watermark.PnChips) / float64(chipRate) nFrames := duration.Seconds() / frameSeconds fmt.Printf("Ferrite watermark self-test\n") fmt.Printf(" Key: %s\n", *key) fmt.Printf(" ChipRate: %d Hz (PN bandwidth 0–%d Hz)\n", chipRate, chipRate/2) fmt.Printf(" Frame: %.1fs (%d chips × 128 bits @ %d Hz)\n", frameSeconds, watermark.PnChips, chipRate) fmt.Printf(" Duration: %s (%d samples @ %dHz, %.1f frames)\n\n", *duration, nSamples, recRate, nFrames) embedder := watermark.NewEmbedder(*key) samples := make([]float64, 0, nSamples) // Drive embedder at composite rate, collect samples at recording rate. // Bresenham: accumulate recRate each composite step; when >= compRate, // emit one recording sample and subtract compRate. accum := 0 var last float64 for len(samples) < nSamples { last = embedder.NextSample() accum += recRate if accum >= compRate { accum -= compRate samples = append(samples, last) } } // RMS var rmsAcc float64 for _, s := range samples { rmsAcc += s * s } rms := math.Sqrt(rmsAcc / float64(len(samples))) fmt.Printf("Watermark RMS: %.1f dBFS (nominal -48 dBFS)\n", 20*math.Log10(rms+1e-12)) if err := writeMonoWAV(*output, samples, recRate); err != nil { fmt.Fprintf(os.Stderr, "write WAV: %v\n", err) os.Exit(1) } fmt.Printf("Written: %s\n\n", *output) fmt.Printf("Decode with:\n") fmt.Printf(" .\\wmdecode.exe %s %q\n\n", *output, *key) fmt.Printf("Expected: RS decode clean + MATCH\n") } func writeMonoWAV(path string, samples []float64, rate int) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() le := binary.LittleEndian dataSz := uint32(len(samples) * 2) f.Write([]byte("RIFF")) binary.Write(f, le, 36+dataSz) f.Write([]byte("WAVE")) f.Write([]byte("fmt ")) binary.Write(f, le, uint32(16)) binary.Write(f, le, uint16(1)) // PCM binary.Write(f, le, uint16(1)) // mono binary.Write(f, le, uint32(rate)) binary.Write(f, le, uint32(rate*2)) // byte rate binary.Write(f, le, uint16(2)) // block align binary.Write(f, le, uint16(16)) // bits/sample f.Write([]byte("data")) binary.Write(f, le, dataSz) for _, s := range samples { // Scale to int16 range — watermark at -48dBFS → ~0.004 amplitude // Multiply by 32767 to get full 16-bit range v := s * 32767.0 if v > 32767 { v = 32767 } if v < -32768 { v = -32768 } binary.Write(f, le, int16(v)) } return nil }