|
- package audio
-
- import (
- "bytes"
- "encoding/binary"
- "io"
- "math"
- "sync"
- "sync/atomic"
- "testing"
- )
-
- func TestStreamSource_WriteRead(t *testing.T) {
- s := NewStreamSource(1024, 44100)
- if s.size != 1024 {
- t.Fatalf("expected size 1024, got %d", s.size)
- }
-
- // Write and read a frame
- f := NewFrame(0.5, -0.3)
- if !s.WriteFrame(f) {
- t.Fatal("write failed")
- }
- if s.Available() != 1 {
- t.Fatalf("expected 1 available, got %d", s.Available())
- }
-
- out := s.ReadFrame()
- if out.L != 0.5 || out.R != -0.3 {
- t.Fatalf("read mismatch: got L=%.2f R=%.2f", out.L, out.R)
- }
- if s.Available() != 0 {
- t.Fatalf("expected 0 available, got %d", s.Available())
- }
- }
-
- func TestStreamSource_Underrun(t *testing.T) {
- s := NewStreamSource(16, 44100)
-
- // Read from empty buffer — should return silence
- f := s.ReadFrame()
- if f.L != 0 || f.R != 0 {
- t.Fatal("expected silence on underrun")
- }
- if s.Underruns.Load() != 1 {
- t.Fatalf("expected 1 underrun, got %d", s.Underruns.Load())
- }
- stats := s.Stats()
- if stats.UnderrunStreak != 1 || stats.MaxUnderrunStreak != 1 {
- t.Fatalf("unexpected streak: %d/%d", stats.UnderrunStreak, stats.MaxUnderrunStreak)
- }
- }
-
- func TestStreamSource_UnderrunStreakTracking(t *testing.T) {
- s := NewStreamSource(16, 44100)
- for i := 0; i < 3; i++ {
- s.ReadFrame()
- }
- stats := s.Stats()
- if stats.UnderrunStreak != 3 {
- t.Fatalf("expected streak 3, got %d", stats.UnderrunStreak)
- }
- if stats.MaxUnderrunStreak != 3 {
- t.Fatalf("expected max streak 3, got %d", stats.MaxUnderrunStreak)
- }
-
- if !s.WriteFrame(NewFrame(0, 0)) {
- t.Fatal("expected write to succeed")
- }
- s.ReadFrame()
- stats = s.Stats()
- if stats.UnderrunStreak != 0 {
- t.Fatalf("expected streak reset to 0, got %d", stats.UnderrunStreak)
- }
- if stats.MaxUnderrunStreak != 3 {
- t.Fatalf("expected max streak to stay 3, got %d", stats.MaxUnderrunStreak)
- }
- }
-
- func TestStreamSource_Overflow(t *testing.T) {
- s := NewStreamSource(4, 44100) // size rounds up to 4
-
- // Fill completely
- for i := 0; i < 4; i++ {
- if !s.WriteFrame(NewFrame(Sample(float64(i)/10), 0)) {
- t.Fatalf("write %d failed", i)
- }
- }
-
- // Next write should overflow
- if s.WriteFrame(NewFrame(1, 1)) {
- t.Fatal("expected overflow")
- }
- if s.Overflows.Load() != 1 {
- t.Fatalf("expected 1 overflow, got %d", s.Overflows.Load())
- }
- }
-
- func TestStreamSource_PowerOf2Rounding(t *testing.T) {
- tests := []struct{ in, expect int }{
- {1, 1}, {2, 2}, {3, 4}, {5, 8}, {100, 128}, {1024, 1024}, {1025, 2048},
- }
- for _, tt := range tests {
- s := NewStreamSource(tt.in, 44100)
- if s.size != tt.expect {
- t.Fatalf("NewStreamSource(%d): size=%d, expected %d", tt.in, s.size, tt.expect)
- }
- }
- }
-
- func TestStreamSource_FIFO(t *testing.T) {
- s := NewStreamSource(64, 44100)
- n := 50
- for i := 0; i < n; i++ {
- s.WriteFrame(NewFrame(Sample(float64(i)), 0))
- }
- for i := 0; i < n; i++ {
- f := s.ReadFrame()
- if int(f.L) != i {
- t.Fatalf("FIFO order broken at %d: got %d", i, int(f.L))
- }
- }
- }
-
- func TestStreamSource_Wraparound(t *testing.T) {
- s := NewStreamSource(8, 44100) // size = 8
-
- // Write and read more than buffer size to test wraparound
- for round := 0; round < 10; round++ {
- for i := 0; i < 8; i++ {
- val := float64(round*8 + i)
- if !s.WriteFrame(NewFrame(Sample(val), 0)) {
- t.Fatalf("write failed round=%d i=%d", round, i)
- }
- }
- for i := 0; i < 8; i++ {
- expected := float64(round*8 + i)
- f := s.ReadFrame()
- if float64(f.L) != expected {
- t.Fatalf("round=%d i=%d: got %f expected %f", round, i, float64(f.L), expected)
- }
- }
- }
-
- stats := s.Stats()
- if stats.Underruns != 0 || stats.Overflows != 0 {
- t.Fatalf("unexpected errors: underruns=%d overflows=%d", stats.Underruns, stats.Overflows)
- }
- }
-
- func TestStreamSource_WritePCM(t *testing.T) {
- s := NewStreamSource(256, 44100)
-
- // Create 10 stereo frames of S16LE PCM
- var buf bytes.Buffer
- for i := 0; i < 10; i++ {
- l := int16(i * 1000)
- r := int16(-i * 1000)
- binary.Write(&buf, binary.LittleEndian, l)
- binary.Write(&buf, binary.LittleEndian, r)
- }
-
- written := s.WritePCM(buf.Bytes())
- if written != 10 {
- t.Fatalf("expected 10 frames, wrote %d", written)
- }
-
- // Verify first frame
- f := s.ReadFrame()
- if f.L != 0 || f.R != 0 {
- t.Fatalf("frame 0: L=%.4f R=%.4f, expected 0", f.L, f.R)
- }
- // Verify frame 5
- for i := 1; i < 5; i++ {
- s.ReadFrame()
- }
- f = s.ReadFrame()
- expectedL := 5000.0 / 32768.0
- if math.Abs(float64(f.L)-expectedL) > 0.001 {
- t.Fatalf("frame 5 L=%.4f, expected %.4f", f.L, expectedL)
- }
- }
-
- func TestStreamSource_ConcurrentSPSC(t *testing.T) {
- s := NewStreamSource(4096, 44100)
- frames := 50000
- var producerDone atomic.Bool
-
- var wg sync.WaitGroup
- wg.Add(2)
-
- // Producer
- go func() {
- defer wg.Done()
- for i := 0; i < frames; i++ {
- for !s.WriteFrame(NewFrame(Sample(float64(i+1)), 0)) {
- // Buffer full — yield
- }
- }
- producerDone.Store(true)
- }()
-
- // Consumer
- var lastVal float64
- var orderOK = true
- var readCount int
- go func() {
- defer wg.Done()
- for {
- if s.Available() == 0 {
- if producerDone.Load() {
- break
- }
- continue
- }
- f := s.ReadFrame()
- readCount++
- v := float64(f.L)
- if v > 0 && v < lastVal {
- orderOK = false
- }
- if v > 0 {
- lastVal = v
- }
- }
- }()
-
- wg.Wait()
-
- if !orderOK {
- t.Fatal("FIFO order broken in concurrent SPSC")
- }
- if readCount < frames/2 {
- t.Fatalf("read too few frames: %d (expected ~%d)", readCount, frames)
- }
- }
-
- func TestStreamSource_StatsBufferedDuration(t *testing.T) {
- rate := 48000
- s := NewStreamSource(128, rate)
- for i := 0; i < 24; i++ {
- s.WriteFrame(NewFrame(0, 0))
- }
- stats := s.Stats()
- if stats.BufferedDurationSeconds <= 0 {
- t.Fatalf("expected buffered duration > 0, got %.6f", stats.BufferedDurationSeconds)
- }
- expected := float64(stats.Available) / float64(rate)
- if math.Abs(stats.BufferedDurationSeconds-expected) > 1e-9 {
- t.Fatalf("buffered duration %.9f != expected %.9f", stats.BufferedDurationSeconds, expected)
- }
- }
-
- func TestStreamSource_StatsHighWatermark(t *testing.T) {
- rate := 44100
- s := NewStreamSource(64, rate)
- for i := 0; i < 12; i++ {
- s.WriteFrame(NewFrame(0, 0))
- }
- for i := 0; i < 5; i++ {
- s.ReadFrame()
- }
- stats := s.Stats()
- if stats.HighWatermark != 12 {
- t.Fatalf("expected high watermark 12, got %d", stats.HighWatermark)
- }
- expected := float64(stats.HighWatermark) / float64(rate)
- if math.Abs(stats.HighWatermarkDurationSeconds-expected) > 1e-9 {
- t.Fatalf("high watermark duration %.9f != %.9f", stats.HighWatermarkDurationSeconds, expected)
- }
- if stats.HighWatermark < stats.Available {
- t.Fatalf("high watermark %d < available %d", stats.HighWatermark, stats.Available)
- }
- }
-
-
- // --- StreamResampler tests ---
-
- func TestStreamResampler_1to1(t *testing.T) {
- s := NewStreamSource(256, 44100)
- r := NewStreamResampler(s, 44100) // 1:1
-
- for i := 0; i < 100; i++ {
- s.WriteFrame(NewFrame(Sample(float64(i)/100), 0))
- }
-
- // At 1:1 ratio, output should track input with a small startup delay.
- // Skip first few samples (resampler priming), then verify monotonic increase.
- prev := -1.0
- for i := 0; i < 90; i++ {
- f := r.NextFrame()
- v := float64(f.L)
- if i > 5 && v < prev-0.001 {
- t.Fatalf("sample %d: non-monotonic %.4f < %.4f", i, v, prev)
- }
- if v > 0 {
- prev = v
- }
- }
- // Final value should be close to 0.9 (we wrote 0..0.99)
- if prev < 0.5 {
- t.Fatalf("final value %.4f too low (expected > 0.5)", prev)
- }
- }
-
- func TestStreamResampler_Upsample(t *testing.T) {
- // 44100 → 228000 (ratio ≈ 0.1934, ~5.17× upsampling)
- s := NewStreamSource(4096, 44100)
- r := NewStreamResampler(s, 228000)
-
- // Write 1000 frames of a 1kHz sine at 44100 Hz
- for i := 0; i < 1000; i++ {
- v := math.Sin(2 * math.Pi * 1000 * float64(i) / 44100)
- s.WriteFrame(NewFrame(Sample(v), Sample(v)))
- }
-
- // Read upsampled output — should be ~5170 samples for 1000 input
- // (minus a few for resampler priming)
- out := make([]float64, 0, 5200)
- for i := 0; i < 5000; i++ {
- f := r.NextFrame()
- out = append(out, float64(f.L))
- }
-
- // Verify the output is a smooth sine, not clicks or zeros
- // Check that max amplitude is close to 1.0
- maxAmp := 0.0
- for _, v := range out[100:] { // skip initial ramp
- if math.Abs(v) > maxAmp {
- maxAmp = math.Abs(v)
- }
- }
- if maxAmp < 0.8 {
- t.Fatalf("max amplitude %.4f too low (expected ~1.0)", maxAmp)
- }
-
- // Check smoothness: no sudden jumps > 0.1 between adjacent samples
- maxJump := 0.0
- for i := 101; i < len(out); i++ {
- d := math.Abs(out[i] - out[i-1])
- if d > maxJump {
- maxJump = d
- }
- }
- // At 228kHz with 1kHz tone: max step ≈ sin(2π*1000/228000) ≈ 0.0276
- if maxJump > 0.05 {
- t.Fatalf("max inter-sample jump %.4f (expected < 0.05 for smooth sine)", maxJump)
- }
- }
-
- func TestStreamResampler_Downsample(t *testing.T) {
- // 96000 → 44100 (ratio ≈ 2.177, downsampling)
- s := NewStreamSource(8192, 96000)
- r := NewStreamResampler(s, 44100)
-
- // Write 4000 frames at 96kHz
- for i := 0; i < 4000; i++ {
- v := math.Sin(2 * math.Pi * 440 * float64(i) / 96000)
- s.WriteFrame(NewFrame(Sample(v), 0))
- }
-
- // Should get ~1837 output frames (4000 × 44100/96000)
- count := 0
- for i := 0; i < 1800; i++ {
- f := r.NextFrame()
- _ = f
- count++
- }
- if count != 1800 {
- t.Fatalf("expected 1800 reads, got %d", count)
- }
- }
-
- func TestStreamResampler_NilSource(t *testing.T) {
- r := NewStreamResampler(nil, 228000)
- f := r.NextFrame()
- if f.L != 0 || f.R != 0 {
- t.Fatal("expected silence from nil source")
- }
- }
-
- // --- IngestReader test ---
-
- func TestIngestReader(t *testing.T) {
- s := NewStreamSource(4096, 44100)
-
- // Create PCM data: 100 stereo frames
- var buf bytes.Buffer
- for i := 0; i < 100; i++ {
- l := int16(i * 100)
- r := int16(-i * 100)
- binary.Write(&buf, binary.LittleEndian, l)
- binary.Write(&buf, binary.LittleEndian, r)
- }
-
- // IngestReader should read all data and return nil (EOF)
- err := IngestReader(bytes.NewReader(buf.Bytes()), s)
- if err != nil {
- t.Fatalf("IngestReader: %v", err)
- }
-
- if s.Available() != 100 {
- t.Fatalf("expected 100 frames, got %d", s.Available())
- }
-
- // Verify first and last
- f := s.ReadFrame()
- if f.L != 0 {
- t.Fatalf("frame 0 L=%.4f, expected 0", f.L)
- }
- for i := 1; i < 99; i++ {
- s.ReadFrame()
- }
- f = s.ReadFrame()
- expectedL := 9900.0 / 32768.0
- if math.Abs(float64(f.L)-expectedL) > 0.01 {
- t.Fatalf("frame 99 L=%.4f, expected ~%.4f", f.L, expectedL)
- }
- }
-
- func TestIngestReader_Error(t *testing.T) {
- s := NewStreamSource(256, 44100)
- errReader := &errAfterN{n: 10}
- err := IngestReader(errReader, s)
- if err == nil {
- t.Fatal("expected error")
- }
- }
-
- type errAfterN struct {
- n, count int
- }
-
- func (r *errAfterN) Read(p []byte) (int, error) {
- if r.count >= r.n {
- return 0, io.ErrUnexpectedEOF
- }
- r.count++
- // Return 4 bytes (one stereo frame)
- if len(p) >= 4 {
- p[0], p[1], p[2], p[3] = 0, 0, 0, 0
- return 4, nil
- }
- return 0, nil
- }
|