ditty/audio.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()
}