Merge remote-tracking branch 'upstream/master'

This commit is contained in:
tramhao 2021-03-01 12:04:52 +08:00
commit 0150f6b3da
10 changed files with 262 additions and 207 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"github.com/issadarkthing/gomu/player"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
)
@ -198,12 +199,12 @@ func (c Command) defineCommands() {
}
gomu.queue.pushFront(a)
gomu.player.skip()
gomu.player.Skip()
}
})
c.define("toggle_loop", func() {
gomu.queue.isLoop = gomu.player.toggleLoop()
gomu.queue.isLoop = !gomu.queue.isLoop
gomu.queue.updateTitle()
})
@ -248,27 +249,27 @@ func (c Command) defineCommands() {
})
c.define("toggle_pause", func() {
gomu.player.togglePause()
gomu.player.TogglePause()
})
c.define("volume_up", func() {
v := volToHuman(gomu.player.volume)
v := player.VolToHuman(gomu.player.GetVolume())
if v < 100 {
vol := gomu.player.setVolume(0.5)
vol := gomu.player.SetVolume(0.5)
volumePopup(vol)
}
})
c.define("volume_down", func() {
v := volToHuman(gomu.player.volume)
v := player.VolToHuman(gomu.player.GetVolume())
if v > 0 {
vol := gomu.player.setVolume(-0.5)
vol := gomu.player.SetVolume(-0.5)
volumePopup(vol)
}
})
c.define("skip", func() {
gomu.player.skip()
gomu.player.Skip()
})
c.define("toggle_help", func() {
@ -299,10 +300,10 @@ func (c Command) defineCommands() {
})
c.define("forward", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
position := gomu.playingBar.progress + 10
if position < gomu.playingBar.full {
err := gomu.player.seek(position)
err := gomu.player.Seek(position)
if err != nil {
logError(err)
}
@ -312,16 +313,16 @@ func (c Command) defineCommands() {
})
c.define("rewind", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
position := gomu.playingBar.progress - 10
if position-1 > 0 {
err := gomu.player.seek(position)
err := gomu.player.Seek(position)
if err != nil {
logError(err)
}
gomu.playingBar.progress = position
} else {
err := gomu.player.seek(0)
err := gomu.player.Seek(0)
if err != nil {
logError(err)
}
@ -331,10 +332,10 @@ func (c Command) defineCommands() {
})
c.define("forward_fast", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
position := gomu.playingBar.progress + 60
if position < gomu.playingBar.full {
err := gomu.player.seek(position)
err := gomu.player.Seek(position)
if err != nil {
logError(err)
}
@ -344,16 +345,16 @@ func (c Command) defineCommands() {
})
c.define("rewind_fast", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
position := gomu.playingBar.progress - 60
if position-1 > 0 {
err := gomu.player.seek(position)
err := gomu.player.Seek(position)
if err != nil {
logError(err)
}
gomu.playingBar.progress = position
} else {
err := gomu.player.seek(0)
err := gomu.player.Seek(0)
if err != nil {
logError(err)
}

10
gomu.go
View File

@ -1,10 +1,12 @@
package main
import (
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/issadarkthing/gomu/player"
)
var VERSION = "N/A"
@ -16,7 +18,7 @@ type Gomu struct {
playingBar *PlayingBar
queue *Queue
playlist *Playlist
player *Player
player *player.Player
pages *tview.Pages
colors *Colors
command Command
@ -49,7 +51,7 @@ func (g *Gomu) initPanels(app *tview.Application, args Args) {
g.playingBar = newPlayingBar()
g.queue = newQueue()
g.playlist = newPlaylist(args)
g.player = newPlayer()
g.player = player.New(g.anko.GetInt("General.volume"))
g.pages = tview.NewPages()
g.panels = []Panel{g.playlist, g.queue, g.playingBar}
}

View File

@ -1,9 +1,6 @@
// Copyright (C) 2020 Raziman
package main
package player
import (
"fmt"
"os"
"time"
@ -14,30 +11,33 @@ import (
"github.com/ztrue/tracerr"
)
type Audio interface {
Name() string
Path() string
}
type Player struct {
hasInit bool
isLoop bool
isRunning bool
volume float64
isSkipped chan struct{}
done chan struct{}
// to control the vol internally
vol *effects.Volume
ctrl *beep.Ctrl
format *beep.Format
length time.Duration
currentSong *AudioFile
currentSong Audio
streamSeekCloser beep.StreamSeekCloser
// is used to send progress
i int
songFinish func(Audio)
songStart func(Audio)
songSkip func(Audio)
}
func newPlayer() *Player {
// New returns new Player instance.
func New(volume int) *Player {
volume := gomu.anko.GetInt("General.volume")
// Read initial volume from config
initVol := absVolume(volume)
initVol := AbsVolume(volume)
// making sure user does not give invalid volume
if volume > 100 || volume < 0 {
@ -47,28 +47,61 @@ func newPlayer() *Player {
return &Player{volume: initVol}
}
func (p *Player) run(currSong *AudioFile) error {
// SetSongFinish accepts callback which will be executed when the song finishes.
func (p *Player) SetSongFinish(f func(Audio)) {
p.songFinish = f
}
p.isSkipped = make(chan struct{}, 1)
f, err := os.Open(currSong.path)
// SetSongStart accepts callback which will be executed when the song starts.
func (p *Player) SetSongStart(f func(Audio)) {
p.songStart = f
}
// SetSongSkip accepts callback which will be executed when the song is skipped.
func (p *Player) SetSongSkip(f func(Audio)) {
p.songSkip = f
}
// executes songFinish callback.
func (p *Player) execSongFinish(a Audio) {
if p.songFinish != nil {
p.songFinish(a)
}
}
// executes songStart callback.
func (p *Player) execSongStart(a Audio) {
if p.songStart != nil {
p.songStart(a)
}
}
// executes songFinish callback.
func (p *Player) execSongSkip(a Audio) {
if p.songSkip != nil {
p.songSkip(a)
}
}
// Run plays the passed Audio.
func (p *Player) Run(currSong Audio) error {
p.isRunning = true
p.execSongStart(currSong)
f, err := os.Open(currSong.Path())
if err != nil {
return tracerr.Wrap(err)
}
defer f.Close()
stream, format, err := mp3.Decode(f)
if err != nil {
return tracerr.Wrap(err)
}
p.streamSeekCloser = stream
p.format = &format
if err != nil {
return tracerr.Wrap(err)
}
defer stream.Close()
// song duration
p.length = p.format.SampleRate.D(p.streamSeekCloser.Len())
@ -86,18 +119,14 @@ func (p *Player) run(currSong *AudioFile) error {
p.currentSong = currSong
popupMessage := fmt.Sprintf("%s\n\n[ %s ]",
currSong.name, fmtDuration(p.length))
defaultTimedPopup(" Current Song ", popupMessage)
// resample to adapt to sample rate of new songs
resampled := beep.Resample(4, p.format.SampleRate, sr, p.streamSeekCloser)
done := make(chan struct{}, 1)
p.done = done
sstreamer := beep.Seq(resampled, beep.Callback(func() {
done <- struct{}{}
p.isRunning = false
p.format = nil
p.streamSeekCloser.Close()
go p.execSongFinish(currSong)
}))
ctrl := &beep.Ctrl{
@ -106,7 +135,6 @@ func (p *Player) run(currSong *AudioFile) error {
}
p.ctrl = ctrl
resampler := beep.ResampleRatio(4, 1, ctrl)
volume := &effects.Volume{
@ -122,94 +150,28 @@ func (p *Player) run(currSong *AudioFile) error {
// starts playing the audio
speaker.Play(p.vol)
gomu.hook.RunHooks("new_song")
p.isRunning = true
gomu.playingBar.newProgress(currSong, int(p.length.Seconds()))
go func() {
if err := gomu.playingBar.run(); err != nil {
logError(err)
}
}()
// is used to send progress
p.i = 0
next:
for {
select {
case <-done:
if p.isLoop {
gomu.queue.enqueue(currSong)
gomu.app.Draw()
}
p.isRunning = false
p.format = nil
gomu.playingBar.stop()
nextSong, err := gomu.queue.dequeue()
gomu.app.Draw()
if err != nil {
// when there are no songs to be played, set currentSong as nil
p.currentSong = nil
gomu.playingBar.setDefault()
gomu.app.Draw()
break next
}
go func() {
if err := p.run(nextSong); err != nil {
logError(err)
}
}()
break next
case <-time.After(time.Second):
// stop progress bar from progressing when paused
if !p.isRunning {
continue
}
p.i++
if p.i >= gomu.playingBar.full {
done <- struct{}{}
continue
}
gomu.playingBar.update <- struct{}{}
}
}
return nil
}
func (p *Player) pause() {
gomu.hook.RunHooks("pause")
// Pause pauses Player.
func (p *Player) Pause() {
speaker.Lock()
p.ctrl.Paused = true
p.isRunning = false
speaker.Unlock()
}
func (p *Player) play() {
gomu.hook.RunHooks("play")
// Play unpauses Player.
func (p *Player) Play() {
speaker.Lock()
p.ctrl.Paused = false
p.isRunning = true
speaker.Unlock()
gomu.playingBar.setSongTitle(p.currentSong.name)
}
// volume up and volume down using -0.5 or +0.5
func (p *Player) setVolume(v float64) float64 {
// Volume up and volume down using -0.5 or +0.5.
func (p *Player) SetVolume(v float64) float64 {
// check if no songs playing currently
if p.vol == nil {
@ -224,56 +186,59 @@ func (p *Player) setVolume(v float64) float64 {
return p.volume
}
func (p *Player) togglePause() {
// Toggles the pause state.
func (p *Player) TogglePause() {
if p.ctrl == nil {
return
}
if p.ctrl.Paused {
p.play()
p.Play()
} else {
p.pause()
p.Pause()
}
}
// skips current song
func (p *Player) skip() {
// Skips current song.
func (p *Player) Skip() {
gomu.hook.RunHooks("skip")
p.execSongSkip(p.currentSong)
if p.currentSong == nil {
return
}
// drain the stream
p.ctrl.Streamer = nil
p.streamSeekCloser.Close()
p.done <- struct{}{}
p.isRunning = false
p.format = nil
p.execSongFinish(p.currentSong)
}
// Toggles the queue to loop
// dequeued item will be enqueued back
// function returns loop state
func (p *Player) toggleLoop() bool {
p.isLoop = !p.isLoop
return p.isLoop
}
func (p *Player) getPosition() time.Duration {
// GetPosition returns the current position of audio file.
func (p *Player) GetPosition() time.Duration {
if p.format == nil || p.streamSeekCloser == nil {
return 1
}
return p.format.SampleRate.D(p.streamSeekCloser.Position())
}
// seek is the function to move forward and rewind
func (p *Player) seek(pos int) error {
func (p *Player) Seek(pos int) error {
speaker.Lock()
defer speaker.Unlock()
err := p.streamSeekCloser.Seek(pos * int(p.format.SampleRate))
p.i = pos
return err
}
// isPaused is used to distinguish the player between pause and stop
func (p *Player) isPaused() bool {
// IsPaused is used to distinguish the player between pause and stop
func (p *Player) IsPaused() bool {
if p.ctrl == nil {
return false
}
@ -281,8 +246,29 @@ func (p *Player) isPaused() bool {
return p.ctrl.Paused
}
// GetVolume returns current volume.
func (p *Player) GetVolume() float64 {
return p.volume
}
// GetCurrentSong returns current song.
func (p *Player) GetCurrentSong() Audio {
return p.currentSong
}
// HasInit checks if the speaker has been initialized or not. Speaker
// initialization will only happen once.
func (p *Player) HasInit() bool {
return p.hasInit
}
// IsRunning returns true if Player is running an audio.
func (p *Player) IsRunning() bool {
return p.isRunning
}
// Gets the length of the song in the queue
func getLength(audioPath string) (time.Duration, error) {
func GetLength(audioPath string) (time.Duration, error) {
f, err := os.Open(audioPath)
if err != nil {
@ -301,14 +287,14 @@ func getLength(audioPath string) (time.Duration, error) {
return format.SampleRate.D(streamer.Len()), nil
}
// volToHuman converts float64 volume that is used by audio library to human
// VolToHuman converts float64 volume that is used by audio library to human
// readable form (0 - 100)
func volToHuman(volume float64) int {
func VolToHuman(volume float64) int {
return int(volume*10) + 100
}
// absVolume converts human readable form volume (0 - 100) to float64 volume
// AbsVolume converts human readable form volume (0 - 100) to float64 volume
// that is used by the audio library
func absVolume(volume int) float64 {
func AbsVolume(volume int) float64 {
return (float64(volume) - 100) / 10
}

View File

@ -67,12 +67,10 @@ func (p *PlayingBar) run() error {
break
}
<-p.update
p.progress++
p.progress = int(gomu.player.GetPosition().Seconds())
p.text.Clear()
start, err := time.ParseDuration(strconv.Itoa(p.progress) + "s")
if err != nil {
return tracerr.Wrap(err)
}
@ -129,20 +127,29 @@ func (p *PlayingBar) run() error {
}
}
p.text.SetText(fmt.Sprintf("%s ┃%s┫ %s\n%v",
fmtDuration(start),
progressBar,
fmtDuration(end),
lyricText,
))
gomu.app.QueueUpdateDraw(func() {
p.text.Clear()
p.text.SetText(fmt.Sprintf("%s ┃%s┫ %s\n%v",
fmtDuration(start),
progressBar,
fmtDuration(end),
lyricText,
))
})
} else {
p.text.SetText(fmt.Sprintf("%s ┃%s┫ %s",
fmtDuration(start),
progressBar,
fmtDuration(end),
))
gomu.app.QueueUpdateDraw(func() {
p.text.Clear()
p.text.SetText(fmt.Sprintf("%s ┃%s┫ %s",
fmtDuration(start),
progressBar,
fmtDuration(end),
))
})
}
gomu.app.Draw()
<-time.After(time.Second)
}
return nil
@ -241,7 +248,7 @@ func (p *PlayingBar) switchLyrics() {
func (p *PlayingBar) delayLyric(lyricDelay int) (err error) {
p.subtitle.ResyncSubs(lyricDelay)
err = embedLyric(gomu.player.currentSong.path, p.subtitle.AsSRT(), p.langLyricCurrentPlaying)
err = embedLyric(gomu.player.GetCurrentSong().Path(), p.subtitle.AsSRT(), p.langLyricCurrentPlaying)
if err != nil {
return tracerr.Wrap(err)
}

View File

@ -18,10 +18,12 @@ import (
"github.com/rivo/tview"
spin "github.com/tj/go-spin"
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/player"
)
// AudioFile is representing directories and mp3 files
// if isAudioFile equals to false it is a directory
// AudioFile represents directories and mp3 files
// isAudioFile equals to false if it is a directory
type AudioFile struct {
name string
path string
@ -31,6 +33,14 @@ type AudioFile struct {
parent *tview.TreeNode
}
func (a *AudioFile) Name() string {
return a.name
}
func (a *AudioFile) Path() string {
return a.path
}
// Playlist struct represents playlist panel
// that shows the tree of the music directory
type Playlist struct {
@ -238,8 +248,8 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
// Here we remove the song from queue
songPaths := gomu.queue.getItems()
if audioName == getName(gomu.player.currentSong.name) {
gomu.player.skip()
if audioName == getName(gomu.player.GetCurrentSong().Name()) {
gomu.player.Skip()
}
for i, songPath := range songPaths {
if strings.Contains(songPath, audioName) {
@ -310,9 +320,12 @@ func (p *Playlist) addAllToQueue(root *tview.TreeNode) {
for _, v := range childrens {
currNode := v.GetReference().(*AudioFile)
if currNode.isAudioFile {
if currNode != gomu.player.currentSong {
currSong := gomu.player.GetCurrentSong()
if currSong == nil || (currNode.Name() != currSong.Name()) {
gomu.queue.enqueue(currNode)
}
}
}
@ -356,7 +369,7 @@ func (p *Playlist) addSongToPlaylist(
songName := getName(audioPath)
node := tview.NewTreeNode(songName)
audioLength, err := getLength(audioPath)
audioLength, err := player.GetLength(audioPath)
if err != nil {
return tracerr.Wrap(err)
}
@ -370,7 +383,7 @@ func (p *Playlist) addSongToPlaylist(
parent: selPlaylist,
}
displayText := setDisplayText(songName)
displayText := setDisplayText(audioFile)
node.SetReference(audioFile)
node.SetText(displayText)
@ -708,7 +721,7 @@ func populate(root *tview.TreeNode, rootPath string) error {
parent: root,
}
displayText := setDisplayText(songName)
displayText := setDisplayText(audioFile)
child.SetReference(audioFile)
child.SetText(displayText)
@ -726,7 +739,7 @@ func populate(root *tview.TreeNode, rootPath string) error {
parent: root,
}
displayText := setDisplayText(songName)
displayText := setDisplayText(audioFile)
child.SetReference(audioFile)
child.SetColor(gomu.colors.accent)
@ -799,14 +812,19 @@ func (p *Playlist) paste() error {
return nil
}
func setDisplayText(songName string) string {
func setDisplayText(audioFile *AudioFile) string {
useEmoji := gomu.anko.GetBool("General.use_emoji")
if !useEmoji {
return songName
return audioFile.name
}
emojiFile := gomu.anko.GetString("Emoji.file")
return fmt.Sprintf(" %s %s", emojiFile, songName)
if audioFile.isAudioFile {
emojiFile := gomu.anko.GetString("Emoji.file")
return fmt.Sprintf(" %s %s", emojiFile, audioFile.name)
}
emojiDir := gomu.anko.GetString("Emoji.playlist")
return fmt.Sprintf(" %s %s", emojiDir, audioFile.name)
}
// populateAudioLength is the most time consuming part of startup,
@ -815,7 +833,7 @@ func populateAudioLength(root *tview.TreeNode) error {
root.Walk(func(node *tview.TreeNode, _ *tview.TreeNode) bool {
audioFile := node.GetReference().(*AudioFile)
if audioFile.isAudioFile {
audioLength, err := getLength(audioFile.path)
audioLength, err := player.GetLength(audioFile.path)
if err != nil {
logError(err)
return false

View File

@ -7,13 +7,14 @@ import (
"testing"
"github.com/rivo/tview"
"github.com/issadarkthing/gomu/player"
)
// Prepares for test
func prepareTest() *Gomu {
gomu := newGomu()
gomu.player = &Player{}
gomu.player = player.New(0)
gomu.queue = newQueue()
gomu.playlist = &Playlist{
TreeView: tview.NewTreeView(),

View File

@ -18,6 +18,7 @@ import (
"github.com/issadarkthing/gomu/invidious"
"github.com/issadarkthing/gomu/lyric"
"github.com/issadarkthing/gomu/player"
)
// this is used to make the popup unique
@ -200,7 +201,7 @@ func defaultTimedPopup(title, description string) {
// Shows popup for the current volume
func volumePopup(volume float64) {
currVol := volToHuman(volume)
currVol := player.VolToHuman(volume)
maxVol := 100
// max progress bar length
maxLength := 50

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/gdamore/tcell/v2"
"github.com/issadarkthing/gomu/player"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
)
@ -154,19 +155,14 @@ func (q *Queue) dequeue() (*AudioFile, error) {
// Add item to the list and returns the length of the queue
func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
player := gomu.player
isTestEnv := os.Getenv("TEST") == "false"
if !player.isRunning && !player.isPaused() && os.Getenv("TEST") == "false" {
if !gomu.player.IsRunning() && !gomu.player.IsPaused() && isTestEnv {
gomu.player.isRunning = true
go func() {
if err := gomu.player.run(audioFile); err != nil {
logError(err)
}
}()
err := gomu.player.Run(audioFile)
if err != nil {
die(err)
}
return q.GetItemCount(), nil
}
@ -176,7 +172,7 @@ func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
}
q.items = append(q.items, audioFile)
songLength, err := getLength(audioFile.path)
songLength, err := player.GetLength(audioFile.path)
if err != nil {
return 0, tracerr.Wrap(err)
@ -214,8 +210,8 @@ func (q *Queue) saveQueue(isQuit bool) error {
songPaths := q.getItems()
var content strings.Builder
if gomu.player.hasInit && isQuit && gomu.player.currentSong != nil {
currentSongPath := gomu.player.currentSong.path
if gomu.player.HasInit() && isQuit && gomu.player.GetCurrentSong() != nil {
currentSongPath := gomu.player.GetCurrentSong().Path()
currentSongInQueue := false
for _, songPath := range songPaths {
if getName(songPath) == getName(currentSongPath) {
@ -347,7 +343,7 @@ func (q *Queue) shuffle() {
q.Clear()
for _, v := range q.items {
audioLen, err := getLength(v.path)
audioLen, err := player.GetLength(v.path)
if err != nil {
logError(err)
}

View File

@ -91,12 +91,13 @@ func TestPushFront(t *testing.T) {
gomu = prepareTest()
rapPlaylist := gomu.playlist.GetRoot().GetChildren()[1]
gomu.playlist.addAllToQueue(rapPlaylist)
selSong, err := gomu.queue.deleteItem(2)
selSong, err := gomu.queue.deleteItem(2)
if err != nil {
panic(err)
t.Error(err)
}
gomu.queue.pushFront(selSong)

View File

@ -13,10 +13,12 @@ import (
"syscall"
"github.com/gdamore/tcell/v2"
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/rivo/tview"
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/anko"
"github.com/issadarkthing/gomu/hook"
"github.com/issadarkthing/gomu/player"
)
// Panel is used to keep track of childrens in slices
@ -318,6 +320,47 @@ func start(application *tview.Application, args Args) {
gomu.initPanels(application, args)
gomu.player.SetSongStart(func(audio player.Audio) {
name := audio.Name()
defaultTimedPopup(" Now Playing ", name)
duration, err := player.GetLength(audio.Path())
if err != nil {
logError(err)
return
}
audioFile := audio.(*AudioFile)
gomu.playingBar.newProgress(audioFile, int(duration.Seconds()))
go func() {
err := gomu.playingBar.run()
if err != nil {
logError(err)
}
}()
})
gomu.player.SetSongFinish(func(_ player.Audio) {
audio, err := gomu.queue.dequeue()
if err != nil {
gomu.playingBar.setDefault()
return
}
err = gomu.player.Run(audio)
if err != nil {
die(err)
}
if gomu.queue.isLoop {
_, err = gomu.queue.enqueue(audio)
if err != nil {
logError(err)
}
}
})
flex := layout(gomu)
gomu.pages.AddPage("main", flex, true, true)
@ -329,8 +372,7 @@ func start(application *tview.Application, args Args) {
isQueueLoop := gomu.anko.GetBool("General.queue_loop")
gomu.player.isLoop = isQueueLoop
gomu.queue.isLoop = gomu.player.isLoop
gomu.queue.isLoop = isQueueLoop
loadQueue := gomu.anko.GetBool("General.load_prev_queue")
@ -427,7 +469,7 @@ func start(application *tview.Application, args Args) {
init := false
gomu.app.SetAfterDrawFunc(func(_ tcell.Screen) {
if !init && !gomu.player.isRunning {
if !init && !gomu.player.IsRunning() {
gomu.playingBar.setDefault()
init = true
}