Files

99 lines
2.4 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"math"
"os"
"os/exec"
"strconv"
"sync"
"time"
)
const volumeFile = "/data/opt/alice-backend/station-volume"
const pulseControl = "/data/opt/alice-backend/spotify/pactl"
var volumeMu sync.Mutex
var toneMu sync.Mutex
func changeVolume(delta int) (int, error) {
volumeMu.Lock()
defer volumeMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var sinks []struct {
Name string
Volume map[string]struct{ Value int }
}
data, err := exec.CommandContext(ctx, pulseControl, "--format=json", "list", "sinks").Output()
if err != nil {
return 0, err
}
if err := json.Unmarshal(data, &sinks); err != nil {
return 0, err
}
for _, sink := range sinks {
if sink.Name != "station" {
continue
}
channel, ok := sink.Volume["front-left"]
if !ok {
break
}
current := (channel.Value*100 + 32768) / 65536
next := max(0, min(100, current+delta))
if next == current {
return current, nil
}
return next, applyVolume(ctx, next)
}
return 0, errors.New("station sink unavailable")
}
func setStationVolume(level int) error {
volumeMu.Lock()
defer volumeMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return applyVolume(ctx, level)
}
func applyVolume(ctx context.Context, level int) error {
volume := strconv.Itoa(level)
if err := exec.CommandContext(ctx, pulseControl, "set-sink-volume", "station", volume+"%").Run(); err != nil {
return err
}
if err := os.WriteFile(volumeFile+".tmp", []byte(volume+"\n"), 0600); err != nil {
return err
}
return os.Rename(volumeFile+".tmp", volumeFile)
}
func playVolumeTone(playback string, level int) {
if level == 0 || !toneMu.TryLock() {
return
}
defer toneMu.Unlock()
// The master output attenuates this short, soft tone at the selected level.
const sampleRate = 24000
samples := make([]int16, sampleRate*90/1000)
frequency := 430 + 4*float64(level)
for i := range samples {
envelope := math.Sin(math.Pi * float64(i) / float64(len(samples)-1))
samples[i] = int16(3400 * envelope * envelope * math.Sin(2*math.Pi*frequency*float64(i)/sampleRate))
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
player := playbackCommand(ctx, playback)
player.Stdin = bytes.NewReader(pcm(samples))
if err := player.Run(); err != nil {
log.Printf("volume tone: %v", err)
}
}