Files
aego/audio/codec/held_stream_vorbis_test.go
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

157 lines
4.0 KiB
Go

package codec_test
import (
"math"
"os"
"sync"
"testing"
"time"
"aego/audio/codec"
"aego/audio/mix"
)
// Observe a real decoder without changing its PCM or seek implementation.
type heldVorbisDecoder struct {
codec.Decoder
infos chan struct{}
seeks chan int64
gate chan struct{}
}
func (d *heldVorbisDecoder) Info() codec.Info {
d.infos <- struct{}{}
return d.Decoder.Info()
}
func (d *heldVorbisDecoder) SeekFrame(frame int64) error {
d.seeks <- frame
<-d.gate
return d.Decoder.SeekFrame(frame)
}
func TestHeldVorbisCombinedSeekAndLoopPCM(t *testing.T) {
data, err := os.ReadFile("../../examples/platformer/assets/audio/glorious-morning.ogg")
if err != nil {
t.Fatal(err)
}
reference := open(t, data, "glorious-morning.ogg")
defer reference.Close()
info := reference.Info()
if info.Channels != 2 || info.SampleRate != 44100 || info.Frames < 60000 {
t.Fatalf("unexpected Vorbis fixture: %+v", info)
}
const loopStart, loopEnd = 50000, 60000
pcm := make([]float32, loopEnd*info.Channels)
for pos := 0; pos < loopEnd; {
n, err := reference.Read(pcm[pos*info.Channels:])
if err != nil || n == 0 {
t.Fatalf("reference decode at %d: %d, %v", pos, n, err)
}
pos += n
}
decoder := &heldVorbisDecoder{
Decoder: open(t, data, "glorious-morning.ogg"),
infos: make(chan struct{}, 16), seeks: make(chan int64, 16), gate: make(chan struct{}),
}
var gateOnce sync.Once
openGate := func() { gateOnce.Do(func() { close(decoder.gate) }) }
s := mix.NewStream(info.SampleRate, info.Channels, 256)
cancel := make(chan struct{})
done := codec.Stream(decoder, s, codec.StreamOptions{Loop: true, LoopStart: loopStart, LoopEnd: loopEnd, Chunk: 64}, cancel)
t.Cleanup(func() {
openGate()
close(cancel)
select {
case err := <-done:
if err != nil {
t.Errorf("Vorbis feeder: %v", err)
}
case <-time.After(5 * time.Second):
t.Error("Vorbis feeder did not cancel")
}
if err := decoder.Close(); err != nil {
t.Error(err)
}
})
waitInfo := func() {
t.Helper()
select {
case <-decoder.infos:
case <-time.After(5 * time.Second):
t.Fatal("Vorbis feeder did not process the skip")
}
}
waitRing := func(frames int) {
t.Helper()
deadline := time.NewTimer(5 * time.Second)
defer deadline.Stop()
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for s.Ring.Len() < frames {
select {
case <-deadline.C:
t.Fatalf("Vorbis ring has %d frames, want %d", s.Ring.Len(), frames)
case <-ticker.C:
}
}
}
waitInfo()
waitRing(s.Ring.Cap())
m, err := mix.New(mix.Config{SampleRate: info.SampleRate, Channels: 2})
if err != nil {
t.Fatal(err)
}
if id := m.Play(mix.PlayParams{Stream: s, Gain: 1}); !id.Valid() {
t.Fatal("could not start Vorbis stream")
}
m.Skip(0)
m.Skip(129000)
waitInfo()
m.Skip(901)
waitInfo()
select {
case frame := <-decoder.seeks:
t.Fatalf("Vorbis sought to %d while held", frame)
default:
}
// Block the seek until Mix returns, so it cannot consume the output in
// parallel with this test. Mixing writes only to an in-memory buffer.
m.Mix(make([]float32, 2))
select {
case frame := <-decoder.seeks:
if frame != 59900 {
t.Fatalf("combined Vorbis seek = %d, want 59900", frame)
}
case <-time.After(5 * time.Second):
t.Fatal("Vorbis seek did not start")
}
openGate()
waitRing(256)
got := make([]float32, 256*info.Channels)
if n := s.Ring.Read(got); n != 256 {
t.Fatalf("read %d frames, want 256", n)
}
for i, sample := range got {
frame := 59900 + i/info.Channels
if frame >= loopEnd {
frame = loopStart + frame - loopEnd
}
want := pcm[frame*info.Channels+i%info.Channels]
if math.Abs(float64(sample-want)) > 1e-3 {
t.Fatalf("sample %d (source frame %d) = %g, want %g", i, frame, sample, want)
}
}
select {
case frame := <-decoder.seeks:
if frame != loopStart {
t.Fatalf("loop seek = %d, want %d", frame, loopStart)
}
case <-time.After(5 * time.Second):
t.Fatal("Vorbis did not seek to the loop start")
}
}
var _ codec.Decoder = (*heldVorbisDecoder)(nil)