diff --git a/audio/audio.go b/audio/audio.go index b3729fb91..ba8c5f2ba 100644 --- a/audio/audio.go +++ b/audio/audio.go @@ -497,5 +497,5 @@ func Resample(source io.ReadSeeker, size int64, from, to int) io.ReadSeeker { if from == to { return source } - return convert.NewResampling(source, size, from, to) + return convert.NewResampling(source, size, from, to, bitDepthInBytesInt16) } diff --git a/audio/internal/convert/resampling.go b/audio/internal/convert/resampling.go index 912f3bd0c..6377d9e00 100644 --- a/audio/internal/convert/resampling.go +++ b/audio/internal/convert/resampling.go @@ -78,33 +78,40 @@ func sinc01(x float64) float64 { } type Resampling struct { - source io.ReadSeeker - size int64 - from int - to int - pos int64 - srcBlock int64 - srcBufL map[int64][]float64 - srcBufR map[int64][]float64 - lruSrcBlocks []int64 + source io.ReadSeeker + size int64 + from int + to int + bitDepthInBytes int + pos int64 + srcBlock int64 + srcBufL map[int64][]float64 + srcBufR map[int64][]float64 + lruSrcBlocks []int64 } -func NewResampling(source io.ReadSeeker, size int64, from, to int) *Resampling { +func NewResampling(source io.ReadSeeker, size int64, from, to int, bitDepthInBytes int) *Resampling { r := &Resampling{ - source: source, - size: size, - from: from, - to: to, - srcBlock: -1, - srcBufL: map[int64][]float64{}, - srcBufR: map[int64][]float64{}, + source: source, + size: size, + from: from, + bitDepthInBytes: bitDepthInBytes, + to: to, + srcBlock: -1, + srcBufL: map[int64][]float64{}, + srcBufR: map[int64][]float64{}, } return r } +func (r *Resampling) bytesPerSample() int { + const channelNum = 2 + return r.bitDepthInBytes * channelNum +} + func (r *Resampling) Length() int64 { s := int64(float64(r.size) * float64(r.to) / float64(r.from)) - return s / 4 * 4 + return s / int64(r.bytesPerSample()) * int64(r.bytesPerSample()) } func (r *Resampling) src(i int64) (float64, float64, error) { @@ -113,17 +120,18 @@ func (r *Resampling) src(i int64) (float64, float64, error) { if i < 0 { return 0, 0, nil } - if r.size/4 <= i { + sizePerSample := int64(r.bytesPerSample()) + if r.size/sizePerSample <= i { return 0, 0, nil } nextPos := int64(i) / resamplingBufferSize if _, ok := r.srcBufL[nextPos]; !ok { if r.srcBlock+1 != nextPos { - if _, err := r.source.Seek(nextPos*resamplingBufferSize*4, io.SeekStart); err != nil { + if _, err := r.source.Seek(nextPos*resamplingBufferSize*sizePerSample, io.SeekStart); err != nil { return 0, 0, err } } - buf := make([]byte, resamplingBufferSize*4) + buf := make([]byte, resamplingBufferSize*sizePerSample) c := 0 for c < len(buf) { n, err := r.source.Read(buf[c:]) @@ -138,9 +146,19 @@ func (r *Resampling) src(i int64) (float64, float64, error) { buf = buf[:c] sl := make([]float64, resamplingBufferSize) sr := make([]float64, resamplingBufferSize) - for i := 0; i < len(buf)/4; i++ { - sl[i] = float64(int16(buf[4*i])|(int16(buf[4*i+1])<<8)) / (1<<15 - 1) - sr[i] = float64(int16(buf[4*i+2])|(int16(buf[4*i+3])<<8)) / (1<<15 - 1) + switch r.bitDepthInBytes { + case 2: + for i := 0; i < len(buf)/int(sizePerSample); i++ { + sl[i] = float64(int16(buf[4*i])|(int16(buf[4*i+1])<<8)) / (1<<15 - 1) + sr[i] = float64(int16(buf[4*i+2])|(int16(buf[4*i+3])<<8)) / (1<<15 - 1) + } + case 4: + for i := 0; i < len(buf)/int(sizePerSample); i++ { + sl[i] = float64(math.Float32frombits(uint32(buf[8*i]) | uint32(buf[8*i+1])<<8 | uint32(buf[8*i+2])<<16 | uint32(buf[8*i+3])<<24)) + sr[i] = float64(math.Float32frombits(uint32(buf[8*i+4]) | uint32(buf[8*i+5])<<8 | uint32(buf[8*i+6])<<16 | uint32(buf[8*i+7])<<24)) + } + default: + panic("not reached") } r.srcBlock = nextPos r.srcBufL[r.srcBlock] = sl @@ -180,12 +198,13 @@ func (r *Resampling) at(t int64) (float64, float64, error) { if startN < 0 { startN = 0 } - if r.size/4 <= startN { - startN = r.size/4 - 1 + sizePerSample := int64(r.bytesPerSample()) + if r.size/sizePerSample <= startN { + startN = r.size/sizePerSample - 1 } endN := int64(tInSrc + windowSize) - if r.size/4 <= endN { - endN = r.size/4 - 1 + if r.size/sizePerSample <= endN { + endN = r.size/sizePerSample - 1 } lv := 0.0 rv := 0.0 @@ -219,21 +238,46 @@ func (r *Resampling) Read(b []byte) (int, error) { if r.pos == r.Length() { return 0, io.EOF } - n := len(b) / 4 * 4 + size := r.bytesPerSample() + n := len(b) / size * size if r.Length()-r.pos <= int64(n) { n = int(r.Length() - r.pos) } - for i := 0; i < n/4; i++ { - l, r, err := r.at(r.pos/4 + int64(i)) - if err != nil { - return 0, err + switch r.bitDepthInBytes { + case 2: + for i := 0; i < n/size; i++ { + l, r, err := r.at(r.pos/int64(size) + int64(i)) + if err != nil { + return 0, err + } + l16 := int16(l * (1<<15 - 1)) + r16 := int16(r * (1<<15 - 1)) + b[4*i] = byte(l16) + b[4*i+1] = byte(l16 >> 8) + b[4*i+2] = byte(r16) + b[4*i+3] = byte(r16 >> 8) } - l16 := int16(l * (1<<15 - 1)) - r16 := int16(r * (1<<15 - 1)) - b[4*i] = byte(l16) - b[4*i+1] = byte(l16 >> 8) - b[4*i+2] = byte(r16) - b[4*i+3] = byte(r16 >> 8) + case 4: + for i := 0; i < n/size; i++ { + l, r, err := r.at(r.pos/int64(size) + int64(i)) + if err != nil { + return 0, err + } + l32 := float32(l) + r32 := float32(r) + l32b := math.Float32bits(l32) + r32b := math.Float32bits(r32) + b[8*i] = byte(l32b) + b[8*i+1] = byte(l32b >> 8) + b[8*i+2] = byte(l32b >> 16) + b[8*i+3] = byte(l32b >> 24) + b[8*i+4] = byte(r32b) + b[8*i+5] = byte(r32b >> 8) + b[8*i+6] = byte(r32b >> 16) + b[8*i+7] = byte(r32b >> 24) + } + default: + panic("not reached") } r.pos += int64(n) return n, nil diff --git a/audio/internal/convert/resampling_test.go b/audio/internal/convert/resampling_test.go index 590931a4a..ab31057e7 100644 --- a/audio/internal/convert/resampling_test.go +++ b/audio/internal/convert/resampling_test.go @@ -16,6 +16,7 @@ package convert_test import ( "bytes" + "fmt" "io" "math" "testing" @@ -29,7 +30,7 @@ func soundAt(timeInSecond float64) float64 { amp := []float64{1.0, 0.8, 0.6, 0.4, 0.2} v := 0.0 for j := 0; j < len(amp); j++ { - v += amp[j] * math.Sin(2.0*math.Pi*timeInSecond*freq*float64(j+1)) + v += amp[j] * math.Sin(2.0*math.Pi*timeInSecond*freq*float64(j+1)) / 2 } if v > 1 { v = 1 @@ -40,15 +41,28 @@ func soundAt(timeInSecond float64) float64 { return v } -func newSoundBytes(sampleRate int) []byte { +func newSoundBytes(sampleRate int, bitDepthInBytes int) []byte { b := make([]byte, sampleRate*4) // 1 second - for i := 0; i < len(b)/4; i++ { + for i := 0; i < len(b)/(bitDepthInBytes*2); i++ { v := soundAt(float64(i) / float64(sampleRate)) - v16 := int16(v * (1<<15 - 1)) - b[4*i] = byte(v16) - b[4*i+1] = byte(v16 >> 8) - b[4*i+2] = byte(v16) - b[4*i+3] = byte(v16 >> 8) + switch bitDepthInBytes { + case 2: + v16 := int16(v * (1<<15 - 1)) + b[4*i] = byte(v16) + b[4*i+1] = byte(v16 >> 8) + b[4*i+2] = byte(v16) + b[4*i+3] = byte(v16 >> 8) + case 4: + v32 := math.Float32bits(float32(v)) + b[8*i] = byte(v32) + b[8*i+1] = byte(v32 >> 8) + b[8*i+2] = byte(v32 >> 16) + b[8*i+3] = byte(v32 >> 24) + b[8*i+4] = byte(v32) + b[8*i+5] = byte(v32 >> 8) + b[8*i+6] = byte(v32 >> 16) + b[8*i+7] = byte(v32 >> 24) + } } return b } @@ -68,22 +82,37 @@ func TestResampling(t *testing.T) { }, } for _, c := range cases { - inB := newSoundBytes(c.In) - outS := convert.NewResampling(bytes.NewReader(inB), int64(len(inB)), c.In, c.Out) - gotB, err := io.ReadAll(outS) - if err != nil { - t.Fatal(err) - } - wantB := newSoundBytes(c.Out) - if len(gotB) != len(wantB) { - t.Errorf("len(gotB) == %d but len(wantB) == %d", len(gotB), len(wantB)) - } - for i := 0; i < len(gotB)/2; i++ { - got := float64(int16(gotB[2*i])|(int16(gotB[2*i+1])<<8)) / (1<<15 - 1) - want := float64(int16(wantB[2*i])|(int16(wantB[2*i+1])<<8)) / (1<<15 - 1) - if math.Abs(got-want) > 0.025 { - t.Errorf("sample rate: %d, index: %d: got: %f, want: %f", c.Out, i, got, want) + c := c + t.Run(fmt.Sprintf("%d to %d", c.In, c.Out), func(t *testing.T) { + for _, bitDepthInBytes := range []int{2, 4} { + bitDepthInBytes := bitDepthInBytes + t.Run(fmt.Sprintf("bitDepthInBytes=%d", bitDepthInBytes), func(t *testing.T) { + inB := newSoundBytes(c.In, bitDepthInBytes) + outS := convert.NewResampling(bytes.NewReader(inB), int64(len(inB)), c.In, c.Out, bitDepthInBytes) + gotB, err := io.ReadAll(outS) + if err != nil { + t.Fatal(err) + } + wantB := newSoundBytes(c.Out, bitDepthInBytes) + if len(gotB) != len(wantB) { + t.Errorf("len(gotB) == %d but len(wantB) == %d", len(gotB), len(wantB)) + } + for i := 0; i < len(gotB)/bitDepthInBytes; i++ { + var got, want float64 + switch bitDepthInBytes { + case 2: + got = float64(int16(gotB[2*i])|(int16(gotB[2*i+1])<<8)) / (1<<15 - 1) + want = float64(int16(wantB[2*i])|(int16(wantB[2*i+1])<<8)) / (1<<15 - 1) + case 4: + got = float64(math.Float32frombits(uint32(gotB[4*i]) | (uint32(gotB[4*i+1]) << 8) | (uint32(gotB[4*i+2]) << 16) | (uint32(gotB[4*i+3]) << 24))) + want = float64(math.Float32frombits(uint32(wantB[4*i]) | (uint32(wantB[4*i+1]) << 8) | (uint32(wantB[4*i+2]) << 16) | (uint32(wantB[4*i+3]) << 24))) + } + if math.Abs(got-want) > 0.025 { + t.Errorf("sample rate: %d, index: %d: got: %f, want: %f", c.Out, i, got, want) + } + } + }) } - } + }) } } diff --git a/audio/mp3/decode.go b/audio/mp3/decode.go index 815be418f..f9e9fb5e1 100644 --- a/audio/mp3/decode.go +++ b/audio/mp3/decode.go @@ -27,6 +27,10 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio/internal/convert" ) +const ( + bitDepthInBytesInt16 = 2 +) + // Stream is a decoded stream. type Stream struct { orig *mp3.Decoder @@ -105,7 +109,7 @@ func DecodeWithSampleRate(sampleRate int, src io.Reader) (*Stream, error) { var r *convert.Resampling if d.SampleRate() != sampleRate { - r = convert.NewResampling(d, d.Length(), d.SampleRate(), sampleRate) + r = convert.NewResampling(d, d.Length(), d.SampleRate(), sampleRate, bitDepthInBytesInt16) } s := &Stream{ orig: d, diff --git a/audio/vorbis/vorbis.go b/audio/vorbis/vorbis.go index 08c1b2214..76bb9a973 100644 --- a/audio/vorbis/vorbis.go +++ b/audio/vorbis/vorbis.go @@ -25,6 +25,10 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio/internal/convert" ) +const ( + bitDepthInBytesInt16 = 2 +) + // Stream is a decoded audio stream. type Stream struct { readSeeker io.ReadSeeker @@ -200,7 +204,7 @@ func DecodeWithSampleRate(sampleRate int, src io.Reader) (*Stream, error) { length *= 2 } if origSampleRate != sampleRate { - r := convert.NewResampling(s, length, origSampleRate, sampleRate) + r := convert.NewResampling(s, length, origSampleRate, sampleRate, bitDepthInBytesInt16) s = r length = r.Length() } diff --git a/audio/wav/decode.go b/audio/wav/decode.go index 097795d3f..23e0e6a06 100644 --- a/audio/wav/decode.go +++ b/audio/wav/decode.go @@ -24,6 +24,10 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio/internal/convert" ) +const ( + bitDepthInBytesInt16 = 2 +) + // Stream is a decoded audio stream. type Stream struct { inner io.ReadSeeker @@ -153,7 +157,7 @@ func DecodeWithSampleRate(sampleRate int, src io.Reader) (*Stream, error) { return s, nil } - r := convert.NewResampling(s.inner, s.size, s.sampleRate, sampleRate) + r := convert.NewResampling(s.inner, s.size, s.sampleRate, sampleRate, bitDepthInBytesInt16) return &Stream{ inner: r, size: r.Length(),