156 lines
5.4 KiB
Go
156 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|