package player import ( "fmt" "log" "math" "math/rand" "os" "os/exec" "os/signal" "strings" "syscall" "time" "git.nemunai.re/nemunaire/reveil/config" "git.nemunai.re/nemunaire/reveil/model" ) var CommonPlayer *Player type Player struct { Playlist []string MaxRunTime time.Duration MaxVolume uint16 Stopper chan bool currentCmd *exec.Cmd currentCmdCh chan bool weatherTime time.Duration weatherAction *reveil.Action claironTime time.Duration claironFile string endRoutines []*reveil.Routine ntick int64 hasSpokeWeather bool launched time.Time volume uint16 dontUpdateVolume bool reverseOrder bool playedItem int } func WakeUp(cfg *config.Config, routine []reveil.Identifier, federated bool) (err error) { if CommonPlayer != nil { return fmt.Errorf("Unable to start the player: a player is already running") } seed := time.Now().Unix() seed -= seed % 172800 if federated { settings, err := reveil.ReadSettings(cfg.SettingsFile) if err != nil { return fmt.Errorf("Unable to read settings: %w", err) } for k, srv := range settings.Federation { FederatedWakeUp(k, srv, seed) } } return WakeUpFromFederation(cfg, seed, routine) } func WakeUpFromFederation(cfg *config.Config, seed int64, routine []reveil.Identifier) (err error) { rand.Seed(seed) CommonPlayer, err = NewPlayer(cfg, routine) if err != nil { return err } go CommonPlayer.WakeUp(cfg) return nil } func NewPlayer(cfg *config.Config, routines []reveil.Identifier) (*Player, error) { // Load our settings settings, err := reveil.ReadSettings(cfg.SettingsFile) if err != nil { return nil, fmt.Errorf("Unable to read settings: %w", err) } // Load weather action wact, err := reveil.LoadAction(cfg, settings.WeatherAction) if err != nil { log.Println("Unable to load weather action:", err.Error()) } p := Player{ Stopper: make(chan bool, 1), currentCmdCh: make(chan bool, 1), MaxRunTime: settings.MaxRunTime * time.Minute, MaxVolume: uint16(settings.MaxVolume), weatherTime: settings.WeatherDelay * time.Minute, weatherAction: wact, claironTime: settings.GongInterval * time.Minute, claironFile: reveil.CurrentGongPath(cfg), reverseOrder: int(time.Now().Unix()/86400)%2 == 0, } // Load routines for _, routine := range routines { r, err := reveil.LoadRoutineFromId(routine, cfg) if err != nil { log.Printf("Unable to load routine %x: %s", routine, err.Error()) continue } p.endRoutines = append(p.endRoutines, r) } // Load our track list tracks, err := reveil.LoadTracks(cfg) if err != nil { return nil, fmt.Errorf("Unable to load tracks: %w", err) } // Creating playlist log.Println("Loading playlist...") for _, track := range tracks { if !track.Enabled { continue } p.Playlist = append(p.Playlist, track.Path) } log.Println("Shuffling playlist...") // Shuffle the playlist rand.Shuffle(len(p.Playlist), func(i, j int) { p.Playlist[i], p.Playlist[j] = p.Playlist[j], p.Playlist[i] }) return &p, nil } func (p *Player) launchAction(cfg *config.Config, a *reveil.Action) (err error) { settings, err := reveil.ReadSettings(cfg.SettingsFile) if err != nil { return fmt.Errorf("unable to read settings: %w", err) } p.currentCmd, err = a.Launch(settings) log.Println("Running action ", a.Name) err = p.currentCmd.Wait() p.currentCmdCh <- true return } func (p *Player) playFile(filepath string) (err error) { p.currentCmd = exec.Command(playCommand, filepath) if err = p.currentCmd.Start(); err != nil { log.Println("Running paplay err: ", err.Error()) p.currentCmdCh <- true return } log.Println("Running paplay ", filepath) err = p.currentCmd.Wait() p.currentCmdCh <- true return } func (p *Player) WakeUp(cfg *config.Config) { log.Println("Playlist in use:", strings.Join(p.Playlist, " ; ")) // Prepare sound player p.volume = 3500 ticker := time.NewTicker(time.Second) defer ticker.Stop() p.launched = time.Now() // Prepare graceful shutdown maxRun := time.After(p.MaxRunTime) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGHUP) p.currentCmdCh <- true loop: for { select { case <-p.currentCmdCh: if time.Since(p.launched) >= p.claironTime { log.Println("clairon time!") p.claironTime += p.claironTime / 2 p.SetVolume(65535) p.dontUpdateVolume = true go p.playFile(p.claironFile) } else if p.weatherAction != nil && !p.hasSpokeWeather && time.Since(p.launched) >= p.weatherTime { log.Println("weather time!") p.dontUpdateVolume = true p.hasSpokeWeather = true go p.launchAction(cfg, p.weatherAction) } else { p.dontUpdateVolume = false p.volume = uint16(math.Log(1+float64(p.ntick)/8) * 9500) p.SetVolume(p.volume) if p.reverseOrder { p.playedItem -= 1 } else { p.playedItem += 1 } if p.playedItem >= len(p.Playlist) { p.playedItem = 0 } else if p.playedItem < 0 { p.playedItem = len(p.Playlist) - 1 } log.Println("Next track: ", p.Playlist[p.playedItem]) go p.playFile(p.Playlist[p.playedItem]) } case <-ticker.C: p.ntick += 1 if !p.dontUpdateVolume { p.volume = 3500 + uint16(math.Log(1+float64(p.ntick)/8)*9500) p.SetVolume(p.volume) } case <-p.Stopper: log.Println("Stopper activated") break loop case <-maxRun: log.Println("Max run time exhausted") break loop case <-interrupt: break loop } } log.Println("Stopping the player...") // Calm down music loopcalm: for i := 0; i < 128 && p.volume >= 768; i += 1 { timer := time.NewTimer(40 * time.Millisecond) p.volume -= 768 p.SetVolume(p.volume) select { case <-p.Stopper: log.Println("Hard stop received...") timer.Stop() p.volume = 0 break loopcalm case <-timer.C: break } } if p.currentCmd != nil && p.currentCmd.Process != nil { p.currentCmd.Process.Kill() } p.SetVolume(65535) if p == CommonPlayer { log.Println("Destoying common player") CommonPlayer = nil } // TODO: Start Routine if any for _, r := range p.endRoutines { go r.Launch(cfg) } } func (p *Player) NextTrack() { if p.currentCmd != nil && p.currentCmd.Process != nil { p.currentCmd.Process.Kill() } } func (p *Player) SetVolume(volume uint16) error { if p.MaxVolume == 0 { p.MaxVolume = 65535 } cmd := exec.Command("amixer", "-D", mixerCard, "set", mixerName, fmt.Sprintf("%d", uint32(volume)*uint32(p.MaxVolume)/65535)) return cmd.Run() } func (p *Player) Stop() error { log.Println("Trying to stop the player") p.Stopper <- true return nil }