397 lines
7 KiB
Go
397 lines
7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gopxl/beep/v2"
|
|
"github.com/gopxl/beep/v2/effects"
|
|
"github.com/gopxl/beep/v2/flac"
|
|
"github.com/gopxl/beep/v2/mp3"
|
|
"github.com/gopxl/beep/v2/speaker"
|
|
"github.com/gopxl/beep/v2/vorbis"
|
|
"github.com/gopxl/beep/v2/wav"
|
|
)
|
|
|
|
const (
|
|
defaultSampleRate = 48000
|
|
defaultBufferSize = 250 * time.Millisecond
|
|
defaultResampleQuality = 10
|
|
)
|
|
|
|
var (
|
|
startingVolumeLevel float64
|
|
startingVolumeSilent bool
|
|
|
|
playingFileName string
|
|
playingFileInfo string
|
|
playingFileID int64
|
|
playingStreamer beep.StreamSeekCloser
|
|
playingFormat beep.Format
|
|
|
|
pauseNext bool
|
|
seekNext int
|
|
|
|
volume *effects.Volume
|
|
ctrl *beep.Ctrl
|
|
|
|
initialized bool
|
|
)
|
|
|
|
type audioFile struct {
|
|
File *os.File
|
|
Streamer beep.StreamSeekCloser
|
|
Format beep.Format
|
|
Metadata *metadata
|
|
}
|
|
|
|
func initAudio() {
|
|
samples := sampleRate.N(bufferSize)
|
|
if samples < 1 {
|
|
samples = 1
|
|
}
|
|
err := speaker.Init(sampleRate, samples)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to initialize audio device: %s", err))
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if !initialized {
|
|
initAudio()
|
|
initialized = true
|
|
}
|
|
|
|
speaker.Lock()
|
|
if playingStreamer != nil {
|
|
playingStreamer.Close()
|
|
}
|
|
|
|
thisFileID := time.Now().UnixNano()
|
|
|
|
if audioFile.Format.SampleRate != sampleRate {
|
|
resampler := beep.Resample(resampleQuality, audioFile.Format.SampleRate, sampleRate, audioFile.Streamer)
|
|
audioFile.Streamer = closableResampler{Resampler: resampler, streamer: audioFile.Streamer}
|
|
}
|
|
|
|
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 {
|
|
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()
|
|
}
|
|
|
|
type closableResampler struct {
|
|
*beep.Resampler
|
|
streamer beep.StreamSeekCloser
|
|
}
|
|
|
|
func (r closableResampler) Position() int {
|
|
return r.streamer.Position()
|
|
}
|
|
|
|
func (r closableResampler) Seek(p int) error {
|
|
return r.streamer.Seek(p)
|
|
}
|
|
|
|
func (r closableResampler) Len() int {
|
|
return r.streamer.Len()
|
|
}
|
|
|
|
func (r closableResampler) Close() error {
|
|
return r.streamer.Close()
|
|
}
|