fed33a4e16
Playback was `go playAudio(reply)` — fire and forget, nobody holding the process handle. Two audible consequences fell out of that. She answered herself. The capture loop kept feeding the VAD while the speaker was running, so her own reply came back in through the mic, tripped the VAD, and was shipped to the daemon as a fresh command. There is no acoustic echo canceller in this pipeline, so the fix is half-duplex: while she is speaking, the capture side is muted. That part is unconditional — it repairs a defect, it is not a new capability. And talking over her did nothing, because there was no handle to cancel. -barge-in now cuts playback when sustained energy clears a room-tuned threshold (-barge-in-rms, default 0.12 normalised, over -barge-in-frames consecutive frames, default 5). It is off by default: without an echo canceller the only way to tell "he is talking over her" from "the mic is hearing her" is that he is much louder, and how much louder depends on where the mic sits. The frame decision moved out of main.go into session.feed, behind a player and an utteranceSender interface, so all of it is testable with no mic, no speaker and no daemon. Nine tests cover the self-hearing case, the off-by-default case, the consecutive-frame requirement, speaker-leak-level audio not triggering, capturing the interrupting utterance after a cut, and failed round-trips not starting playback. The other seven items on #287 (partial STT, per-segment retry, mic profiles, noise-floor calibration, short-response-while-speaking) are untouched and stay on the task.
34 lines
864 B
Go
34 lines
864 B
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/audio"
|
|
)
|
|
|
|
// The real player must be safe to poke when nothing is playing — the capture
|
|
// loop calls Playing() on every 30ms frame, and Stop() lands on an idle
|
|
// player whenever a barge-in races the end of a reply. Neither may need
|
|
// aplay(1) to be installed.
|
|
func TestAplayPlayerIdleIsSafe(t *testing.T) {
|
|
p := newAplayPlayer()
|
|
if p.Playing() {
|
|
t.Fatal("a fresh player reports playing")
|
|
}
|
|
p.Stop()
|
|
p.Stop()
|
|
if p.Playing() {
|
|
t.Fatal("playing after Stop on an idle player")
|
|
}
|
|
// Empty audio is a text-only turn: nothing to play, no process to spawn.
|
|
p.Play(audio.Audio{Format: audio.PCM16kMono})
|
|
if p.Playing() {
|
|
t.Fatal("empty audio started playback")
|
|
}
|
|
}
|
|
|
|
func TestAplayPlayerSatisfiesPlayer(t *testing.T) {
|
|
var _ player = newAplayPlayer()
|
|
var _ player = &fakePlayer{}
|
|
}
|