diff --git a/audio/internal/convert/stereof32.go b/audio/internal/convert/stereof32.go new file mode 100644 index 000000000..6c0a5a155 --- /dev/null +++ b/audio/internal/convert/stereof32.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package convert + +import ( + "io" +) + +type StereoF32 struct { + source io.ReadSeeker + mono bool + buf []byte +} + +func NewStereoF32(source io.ReadSeeker, mono bool) *StereoF32 { + return &StereoF32{ + source: source, + mono: mono, + } +} + +func (s *StereoF32) Read(b []byte) (int, error) { + l := len(b) / 8 * 8 + if s.mono { + l /= 2 + } + + if cap(s.buf) < l { + s.buf = make([]byte, l) + } + + n, err := s.source.Read(s.buf[:l]) + if err != nil && err != io.EOF { + return 0, err + } + if s.mono { + for i := 0; i < n/4; i++ { + b[8*i] = s.buf[4*i] + b[8*i+1] = s.buf[4*i+1] + b[8*i+2] = s.buf[4*i+2] + b[8*i+3] = s.buf[4*i+3] + b[8*i+4] = s.buf[4*i] + b[8*i+5] = s.buf[4*i+1] + b[8*i+6] = s.buf[4*i+2] + b[8*i+7] = s.buf[4*i+3] + } + n *= 2 + } else { + copy(b[:n], s.buf[:n]) + } + return n, err +} + +func (s *StereoF32) Seek(offset int64, whence int) (int64, error) { + offset = offset / 8 * 8 + if s.mono { + offset /= 2 + } + return s.source.Seek(offset, whence) +} diff --git a/audio/internal/convert/stereof32_test.go b/audio/internal/convert/stereof32_test.go new file mode 100644 index 000000000..53db5b6c2 --- /dev/null +++ b/audio/internal/convert/stereof32_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package convert_test + +import ( + "bytes" + "fmt" + "io" + "math" + "math/rand" // TODO: Use math/rand/v2 when the minimum supported version becomes Go 1.22. + "testing" + + "github.com/hajimehoshi/ebiten/v2/audio/internal/convert" +) + +func randFloat32s(n int) []float32 { + r := make([]float32, n) + for i := range r { + r[i] = rand.Float32()*2 - 1 + } + return r +} + +func TestStereoF32(t *testing.T) { + testCases := []struct { + Name string + In []float32 + }{ + { + Name: "nil", + In: nil, + }, + { + Name: "-1, 0, 1", + In: []float32{-1, 0, 1}, + }, + { + Name: "8 0s", + In: []float32{0, 0, 0, 0, 0, 0, 0, 0}, + }, + { + Name: "random 256 values", + In: randFloat32s(256), + }, + { + Name: "random 65536 values", + In: randFloat32s(65536), + }, + } + for _, tc := range testCases { + tc := tc + for _, mono := range []bool{false, true} { + mono := mono + t.Run(fmt.Sprintf("%s (mono=%t)", tc.Name, mono), func(t *testing.T) { + var inBytes, outBytes []byte + for _, v := range tc.In { + b := math.Float32bits(v) + inBytes = append(inBytes, byte(b), byte(b>>8), byte(b>>16), byte(b>>24)) + if mono { + // As the source is mono, the output should be stereo. + outBytes = append(outBytes, byte(b), byte(b>>8), byte(b>>16), byte(b>>24), byte(b), byte(b>>8), byte(b>>16), byte(b>>24)) + } else { + outBytes = append(outBytes, byte(b), byte(b>>8), byte(b>>16), byte(b>>24)) + } + } + s := convert.NewStereoF32(bytes.NewReader(inBytes), mono) + var got []byte + for { + var buf [97]byte + n, err := s.Read(buf[:]) + got = append(got, buf[:n]...) + if err != nil { + if err != io.EOF { + t.Fatal(err) + } + break + } + if _, err := s.Seek(0, io.SeekCurrent); err != nil { + if err != io.EOF { + t.Fatal(err) + } + break + } + } + want := outBytes + if !bytes.Equal(got, want) { + t.Errorf("got: %v, want: %v", got, want) + } + }) + } + } +}