257 lines
5.3 KiB
Go
257 lines
5.3 KiB
Go
|
package player
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"math"
|
||
|
"math/rand"
|
||
|
"os"
|
||
|
"os/signal"
|
||
|
"path"
|
||
|
"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"
|
||
|
|
||
|
"git.nemunai.re/nemunaire/reveil/config"
|
||
|
"git.nemunai.re/nemunaire/reveil/model"
|
||
|
)
|
||
|
|
||
|
var CommonPlayer *Player
|
||
|
|
||
|
type Player struct {
|
||
|
Playlist []string
|
||
|
MaxRunTime time.Duration
|
||
|
Stopper chan bool
|
||
|
|
||
|
sampleRate beep.SampleRate
|
||
|
|
||
|
claironTime time.Duration
|
||
|
claironFile string
|
||
|
|
||
|
ntick int64
|
||
|
hasClaironed bool
|
||
|
launched time.Time
|
||
|
volume *effects.Volume
|
||
|
dontUpdateVolume bool
|
||
|
reverseOrder bool
|
||
|
playedItem int
|
||
|
}
|
||
|
|
||
|
func WakeUp(cfg *config.Config) (err error) {
|
||
|
if CommonPlayer != nil {
|
||
|
return fmt.Errorf("Unable to start the player: a player is already running")
|
||
|
}
|
||
|
|
||
|
CommonPlayer, err = NewPlayer(cfg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
go CommonPlayer.WakeUp()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func NewPlayer(cfg *config.Config) (*Player, error) {
|
||
|
// Load our settings
|
||
|
settings, err := reveil.ReadSettings(cfg.SettingsFile)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Unable to read settings: %w", err)
|
||
|
}
|
||
|
|
||
|
p := Player{
|
||
|
Stopper: make(chan bool, 1),
|
||
|
MaxRunTime: settings.MaxRunTime * time.Minute,
|
||
|
sampleRate: beep.SampleRate(cfg.SampleRate),
|
||
|
claironTime: settings.GongInterval * time.Minute,
|
||
|
claironFile: reveil.CurrentGongPath(cfg),
|
||
|
}
|
||
|
|
||
|
// Load our track list
|
||
|
tracks, err := reveil.LoadTracks(cfg)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Unable to load tracks: %w", err)
|
||
|
}
|
||
|
|
||
|
var playlist []string
|
||
|
|
||
|
// 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(playlist), func(i, j int) {
|
||
|
playlist[i], playlist[j] = playlist[j], playlist[i]
|
||
|
})
|
||
|
|
||
|
return &p, nil
|
||
|
}
|
||
|
|
||
|
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 (p *Player) WakeUp() {
|
||
|
log.Println("RUN WAKEUP FUNC")
|
||
|
|
||
|
log.Println("Playlist in use:", strings.Join(p.Playlist, " ; "))
|
||
|
|
||
|
// Create infinite stream
|
||
|
stream := beep.Iterate(func() beep.Streamer {
|
||
|
if !p.hasClaironed && time.Since(p.launched) >= p.claironTime {
|
||
|
log.Println("clairon time!")
|
||
|
p.claironTime += p.claironTime / 2
|
||
|
_, sample, format, err := loadFile(p.claironFile)
|
||
|
if err == nil {
|
||
|
p.volume.Volume = 0.1
|
||
|
p.dontUpdateVolume = true
|
||
|
if format.SampleRate != p.sampleRate {
|
||
|
return beep.Resample(3, format.SampleRate, p.sampleRate, sample)
|
||
|
} else {
|
||
|
return sample
|
||
|
}
|
||
|
} else {
|
||
|
log.Println("Error loading clairon:", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
p.dontUpdateVolume = false
|
||
|
p.volume.Volume = -2 - math.Log(5/float64(p.ntick))/3
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Load our current item
|
||
|
_, sample, format, err := loadFile(p.Playlist[p.playedItem])
|
||
|
if err != nil {
|
||
|
log.Println("Error loading audio file %s: %s", p.Playlist[p.playedItem], err.Error())
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Resample if needed
|
||
|
log.Println("playing list item:", p.playedItem, "/", len(p.Playlist), ":", p.Playlist[p.playedItem])
|
||
|
if format.SampleRate != p.sampleRate {
|
||
|
return beep.Resample(3, format.SampleRate, p.sampleRate, sample)
|
||
|
} else {
|
||
|
return sample
|
||
|
}
|
||
|
})
|
||
|
|
||
|
// Prepare sound player
|
||
|
log.Println("Initializing sound player...")
|
||
|
speaker.Init(p.sampleRate, p.sampleRate.N(time.Second/10))
|
||
|
defer speaker.Close()
|
||
|
|
||
|
p.volume = &effects.Volume{stream, 10, -2, false}
|
||
|
speaker.Play(p.volume)
|
||
|
|
||
|
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)
|
||
|
|
||
|
loop:
|
||
|
for {
|
||
|
select {
|
||
|
case <-p.Stopper:
|
||
|
log.Println("Stopper activated")
|
||
|
break loop
|
||
|
case <-maxRun:
|
||
|
log.Println("Max run time exhausted")
|
||
|
break loop
|
||
|
case <-ticker.C:
|
||
|
p.ntick += 1
|
||
|
if !p.dontUpdateVolume {
|
||
|
p.volume.Volume = -2 - math.Log(5/float64(p.ntick))/3
|
||
|
}
|
||
|
case <-interrupt:
|
||
|
break loop
|
||
|
}
|
||
|
}
|
||
|
|
||
|
log.Println("Stopping the player...")
|
||
|
|
||
|
// Calm down music
|
||
|
loopcalm:
|
||
|
for i := 0; i < 2000; i += 1 {
|
||
|
p.volume.Volume -= 0.001
|
||
|
|
||
|
timer := time.NewTimer(4 * time.Millisecond)
|
||
|
select {
|
||
|
case <-p.Stopper:
|
||
|
log.Println("Hard stop received...")
|
||
|
timer.Stop()
|
||
|
p.volume.Volume = 0
|
||
|
break loopcalm
|
||
|
case <-timer.C:
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if p == CommonPlayer {
|
||
|
log.Println("Destoying common player")
|
||
|
|
||
|
CommonPlayer = nil
|
||
|
|
||
|
// TODO: find a better way to deallocate the card
|
||
|
os.Exit(42)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *Player) Stop() error {
|
||
|
log.Println("Trying to stop the player")
|
||
|
p.Stopper <- true
|
||
|
|
||
|
return nil
|
||
|
}
|