You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
6.3 KiB
358 lines
6.3 KiB
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 = 500 * 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() |
|
}
|
|
|