package audio import ( "bytes" "encoding/binary" "math" "testing" "mxl-pattern-generator/internal/flowdef" ) const testAudioID = "b3bb5be7-9fe9-4324-a5bb-4c70e1084449" func testConfig(t *testing.T, channels uint, rate flowdef.Rational, level float64) Config { t.Helper() definition, err := flowdef.NewFloat32Audio(testAudioID, channels, rate) if err != nil { t.Fatalf("NewFloat32Audio: %v", err) } return Config{Definition: definition, LevelDBFS: level} } func decodeSample(data []byte, index int) float32 { return math.Float32frombits(binary.LittleEndian.Uint32(data[index*4:])) } func requireSampleNear(t *testing.T, got, want float32) { t.Helper() if math.Abs(float64(got-want)) > 1e-6 { t.Fatalf("sample = %.8f, want %.8f", got, want) } } func TestNewSineGeneratorValidation(t *testing.T) { valid := testConfig(t, 2, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelEBUDBFS) tests := []struct { name string cfg Config baseHz float64 wantErr bool }{ {name: "valid", cfg: valid, baseHz: 1000}, {name: "zero channels", cfg: Config{Definition: flowdef.Audio{SampleRate: flowdef.Rational{Numerator: 48000, Denominator: 1}}, LevelDBFS: LevelEBUDBFS}, baseHz: 1000, wantErr: true}, {name: "zero numerator", cfg: Config{Definition: flowdef.Audio{SampleRate: flowdef.Rational{Denominator: 1}, ChannelCount: 1}, LevelDBFS: LevelEBUDBFS}, baseHz: 1000, wantErr: true}, {name: "zero denominator", cfg: Config{Definition: flowdef.Audio{SampleRate: flowdef.Rational{Numerator: 48000}, ChannelCount: 1}, LevelDBFS: LevelEBUDBFS}, baseHz: 1000, wantErr: true}, {name: "zero base", cfg: valid, baseHz: 0, wantErr: true}, {name: "NaN base", cfg: valid, baseHz: math.NaN(), wantErr: true}, {name: "NaN level", cfg: Config{Definition: valid.Definition, LevelDBFS: math.NaN()}, baseHz: 1000, wantErr: true}, {name: "above full scale", cfg: Config{Definition: valid.Definition, LevelDBFS: 1}, baseHz: 1000, wantErr: true}, {name: "too quiet", cfg: Config{Definition: valid.Definition, LevelDBFS: -101}, baseHz: 1000, wantErr: true}, {name: "Nyquist", cfg: testConfig(t, 24, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelEBUDBFS), baseHz: 1000, wantErr: true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := NewSineGenerator(tc.cfg, tc.baseHz) if (err != nil) != tc.wantErr { t.Fatalf("error = %v, wantErr %v", err, tc.wantErr) } }) } } func TestNewSineGeneratorPreservesRationalSampleRate(t *testing.T) { cfg := testConfig(t, 1, flowdef.Rational{Numerator: 48000, Denominator: 1001}, LevelEBUDBFS) gen, err := NewSineGenerator(cfg, 1) if err != nil { t.Fatalf("NewSineGenerator: %v", err) } want := 48000.0 / 1001.0 if math.Abs(gen.sampleRate-want) > 1e-12 { t.Fatalf("sample rate = %.12f, want %.12f", gen.sampleRate, want) } } func TestSineGeneratorKnownSamplesAndChannels(t *testing.T) { cfg := testConfig(t, 2, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelSMPTEDBFS) gen, err := NewSineGenerator(cfg, 1000) if err != nil { t.Fatalf("NewSineGenerator: %v", err) } channel0 := make([]byte, 37*4) if err := gen.Generate(0, 0, channel0); err != nil { t.Fatalf("Generate channel 0: %v", err) } requireSampleNear(t, decodeSample(channel0, 0), 0) requireSampleNear(t, decodeSample(channel0, 12), 0.1) requireSampleNear(t, decodeSample(channel0, 24), 0) requireSampleNear(t, decodeSample(channel0, 36), -0.1) channel1 := make([]byte, 7*4) if err := gen.Generate(1, 0, channel1); err != nil { t.Fatalf("Generate channel 1: %v", err) } requireSampleNear(t, decodeSample(channel1, 6), 0.1) } func TestSineGeneratorUsesConfiguredBaseFrequency(t *testing.T) { cfg := testConfig(t, 1, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelEBUDBFS) gen, err := NewSineGenerator(cfg, 2000) if err != nil { t.Fatalf("NewSineGenerator: %v", err) } data := make([]byte, 7*4) if err := gen.Generate(0, 0, data); err != nil { t.Fatalf("Generate: %v", err) } wantPeak := float32(math.Pow(10, LevelEBUDBFS/20)) requireSampleNear(t, decodeSample(data, 6), wantPeak) } func TestSineGeneratorFragmentContinuity(t *testing.T) { cfg := testConfig(t, 1, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelEBUDBFS) gen, err := NewSineGenerator(cfg, 1000) if err != nil { t.Fatalf("NewSineGenerator: %v", err) } whole := make([]byte, 40*4) if err := gen.Generate(0, 100, whole); err != nil { t.Fatalf("Generate whole: %v", err) } first := make([]byte, 13*4) second := make([]byte, 27*4) if err := gen.Generate(0, 100, first, second); err != nil { t.Fatalf("Generate fragments: %v", err) } joined := append(append([]byte(nil), first...), second...) if !bytes.Equal(joined, whole) { t.Fatal("fragmented output differs from contiguous output") } } func TestSineGeneratorRejectsInvalidInputWithoutWriting(t *testing.T) { cfg := testConfig(t, 1, flowdef.Rational{Numerator: 48000, Denominator: 1}, LevelEBUDBFS) gen, err := NewSineGenerator(cfg, 1000) if err != nil { t.Fatalf("NewSineGenerator: %v", err) } first := bytes.Repeat([]byte{0xAA}, 8) before := append([]byte(nil), first...) if err := gen.Generate(0, 0, first, make([]byte, 3)); err == nil { t.Fatal("Generate accepted a misaligned fragment") } if !bytes.Equal(first, before) { t.Fatal("Generate modified data before reporting an invalid fragment") } if err := gen.Generate(1, 0, make([]byte, 4)); err == nil { t.Fatal("Generate accepted an out-of-range channel") } }