128 lines
2.7 KiB
Go
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()
|
|
}
|