package main import ( "bytes" "flag" "fmt" "log" "math" "math/rand" "os" "os/exec" "os/signal" "path" "strconv" "strings" "syscall" "time" "github.com/faiface/beep" "github.com/faiface/beep/effects" "github.com/faiface/beep/flac" "github.com/faiface/beep/mp3" "github.com/faiface/beep/speaker" "github.com/faiface/beep/wav" ) var ( MaxRunTime = 1 * time.Hour ntick int64 = 0 ) func xPrintIdle() (idle uint64) { cmd := exec.Command("xprintidle") cmd.Env = append(os.Environ(), "DISPLAY=:0") var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { log.Println(err) } else { s := string(out.Bytes()) if idle, err = strconv.ParseUint(strings.TrimSpace(s), 10, 64); err != nil { log.Println(err) } } return } func speakToday() { cmdSetVolume := exec.Command("amixer", "-D", "pulse", "set", "Master", fmt.Sprintf("%d%%", 50+50/(int64(MaxRunTime.Seconds()/3)/ntick+1))) if err := cmdSetVolume.Run(); err != nil { log.Println(err) } cmd0 := exec.Command("/home/nemunaire/scripts/wakeup/today.sh") if err := cmd0.Run(); err != nil { log.Println(err) } cmdSetBackVolume := exec.Command("amixer", "-D", "pulse", "set", "Master", "100%") if err := cmdSetBackVolume.Run(); err != nil { log.Println(err) } } func speakWeather() { var icon = "partly-cloudy-day" cmd := exec.Command("/home/nemunaire/scripts/wakeup/ind_weather.sh") var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { log.Println(err) } else { icon = string(out.Bytes()) } var preset = "pika-summer-forest.xml" switch icon { case "clear-day", "clear-night": preset = "grassland.xml" case "rain", "hail": preset = "the-perfect-storm.xml" case "snow", "sleet": preset = "a-walk-in-the-cold.xml" case "wind", "tornado": preset = "desert-wind.xml" case "fog": preset = "silent-hill-fog-world.xml" case "cloudy": preset = "autumn-forest.xml" case "thunderstorm": preset = "heavy-thunderstorm-for-me.xml" default: preset = "autumn-forest.xml" } cmdAmbiant := exec.Command("python3", "ambient.py", path.Join("presets", preset)) cmdAmbiant.Dir = "/home/nemunaire/workspace/pyambientmixer" if err := cmdAmbiant.Start(); err != nil { log.Println(err) } cmdSetVolume := exec.Command("amixer", "-D", "pulse", "set", "Master", fmt.Sprintf("%d%%", 50+50/(int64(MaxRunTime.Seconds()/3)/ntick+1))) if err := cmdSetVolume.Run(); err != nil { log.Println(err) } cmd0 := exec.Command("/home/nemunaire/scripts/wakeup/today.sh") if err := cmd0.Run(); err != nil { log.Println(err) } cmd1 := exec.Command("/home/nemunaire/scripts/wakeup/weather.sh") if err := cmd1.Run(); err != nil { log.Println(err) } cmd2 := exec.Command("/home/nemunaire/scripts/wakeup/airparif.sh") if err := cmd2.Run(); err != nil { log.Println(err) } cmd3 := exec.Command("/home/nemunaire/scripts/wakeup/ratp-traffic.sh", "rers", "B") if err := cmd3.Run(); err != nil { log.Println(err) } if cmdAmbiant.Process != nil { (*cmdAmbiant.Process).Kill() } cmdAmbiant.Process.Wait() cmdSetBackVolume := exec.Command("amixer", "-D", "pulse", "set", "Master", "100%") if err := cmdSetBackVolume.Run(); err != nil { log.Println(err) } } func loadFile(filepath string) (name string, s beep.StreamSeekCloser, format beep.Format, err error) { var fd *os.File name = path.Base(filepath) fd, err = os.Open(filepath) if err != nil { return } switch strings.ToLower(path.Ext(filepath)) { case ".flac": s, format, err = flac.Decode(fd) case ".mp3": s, format, err = mp3.Decode(fd) default: s, format, err = wav.Decode(fd) } if err != nil { fd.Close() return } return } func main() { var weatherTime = flag.Duration("weather", -1, "Speak weather?") var noshuffle = flag.Bool("noshuffle", false, "Don't shuffle music order") var ignoreidle = flag.Bool("ignoreidle", false, "Don't stop the reveil on idle detection change") var sr = flag.Int("samplerate", 44100, "Samplerate for unifying output stream") var claironTime = flag.Duration("clairon", -1, "Time before running the wake up clairon song") flag.DurationVar(&MaxRunTime, "maxruntime", MaxRunTime, "Maximum duration before auto exit") flag.Parse() if len(flag.Args()) < 1 { log.Println("missing required argument: input file name") return } seed := time.Now().Unix() seed -= seed % 172800 reverseOrder := int(time.Now().Unix()/86400)%2 == 0 log.Println("Starting reveil with seed:", seed, "; order:", reverseOrder) rand.Seed(seed) sampleRate := beep.SampleRate(*sr) paths := []string{} playlist := []beep.Streamer{} formats := []beep.Format{} // Load playlist log.Println("Loading playlist...") for _, arg := range flag.Args() { p, s, f, err := loadFile(arg) if err != nil { log.Printf("Unable to load %s: %s", arg, err) continue } paths = append(paths, p) playlist = append(playlist, s) formats = append(formats, f) } if noshuffle == nil || *noshuffle == false { log.Println("Shuffling playlist...") // Shuffle the playlist rand.Shuffle(len(playlist), func(i, j int) { paths[i], paths[j] = paths[j], paths[i] playlist[i], playlist[j] = playlist[j], playlist[i] formats[i], formats[j] = formats[j], formats[i] }) } log.Println("Playlist in use:", strings.Join(paths, " ; ")) var launched time.Time var volume *effects.Volume dontUpdateVolume := false hasClaironed := claironTime == nil || *claironTime == -1 hasSpokeWeather := weatherTime == nil || *weatherTime == -1 playedItem := -1 // Create infinite stream stream := beep.Iterate(func() beep.Streamer { if !hasClaironed && time.Since(launched) >= *claironTime { log.Println("clairon time!") *claironTime += *claironTime / 2 //_, sample, format, err := loadFile("/home/nemunaire/www/audio/miracle-morning/clairon-reveil.mp3") //_, sample, format, err := loadFile("/home/nemunaire/www/audio/miracle-morning/coq.flac") _, sample, format, err := loadFile("/home/nemunaire/www/audio/miracle-morning/NukeAnthem.flac") if err == nil { volume.Volume = 0.1 dontUpdateVolume = true if format.SampleRate != sampleRate { return beep.Resample(3, format.SampleRate, sampleRate, sample) } else { return sample } } else { log.Println("Error loading clairon:", err) } } if !hasSpokeWeather && time.Since(launched) >= *weatherTime { log.Println("weather time!") hasSpokeWeather = true return beep.Callback(speakWeather) } dontUpdateVolume = false volume.Volume = -2 - math.Log(5/float64(ntick))/3 if reverseOrder { playedItem -= 1 } else { playedItem += 1 } if playedItem >= len(playlist) { playedItem = 0 } else if playedItem < 0 { playedItem = len(playlist) - 1 } if i, ok := playlist[playedItem].(beep.StreamSeekCloser); ok { // In case of loop, ensure we are at the beginning of the stream i.Seek(0) } // Resample if needed log.Println("playing list item:", playedItem, "/", len(playlist)) if formats[playedItem].SampleRate != sampleRate { return beep.Resample(3, formats[playedItem].SampleRate, sampleRate, playlist[playedItem]) } else { return playlist[playedItem] } }) // Prepare sound player log.Println("Initializing sound player...") speaker.Init(sampleRate, sampleRate.N(time.Second/10)) volume = &effects.Volume{stream, 10, -2, false} speaker.Play(volume) ticker := time.NewTicker(time.Second) defer ticker.Stop() launched = time.Now() idle := xPrintIdle() // Prepare graceful shutdown maxRun := time.After(MaxRunTime) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) loop: for { select { case <-maxRun: break loop case <-ticker.C: ntick += 1 if !dontUpdateVolume { volume.Volume = -2 - math.Log(5/float64(ntick))/3 } if ignoreidle == nil || !*ignoreidle { if idle < 60000 { idle = xPrintIdle() } else if xPrintIdle() < idle { break loop } } case <-interrupt: break loop } } // Tell parent process that it can launch the wake up procedure if time.Since(launched) < MaxRunTime && time.Since(launched) > 60*time.Second { if proc, err := os.FindProcess(os.Getppid()); err != nil { log.Println(err) } else if err := proc.Signal(syscall.SIGHUP); err != nil { log.Println(err) } } // Calm down music for i := 0; i < 2000; i += 1 { volume.Volume -= 0.001 time.Sleep(4 * time.Millisecond) } }