From 5a6378ff9be64e8fa27d83245eb53fd774f44234 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 10 Jul 2024 10:57:54 +0200 Subject: [PATCH 1/2] Test format decode and encode methods --- buffer_test.go | 151 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/buffer_test.go b/buffer_test.go index 2bebbdf..b40de94 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -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() { From 4357b75898482579afd865e6cead676d52887f6e Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 10 Jul 2024 11:17:48 +0200 Subject: [PATCH 2/2] Test WAV encoding-decoding round trip --- wav/encode_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/wav/encode_test.go b/wav/encode_test.go index 9a65e4b..8563ef4 100644 --- a/wav/encode_test.go +++ b/wav/encode_test.go @@ -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" @@ -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]) + } + } + }) + } + } +}