From b3ccea5f0eea2b23854c25d04165893542ac8dad Mon Sep 17 00:00:00 2001 From: sergystepanov Date: Mon, 15 Dec 2025 15:41:51 +0300 Subject: [PATCH] Refactor media buffer Mostly cleanup. --- pkg/resampler/simple.go | 62 ++++ pkg/resampler/speex.go | 106 ++++++ .../media => resampler}/speex_resampler.h | 11 + pkg/worker/media/buffer.go | 165 ++++----- pkg/worker/media/buffer_test.go | 337 +++++++++++++++--- pkg/worker/media/media_test.go | 68 ---- pkg/worker/media/speex.go | 97 ----- 7 files changed, 531 insertions(+), 315 deletions(-) create mode 100644 pkg/resampler/simple.go create mode 100644 pkg/resampler/speex.go rename pkg/{worker/media => resampler}/speex_resampler.h (82%) delete mode 100644 pkg/worker/media/speex.go diff --git a/pkg/resampler/simple.go b/pkg/resampler/simple.go new file mode 100644 index 00000000..f2859a3e --- /dev/null +++ b/pkg/resampler/simple.go @@ -0,0 +1,62 @@ +package resampler + +func Linear(dst, src []int16) { + nSrc, nDst := len(src), len(dst) + if nSrc < 2 || nDst < 2 { + return + } + + srcPairs, dstPairs := nSrc>>1, nDst>>1 + + // replicate single pair input or output + if srcPairs == 1 || dstPairs == 1 { + for i := 0; i < dstPairs; i++ { + dst[i*2], dst[i*2+1] = src[0], src[1] + } + return + } + + ratio := ((srcPairs - 1) << 16) / (dstPairs - 1) + lastSrc := nSrc - 2 + + // interpolate all pairs except the last + for i, pos := 0, 0; i < dstPairs-1; i, pos = i+1, pos+ratio { + idx := (pos >> 16) << 1 + di := i << 1 + frac := int32(pos & 0xFFFF) + l0, r0 := int32(src[idx]), int32(src[idx+1]) + + // L = L0 + (L1-L0)*frac + dst[di] = int16(l0 + ((int32(src[idx+2])-l0)*frac)>>16) + // R = R0 + (R1-R0)*frac + dst[di+1] = int16(r0 + ((int32(src[idx+3])-r0)*frac)>>16) + } + + // last output pair = last input pair (avoids precision loss at the edge) + lastDst := (dstPairs - 1) << 1 + dst[lastDst], dst[lastDst+1] = src[lastSrc], src[lastSrc+1] +} + +func Nearest(dst, src []int16) { + nSrc, nDst := len(src), len(dst) + if nSrc < 2 || nDst < 2 { + return + } + + srcPairs, dstPairs := nSrc>>1, nDst>>1 + + if srcPairs == 1 || dstPairs == 1 { + for i := 0; i < dstPairs; i++ { + dst[i*2], dst[i*2+1] = src[0], src[1] + } + return + } + + ratio := (srcPairs << 16) / dstPairs + + for i, pos := 0, 0; i < dstPairs; i, pos = i+1, pos+ratio { + si := (pos >> 16) << 1 + di := i << 1 + dst[di], dst[di+1] = src[si], src[si+1] + } +} diff --git a/pkg/resampler/speex.go b/pkg/resampler/speex.go new file mode 100644 index 00000000..b62d2be1 --- /dev/null +++ b/pkg/resampler/speex.go @@ -0,0 +1,106 @@ +package resampler + +/* + #cgo pkg-config: speexdsp + #cgo st LDFLAGS: -l:libspeexdsp.a + + #include + #include "speex_resampler.h" +*/ +import "C" + +import ( + "errors" + "unsafe" +) + +// Quality +const ( + QualityMax = 10 + QualityMin = 0 + QualityDefault = 4 + QualityDesktop = 5 + QualityVoid = 3 +) + +// Errors +const ( + ErrorSuccess = iota + ErrorAllocFailed + ErrorBadState + ErrorInvalidArg + ErrorPtrOverlap + ErrorMaxError +) + +type Resampler struct { + resampler *C.SpeexResamplerState + channels int + inRate int + outRate int +} + +func Init(channels, inRate, outRate, quality int) (*Resampler, error) { + var err C.int + r := &Resampler{ + channels: channels, + inRate: inRate, + outRate: outRate, + } + + r.resampler = C.speex_resampler_init( + C.spx_uint32_t(channels), + C.spx_uint32_t(inRate), + C.spx_uint32_t(outRate), + C.int(quality), + &err, + ) + + if r.resampler == nil { + return nil, StrError(int(err)) + } + + C.speex_resampler_skip_zeros(r.resampler) + + return r, nil +} + +func (r *Resampler) Destroy() { + if r.resampler != nil { + C.speex_resampler_destroy(r.resampler) + r.resampler = nil + } +} + +// Process performs resampling. +// Returns written samples count and error if any. +func (r *Resampler) Process(out, in []int16) (int, error) { + if len(in) == 0 || len(out) == 0 { + return 0, nil + } + + inLen := C.spx_uint32_t(len(in) / r.channels) + outLen := C.spx_uint32_t(len(out) / r.channels) + + res := C.speex_resampler_process_interleaved_int( + r.resampler, + (*C.spx_int16_t)(unsafe.Pointer(&in[0])), + &inLen, + (*C.spx_int16_t)(unsafe.Pointer(&out[0])), + &outLen, + ) + + if res != ErrorSuccess { + return 0, StrError(int(res)) + } + + return int(outLen) * r.channels, nil +} + +func StrError(errorCode int) error { + cS := C.speex_resampler_strerror(C.int(errorCode)) + if cS == nil { + return nil + } + return errors.New(C.GoString(cS)) +} diff --git a/pkg/worker/media/speex_resampler.h b/pkg/resampler/speex_resampler.h similarity index 82% rename from pkg/worker/media/speex_resampler.h rename to pkg/resampler/speex_resampler.h index 27d510f5..9e046ed7 100644 --- a/pkg/worker/media/speex_resampler.h +++ b/pkg/resampler/speex_resampler.h @@ -41,6 +41,17 @@ SpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels, */ void speex_resampler_destroy(SpeexResamplerState *st); + +/** Make sure that the first samples to go out of the resamplers don't have + * leading zeros. This is only useful before starting to use a newly created + * resampler. It is recommended to use that when resampling an audio file, as + * it will generate a file with the same length. For real-time processing, + * it is probably easier not to use this call (so that the output duration + * is the same for the first frame). + * @param st Resampler state + */ +int speex_resampler_skip_zeros(SpeexResamplerState *st); + /** Resample an interleaved int array. The input and output buffers must *not* overlap. * @param st Resampler state * @param in Input buffer diff --git a/pkg/worker/media/buffer.go b/pkg/worker/media/buffer.go index 57adeb90..e13bb1f0 100644 --- a/pkg/worker/media/buffer.go +++ b/pkg/worker/media/buffer.go @@ -1,6 +1,10 @@ package media -import "errors" +import ( + "errors" + + "github.com/giongto35/cloud-game/v3/pkg/resampler" +) type ResampleAlgo uint8 @@ -11,14 +15,15 @@ const ( ) type buffer struct { - raw samples - scratch samples - buckets []bucket - resampler *Resampler - srcHz int - dstHz int - bi int - algo ResampleAlgo + raw samples + scratch samples + buckets []bucket + srcHz int + dstHz int + bi int + algo ResampleAlgo + + resampler *resampler.Resampler } type bucket struct { @@ -33,30 +38,31 @@ func newBuffer(frames []float32, hz int) (*buffer, error) { return nil, errors.New("invalid params") } - var totalSize int - for _, f := range frames { - totalSize += stereoSamples(hz, f) + buckets := make([]bucket, len(frames)) + var total int + for i, ms := range frames { + n := stereoSamples(hz, ms) + buckets[i] = bucket{ms: ms, dst: n} + total += n } - if totalSize == 0 { + if total == 0 { return nil, errors.New("zero buffer size") } - buf := &buffer{ - raw: make(samples, totalSize), + raw := make(samples, total) + for i, off := 0, 0; i < len(buckets); i++ { + buckets[i].mem = raw[off : off+buckets[i].dst] + off += buckets[i].dst + } + + return &buffer{ + raw: raw, scratch: make(samples, 5760), + buckets: buckets, srcHz: hz, dstHz: hz, - } - - offset := 0 - for _, f := range frames { - size := stereoSamples(hz, f) - buf.buckets = append(buf.buckets, bucket{mem: buf.raw[offset : offset+size], ms: f, dst: size}) - offset += size - } - buf.bi = len(buf.buckets) - 1 - - return buf, nil + bi: len(buckets) - 1, + }, nil } func (b *buffer) close() { @@ -66,43 +72,38 @@ func (b *buffer) close() { } } -func (b *buffer) resample(targetHz int, algo ResampleAlgo) error { - b.algo = algo - b.dstHz = targetHz - +func (b *buffer) resample(hz int, algo ResampleAlgo) error { + b.algo, b.dstHz = algo, hz for i := range b.buckets { - b.buckets[i].dst = stereoSamples(targetHz, b.buckets[i].ms) + b.buckets[i].dst = stereoSamples(hz, b.buckets[i].ms) } - if algo == ResampleSpeex { var err error - if b.resampler, err = ResamplerInit(2, b.srcHz, targetHz, QualityMax); err != nil { - return err - } + b.resampler, err = resampler.Init(2, b.srcHz, hz, resampler.QualityMax) + return err } return nil } func (b *buffer) write(s samples, onFull func(samples, float32)) int { - read := 0 - for read < len(s) { + n := len(s) + for i := 0; i < n; { cur := &b.buckets[b.bi] - n := copy(cur.mem[cur.p:], s[read:]) - read += n - cur.p += n - + c := copy(cur.mem[cur.p:], s[i:]) + i += c + cur.p += c if cur.p == len(cur.mem) { onFull(b.stretch(cur.mem, cur.dst), cur.ms) - b.choose(len(s) - read) + b.choose(n - i) b.buckets[b.bi].p = 0 } } - return read + return n } -func (b *buffer) choose(remaining int) { +func (b *buffer) choose(rem int) { for i := len(b.buckets) - 1; i >= 0; i-- { - if remaining >= len(b.buckets[i].mem) { + if rem >= len(b.buckets[i].mem) { b.bi = i return } @@ -110,65 +111,29 @@ func (b *buffer) choose(remaining int) { b.bi = 0 } -func (b *buffer) stretch(src samples, dstSize int) samples { - switch b.algo { - case ResampleSpeex: - if b.resampler != nil { - if _, out, err := b.resampler.ProcessIntInterleaved(src); err == nil { - if len(out) == dstSize { - return out - } - src = out // use speex output for linear correction +func (b *buffer) stretch(src samples, size int) samples { + if len(src) == size { + return src + } + + if cap(b.scratch) < size { + b.scratch = make(samples, size) + } + out := b.scratch[:size] + + if b.algo == ResampleSpeex && b.resampler != nil { + if n, _ := b.resampler.Process(out, src); n > 0 { + for i := n; i < size; i += 2 { + out[i], out[i+1] = out[n-2], out[n-1] } - } - fallthrough - case ResampleLinear: - return b.linear(src, dstSize) - case ResampleNearest: - return b.nearest(src, dstSize) - default: - return b.linear(src, dstSize) - } -} - -func (b *buffer) linear(src samples, dstSize int) samples { - srcLen := len(src) - if srcLen < 2 || dstSize < 2 { - return b.scratch[:dstSize] - } - - out := b.scratch[:dstSize] - srcPairs, dstPairs := srcLen/2, dstSize/2 - ratio := ((srcPairs - 1) << 16) / (dstPairs - 1) - - for i := 0; i < dstPairs; i++ { - pos := i * ratio - idx, frac := (pos>>16)*2, pos&0xFFFF - di := i * 2 - - if idx >= srcLen-2 { - out[di], out[di+1] = src[srcLen-2], src[srcLen-1] - } else { - out[di] = int16(int32(src[idx]) + ((int32(src[idx+2])-int32(src[idx]))*int32(frac))>>16) - out[di+1] = int16(int32(src[idx+1]) + ((int32(src[idx+3])-int32(src[idx+1]))*int32(frac))>>16) + return out } } - return out -} -func (b *buffer) nearest(src samples, dstSize int) samples { - srcLen := len(src) - if srcLen < 2 || dstSize < 2 { - return b.scratch[:dstSize] - } - - out := b.scratch[:dstSize] - srcPairs, dstPairs := srcLen/2, dstSize/2 - - for i := 0; i < dstPairs; i++ { - si := (i * srcPairs / dstPairs) * 2 - di := i * 2 - out[di], out[di+1] = src[si], src[si+1] + if b.algo == ResampleNearest { + resampler.Nearest(out, src) + } else { + resampler.Linear(out, src) } return out } diff --git a/pkg/worker/media/buffer_test.go b/pkg/worker/media/buffer_test.go index 28a596ba..a2be89d8 100644 --- a/pkg/worker/media/buffer_test.go +++ b/pkg/worker/media/buffer_test.go @@ -3,79 +3,316 @@ package media import ( "reflect" "testing" + + "github.com/giongto35/cloud-game/v3/pkg/resampler" ) -type bufWrite struct { - sample int16 - len int +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 := 0; i < pairs; i++ { + 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 { - bufLen int - writes []bufWrite - expect samples + name string + writes []struct { + v int16 + n int + } + want samples }{ { - bufLen: 2000, - writes: []bufWrite{ - {sample: 1, len: 10}, - {sample: 2, len: 20}, - {sample: 3, len: 30}, - }, - expect: 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, + 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, }, }, { - bufLen: 2000, - writes: []bufWrite{ - {sample: 1, len: 3}, - {sample: 2, len: 18}, - {sample: 3, len: 2}, - }, - expect: samples{1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}, + 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 _, test := range tests { - var lastResult samples - buf, err := newBuffer([]float32{10, 5}, test.bufLen) - if err != nil { - t.Fatalf("oof, %v", err) + 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) } - for _, w := range test.writes { - buf.write(samplesOf(w.sample, w.len), - func(s samples, ms float32) { lastResult = s }, - ) - } - if !reflect.DeepEqual(test.expect, lastResult) { - t.Errorf("not expted buffer, %v != %v, %v", lastResult, test.expect, len(buf.buckets)) + }) + 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 BenchmarkBufferWrite(b *testing.B) { - fn := func(_ samples, _ float32) {} - l := 2000 - buf, err := newBuffer([]float32{10}, l) - if err != nil { - b.Fatalf("oof: %v", err) +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}, } - samples1 := samplesOf(1, l/2) - samples2 := samplesOf(2, l*2) - for i := 0; i < b.N; i++ { - buf.write(samples1, fn) - buf.write(samples2, fn) + 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 samplesOf(v int16, len int) (s samples) { - s = make(samples, len) - for i := range s { - s[i] = v +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") } - return +} + +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) + } + }) } diff --git a/pkg/worker/media/media_test.go b/pkg/worker/media/media_test.go index 10152bf5..3e264e80 100644 --- a/pkg/worker/media/media_test.go +++ b/pkg/worker/media/media_test.go @@ -110,71 +110,3 @@ func genTestImage(w, h int, seed float32) *image.RGBA { } return img } - -func TestResampleStretch(t *testing.T) { - type args struct { - pcm samples - size int - } - tests := []struct { - name string - args args - want []int16 - }{ - //1764:1920 - {name: "", args: args{pcm: gen(1764), size: 1920}, want: nil}, - } - for _, tt := range tests { - buf, _ := newBuffer([]float32{20}, 2000) - t.Run(tt.name, func(t *testing.T) { - rez2 := buf.nearest(tt.args.pcm, tt.args.size) - if rez2[0] != tt.args.pcm[0] || rez2[1] != tt.args.pcm[1] || - rez2[len(rez2)-1] != tt.args.pcm[len(tt.args.pcm)-1] || - rez2[len(rez2)-2] != tt.args.pcm[len(tt.args.pcm)-2] { - t.Logf("%v\n%v", tt.args.pcm, rez2) - t.Errorf("2nd is wrong (2)") - } - }) - } -} - -func BenchmarkResampler(b *testing.B) { - pcm := samples(gen(1764)) - size := 1920 - buf, _ := newBuffer([]float32{20}, 1000) - for i := 0; i < b.N; i++ { - buf.linear(pcm, size) - } -} - -func gen(l int) []int16 { - nums := make([]int16, l) - for i := range nums { - nums[i] = int16(rand.IntN(10)) - } - return nums -} - -func TestFrame(t *testing.T) { - type args struct { - hz int - frame float32 - } - tests := []struct { - name string - args args - want int - }{ - {name: "mGBA", args: args{hz: 32768, frame: 10}, want: 656}, - {name: "mGBA", args: args{hz: 32768, frame: 5}, want: 328}, - {name: "mGBA", args: args{hz: 32768, frame: 2.5}, want: 164}, - {name: "nes", args: args{hz: 48000, frame: 2.5}, want: 240}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := stereoSamples(tt.args.hz, tt.args.frame); got != tt.want { - t.Errorf("frame() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/worker/media/speex.go b/pkg/worker/media/speex.go deleted file mode 100644 index a306fd2b..00000000 --- a/pkg/worker/media/speex.go +++ /dev/null @@ -1,97 +0,0 @@ -package media - -/* - #cgo pkg-config: speexdsp - #cgo st LDFLAGS: -l:libspeexdsp.a - - #include - #include "speex_resampler.h" -*/ -import "C" - -import "errors" - -type Resampler struct { - resampler *C.SpeexResamplerState - outBuff []int16 // one of these buffers used when typed data read - outBuffFloat []float32 - channels int - multiplier float32 -} - -// Quality -const ( - QualityMax = 10 - QualityMin = 0 - QualityDefault = 4 - QualityDesktop = 5 - QualityVoid = 3 -) - -// Errors -const ( - ErrorSuccess = iota - ErrorAllocFailed - ErrorBadState - ErrorInvalidArg - ErrorPtrOverlap - ErrorMaxError -) - -const ( - reserve = 1.1 -) - -// ResamplerInit Create a new resampler with integer input and output rates -// Resampling quality between 0 and 10, where 0 has poor quality -// and 10 has very high quality -func ResamplerInit(channels, inRate, outRate, quality int) (*Resampler, error) { - err := C.int(0) - r := &Resampler{channels: channels} - r.multiplier = float32(outRate) / float32(inRate) * 1.1 - r.resampler = C.speex_resampler_init(C.spx_uint32_t(channels), - C.spx_uint32_t(inRate), C.spx_uint32_t(outRate), C.int(quality), &err) - if r.resampler == nil { - return nil, StrError(int(err)) - } - return r, nil -} - -// Destroy a resampler -func (r *Resampler) Destroy() error { - if r.resampler != nil { - C.speex_resampler_destroy((*C.SpeexResamplerState)(r.resampler)) - return nil - } - return StrError(ErrorInvalidArg) -} - -// ProcessIntInterleaved Resample an int slice interleaved -func (r *Resampler) ProcessIntInterleaved(in []int16) (int, []int16, error) { - outBuffCap := int(float32(len(in)) * r.multiplier) - if outBuffCap > cap(r.outBuff) { - r.outBuff = make([]int16, int(float32(outBuffCap)*reserve)*4) - } - inLen := C.spx_uint32_t(len(in) / r.channels) - outLen := C.spx_uint32_t(len(r.outBuff) / r.channels) - res := C.speex_resampler_process_interleaved_int( - r.resampler, - (*C.spx_int16_t)(&in[0]), - &inLen, - (*C.spx_int16_t)(&r.outBuff[0]), - &outLen, - ) - if res != ErrorSuccess { - return 0, nil, StrError(ErrorInvalidArg) - } - return int(inLen) * r.channels, r.outBuff[:outLen*2], nil -} - -// StrError returns error message -func StrError(errorCode int) error { - cS := C.speex_resampler_strerror(C.int(errorCode)) - if cS == nil { - return nil - } - return errors.New(C.GoString(cS)) -}