|
- // 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 <license-key> --output test.wav --duration 30s
- // wmdecode test.wav <license-key>
- 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
- }
|