Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
package sound
|
|
|
|
import "math"
|
|
|
|
const rate = 48000
|
|
|
|
type wave uint8
|
|
|
|
const (
|
|
sine wave = iota
|
|
triangle
|
|
)
|
|
|
|
// note is one tone sweeping from f0 to f1 Hz.
|
|
type note struct {
|
|
at, dur float64
|
|
f0, f1 float64
|
|
gain float64
|
|
wave wave
|
|
}
|
|
|
|
var scores = [cueCount][]note{
|
|
Tap: {{0, 0.025, 1800, 1200, 0.5, sine}},
|
|
Toggle: {{0, 0.03, 880, 880, 0.4, sine}, {0.035, 0.03, 1320, 1320, 0.4, sine}},
|
|
Select: {{0, 0.05, 1046, 1046, 0.4, sine}},
|
|
Tick: {{0, 0.008, 3000, 3000, 0.3, sine}},
|
|
Open: {{0, 0.09, 500, 900, 0.4, sine}},
|
|
Close: {{0, 0.09, 900, 500, 0.4, sine}},
|
|
Pop: {{0, 0.04, 400, 1200, 0.5, sine}},
|
|
Drop: {{0, 0.08, 700, 200, 0.5, sine}},
|
|
Success: {{0, 0.12, 523, 523, 0.3, sine}, {0.07, 0.12, 659, 659, 0.3, sine}, {0.14, 0.18, 784, 784, 0.3, sine}},
|
|
Warning: {{0, 0.09, 660, 660, 0.35, triangle}, {0.13, 0.09, 660, 660, 0.35, triangle}},
|
|
Error: {{0, 0.08, 220, 180, 0.4, triangle}, {0.1, 0.08, 220, 180, 0.4, triangle}},
|
|
Notify: {{0, 0.25, 784, 784, 0.3, sine}, {0.1, 0.3, 1046, 1046, 0.3, sine}},
|
|
Send: {{0, 0.12, 300, 1500, 0.35, triangle}},
|
|
Delete: {{0, 0.14, 600, 150, 0.4, triangle}},
|
|
Swipe: {{0, 0.07, 1200, 400, 0.25, sine}},
|
|
}
|
|
|
|
// synth renders a cue's notes to mono samples.
|
|
func synth(c Cue) []float32 {
|
|
score := scores[c]
|
|
if len(score) == 0 {
|
|
panic("sound: no score for " + c.String())
|
|
}
|
|
end := 0.0
|
|
for _, n := range score {
|
|
end = max(end, n.at+n.dur)
|
|
}
|
|
out := make([]float32, int(end*rate)+1)
|
|
for _, n := range score {
|
|
start, frames := int(n.at*rate), int(n.dur*rate)
|
|
phase := 0.0
|
|
for i := range frames {
|
|
t := float64(i) / rate
|
|
f := n.f0 + (n.f1-n.f0)*t/n.dur
|
|
phase += 2 * math.Pi * f / rate
|
|
out[start+i] += float32(n.gain * envelope(t, n.dur) * shape(n.wave, phase))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// envelope: 3 ms attack, decay, 2 ms fade.
|
|
func envelope(t, dur float64) float64 {
|
|
e := math.Exp(-5 * t / dur)
|
|
if t < 0.003 {
|
|
e *= t / 0.003
|
|
}
|
|
if left := dur - t; left < 0.002 {
|
|
e *= left / 0.002
|
|
}
|
|
return e
|
|
}
|
|
|
|
func shape(w wave, phase float64) float64 {
|
|
if w == triangle {
|
|
return 2 / math.Pi * math.Asin(math.Sin(phase))
|
|
}
|
|
return math.Sin(phase)
|
|
}
|