audio: Blend the loop start and after-the-loop part to reduce noises at InfiniteLoop

Closes #1888
This commit is contained in:
Hajime Hoshi 2021-11-29 23:01:19 +09:00
parent 90f59aad7c
commit c91a6fb81f
3 changed files with 180 additions and 2 deletions

View File

@ -152,3 +152,7 @@ func PlayersNumForTesting() int {
func ResetContextForTesting() {
theContext = nil
}
func (i *InfiniteLoop) SetNoBlendForTesting(value bool) {
i.noBlendForTesting = value
}

View File

@ -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
}

View File

@ -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)
}
}