diff --git a/audio/export_test.go b/audio/export_test.go index b0986dcf8..1acc3e6a7 100644 --- a/audio/export_test.go +++ b/audio/export_test.go @@ -152,3 +152,7 @@ func PlayersNumForTesting() int { func ResetContextForTesting() { theContext = nil } + +func (i *InfiniteLoop) SetNoBlendForTesting(value bool) { + i.noBlendForTesting = value +} diff --git a/audio/loop.go b/audio/loop.go index c6e7b52e2..ad3b6df77 100644 --- a/audio/loop.go +++ b/audio/loop.go @@ -25,6 +25,17 @@ type InfiniteLoop struct { lstart int64 llength int64 pos int64 + + // extra is the remainder in the case when the read byte sizes are not multiple of bitDepthInBytes. + extra []byte + + // afterLoop is data after the loop. + afterLoop []byte + + // blending represents whether the loop start and afterLoop are blended or not. + blending bool + + noBlendForTesting bool } // NewInfiniteLoop creates a new infinite loop stream with a source stream and length in bytes. @@ -62,6 +73,18 @@ func (i *InfiniteLoop) ensurePos() error { return nil } +func (i *InfiniteLoop) blendRate(pos int64) float64 { + if pos < i.lstart { + return 0 + } + if pos >= i.lstart+int64(len(i.afterLoop)) { + return 0 + } + p := (pos - i.lstart) / bytesPerSample + l := len(i.afterLoop) / bytesPerSample + return 1 - float64(p)/float64(l) +} + // Read is implementation of ReadSeeker's Read. func (i *InfiniteLoop) Read(b []byte) (int, error) { if err := i.ensurePos(); err != nil { @@ -72,17 +95,80 @@ func (i *InfiniteLoop) Read(b []byte) (int, error) { b = b[:i.length()-i.pos] } - n, err := i.src.Read(b) + extralen := len(i.extra) + copy(b, i.extra) + i.extra = i.extra[:0] + + n, err := i.src.Read(b[extralen:]) + n += extralen i.pos += int64(n) if i.pos > i.length() { panic(fmt.Sprintf("audio: position must be <= length but not at (*InfiniteLoop).Read: pos: %d, length: %d", i.pos, i.length())) } + // Save the remainder part to extra. This will be used at the next Read. + if rem := n % bitDepthInBytes; rem != 0 { + i.extra = append(i.extra, b[n-rem:n]...) + b = b[:n-rem] + n = n - rem + } + + // Blend afterLoop and the loop start to reduce noises (#1888). + // Ideally, afterLoop and the loop start should be identical, but they can have very slight differences. + if !i.noBlendForTesting && i.blending && i.pos >= i.lstart && i.pos-int64(n) < i.lstart+int64(len(i.afterLoop)) { + if n%bitDepthInBytes != 0 { + panic(fmt.Sprintf("audio: n must be a multiple of bitDepthInBytes but not: %d", n)) + } + for idx := 0; idx < n/bitDepthInBytes; idx++ { + abspos := i.pos - int64(n) + int64(idx)*bitDepthInBytes + rate := i.blendRate(abspos) + if rate == 0 { + continue + } + + // This assumes that bitDepthInBytes is 2. + relpos := abspos - i.lstart + afterLoop := int16(i.afterLoop[relpos]) | (int16(i.afterLoop[relpos+1]) << 8) + orig := int16(b[2*idx]) | (int16(b[2*idx+1]) << 8) + + newval := int16(float64(afterLoop)*rate + float64(orig)*(1-rate)) + b[2*idx] = byte(newval) + b[2*idx+1] = byte(newval >> 8) + } + } + if err != nil && err != io.EOF { return 0, err } - if err == io.EOF || i.pos == i.length() { + // Read the afterLoop part if necessary. + if i.pos == i.length() && err == nil { + if i.afterLoop == nil { + buflen := int64(256 * bytesPerSample) + if buflen > i.length() { + buflen = i.length() + } + + buf := make([]byte, buflen) + pos := 0 + for pos < len(buf) { + n, err := i.src.Read(buf[pos:]) + if err != nil && err != io.EOF { + return 0, err + } + pos += n + if err == io.EOF { + break + } + } + i.afterLoop = buf[:pos] + } + if len(i.afterLoop) > 0 { + i.blending = true + } + } + + if i.pos == i.length() || err == io.EOF { // Ignore the new position returned by Seek since the source position might not be match with the position // managed by this. if _, err := i.src.Seek(i.lstart, io.SeekStart); err != nil { @@ -95,6 +181,7 @@ func (i *InfiniteLoop) Read(b []byte) (int, error) { // Seek is implementation of ReadSeeker's Seek. func (i *InfiniteLoop) Seek(offset int64, whence int) (int64, error) { + i.blending = false if err := i.ensurePos(); err != nil { return 0, err } diff --git a/audio/loop_test.go b/audio/loop_test.go index b1c751211..bc10efa7e 100644 --- a/audio/loop_test.go +++ b/audio/loop_test.go @@ -95,7 +95,9 @@ func TestInfiniteLoopWithIntro(t *testing.T) { src[i] = indexToByte(i) } srcInf := audio.NewInfiniteLoop(bytes.NewReader(src), srcLength) + srcInf.SetNoBlendForTesting(true) l := audio.NewInfiniteLoopWithIntro(srcInf, introLength, loopLength) + l.SetNoBlendForTesting(true) buf := make([]byte, srcLength*4) if _, err := io.ReadFull(l, buf); err != nil { @@ -170,3 +172,88 @@ func TestInfiniteLoopWithIncompleteSize(t *testing.T) { t.Errorf("got: %d, want: %d", got, want) } } + +type slowReader struct { + src io.ReadSeeker + eof bool +} + +func (s *slowReader) Read(buf []byte) (int, error) { + if len(buf) == 0 { + if s.eof { + return 0, io.EOF + } + return 0, nil + } + + n, err := s.src.Read(buf[:1]) + if err == io.EOF { + s.eof = true + } + return n, err +} + +func (s *slowReader) Seek(offset int64, whence int) (int64, error) { + s.eof = false + return s.src.Seek(offset, whence) +} + +func TestInfiniteLoopWithSlowSource(t *testing.T) { + src := make([]byte, 4096) + for i := range src { + src[i] = byte(i) + } + r := &slowReader{ + src: bytes.NewReader(src), + } + loop := audio.NewInfiniteLoop(r, 4096) + + buf := make([]byte, 4096) + + // With a slow source, whose Read always reads at most one byte, + // an infinite loop should adjust the reading size along with bitDepthInBytes (= 2). + + n0, err := loop.Read(buf) + if err != nil { + t.Error(err) + } + if got, want := n0, 0; got != want { + t.Errorf("got: %d, want: %d", got, want) + } + + n1, err := loop.Read(buf) + if err != nil { + t.Error(err) + } + if got, want := n1, 2; got != want { + t.Errorf("got: %d, want: %d", got, want) + } + if got, want := buf[0], byte(0); got != want { + t.Errorf("got: %d, want: %d", got, want) + } + if got, want := buf[1], byte(1); got != want { + t.Errorf("got: %d, want: %d", got, want) + } + + n2, err := loop.Read(buf) + if err != nil { + t.Error(err) + } + if got, want := n2, 0; got != want { + t.Errorf("got: %d, want: %d", got, want) + } + + n3, err := loop.Read(buf) + if err != nil { + t.Error(err) + } + if got, want := n3, 2; got != want { + t.Errorf("got: %d, want: %d", got, want) + } + if got, want := buf[0], byte(2); got != want { + t.Errorf("got: %d, want: %d", got, want) + } + if got, want := buf[1], byte(3); got != want { + t.Errorf("got: %d, want: %d", got, want) + } +}