Pierre-Olivier Mercier
25e5362d01
All checks were successful
continuous-integration/drone/push Build is passing
305 lines
6.5 KiB
Go
305 lines
6.5 KiB
Go
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
|
|
}
|