cloud-game/pkg/worker/media/buffer_test.go
2025-12-22 15:08:50 +03:00

318 lines
6.7 KiB
Go

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)
}
})
}