package media import ( "reflect" "testing" "github.com/giongto35/cloud-game/v3/pkg/resampler" ) func mustBuffer(t *testing.T, frames []float32, hz int) *buffer { t.Helper() buf, err := newBuffer(frames, hz) if err != nil { t.Fatalf("failed to create buffer: %v", err) } return buf } func samplesOf(v int16, n int) samples { s := make(samples, n) for i := range s { s[i] = v } return s } func ramp(pairs int) samples { s := make(samples, pairs*2) for i := range pairs { s[i*2], s[i*2+1] = int16(i), int16(i) } return s } func TestNewBuffer(t *testing.T) { tests := []struct { name string frames []float32 hz int wantErr bool }{ {"valid single", []float32{10}, 48000, false}, {"valid multi", []float32{10, 20}, 48000, false}, {"hz too low", []float32{10}, 1999, true}, {"empty frames", []float32{}, 48000, true}, {"nil frames", nil, 48000, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf, err := newBuffer(tt.frames, tt.hz) if (err != nil) != tt.wantErr { t.Errorf("err = %v, wantErr %v", err, tt.wantErr) } if buf != nil { buf.close() } }) } } func TestBufferBucketSizes(t *testing.T) { buf := mustBuffer(t, []float32{10, 20}, 48000) defer buf.close() if len(buf.buckets) != 2 { t.Fatalf("got %d buckets, want 2", len(buf.buckets)) } if n := len(buf.buckets[0].mem); n != 960 { t.Errorf("bucket[0] = %d, want 960", n) } if n := len(buf.buckets[1].mem); n != 1920 { t.Errorf("bucket[1] = %d, want 1920", n) } } func TestBufferClose(t *testing.T) { buf := mustBuffer(t, []float32{10}, 48000) buf.close() buf.close() // idempotent if buf.resampler != nil { t.Error("resampler should be nil after close") } } func TestBufferWrite(t *testing.T) { tests := []struct { name string writes []struct { v int16 n int } want samples }{ { name: "overflow triggers callback", writes: []struct { v int16 n int }{{1, 10}, {2, 20}, {3, 30}}, want: samples{ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, }, }, { name: "partial fill", writes: []struct { v int16 n int }{{1, 3}, {2, 18}, {3, 2}}, want: samples{1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := mustBuffer(t, []float32{10, 5}, 2000) defer buf.close() var got samples for _, w := range tt.writes { buf.write(samplesOf(w.v, w.n), func(s samples, _ float32) { got = s }) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("\ngot: %v\nwant: %v", got, tt.want) } }) } } func TestBufferWriteExact(t *testing.T) { buf := mustBuffer(t, []float32{10}, 2000) // 40 samples defer buf.close() calls := 0 buf.write(samplesOf(1, 40), func(_ samples, ms float32) { calls++ if ms != 10 { t.Errorf("ms = %v, want 10", ms) } }) if calls != 1 { t.Errorf("calls = %d, want 1", calls) } } func TestBufferWriteReturn(t *testing.T) { buf := mustBuffer(t, []float32{10}, 2000) defer buf.close() if n := buf.write(samplesOf(1, 100), func(samples, float32) {}); n != 100 { t.Errorf("return = %d, want 100", n) } } func TestBufferChoose(t *testing.T) { buf := mustBuffer(t, []float32{20, 10, 5}, 48000) // 1920, 960, 480 defer buf.close() tests := []struct{ rem, want int }{ {10000, 2}, {500, 2}, {479, 0}, {0, 0}, } for _, tt := range tests { buf.choose(tt.rem) if buf.bi != tt.want { t.Errorf("choose(%d) = %d, want %d", tt.rem, buf.bi, tt.want) } } } func TestStereoSamples(t *testing.T) { tests := []struct { hz int ms float32 want int }{ {16000, 5, 160}, {32768, 10, 656}, {32768, 2.5, 164}, {32768, 5, 328}, {44100, 10, 882}, {48000, 10, 960}, {48000, 2.5, 240}, } for _, tt := range tests { if got := stereoSamples(tt.hz, tt.ms); got != tt.want { t.Errorf("stereoSamples(%d, %.0f) = %d, want %d", tt.hz, tt.ms, got, tt.want) } } } func TestStretchPassthrough(t *testing.T) { buf := mustBuffer(t, []float32{10}, 48000) defer buf.close() src := samples{1, 2, 3, 4} if res := buf.stretch(src, 4); &res[0] != &src[0] { t.Error("expected zero-copy when sizes match") } } func TestLinear(t *testing.T) { t.Run("interpolation", func(t *testing.T) { out := make(samples, 8) resampler.Linear(out, samples{0, 0, 100, 100}) if out[2] <= 0 || out[2] >= 100 { t.Errorf("middle value %d not interpolated", out[2]) } }) t.Run("sizes", func(t *testing.T) { cases := []struct{ srcPairs, dstSize int }{ {4, 16}, {8, 8}, {4, 8}, } for _, tc := range cases { out := make(samples, tc.dstSize) resampler.Linear(out, ramp(tc.srcPairs)) if len(out) != tc.dstSize { t.Errorf("len = %d, want %d", len(out), tc.dstSize) } } }) } func TestNearest(t *testing.T) { tests := []struct { src samples want samples }{ {samples{10, 20, 30, 40}, samples{10, 20, 10, 20, 30, 40, 30, 40}}, {samples{10, 20, 30, 40, 50, 60, 70, 80}, samples{10, 20, 50, 60}}, } for _, tt := range tests { out := make(samples, len(tt.want)) resampler.Nearest(out, tt.src) if !reflect.DeepEqual(out, tt.want) { t.Errorf("nearest(%v) = %v, want %v", tt.src, out, tt.want) } } } func TestSpeex(t *testing.T) { buf := mustBuffer(t, []float32{10}, 48000) defer buf.close() if err := buf.resample(24000, ResampleSpeex); err != nil { t.Fatal(err) } t.Run("stretch", func(t *testing.T) { res := buf.stretch(samplesOf(1000, 960), 480) if len(res) != 480 { t.Errorf("len = %d, want 480", len(res)) } for _, s := range res { if s != 0 { return } } t.Error("output is silent") }) t.Run("write", func(t *testing.T) { calls := 0 buf.write(samplesOf(5000, 960), func(s samples, ms float32) { calls++ if len(s) != 480 { t.Errorf("len = %d, want 480", len(s)) } if ms != 10 { t.Errorf("ms = %v, want 10", ms) } }) if calls != 1 { t.Errorf("calls = %d, want 1", calls) } }) } func BenchmarkStretch(b *testing.B) { src := samplesOf(1000, 1920) // 20ms @ 48kHz b.Run("speex", func(b *testing.B) { buf, _ := newBuffer([]float32{20}, 48000) defer buf.close() _ = buf.resample(24000, ResampleSpeex) b.ResetTimer() for i := 0; i < b.N; i++ { buf.stretch(src, 960) } }) b.Run("linear", func(b *testing.B) { buf, _ := newBuffer([]float32{20}, 48000) defer buf.close() _ = buf.resample(24000, ResampleLinear) b.ResetTimer() for i := 0; i < b.N; i++ { buf.stretch(src, 960) } }) b.Run("nearest", func(b *testing.B) { buf, _ := newBuffer([]float32{20}, 48000) defer buf.close() _ = buf.resample(24000, ResampleNearest) b.ResetTimer() for i := 0; i < b.N; i++ { buf.stretch(src, 960) } }) }