mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 02:34:42 +00:00
Refactor media buffer
Mostly cleanup.
This commit is contained in:
parent
3178086dd7
commit
b3ccea5f0e
7 changed files with 531 additions and 315 deletions
62
pkg/resampler/simple.go
Normal file
62
pkg/resampler/simple.go
Normal 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
106
pkg/resampler/speex.go
Normal 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))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue