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 tests for WAV and Buffer encoding/decoding #165

Merged
merged 2 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,161 @@ import (
"math/rand"
"testing"

"github.com/stretchr/testify/assert"

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

type bufferFormatTestCase struct {
Name string
Precision int
NumChannels int
Signed bool
Bytes []byte
Samples [2]float64
SkipDecodeTest bool
}

var bufferFormatTests = []bufferFormatTestCase{
// See https://gist.github.com/endolith/e8597a58bcd11a6462f33fa8eb75c43d
// for an explanation about the asymmetry in sample encodings in WAV when
// converting between ints and floats. Note that Beep does not follow the
// suggested solution. Instead, integer samples are divided by 1 more, so
// that the resulting float value falls within the range of -1.0 and 1.0.
// This is similar to how some other tools do the conversion.
{
Name: "1 channel 8bit WAV negative full scale",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0x00},
Samples: [2]float64{-1.0, -1.0},
},
{
Name: "1 channel 8bit WAV midpoint",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0x80},
Samples: [2]float64{0.0, 0.0},
},
{
// Because the WAV integer range is asymmetric, converting it to float
// by division will not result in an exactly 1.0 full scale float value.
// It will be 1 least significant bit integer value lower. "1", converted
// to float for an 8-bit WAV sample is 1 / (1 << 7).
Name: "1 channel 8bit WAV positive full scale minus 1 significant bit",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0xFF},
Samples: [2]float64{1.0 - (1.0 / (1 << 7)), 1.0 - (1.0 / (1 << 7))},
},
{
Name: "2 channel 8bit WAV full scale",
Precision: 1,
NumChannels: 2,
Signed: false,
Bytes: []byte{0x00, 0xFF},
Samples: [2]float64{-1.0, 1.0 - (1.0 / (1 << 7))},
},
{
Name: "1 channel 16bit WAV negative full scale",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0x00, 0x80},
Samples: [2]float64{-1.0, -1.0},
},
{
Name: "1 channel 16bit WAV midpoint",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0x00, 0x00},
Samples: [2]float64{0.0, 0.0},
},
{
// Because the WAV integer range is asymmetric, converting it to float
// by division will not result in an exactly 1.0 full scale float value.
// It will be 1 least significant bit integer value lower. "1", converted
// to float for an 16-bit WAV sample is 1 / (1 << 15).
Name: "1 channel 16bit WAV positive full scale minus 1 significant bit",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0xFF, 0x7F},
Samples: [2]float64{1.0 - (1.0 / (1 << 15)), 1.0 - (1.0 / (1 << 15))},
},
{
Name: "1 channel 8bit WAV float positive full scale clipping test",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0xFF},
Samples: [2]float64{1.0, 1.0},
SkipDecodeTest: true,
},
{
Name: "1 channel 16bit WAV float positive full scale clipping test",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0xFF, 0x7F},
Samples: [2]float64{1.0, 1.0},
SkipDecodeTest: true,
},
}

func TestFormatDecode(t *testing.T) {
for _, test := range bufferFormatTests {
if test.SkipDecodeTest {
continue
}

t.Run(test.Name, func(t *testing.T) {
format := beep.Format{
SampleRate: 44100,
Precision: test.Precision,
NumChannels: test.NumChannels,
}

var sample [2]float64
var n int
if test.Signed {
sample, n = format.DecodeSigned(test.Bytes)
} else {
sample, n = format.DecodeUnsigned(test.Bytes)
}
assert.Equal(t, len(test.Bytes), n)
assert.Equal(t, test.Samples, sample)
})
}
}

func TestFormatEncode(t *testing.T) {
for _, test := range bufferFormatTests {
t.Run(test.Name, func(t *testing.T) {
format := beep.Format{
SampleRate: 44100,
Precision: test.Precision,
NumChannels: test.NumChannels,
}

bytes := make([]byte, test.Precision*test.NumChannels)
var n int
if test.Signed {
n = format.EncodeSigned(bytes, test.Samples)
} else {
n = format.EncodeUnsigned(bytes, test.Samples)
}
assert.Equal(t, len(test.Bytes), n)
assert.Equal(t, test.Bytes, bytes)
})
}
}

func TestFormatEncodeDecode(t *testing.T) {
formats := make(chan beep.Format)
go func() {
Expand Down
65 changes: 65 additions & 0 deletions wav/encode_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package wav

import (
"fmt"
"math"
"testing"

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

"github.com/orcaman/writerseeker"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -72,3 +76,64 @@ func TestEncode(t *testing.T) {
0x00, 0x00, 0x00, 0x00,
}, encoded, "the encoded file isn't formatted as expected")
}

func TestEncodeDecodeRoundTrip(t *testing.T) {
numChannelsS := []int{1, 2}
precisions := []int{1, 2, 3}

for _, numChannels := range numChannelsS {
for _, precision := range precisions {
name := fmt.Sprintf("%d_channel(s)_%d_precision", numChannels, precision)
t.Run(name, func(t *testing.T) {
var s beep.Streamer
s, data := testtools.RandomDataStreamer(1000)

if numChannels == 1 {
s = effects.Mono(s)
for i := range data {
mix := (data[i][0] + data[i][1]) / 2
data[i][0] = mix
data[i][1] = mix
}
}

var w writerseeker.WriterSeeker

format := beep.Format{SampleRate: 44100, NumChannels: numChannels, Precision: precision}

err := Encode(&w, s, format)
assert.NoError(t, err)

s, decodedFormat, err := Decode(w.Reader())
assert.NoError(t, err)
assert.Equal(t, format, decodedFormat)

actual := testtools.Collect(s)
assert.Len(t, actual, 1000)

// Delta is determined as follows:
// The float values range from -1 to 1, which difference is 2.0.
// For each byte of precision, there are 8 bits -> 2^(precision*8) different possible values.
// So, fitting 2^(precision*8) values into a range of 2.0, each "step" must not
// be bigger than 2.0 / math.Exp2(float64(precision*8)).
delta := 2.0 / math.Exp2(float64(precision*8))
for i := range actual {
// Adjust for clipping.
if data[i][0] >= 1.0 {
data[i][0] = 1.0 - 1.0/(math.Exp2(float64(precision)*8-1))
}
if data[i][1] >= 1.0 {
data[i][1] = 1.0 - 1.0/(math.Exp2(float64(precision)*8-1))
}

if actual[i][0] <= data[i][0]-delta || actual[i][0] >= data[i][0]+delta {
t.Fatalf("encoded & decoded sample doesn't match orginal. expected: %v, actual: %v", data[i][0], actual[i][0])
}
if actual[i][1] <= data[i][1]-delta || actual[i][1] >= data[i][1]+delta {
t.Fatalf("encoded & decoded sample doesn't match orginal. expected: %v, actual: %v", data[i][1], actual[i][1])
}
}
})
}
}
}
Loading