Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
148 lines
2.6 KiB
Go
148 lines
2.6 KiB
Go
// Package sound plays the cues components make.
|
||
package sound
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/audio"
|
||
)
|
||
|
||
// Cue names one sound.
|
||
type Cue uint8
|
||
|
||
const (
|
||
Tap Cue = iota
|
||
Toggle
|
||
Select
|
||
Tick
|
||
Open
|
||
Close
|
||
Pop
|
||
Drop
|
||
Success
|
||
Warning
|
||
Error
|
||
Notify
|
||
Send
|
||
Delete
|
||
Swipe
|
||
cueCount
|
||
)
|
||
|
||
var names = [cueCount]string{"tap", "toggle", "select", "tick", "open", "close", "pop", "drop", "success",
|
||
"warning", "error", "notify", "send", "delete", "swipe"}
|
||
|
||
func (c Cue) String() string {
|
||
if c >= cueCount {
|
||
return fmt.Sprintf("cue(%d)", uint8(c))
|
||
}
|
||
return names[c]
|
||
}
|
||
|
||
// Cues lists every cue in order.
|
||
func Cues() []Cue {
|
||
out := make([]Cue, cueCount)
|
||
for i := range out {
|
||
out[i] = Cue(i)
|
||
}
|
||
return out
|
||
}
|
||
|
||
const recentCap = 32
|
||
|
||
var (
|
||
mu sync.Mutex
|
||
// Test binaries start muted; tests read Recent.
|
||
enabled = !testing.Testing()
|
||
volume = 0.5
|
||
recent []Cue
|
||
sounds [cueCount]*audio.Sound
|
||
watching sync.Once
|
||
)
|
||
|
||
// Play records the cue and plays it when enabled.
|
||
func Play(c Cue) {
|
||
if c >= cueCount {
|
||
panic("sound: unknown " + c.String())
|
||
}
|
||
mu.Lock()
|
||
recent = append(recent, c)
|
||
if len(recent) > recentCap {
|
||
recent = recent[len(recent)-recentCap:]
|
||
}
|
||
on, v := enabled, volume
|
||
s := sounds[c]
|
||
if on && s == nil {
|
||
s = audio.NewSound(rate, 1, synth(c))
|
||
sounds[c] = s
|
||
}
|
||
mu.Unlock()
|
||
slog.Debug("crychic: sound", "cue", c.String(), "played", on)
|
||
if !on {
|
||
return
|
||
}
|
||
s.NewPlayer().SetVolume(v).Play()
|
||
watching.Do(func() { go watch() })
|
||
}
|
||
|
||
// watch logs each change of the output's error.
|
||
func watch() {
|
||
var last error
|
||
for range time.Tick(time.Second) {
|
||
last = noteErr(last, audio.Err())
|
||
}
|
||
}
|
||
|
||
func noteErr(last, err error) error {
|
||
switch {
|
||
case err != nil && (last == nil || err.Error() != last.Error()):
|
||
slog.Warn("crychic: sound output unavailable", "err", err)
|
||
case err == nil && last != nil:
|
||
slog.Info("crychic: sound output back")
|
||
}
|
||
return err
|
||
}
|
||
|
||
// SetEnabled turns every cue on or off.
|
||
func SetEnabled(on bool) {
|
||
mu.Lock()
|
||
enabled = on
|
||
mu.Unlock()
|
||
slog.Debug("crychic: sound enabled", "on", on)
|
||
}
|
||
|
||
// Enabled reports whether cues play.
|
||
func Enabled() bool {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
return enabled
|
||
}
|
||
|
||
// SetVolume sets the gain of cues, from 0 to 1.
|
||
func SetVolume(v float64) {
|
||
if !(v >= 0 && v <= 1) {
|
||
panic(fmt.Sprintf("sound: volume %v outside 0–1", v))
|
||
}
|
||
mu.Lock()
|
||
volume = v
|
||
mu.Unlock()
|
||
}
|
||
|
||
// Volume returns the gain of cues.
|
||
func Volume() float64 {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
return volume
|
||
}
|
||
|
||
// Recent returns the last cues played or muted, oldest first.
|
||
func Recent() []Cue {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
return append([]Cue(nil), recent...)
|
||
}
|