package player import ( "os" "os/exec" "sync" "time" ) // Player handles alarm sound playback by shelling out to system audio tools. type Player struct { mu sync.Mutex cmd *exec.Cmd playing bool stopCh chan struct{} } func New() *Player { return &Player{} } // resolveSound finds the actual file path for a sound. Returns "" if no file found. func resolveSound(path string) string { if path != "default" && path != "" { if _, err := os.Stat(path); err == nil { return path } return "" } candidates := []string{ "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga", "/usr/share/sounds/gnome/default/alerts/bark.ogg", "/usr/share/sounds/sound-icons/trumpet-12.wav", } for _, c := range candidates { if _, err := os.Stat(c); err == nil { return c } } return "" } // findPlayer returns the name and arg builder for the first available audio player. func findPlayer() (string, func(string) []string) { players := []struct { name string args func(string) []string }{ {"paplay", func(f string) []string { return []string{f} }}, {"aplay", func(f string) []string { return []string{f} }}, {"mpv", func(f string) []string { return []string{"--no-video", "--no-terminal", f} }}, {"ffplay", func(f string) []string { return []string{"-nodisp", "-autoexit", f} }}, } for _, pl := range players { if _, err := exec.LookPath(pl.name); err == nil { return pl.name, pl.args } } return "", nil } // PlayLoop starts playing a sound file in a loop until Stop() is called. func (p *Player) PlayLoop(path string) { p.mu.Lock() defer p.mu.Unlock() p.stop() resolved := resolveSound(path) if resolved == "" { // No sound file — bell loop p.stopCh = make(chan struct{}) p.playing = true go func() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() os.Stdout.WriteString("\a") for { select { case <-p.stopCh: return case <-ticker.C: os.Stdout.WriteString("\a") } } }() return } name, argsFn := findPlayer() if name == "" { os.Stdout.WriteString("\a") return } p.stopCh = make(chan struct{}) p.playing = true go func() { for { select { case <-p.stopCh: return default: } cmd := exec.Command(name, argsFn(resolved)...) cmd.Stdout = nil cmd.Stderr = nil p.mu.Lock() p.cmd = cmd p.mu.Unlock() err := cmd.Run() p.mu.Lock() p.cmd = nil p.mu.Unlock() if err != nil { // Check if we were told to stop select { case <-p.stopCh: return default: } } // Brief pause between loops to avoid hammering select { case <-p.stopCh: return case <-time.After(300 * time.Millisecond): } } }() } // Stop stops any currently playing sound. func (p *Player) Stop() { p.mu.Lock() defer p.mu.Unlock() p.stop() } func (p *Player) stop() { if p.stopCh != nil { close(p.stopCh) p.stopCh = nil } if p.cmd != nil && p.cmd.Process != nil { _ = p.cmd.Process.Kill() _ = p.cmd.Wait() p.cmd = nil } p.playing = false } func (p *Player) IsPlaying() bool { p.mu.Lock() defer p.mu.Unlock() return p.playing }