161 lines
3.6 KiB
Go
161 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
type keywordDetector struct {
|
|
cmd *exec.Cmd
|
|
input io.WriteCloser
|
|
output *os.File
|
|
packet [1 + hop*2]byte
|
|
}
|
|
|
|
func startKeywordDetector(helper, model string) (*keywordDetector, error) {
|
|
cmd := exec.Command(helper, model)
|
|
cmd.Stderr = os.Stderr
|
|
terminateWithClient(cmd)
|
|
input, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
output, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
input.Close()
|
|
return nil, err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
input.Close()
|
|
output.Close()
|
|
return nil, err
|
|
}
|
|
detector := &keywordDetector{cmd: cmd, input: input, output: output.(*os.File)}
|
|
if _, err := detector.result(5 * time.Second); err != nil {
|
|
detector.close()
|
|
return nil, err
|
|
}
|
|
return detector, nil
|
|
}
|
|
|
|
func (d *keywordDetector) result(timeout time.Duration) (bool, error) {
|
|
if err := d.output.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
|
return false, err
|
|
}
|
|
var result [1]byte
|
|
if _, err := io.ReadFull(d.output, result[:]); err != nil {
|
|
return false, err
|
|
}
|
|
if result[0] > 1 {
|
|
return false, errors.New("invalid keyword detector response")
|
|
}
|
|
return result[0] == 1, nil
|
|
}
|
|
|
|
func (d *keywordDetector) process(samples []int16) (bool, error) {
|
|
d.packet[0] = 1
|
|
for i, sample := range samples {
|
|
binary.LittleEndian.PutUint16(d.packet[1+i*2:], uint16(sample))
|
|
}
|
|
if _, err := d.input.Write(d.packet[:]); err != nil {
|
|
return false, err
|
|
}
|
|
return d.result(time.Second)
|
|
}
|
|
|
|
func (d *keywordDetector) reset() error {
|
|
if _, err := d.input.Write([]byte{0}); err != nil {
|
|
return err
|
|
}
|
|
_, err := d.result(time.Second)
|
|
return err
|
|
}
|
|
|
|
func (d *keywordDetector) close() {
|
|
d.input.Close()
|
|
d.cmd.Process.Kill()
|
|
d.cmd.Wait()
|
|
}
|
|
|
|
func runKeywordWake(live *liveReader, detector *keywordDetector, panelWake <-chan struct{}, activate func([]int16) error, once bool) error {
|
|
if detector == nil {
|
|
log.Print("Listening locally with the Mini 2 firmware spotter")
|
|
} else {
|
|
log.Print("Listening locally with microWakeWord")
|
|
}
|
|
// Keep 800 ms locally so recognition can see the whole wake word and
|
|
// any beginning of the command that overlaps the detector decision.
|
|
preroll := make([]int16, 8*rate/10)
|
|
next := 0
|
|
available := 0
|
|
muted := false
|
|
for {
|
|
frame, err := live.next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if frame.muted || !unmuted() {
|
|
next, available = 0, 0
|
|
if detector != nil && !muted {
|
|
if err := detector.reset(); err != nil {
|
|
return err
|
|
}
|
|
muted = true
|
|
}
|
|
continue
|
|
}
|
|
muted = false
|
|
button := false
|
|
select {
|
|
case <-panelWake:
|
|
button = true
|
|
default:
|
|
}
|
|
copy(preroll[next:], frame.pcm)
|
|
next = (next + hop) % len(preroll)
|
|
available = min(available+hop, len(preroll))
|
|
detected := frame.wake
|
|
if detector != nil {
|
|
detected, err = detector.process(frame.pcm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !detected && !button {
|
|
continue
|
|
}
|
|
audio := make([]int16, 0, available)
|
|
if button {
|
|
log.Print("touch panel activated assistant")
|
|
live.discardPending()
|
|
} else {
|
|
log.Print("wake word detected")
|
|
if available == len(preroll) {
|
|
audio = append(audio, preroll[next:]...)
|
|
}
|
|
audio = append(audio, preroll[:next]...)
|
|
}
|
|
if err := activate(audio); err != nil {
|
|
log.Printf("session: %v", err)
|
|
if !errors.Is(err, errMuted) && !errors.Is(err, errNoSpeech) && !errors.Is(err, context.Canceled) {
|
|
lights.flashError()
|
|
}
|
|
} else if once {
|
|
return nil
|
|
}
|
|
live.discardPending()
|
|
next, available = 0, 0
|
|
if detector != nil {
|
|
if err := detector.reset(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|