|
- package offline
-
- import (
- "math"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "time"
-
- cfgpkg "github.com/jan/fm-rds-tx/internal/config"
- )
-
- func TestGenerateFrame(t *testing.T) {
- g := NewGenerator(cfgpkg.Default())
- frame := g.GenerateFrame(50 * time.Millisecond)
- if frame == nil {
- t.Fatal("expected frame")
- }
- if len(frame.Samples) == 0 {
- t.Fatal("expected samples")
- }
- }
-
- func TestGenerateFrameFMIQ(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.FM.FMModulationEnabled = true
- g := NewGenerator(cfg)
- frame := g.GenerateFrame(10 * time.Millisecond)
-
- // With FM modulation, IQ samples should have magnitude ~1
- for i := 100; i < len(frame.Samples) && i < 200; i++ {
- s := frame.Samples[i]
- mag := math.Sqrt(float64(s.I)*float64(s.I) + float64(s.Q)*float64(s.Q))
- if math.Abs(mag-1.0) > 0.01 {
- t.Fatalf("sample %d: IQ magnitude=%.4f, expected ~1.0", i, mag)
- }
- }
- }
-
- func TestGenerateFrameCompositeOnly(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.FM.FMModulationEnabled = false
- g := NewGenerator(cfg)
- frame := g.GenerateFrame(10 * time.Millisecond)
-
- // Without FM modulation, Q should be 0
- for i := 0; i < len(frame.Samples) && i < 100; i++ {
- if frame.Samples[i].Q != 0 {
- t.Fatalf("sample %d: Q=%.6f, expected 0 in composite mode", i, frame.Samples[i].Q)
- }
- }
- }
-
- func TestWriteFile(t *testing.T) {
- cfg := cfgpkg.Default()
- out := filepath.Join(t.TempDir(), "test.iqf32")
- cfg.Backend.OutputPath = out
- g := NewGenerator(cfg)
- if err := g.WriteFile(out, 20*time.Millisecond); err != nil {
- t.Fatalf("WriteFile failed: %v", err)
- }
- info, err := os.Stat(out)
- if err != nil {
- t.Fatalf("expected output file: %v", err)
- }
- if info.Size() == 0 {
- t.Fatal("expected non-empty file")
- }
- }
-
- func TestSummaryUsesToneFallback(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.Audio.InputPath = ""
- g := NewGenerator(cfg)
- summary := g.Summary(10 * time.Millisecond)
- if !strings.Contains(summary, "source=tones") {
- t.Fatalf("unexpected summary: %s", summary)
- }
- }
-
- func TestSummaryUsesFallbackLabelOnBadWAV(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.Audio.InputPath = "missing.wav"
- g := NewGenerator(cfg)
- summary := g.Summary(10 * time.Millisecond)
- if !strings.Contains(summary, "source=tone-fallback") {
- t.Fatalf("unexpected summary: %s", summary)
- }
- }
-
- func TestSummaryContainsPreemph(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.FM.PreEmphasisUS = 50
- g := NewGenerator(cfg)
- summary := g.Summary(10 * time.Millisecond)
- if !strings.Contains(summary, "preemph=50µs") {
- t.Fatalf("unexpected summary: %s", summary)
- }
- }
-
- func TestSummaryContainsFMIQ(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.FM.FMModulationEnabled = true
- g := NewGenerator(cfg)
- summary := g.Summary(10 * time.Millisecond)
- if !strings.Contains(summary, "FM-IQ") {
- t.Fatalf("unexpected summary: %s", summary)
- }
- }
-
- func TestLimiterPreventsClipping(t *testing.T) {
- cfg := cfgpkg.Default()
- cfg.FM.LimiterEnabled = true
- cfg.FM.LimiterCeiling = 1.0
- cfg.FM.FMModulationEnabled = false // raw composite to check levels
- cfg.Audio.ToneAmplitude = 0.9 // high amplitude to exercise limiter
- cfg.Audio.Gain = 2.0
- cfg.FM.OutputDrive = 1.0
- g := NewGenerator(cfg)
- frame := g.GenerateFrame(50 * time.Millisecond)
-
- for i, s := range frame.Samples {
- if math.Abs(float64(s.I)) > 1.01 {
- t.Fatalf("sample %d: composite=%.4f exceeds ceiling", i, s.I)
- }
- }
- }
|