Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more tests and benchmarks #130

Merged
merged 9 commits into from
Nov 9, 2023
Merged
53 changes: 53 additions & 0 deletions ctrl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package beep_test

import (
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"

"github.com/gopxl/beep"
"github.com/gopxl/beep/internal/testtools"
)

func TestCtrl_CanBePausedAndUnpaused(t *testing.T) {
s, data := testtools.RandomDataStreamer(20)

ctrl := beep.Ctrl{
Streamer: s,
Paused: false,
}

got := testtools.CollectNum(10, &ctrl)
assert.Equal(t, data[:10], got)

ctrl.Paused = true
got = testtools.CollectNum(10, &ctrl)
assert.Equal(t, make([][2]float64, 10), got)

ctrl.Paused = false
got = testtools.CollectNum(10, &ctrl)
assert.Equal(t, data[10:20], got)
}

func TestCtrl_DoesNotStreamFromNilStreamer(t *testing.T) {
ctrl := beep.Ctrl{
Streamer: nil,
Paused: false,
}

buf := make([][2]float64, 10)
n, ok := ctrl.Stream(buf)
assert.Equal(t, 0, n)
assert.False(t, ok)
}

func TestCtrl_PropagatesErrors(t *testing.T) {
ctrl := beep.Ctrl{}

assert.NoError(t, ctrl.Err())

err := errors.New("oh no")
ctrl.Streamer = testtools.ErrorStreamer{Error: err}
assert.Equal(t, err, ctrl.Err())
}
24 changes: 4 additions & 20 deletions flac/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,14 @@ import (
"github.com/gopxl/beep/internal/testtools"
)

func TestDecoder_Stream(t *testing.T) {
func TestDecoder_ReturnBehaviour(t *testing.T) {
f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.flac"))
assert.NoError(t, err)
defer f.Close()

streamer, _, err := flac.Decode(f)
s, _, err := flac.Decode(f)
assert.NoError(t, err)
assert.Equal(t, 22050, s.Len())

// Case 1: return ok with all requested samples
buf := testtools.CollectNum(22000, streamer)
assert.Lenf(t, buf, 22000, "streamer quit prematurely; expected %d samples, got %d", 22000, len(buf))
assert.NoError(t, streamer.Err())

buf = make([][2]float64, 512)

// Case 2: return ok with 0 < n < 512 samples
n, ok := streamer.Stream(buf[:])
assert.True(t, ok)
assert.Equal(t, 50, n)
assert.NoError(t, streamer.Err())

// Case 3: return !ok with n == 0
n, ok = streamer.Stream(buf[:])
assert.False(t, ok)
assert.Equal(t, 0, n)
assert.NoError(t, streamer.Err())
testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len())
}
36 changes: 36 additions & 0 deletions generators/sine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package generators_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep"
"github.com/gopxl/beep/generators"
"github.com/gopxl/beep/internal/testtools"
)

func TestSineTone(t *testing.T) {
epsilon := 0.000001

s, err := generators.SineTone(beep.SampleRate(8000), 400)
assert.NoError(t, err)

// Get a full single phase including the last sample.
phaseLength := 8000 / 400
samples := testtools.CollectNum(phaseLength+1, s)

// The sine wave should be 0 at the start, half a phase and at the end of the phase.
assert.InDelta(t, 0, samples[phaseLength*0][0], epsilon)
assert.InDelta(t, 0, samples[phaseLength*0][1], epsilon)
assert.InDelta(t, 0, samples[phaseLength*1/2][0], epsilon)
assert.InDelta(t, 0, samples[phaseLength*1/2][1], epsilon)
assert.InDelta(t, 0, samples[phaseLength*1][0], epsilon)
assert.InDelta(t, 0, samples[phaseLength*1][1], epsilon)

// The sine wave should be in a peak and trough at 1/4th and 3/4th in the phase respectively.
assert.InDelta(t, 1, samples[phaseLength*1/4][0], epsilon)
assert.InDelta(t, 1, samples[phaseLength*1/4][1], epsilon)
assert.InDelta(t, -1, samples[phaseLength*3/4][0], epsilon)
assert.InDelta(t, -1, samples[phaseLength*3/4][1], epsilon)
}
Binary file not shown.
Binary file added internal/testdata/valid_44100hz_22050_samples.wav
Binary file not shown.
Binary file not shown.
59 changes: 59 additions & 0 deletions internal/testtools/asserts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package testtools

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep"
)

// AssertStreamerHasCorrectReturnBehaviour tests whether the return values returned
// by the streamer s adhere to the description on the Streamer interface.
func AssertStreamerHasCorrectReturnBehaviour(t *testing.T, s beep.Streamer, expectedSamples int) {
const leaveUnreadInFirstCase = 50

if expectedSamples < leaveUnreadInFirstCase+1 {
panic(fmt.Sprintf("AssertStreamerHasCorrectReturnBehaviour must be called with at least %d samples.", leaveUnreadInFirstCase+1))
}

// 1. n == len(samples) && ok
buf := make([][2]float64, 512)
samplesLeft := expectedSamples - leaveUnreadInFirstCase
for samplesLeft > 0 {
toRead := len(buf)
if toRead > samplesLeft {
toRead = samplesLeft
}
n, ok := s.Stream(buf[:toRead])
if !ok {
t.Fatalf("streamer returned !ok before it was expected to be drained")
}
if n < toRead {
t.Fatalf("streamer didn't return all requested samples before it was expected to be drained")
}
if s.Err() != nil {
t.Fatalf("unexpected error in streamer: %v", s.Err())
}
samplesLeft -= n
}

// 2. 0 < n && n < len(samples) && ok
n, ok := s.Stream(buf)
assert.True(t, ok)
assert.Equal(t, leaveUnreadInFirstCase, n)
assert.NoError(t, s.Err())

// 3. n == 0 && !ok
n, ok = s.Stream(buf)
assert.False(t, ok)
assert.Equal(t, 0, n)
assert.NoError(t, s.Err())

// Repeat calls after case 3 must return the same result.
n, ok = s.Stream(buf)
assert.False(t, ok)
assert.Equal(t, 0, n)
assert.NoError(t, s.Err())
}
12 changes: 12 additions & 0 deletions internal/testtools/streamers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,15 @@ func (ds *dataStreamer) Seek(p int) error {
ds.pos = p
return nil
}

type ErrorStreamer struct {
Error error
}

func (e ErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) {
return 0, false
}

func (e ErrorStreamer) Err() error {
return e.Error
}
135 changes: 135 additions & 0 deletions mixer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package beep_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep"
"github.com/gopxl/beep/internal/testtools"
)

func TestMixer_MixesSamples(t *testing.T) {
epsilon := 0.000001

s1, data1 := testtools.RandomDataStreamer(200)
s2, data2 := testtools.RandomDataStreamer(200)

m := beep.Mixer{}
m.Add(s1)
m.Add(s2)

samples := testtools.CollectNum(100, &m)
for i, s := range samples {
wantL := data1[i][0] + data2[i][0]
wantR := data1[i][1] + data2[i][1]

if s[0] < wantL-epsilon || s[0] > wantL+epsilon {
t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantL, s[0])
}
if s[1] < wantR-epsilon || s[1] > wantR+epsilon {
t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantR, s[1])
}
}

s3, data3 := testtools.RandomDataStreamer(100)
m.Add(s3)

samples = testtools.CollectNum(100, &m)
for i, s := range samples {
wantL := data1[100+i][0] + data2[100+i][0] + data3[i][0]
wantR := data1[100+i][1] + data2[100+i][1] + data3[i][1]

if s[0] < wantL-epsilon || s[0] > wantL+epsilon {
t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantL, s[0])
}
if s[1] < wantR-epsilon || s[1] > wantR+epsilon {
t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantR, s[1])
}
}
}

func TestMixer_DrainedStreamersAreRemoved(t *testing.T) {
s1, _ := testtools.RandomDataStreamer(50)
s2, _ := testtools.RandomDataStreamer(60)

m := beep.Mixer{}
m.Add(s1)
m.Add(s2)

// Drain s1 but not so far it returns false.
samples := testtools.CollectNum(50, &m)
assert.Len(t, samples, 50)
assert.Equal(t, 2, m.Len())

// Fully drain s1.
// Drain s2 but not so far it returns false.
samples = testtools.CollectNum(10, &m)
assert.Len(t, samples, 10)
assert.Equal(t, 1, m.Len())

// Fully drain s2.
samples = testtools.CollectNum(10, &m)
assert.Len(t, samples, 10)
assert.Equal(t, 0, m.Len())
}

func TestMixer_PlaysSilenceWhenNoStreamersProduceSamples(t *testing.T) {
m := beep.Mixer{}

// Test silence before streamers are added.
samples := testtools.CollectNum(10, &m)
assert.Len(t, samples, 10)
assert.Equal(t, make([][2]float64, 10), samples)

// Test silence after streamer is partly drained.
s, _ := testtools.RandomDataStreamer(50)
m.Add(s)

samples = testtools.CollectNum(100, &m)
assert.Len(t, samples, 100)
assert.Equal(t, 1, m.Len())
assert.Equal(t, make([][2]float64, 50), samples[50:])

// Test silence when streamer is fully drained.
samples = testtools.CollectNum(10, &m)
assert.Len(t, samples, 10)
assert.Equal(t, 0, m.Len())
assert.Equal(t, make([][2]float64, 10), samples)

// Test silence after streamer was fully drained.
samples = testtools.CollectNum(10, &m)
assert.Len(t, samples, 10)
assert.Equal(t, make([][2]float64, 10), samples)
}

func BenchmarkMixer_MultipleStreams(b *testing.B) {
s1, _ := testtools.RandomDataStreamer(b.N)
s2, _ := testtools.RandomDataStreamer(b.N)

m := beep.Mixer{}
m.Add(s1)
m.Add(s2)

b.StartTimer()

testtools.CollectNum(b.N, &m)
}

func BenchmarkMixer_OneStream(b *testing.B) {
s, _ := testtools.RandomDataStreamer(b.N)

m := beep.Mixer{}
m.Add(s)

b.StartTimer()
testtools.CollectNum(b.N, &m)
}

func BenchmarkMixer_Silence(b *testing.B) {
m := beep.Mixer{}
// Don't add any streamers

b.StartTimer()
testtools.CollectNum(b.N, &m)
}
25 changes: 25 additions & 0 deletions mp3/decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mp3_test

import (
"os"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gopxl/beep/internal/testtools"
"github.com/gopxl/beep/mp3"
)

func TestDecoder_ReturnBehaviour(t *testing.T) {
f, err := os.Open(testtools.TestFilePath("valid_44100hz_x_padded_samples.mp3"))
assert.NoError(t, err)
defer f.Close()

s, _, err := mp3.Decode(f)
assert.NoError(t, err)
// The length of the streamer isn't tested because mp3 files have
// a different padding depending on the decoder used.
// https://superuser.com/a/1393775

testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len())
}
8 changes: 5 additions & 3 deletions speaker/speaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
package speaker

import (
"github.com/ebitengine/oto/v3"
"github.com/gopxl/beep"
"github.com/pkg/errors"
"io"
"sync"

"github.com/ebitengine/oto/v3"
"github.com/pkg/errors"

"github.com/gopxl/beep"
)

const channelCount = 2
Expand Down
Loading