Refactor media buffer

Mostly cleanup.
This commit is contained in:
sergystepanov 2025-12-15 15:41:51 +03:00
parent 3178086dd7
commit b3ccea5f0e
7 changed files with 531 additions and 315 deletions

62
pkg/resampler/simple.go Normal file
View file

@ -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]
}
}

106
pkg/resampler/speex.go Normal file
View file

@ -0,0 +1,106 @@
package resampler
/*
#cgo pkg-config: speexdsp
#cgo st LDFLAGS: -l:libspeexdsp.a
#include <stdint.h>
#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))
}

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -1,97 +0,0 @@
package media
/*
#cgo pkg-config: speexdsp
#cgo st LDFLAGS: -l:libspeexdsp.a
#include <stdint.h>
#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))
}