package main import ( "math" "testing" "time" "github.com/kami/maven/internal/audio" ) // frameRMSQuick computes RMS of raw PCM int16 LE bytes. func frameRMSQuick(raw []byte) float64 { if len(raw) == 0 { return 0 } var sum float64 for i := 0; i < len(raw); i += 2 { s := int16(raw[i]) | int16(raw[i+1])<<8 f := float64(s) / 32768.0 sum += f * f } return math.Sqrt(sum / float64(len(raw)/2)) } // loudFrame returns a 30ms frame of loud sine tone (RMS ~0.5). func loudFrame() []int16 { f := make([]int16, frameSamples) for i := range f { f[i] = int16(16000 * math.Sin(2*math.Pi*440*float64(i)/16000)) } return f } // quietFrame returns a 30ms frame of near-silence (RMS ~0.005). func quietFrame() []int16 { f := make([]int16, frameSamples) for i := range f { f[i] = int16(80 * math.Sin(2*math.Pi*440*float64(i)/16000)) } return f } // silentFrame returns a true silent frame (all zeros). func silentFrame() []int16 { return make([]int16, frameSamples) } func TestVAD_InitialState(t *testing.T) { v := NewVAD(0, 0, 0, 0) if v.State() != StateSilence { t.Fatalf("initial state = %d, want %d (StateSilence)", v.State(), StateSilence) } if v.FrameSamples() != frameSamples { t.Fatalf("FrameSamples = %d, want %d", v.FrameSamples(), frameSamples) } } func TestVAD_SilenceStaysSilence(t *testing.T) { v := NewVAD(0, 0, 0, 0) // Feed silence for many frames — should never transition to speech. for i := 0; i < 50; i++ { utt, state := v.Feed(silentFrame()) if utt.Bytes != nil { t.Fatalf("unexpected utterance at frame %d", i) } if state != StateSilence { t.Fatalf("state = %d at frame %d, want StateSilence", state, i) } } } func TestVAD_SpeechTransition(t *testing.T) { v := NewVAD(0, 0, 0, 0) // Frames needed to trigger speech: ceil(300ms / 30ms) = 10 speechFrames := (defaultSpeechMs + defaultFrameMs - 1) / defaultFrameMs // Feed loud frames one by one. On the last frame state flips to Speech; // no utterance returned — still accumulating. for i := 0; i < speechFrames; i++ { utt, state := v.Feed(loudFrame()) if i < speechFrames-1 { if state != StateSilence { t.Fatalf("state = %d at frame %d, want StateSilence", state, i) } } else { if state != StateSpeech { t.Fatalf("state after trigger = %d, want StateSpeech", state) } } if utt.Bytes != nil { t.Fatalf("unexpected utterance at frame %d (utterance not done)", i) } } } func TestVAD_UtteranceComplete(t *testing.T) { v := NewVAD(0, 0, 0, 0) speechFrames := (defaultSpeechMs + defaultFrameMs - 1) / defaultFrameMs // Silence needs ceil(800/30) = 27 frames; use the tracked duration check. silenceFrames := (defaultSilenceMs + defaultFrameMs - 1) / defaultFrameMs // Trigger speech. for i := 0; i < speechFrames; i++ { v.Feed(loudFrame()) } // Feed a few speech frames. for i := 0; i < 5; i++ { utt, state := v.Feed(loudFrame()) if utt.Bytes != nil { t.Fatalf("unexpected utterance during speech at frame %d", i) } if state != StateSpeech { t.Fatalf("state = %d during speech, want StateSpeech", state) } } // Feed silence frames until utterance completes. for i := 0; i < silenceFrames; i++ { utt, state := v.Feed(quietFrame()) if i < silenceFrames-1 { if utt.Bytes != nil { t.Fatalf("unexpected utterance before silence triggers at frame %d", i) } if state != StateSpeech { t.Fatalf("state = %d during silence tail, want StateSpeech", state) } } else { // Last frame should trigger utterance complete. if state != StateSilence { t.Fatalf("state after utterance = %d, want StateSilence", state) } if utt.Bytes == nil { t.Fatal("expected non-nil utterance after silence trigger") } dur := audio.Audio{Format: audio.PCM16kMono, Bytes: utt.Bytes}.Duration() if dur <= 0 { t.Fatalf("utterance duration %.2f, want > 0", dur) } t.Logf("utterance: %.2fs, %d bytes", dur, len(utt.Bytes)) } } } func TestVAD_MaxUtteranceLength(t *testing.T) { v := NewVAD(0, 0, 0, 100) // max 100ms speechFrames := defaultSpeechMs / defaultFrameMs // Trigger speech. for i := 0; i < speechFrames; i++ { v.Feed(loudFrame()) } // Calculate how many frames until max (100ms / 30ms = 3.33 → ceil 4) maxFrames := 100 / defaultFrameMs // 3 if 100%defaultFrameMs != 0 { maxFrames++ } for i := 0; i < maxFrames-1; i++ { utt, state := v.Feed(loudFrame()) if utt.Bytes != nil { t.Fatalf("unexpected utterance at frame %d before max", i) } if state != StateSpeech { t.Fatalf("unexpected state %d during speech", state) } } // This frame should hit the max and force-end. utt, state := v.Feed(loudFrame()) if state != StateSilence { t.Fatalf("state after max = %d, want StateSilence", state) } if utt.Bytes == nil { t.Fatal("expected utterance after max duration") } dur := audio.Audio{Format: audio.PCM16kMono, Bytes: utt.Bytes}.Duration() if dur > 150*time.Millisecond.Seconds() { t.Fatalf("utterance too long: %.2fs, want <= 150ms", dur) } } func TestVAD_Reset(t *testing.T) { v := NewVAD(0, 0, 0, 0) speechFrames := defaultSpeechMs / defaultFrameMs // Start speaking for i := 0; i < speechFrames; i++ { v.Feed(loudFrame()) } if v.State() != StateSpeech { t.Fatalf("expected StateSpeech after trigger") } // Reset v.Reset() if v.State() != StateSilence { t.Fatalf("expected StateSilence after reset") } // Should be back to silence utt, state := v.Feed(quietFrame()) if utt.Bytes != nil { t.Fatal("expected nil utterance after reset + silence") } if state != StateSilence { t.Fatalf("expected StateSilence after reset, got %d", state) } } func TestVAD_ShortSpeechNotTriggered(t *testing.T) { v := NewVAD(0, 0, 0, 0) // A few loud frames below the speech_ms threshold should not trigger. for i := 0; i < 3; i++ { utt, state := v.Feed(loudFrame()) if utt.Bytes != nil { t.Fatalf("unexpected utterance at frame %d", i) } if state != StateSilence { t.Fatalf("expected StateSilence for short speech, got %d", state) } } } func TestVAD_AdaptiveFloor(t *testing.T) { v := NewVAD(100, 0, 0, 0) // minRMS = 100/10000 = 0.01 speechFrames := defaultSpeechMs / defaultFrameMs // Very quiet frames should lower the floor. for i := 0; i < 30; i++ { v.Feed(silentFrame()) } // floorRMS should be ~0.01 (minRMS clamp) if v.floorRMS < 0.009 || v.floorRMS > 0.011 { t.Fatalf("floorRMS after silence = %.4f, want ~0.01", v.floorRMS) } // A frame at RMS ~0.005 (quietFrame) should now be below the floor // and stay silence. for i := 0; i < speechFrames; i++ { _, state := v.Feed(quietFrame()) if state != StateSilence { t.Fatalf("quiet frame triggered speech (floor=%.4f)", v.floorRMS) } } } func TestVAD_EmptyFrame(t *testing.T) { v := NewVAD(0, 0, 0, 0) utt, state := v.Feed(nil) if utt.Bytes != nil { t.Fatal("expected nil utterance for empty frame") } _ = state // state is undefined for empty; just don't panic } func TestVAD_ConstructedUtterance(t *testing.T) { // Test a realistic scenario: speech followed by silence. v := NewVAD(0, 0, 0, 0) speechFrames := (defaultSpeechMs + defaultFrameMs - 1) / defaultFrameMs silenceFrames := (defaultSilenceMs + defaultFrameMs - 1) / defaultFrameMs // Build frames: trigger threshold + 20 speech frames + silence tail. totalSpeech := speechFrames + 20 frames := make([][]int16, 0, totalSpeech+silenceFrames) for i := 0; i < totalSpeech; i++ { frames = append(frames, loudFrame()) } for i := 0; i < silenceFrames; i++ { frames = append(frames, silentFrame()) } var gotUtterance bool var uttCount int for _, f := range frames { utt, state := v.Feed(f) if utt.Bytes != nil { gotUtterance = true uttCount++ dur := audio.Audio{Format: audio.PCM16kMono, Bytes: utt.Bytes}.Duration() // Utterance includes: 1 trigger frame + 20 speech + all silence // frames before the threshold frames. silenceFrames count already // includes the trailing frame that trips the return. expectedFrames := 1 + 20 + silenceFrames expectedDur := float64(expectedFrames) * float64(defaultFrameMs) / 1000.0 if dur < expectedDur*0.8 || dur > expectedDur*1.2 { t.Fatalf("utterance duration %.2fs (expected ~%.2fs, %d frames, silenceFrames=%d)", dur, expectedDur, expectedFrames, silenceFrames) } t.Logf("utterance: %.2fs, expected ~%.2fs (%d frames)", dur, expectedDur, expectedFrames) } if state != StateSilence && utt.Bytes != nil { t.Fatalf("utterance returned but state = %d, want StateSilence", state) } } if !gotUtterance { t.Fatal("no utterance produced from speech+silence") } if uttCount != 1 { t.Fatalf("expected exactly 1 utterance, got %d", uttCount) } if v.State() != StateSilence { t.Fatalf("final state = %d, want StateSilence", v.State()) } } // Benchmarks. func BenchmarkVAD(b *testing.B) { v := NewVAD(0, 0, 0, 0) f := loudFrame() b.ResetTimer() for i := 0; i < b.N; i++ { v.Feed(f) } } func BenchmarkFrameRMS(b *testing.B) { f := loudFrame() b.ResetTimer() for i := 0; i < b.N; i++ { frameRMS(f) } }