Files
alice-backend/station/conversation.go
T

128 lines
2.7 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"math"
"sync"
"time"
)
type conversation struct {
mu sync.Mutex
cancel context.CancelFunc
commands chan byte
panelWake chan struct{}
}
func (c *conversation) begin() (context.Context, func()) {
ctx, cancel := context.WithCancel(context.Background())
c.mu.Lock()
c.cancel = cancel
c.mu.Unlock()
return ctx, func() {
cancel()
c.mu.Lock()
c.cancel = nil
c.mu.Unlock()
}
}
func (c *conversation) stop() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.cancel != nil {
c.cancel()
return true
}
return false
}
func (c *conversation) command(cmd byte) {
if cmd == 1 {
c.stop()
return
}
c.mu.Lock()
defer c.mu.Unlock()
if c.cancel == nil {
select {
case c.commands <- cmd:
default:
}
}
}
func (c *conversation) localCommands() {
for cmd := range c.commands {
if !unmuted() {
continue
}
if cmd == 2 || cmd == 3 {
delta := 10
if cmd == 3 {
delta = -10
}
level, err := changeVolume(delta)
if err != nil {
log.Printf("local volume: %v", err)
continue
}
lights.showVolume(level)
continue
}
action := [...]string{4: "pause", 5: "resume", 6: "next", 7: "previous"}[cmd]
args, _ := json.Marshal(map[string]string{"action": action})
result := musicCall("spotify_control", args)
var reply struct {
Error string
Accepted bool
}
if json.Unmarshal(result, &reply) != nil || reply.Error != "" {
lights.flashError()
} else if reply.Accepted {
lights.confirm(action)
}
}
}
type turnOptions struct {
FollowUpMs int `json:"followUpMs"`
ActivationSound bool `json:"activationSound"`
spoken bool
}
func (c *conversation) dialogue(url, token string, live *liveReader, preroll []int16, threshold float64, max, silence time.Duration, playback string, once bool) error {
options := turnOptions{}
var followUp time.Duration
for {
ctx, finish := c.begin()
err := exchange(ctx, url, token, live, nil, preroll, threshold, max, silence, playback, followUp, &options)
finish()
if followUp > 0 && errors.Is(err, errNoSpeech) {
return nil
}
if err != nil || once || !options.spoken || options.FollowUpMs <= 0 {
return err
}
followUp = time.Duration(min(options.FollowUpMs, 10000)) * time.Millisecond
preroll = nil
}
}
func activationTone(ctx context.Context, playback string) {
ctx, cancel := context.WithTimeout(ctx, 450*time.Millisecond)
defer cancel()
samples := make([]int16, 2400)
for i := range samples {
envelope := math.Sin(math.Pi * float64(i) / float64(len(samples)))
samples[i] = int16(1600 * envelope * envelope * math.Sin(2*math.Pi*660*float64(i)/24000))
}
player := playbackCommand(ctx, playback)
player.Stdin = bytes.NewReader(pcm(samples))
_ = player.Run()
}