package player import ( "os" "os/exec" "sync" ) // Player handles alarm sound playback by shelling out to system audio tools. type Player struct { mu sync.Mutex cmd *exec.Cmd playing bool } func New() *Player { return &Player{} } // Play starts playing a sound file. If path is "default", it uses a system beep. func (p *Player) Play(path string) { p.mu.Lock() defer p.mu.Unlock() p.stop() if path == "default" || path == "" { // Try common system sounds, fall back to terminal bell 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 { path = c break } } if path == "default" || path == "" { // No sound files found — use terminal bell in a loop go func() { for i := 0; i < 10; i++ { os.Stdout.WriteString("\a") } }() return } } // Try players in order of preference 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", "--loop=inf", f} }}, {"ffplay", func(f string) []string { return []string{"-nodisp", "-autoexit", "-loop", "0", f} }}, } for _, pl := range players { if _, err := exec.LookPath(pl.name); err == nil { p.cmd = exec.Command(pl.name, pl.args(path)...) p.cmd.Stdout = nil p.cmd.Stderr = nil p.playing = true go func() { _ = p.cmd.Run() p.mu.Lock() p.playing = false p.mu.Unlock() }() return } } // Absolute fallback: terminal bell os.Stdout.WriteString("\a") } // 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.cmd != nil && p.cmd.Process != nil && p.playing { _ = p.cmd.Process.Kill() _ = p.cmd.Wait() p.playing = false p.cmd = nil } } func (p *Player) IsPlaying() bool { p.mu.Lock() defer p.mu.Unlock() return p.playing }