Add callbacks, crescendo sound and better configs

This commit is contained in:
2026-02-04 20:55:03 +02:00
parent cdcdf2c644
commit 621815ed0f
5 changed files with 235 additions and 33 deletions

View File

@@ -1,17 +1,28 @@
package player
import (
"fmt"
"os"
"os/exec"
"sync"
"time"
)
// CrescendoConfig holds volume ramp settings.
type CrescendoConfig struct {
Enabled bool
StartPct int // 0-100
EndPct int // 0-100
DurationS int // seconds to ramp
}
// Player handles alarm sound playback by shelling out to system audio tools.
type Player struct {
mu sync.Mutex
stopCh chan struct{}
doneCh chan struct{}
mu sync.Mutex
stopCh chan struct{}
doneCh chan struct{}
crescStopCh chan struct{}
crescDoneCh chan struct{}
}
func New() *Player {
@@ -60,6 +71,11 @@ func findPlayer() (string, func(string) []string) {
// PlayLoop starts playing a sound file in a loop until Stop() is called.
func (p *Player) PlayLoop(path string) {
p.PlayLoopWithCrescendo(path, CrescendoConfig{})
}
// PlayLoopWithCrescendo starts playing with optional volume ramp.
func (p *Player) PlayLoopWithCrescendo(path string, cresc CrescendoConfig) {
p.Stop() // kill any previous playback and wait for it to finish
p.mu.Lock()
@@ -69,6 +85,19 @@ func (p *Player) PlayLoop(path string) {
doneCh := p.doneCh
p.mu.Unlock()
// Start crescendo if enabled and pactl is available
if cresc.Enabled && cresc.DurationS > 0 {
if _, err := exec.LookPath("pactl"); err == nil {
p.mu.Lock()
p.crescStopCh = make(chan struct{})
p.crescDoneCh = make(chan struct{})
crescStopCh := p.crescStopCh
crescDoneCh := p.crescDoneCh
p.mu.Unlock()
go p.crescendoLoop(cresc, crescStopCh, crescDoneCh)
}
}
resolved := resolveSound(path)
if resolved == "" {
go p.bellLoop(stopCh, doneCh)
@@ -146,15 +175,74 @@ func (p *Player) bellLoop(stopCh, doneCh chan struct{}) {
}
}
// crescendoLoop gradually raises PulseAudio volume from StartPct to EndPct over DurationS seconds.
func (p *Player) crescendoLoop(cfg CrescendoConfig, stopCh, doneCh chan struct{}) {
defer close(doneCh)
startPct := cfg.StartPct
endPct := cfg.EndPct
if startPct < 0 {
startPct = 0
}
if endPct > 150 { // allow some overdrive but not ridiculous
endPct = 150
}
if startPct >= endPct {
return
}
// Set initial volume
setVolume(startPct)
steps := cfg.DurationS
if steps < 1 {
steps = 1
}
stepSize := float64(endPct-startPct) / float64(steps)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
currentPct := float64(startPct)
for i := 0; i < steps; i++ {
select {
case <-stopCh:
return
case <-ticker.C:
currentPct += stepSize
setVolume(int(currentPct))
}
}
// Ensure we hit the target
setVolume(endPct)
}
// setVolume sets the default PulseAudio sink volume to the given percentage.
func setVolume(pct int) {
_ = exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("%d%%", pct)).Run()
}
// Stop stops any currently playing sound and waits for the playback goroutine to exit.
func (p *Player) Stop() {
p.mu.Lock()
stopCh := p.stopCh
doneCh := p.doneCh
crescStopCh := p.crescStopCh
crescDoneCh := p.crescDoneCh
p.stopCh = nil
p.doneCh = nil
p.crescStopCh = nil
p.crescDoneCh = nil
p.mu.Unlock()
// Stop crescendo
if crescStopCh != nil {
close(crescStopCh)
}
if crescDoneCh != nil {
<-crescDoneCh
}
// Stop audio
if stopCh != nil {
close(stopCh)
}