package main import ( "fmt" "io" "math" "os" "path" "strings" "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/vorbis" "github.com/faiface/beep/wav" ) const defaultBufferSize = 250 * time.Millisecond var ( startingVolumeLevel float64 startingVolumeSilent bool playingFileName string playingFileInfo string playingFileID int64 playingStreamer beep.StreamSeekCloser playingFormat beep.Format playingSampleRate beep.SampleRate pauseNext bool seekNext int volume *effects.Volume ctrl *beep.Ctrl ) type audioFile struct { File *os.File Streamer beep.StreamSeekCloser Format beep.Format Metadata *metadata } func openFile(filePath string, Metadata *metadata) (*audioFile, error) { f, err := os.Open(filePath) if err != nil { return nil, err } if Metadata == nil { Metadata = readMetadata(f) _, err = f.Seek(0, io.SeekStart) if err != nil { panic(err) } } var ( streamer beep.StreamSeekCloser format beep.Format ) switch strings.ToLower(path.Ext(filePath)) { case ".wav": streamer, format, err = wav.Decode(f) case ".mp3": streamer, format, err = mp3.Decode(f) case ".ogg", ".weba", ".webm": streamer, format, err = vorbis.Decode(f) case ".flac": streamer, format, err = flac.Decode(f) default: err = fmt.Errorf("unsupported file format") } if err != nil { return nil, err } a := audioFile{File: f, Streamer: streamer, Format: format, Metadata: Metadata} return &a, nil } func play(audioFile *audioFile) { speaker.Lock() if playingStreamer != nil { playingStreamer.Close() } thisFileID := time.Now().UnixNano() playingFileID = thisFileID playingStreamer = audioFile.Streamer playingFormat = audioFile.Format playingFileName = audioFile.File.Name() playingFileInfo = "" if audioFile.Metadata.Title != "" { playingFileInfo = audioFile.Metadata.Title if audioFile.Metadata.Artist != "" { playingFileInfo = audioFile.Metadata.Artist + " - " + playingFileInfo } } speaker.Unlock() if streamFdInt == -1 { if audioFile.Format.SampleRate != playingSampleRate { err := speaker.Init(audioFile.Format.SampleRate, audioFile.Format.SampleRate.N(bufferSize)) if err != nil { panic(fmt.Sprintf("failed to initialize audio device: %s", err)) } playingSampleRate = audioFile.Format.SampleRate } else { speaker.Clear() } } speaker.Lock() streamer := beep.Seq(audioFile.Streamer, beep.Callback(func() { if playingFileID != thisFileID { return } go nextTrack() })) if volume != nil { volume.Streamer = streamer ctrl.Paused = false } else { volume = &effects.Volume{ Streamer: streamer, Base: volumeBase, Volume: startingVolumeLevel, Silent: startingVolumeSilent, } ctrl = &beep.Ctrl{ Streamer: volume, Paused: false, } } if pauseNext { ctrl.Paused = true pauseNext = false } if seekNext != 0 { err := playingStreamer.Seek(seekNext) if err != nil && err != io.EOF { // TODO getting seek errors here on state restore statusText = err.Error() go func() { time.Sleep(5 * time.Second) statusText = "" go app.QueueUpdateDraw(updateMain) }() } seekNext = 0 } speaker.Unlock() if streamFdInt >= 0 { if streamFd == nil { streamFd = os.NewFile(uintptr(streamFdInt), "out") go func() { rateLimitedStreamer := newRateLimitedStreamer(ctrl, bufferSize/64) for { _ = wav.Encode(streamFd, rateLimitedStreamer, playingFormat) time.Sleep(250 * time.Millisecond) } }() } } else { speaker.Play(ctrl) } go app.QueueUpdateDraw(func() { updateLists() updateStatus() }) } func pause() { speaker.Lock() defer speaker.Unlock() if ctrl == nil { return } ctrl.Paused = !ctrl.Paused updateStatus() } func nextTrack() { if queueList.GetCurrentItemIndex() < len(queueFiles)-1 { queueNext() entry := selectedQueueEntry() audioFile, err := openFile(entry.RealPath, entry.Metadata) if err != nil { statusText = err.Error() go func() { time.Sleep(5 * time.Second) statusText = "" go app.QueueUpdateDraw(updateMain) }() go app.QueueUpdateDraw(updateMain) return } queuePlaying = queueList.GetCurrentItemIndex() play(audioFile) go app.QueueUpdateDraw(updateQueue) } } func skipPrevious() { if offsetQueueEntry(-1) == nil { return } queuePrevious() go queueSelect(queueList.GetCurrentItemIndex()) } func skipNext() { if offsetQueueEntry(1) == nil { return } queueNext() go queueSelect(queueList.GetCurrentItemIndex()) } func roundUnit(x, unit float64) float64 { return math.Round(x/unit) * unit } func supportedFormat(filePath string) bool { switch strings.ToLower(path.Ext(filePath)) { case ".wav": return true case ".mp3": return true case ".ogg", ".weba": return true case ".flac": return true default: return false } } func fileFormat(fileName string) string { switch strings.ToLower(path.Ext(fileName)) { case ".wav": return "WAV" case ".mp3": return "MP3" case ".ogg", ".weba": return "OGG" case ".flac": return "FLAC" default: return "?" } } func adjustVolume(adjustment float64) { speaker.Lock() defer speaker.Unlock() if volume == nil { startingVolumeLevel += adjustment startingVolumeUpdated() updateStatus() return } volume.Volume += adjustment volumeUpdated() updateStatus() } func setVolume(vol float64) { speaker.Lock() defer speaker.Unlock() if volume == nil { startingVolumeLevel = vol startingVolumeUpdated() updateStatus() return } volume.Volume = vol volumeUpdated() updateStatus() } func decreaseVolume() { adjustVolume(-0.5) } func increaseVolume() { adjustVolume(0.5) } func volumeUpdated() { if volume.Volume <= -7.5 { volume.Volume = -7.5 volume.Silent = true } else { volume.Silent = false if volume.Volume > 0 { volume.Volume = 0 } } } func startingVolumeUpdated() { if startingVolumeLevel <= -7.5 { startingVolumeLevel = -7.5 startingVolumeSilent = true } else { startingVolumeSilent = false if startingVolumeLevel > 0 { startingVolumeLevel = 0 } } } func toggleMute() { speaker.Lock() defer speaker.Unlock() if volume == nil { startingVolumeSilent = !startingVolumeSilent updateStatus() return } volume.Silent = !volume.Silent updateStatus() }