99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package sound
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPositionTonesDecodeAndRiseInPitch(t *testing.T) {
|
|
previousCrossings := 0
|
|
for _, position := range []float64{0, 0.5, 1} {
|
|
data, err := soundData("default", PositionCueName(position))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if binary.LittleEndian.Uint32(data[24:]) != 22050 || binary.LittleEndian.Uint16(data[22:]) != 1 || len(data) != 44+1323*2 {
|
|
t.Fatal("position tone is not 60 ms mono PCM at 22050 Hz")
|
|
}
|
|
crossings := 0
|
|
previousSample := int16(0)
|
|
for offset := 44; offset < len(data); offset += 2 {
|
|
sample := int16(binary.LittleEndian.Uint16(data[offset:]))
|
|
if previousSample < 0 && sample >= 0 {
|
|
crossings++
|
|
}
|
|
previousSample = sample
|
|
}
|
|
if crossings <= previousCrossings {
|
|
t.Fatalf("pitch failed to rise at %g: %d cycles", position, crossings)
|
|
}
|
|
previousCrossings = crossings
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
err = (NativePlayer{Sink: "test"}).Play(ctx, data, 0.3, -0.5)
|
|
cancel()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, value := range []string{"NaN", "+Inf", "-1", "1.1", "bad"} {
|
|
if _, err := soundData("default", positionCuePrefix+value); err == nil {
|
|
t.Fatalf("accepted position %s", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPositionSoundUsesObjectSoundPolicy(t *testing.T) {
|
|
player := &configurationPlayer{played: make(chan configuredPlayback, 8)}
|
|
service := New(slog.New(slog.DiscardHandler), player, "default", 0.3)
|
|
if err := service.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := service.Stop(context.Background()); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
cue := Cue{Name: PositionCueName(0.5), Pan: -0.5}
|
|
if !service.Enqueue(cue) {
|
|
t.Fatal("position cue was rejected")
|
|
}
|
|
select {
|
|
case playback := <-player.played:
|
|
if playback.volume != 0.3 || playback.pan != -0.5 {
|
|
t.Fatalf("position playback = %+v", playback)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("position tone did not reach player")
|
|
}
|
|
service.mutex.Lock()
|
|
cached := len(service.cache)
|
|
service.mutex.Unlock()
|
|
if cached != 0 {
|
|
t.Fatal("continuous position values populated the sound cache")
|
|
}
|
|
for _, configuration := range []Configuration{
|
|
{Enabled: false, Theme: "default", Volume: 0.3},
|
|
{Enabled: true, Theme: "none", Volume: 0.3},
|
|
{Enabled: true, Theme: "default", Volume: 0},
|
|
} {
|
|
service.Configure(configuration)
|
|
if service.Available(cue.Name) || service.Enqueue(cue) {
|
|
t.Fatalf("position ignored policy: %+v", configuration)
|
|
}
|
|
if !service.Available(ProgressCueName(50)) {
|
|
t.Fatal("position policy affected progress tones")
|
|
}
|
|
}
|
|
service.Configure(Configuration{Enabled: true, Theme: "custom", Volume: 0.3})
|
|
if !service.Available(cue.Name) {
|
|
t.Fatal("generated position requires a theme asset")
|
|
}
|
|
}
|