Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/mux/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,21 @@ func (p *Player) IsRegistered() bool {
_, ok := p.p.mux.players[p.p]
return ok
}

// PrimeForMixing puts the player into the playing state with buf as its buffered
// source data and a volume ramp from prevVolume to the current volume, without
// registering the player with the mux. It is used to test the mixing directly.
func (p *Player) PrimeForMixing(buf []byte, prevVolume float64) {
p.p.m.Lock()
defer p.p.m.Unlock()

p.p.state = playerPlay
p.p.buf = buf
p.p.prevVolume = prevVolume
}

// ReadBufferAndAdd mixes the buffered source data into buf and returns the number
// of mixed samples.
func (p *Player) ReadBufferAndAdd(buf []float32) int {
return p.p.readBufferAndAdd(buf)
}
5 changes: 4 additions & 1 deletion internal/mux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,10 @@ func (p *playerImpl) readBufferAndAdd(buf []float32) int {
volume := float32(p.volume)

channelCount := p.mux.channelCount
rateDenom := float32(n / channelCount)
// The division must be done on floats, or a buffer with fewer samples than
// channels makes rateDenom 0 and the ramp rate 0/0, i.e. NaN, silencing the
// whole ramp.
rateDenom := float32(n) / float32(channelCount)

src := p.buf[:n*bitDepthInBytes]

Expand Down
67 changes: 67 additions & 0 deletions internal/mux/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"io"
"math"
"slices"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -635,6 +636,72 @@ func TestInvalidVolumeWhilePlayingIsRecoverable(t *testing.T) {
}
}

// signedInt16LEBytes returns the little-endian 16-bit representation of the given values.
func signedInt16LEBytes(values ...int16) []byte {
bs := make([]byte, 0, 2*len(values))
for _, v := range values {
bs = append(bs, byte(v), byte(v>>8))
}
return bs
}

// A volume ramp must not be dropped when fewer samples than the channel count
// are buffered. With an integer division for the ramp denominator, the
// denominator became 0 and the whole ramp became NaN, silencing the player.
func TestVolumeRampWithFewerSamplesThanChannelsDoesNotSilence(t *testing.T) {
const half = 1 << 14

for _, tc := range []struct {
name string
channelCount int
src []byte
}{
// A stereo player with a single sample left in its buffer.
{
name: "stereo with one sample",
channelCount: 2,
src: signedInt16LEBytes(half),
},
// A stereo player with a partial frame left in its buffer.
{
name: "stereo with a partial frame",
channelCount: 2,
src: signedInt16LEBytes(half, half, half)[:3],
},
// A quadrophonic player with a single sample left in its buffer.
{
name: "quad with one sample",
channelCount: 4,
src: signedInt16LEBytes(half),
},
} {
t.Run(tc.name, func(t *testing.T) {
m := mux.New(48000, tc.channelCount, mux.FormatSignedInt16LE)
p := newPlayer(t, m, &bytes.Reader{})
p.SetVolume(0.5)

// Ramp from the volume 1 to the volume 0.5.
p.PrimeForMixing(slices.Clone(tc.src), 1)

buf := make([]float32, len(tc.src)/mux.FormatSignedInt16LE.ByteLength())
if got, want := p.ReadBufferAndAdd(buf), len(buf); got != want {
t.Fatalf("mixed samples: got %d; want %d", got, want)
}

for i, got := range buf {
if math.IsNaN(float64(got)) {
t.Fatalf("buf[%d]: got NaN; want a finite value", i)
}
// The sample source is 0.5 and the volume is ramped from 1 to 0.5,
// so every mixed sample must lie in [0.25, 0.5].
if got < 0.25 || got > 0.5 {
t.Errorf("buf[%d]: got %v; want a value in [0.25, 0.5]", i, got)
}
}
})
}
}

func TestVolumeLargerThanOneAmplifies(t *testing.T) {
const sampleCount = 256

Expand Down
Loading