|
|
|
@@ -0,0 +1,142 @@ |
|
|
|
package dsp |
|
|
|
|
|
|
|
import ( |
|
|
|
"testing" |
|
|
|
) |
|
|
|
|
|
|
|
func TestDecimateStateful_ContinuousPhase(t *testing.T) { |
|
|
|
// Simulate what happens in the streaming pipeline: |
|
|
|
// Two consecutive frames with non-divisible lengths decimated by 3. |
|
|
|
// Stateful version must produce the same output as decimating the |
|
|
|
// concatenated input in one go. |
|
|
|
|
|
|
|
factor := 3 |
|
|
|
// Frame lengths that don't divide evenly (like real WFM: 41666 % 3 = 2) |
|
|
|
frame1 := make([]complex64, 41666) |
|
|
|
frame2 := make([]complex64, 41666) |
|
|
|
for i := range frame1 { |
|
|
|
frame1[i] = complex(float32(i), 0) |
|
|
|
} |
|
|
|
for i := range frame2 { |
|
|
|
frame2[i] = complex(float32(len(frame1)+i), 0) |
|
|
|
} |
|
|
|
|
|
|
|
// Reference: concatenate and decimate in one shot |
|
|
|
combined := make([]complex64, len(frame1)+len(frame2)) |
|
|
|
copy(combined, frame1) |
|
|
|
copy(combined[len(frame1):], frame2) |
|
|
|
ref := Decimate(combined, factor) |
|
|
|
|
|
|
|
// Stateful: decimate frame by frame |
|
|
|
phase := 0 |
|
|
|
out1 := DecimateStateful(frame1, factor, &phase) |
|
|
|
out2 := DecimateStateful(frame2, factor, &phase) |
|
|
|
|
|
|
|
got := make([]complex64, len(out1)+len(out2)) |
|
|
|
copy(got, out1) |
|
|
|
copy(got[len(out1):], out2) |
|
|
|
|
|
|
|
if len(got) != len(ref) { |
|
|
|
t.Fatalf("length mismatch: stateful=%d reference=%d", len(got), len(ref)) |
|
|
|
} |
|
|
|
for i := range ref { |
|
|
|
if got[i] != ref[i] { |
|
|
|
t.Fatalf("sample %d: got %v want %v", i, got[i], ref[i]) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func TestDecimateStateful_Factor4_NFM(t *testing.T) { |
|
|
|
// NFM scenario: 200kHz/48kHz → decim=4, frame=16666 samples |
|
|
|
// 16666 % 4 = 2 → phase slip every frame with stateless decimation |
|
|
|
factor := 4 |
|
|
|
frameLen := 16666 |
|
|
|
nFrames := 10 |
|
|
|
|
|
|
|
// Build continuous signal |
|
|
|
total := make([]complex64, frameLen*nFrames) |
|
|
|
for i := range total { |
|
|
|
total[i] = complex(float32(i), float32(-i)) |
|
|
|
} |
|
|
|
ref := Decimate(total, factor) |
|
|
|
|
|
|
|
// Stateful frame-by-frame |
|
|
|
phase := 0 |
|
|
|
var got []complex64 |
|
|
|
for f := 0; f < nFrames; f++ { |
|
|
|
chunk := total[f*frameLen : (f+1)*frameLen] |
|
|
|
out := DecimateStateful(chunk, factor, &phase) |
|
|
|
got = append(got, out...) |
|
|
|
} |
|
|
|
|
|
|
|
if len(got) != len(ref) { |
|
|
|
t.Fatalf("length mismatch: stateful=%d reference=%d (frames=%d)", len(got), len(ref), nFrames) |
|
|
|
} |
|
|
|
for i := range ref { |
|
|
|
if got[i] != ref[i] { |
|
|
|
t.Fatalf("frame-boundary glitch at sample %d: got %v want %v", i, got[i], ref[i]) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func TestDecimateStateful_Factor1_Passthrough(t *testing.T) { |
|
|
|
in := []complex64{1 + 2i, 3 + 4i, 5 + 6i} |
|
|
|
phase := 0 |
|
|
|
out := DecimateStateful(in, 1, &phase) |
|
|
|
if len(out) != len(in) { |
|
|
|
t.Fatalf("passthrough: got len %d want %d", len(out), len(in)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func TestDecimateStateful_ExactDivisible(t *testing.T) { |
|
|
|
// When frame length is exactly divisible, phase should stay 0 |
|
|
|
factor := 4 |
|
|
|
frame := make([]complex64, 100) // 100 % 4 = 0 |
|
|
|
for i := range frame { |
|
|
|
frame[i] = complex(float32(i), 0) |
|
|
|
} |
|
|
|
phase := 0 |
|
|
|
out := DecimateStateful(frame, factor, &phase) |
|
|
|
if phase != 0 { |
|
|
|
t.Fatalf("exact divisible: phase should be 0, got %d", phase) |
|
|
|
} |
|
|
|
if len(out) != 25 { |
|
|
|
t.Fatalf("exact divisible: got %d samples, want 25", len(out)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func TestDecimateStateful_VaryingFrameSizes(t *testing.T) { |
|
|
|
// Real-world: buffer jitter causes varying frame sizes |
|
|
|
factor := 3 |
|
|
|
frameSizes := []int{41600, 41700, 41666, 41650, 41680} |
|
|
|
|
|
|
|
// Build total |
|
|
|
totalLen := 0 |
|
|
|
for _, s := range frameSizes { |
|
|
|
totalLen += s |
|
|
|
} |
|
|
|
total := make([]complex64, totalLen) |
|
|
|
for i := range total { |
|
|
|
total[i] = complex(float32(i), float32(i*2)) |
|
|
|
} |
|
|
|
ref := Decimate(total, factor) |
|
|
|
|
|
|
|
phase := 0 |
|
|
|
var got []complex64 |
|
|
|
offset := 0 |
|
|
|
for _, sz := range frameSizes { |
|
|
|
chunk := total[offset : offset+sz] |
|
|
|
out := DecimateStateful(chunk, factor, &phase) |
|
|
|
got = append(got, out...) |
|
|
|
offset += sz |
|
|
|
} |
|
|
|
|
|
|
|
if len(got) != len(ref) { |
|
|
|
t.Fatalf("varying frames: stateful=%d reference=%d", len(got), len(ref)) |
|
|
|
} |
|
|
|
for i := range ref { |
|
|
|
if got[i] != ref[i] { |
|
|
|
t.Fatalf("varying frames: mismatch at %d: got %v want %v", i, got[i], ref[i]) |
|
|
|
} |
|
|
|
} |
|
|
|
} |