99 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|