Files
talk/internal/audio/audio_test.go
T

177 lines
5.4 KiB
Go
Raw Normal View History

package audio
import (
"bytes"
"context"
"encoding/binary"
"math"
"os"
"strings"
"testing"
)
// buildWAV baut nach RIFF-Spezifikation eine WAV-Datei (unabhängig vom
// Parser implementiert, damit Parser und Test-Fixture nicht denselben
// Fehler teilen).
func buildWAV(audioFormat uint16, channels, rate, bits int, samples []byte) []byte {
var buf bytes.Buffer
buf.WriteString("RIFF")
payload := 28 + len(samples) // fmt (8+16) + data (8+n) + 4 bytes WAVE
binary.Write(&buf, binary.LittleEndian, uint32(payload))
buf.WriteString("WAVE")
buf.WriteString("fmt ")
binary.Write(&buf, binary.LittleEndian, uint32(16))
binary.Write(&buf, binary.LittleEndian, audioFormat)
binary.Write(&buf, binary.LittleEndian, uint16(channels))
binary.Write(&buf, binary.LittleEndian, uint32(rate))
blockAlign := channels * bits / 8
binary.Write(&buf, binary.LittleEndian, uint32(rate*blockAlign))
binary.Write(&buf, binary.LittleEndian, uint16(blockAlign))
binary.Write(&buf, binary.LittleEndian, uint16(bits))
buf.WriteString("data")
binary.Write(&buf, binary.LittleEndian, uint32(len(samples)))
buf.Write(samples)
return buf.Bytes()
}
func TestDecodeWAV(t *testing.T) {
t.Run("16-bit mono", func(t *testing.T) {
wav := buildWAV(1, 1, 22050, 16, []byte{1, 2, 3, 4})
f, pcm, err := decodeWAV(wav)
if err != nil {
t.Fatalf("decodeWAV: %v", err)
}
if f.SampleRate != 22050 || f.Channels != 1 || f.BitsPerCoded != 16 || f.Float {
t.Errorf("Format = %+v", f)
}
if string(pcm) != "\x01\x02\x03\x04" {
t.Errorf("PCM = %x", pcm)
}
})
t.Run("8-bit stereo mit Extra-Chunk", func(t *testing.T) {
wav := buildWAV(1, 2, 8000, 8, []byte{7, 8, 9})
// LIST-Chunk (word-aligned, ungerade Länge) zwischen fmt und data einfügen
withList := append(wav[:36], append([]byte("LIST\x03\x00\x00\x00abc\x00"), wav[36:]...)...)
f, pcm, err := decodeWAV(withList)
if err != nil {
t.Fatalf("decodeWAV: %v", err)
}
if f.BitsPerCoded != 8 || f.Channels != 2 {
t.Errorf("Format = %+v", f)
}
if string(pcm) != "\x07\x08\x09" {
t.Errorf("PCM = %x", pcm)
}
})
t.Run("float32", func(t *testing.T) {
wav := buildWAV(3, 1, 44100, 32, []byte{0, 0, 0, 0})
f, _, err := decodeWAV(wav)
if err != nil {
t.Fatalf("decodeWAV: %v", err)
}
if !f.Float || f.BitsPerCoded != 32 {
t.Errorf("Format = %+v, want Float32", f)
}
})
t.Run("leerer data-Chunk ist erlaubt", func(t *testing.T) {
wav := buildWAV(1, 1, 22050, 16, nil)
_, pcm, err := decodeWAV(wav)
if err != nil {
t.Fatalf("decodeWAV: %v", err)
}
if len(pcm) != 0 {
t.Errorf("PCM = %x, want leer", pcm)
}
})
}
func TestDecodeWAVFehler(t *testing.T) {
cases := []struct {
name string
data []byte
want string
}{
{"zu kurz", []byte("RIFF"), "zu kurz"},
{"kein riff", []byte("RIFX" + "\x00\x00\x00\x00" + "WAVE"), "RIFF"},
{"24-bit pcm", buildWAV(1, 1, 44100, 24, []byte{1, 2, 3}), "nicht unterstütztes Format"},
{"3 kanäle", buildWAV(1, 3, 44100, 16, []byte{1, 2, 3, 4, 5, 6}), "Kanäle"},
{"ohne data", func() []byte {
wav := buildWAV(1, 1, 22050, 16, []byte{1, 2})
return wav[:36] // nur fmt-Chunk behalten
}(), "data"},
{"kaputter chunk", append(buildWAV(1, 1, 22050, 16, nil), "data\xff\xff\xff\xff"...), "überragt"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := decodeWAV(tc.data)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("decodeWAV-Fehler = %v, will Meldung mit %q", err, tc.want)
}
})
}
}
func TestNewSink(t *testing.T) {
for _, name := range []string{NameOto, NameNull} {
s, err := NewSink(name)
if err != nil {
t.Fatalf("NewSink(%q): %v", name, err)
}
if s.Name() != name {
t.Errorf("NewSink(%q).Name() = %q", name, s.Name())
}
}
if _, err := NewSink("mpv"); err == nil {
t.Error("NewSink(\"mpv\") soll fehlschlagen")
}
}
func TestNullSinkVerwirftUndLoggt(t *testing.T) {
var buf bytes.Buffer
sink := NullSink{W: &buf}
if err := sink.Play(context.Background(), []byte("audio")); err != nil {
t.Fatalf("NullSink.Play: %v", err)
}
if !strings.Contains(buf.String(), "verworfen") {
t.Errorf("NullSink-Log = %q, will Verwurf-Zeile", buf.String())
}
if sink.Name() != NameNull {
t.Errorf("Name() = %q", sink.Name())
}
}
// TestOtoSinkUngueltigeDaten: OtoSink.Play muss an der WAV-Dekodierung
// scheitern, bevor irgend ein Audio-Gerät angefasst wird — das ist der
// CI-/Server-sichere Fehlerpfad des oto-Sinks.
func TestOtoSinkUngueltigeDaten(t *testing.T) {
err := (OtoSink{}).Play(context.Background(), []byte("kein audio"))
if err == nil {
t.Fatal("OtoSink.Play mit Müll: kein Fehler")
}
if !strings.Contains(err.Error(), "wav:") {
t.Errorf("Fehler soll aus dem WAV-Parser kommen: %v", err)
}
}
// TestOtoSinkEchteWiedergabe läuft nur mit TALK_TEST_REAL_AUDIO=1 (erzeugt
// echten Ton); Standard-Testläufe bleiben stumm.
func TestOtoSinkEchteWiedergabe(t *testing.T) {
if testing.Short() {
t.Skip("Übersprungen (-short)")
}
if os.Getenv("TALK_TEST_REAL_AUDIO") != "1" {
t.Skip("Übersprungen (hörbar) — mit TALK_TEST_REAL_AUDIO=1 ausführen")
}
// 0,1 s 440-Hz-Sinus, 16 Bit mono 22050 Hz.
const rate = 22050
samples := make([]byte, rate/10*2)
for i := 0; i < rate/10; i++ {
v := int16(8000 * math.Sin(float64(i)/float64(rate)*2*math.Pi*440))
binary.LittleEndian.PutUint16(samples[i*2:], uint16(v))
}
if err := (OtoSink{}).Play(context.Background(), buildWAV(1, 1, rate, 16, samples)); err != nil {
t.Fatalf("OtoSink.Play: %v", err)
}
}