diff --git a/backend/config.go b/backend/config.go index 69563d8..a0a8a4d 100644 --- a/backend/config.go +++ b/backend/config.go @@ -110,8 +110,9 @@ type NowPlayingPageConfig struct { } type PlaybackConfig struct { - Autoplay bool - RepeatMode string + Autoplay bool + RepeatMode string + UseWaveformSeekbar bool } type LocalPlaybackConfig struct { @@ -239,8 +240,9 @@ func DefaultConfig(appVersionTag string) *Config { TracklistColumns: []string{"Album", "Time", "Plays"}, }, Playback: PlaybackConfig{ - Autoplay: false, - RepeatMode: "None", + Autoplay: false, + RepeatMode: "None", + UseWaveformSeekbar: true, }, LocalPlayback: LocalPlaybackConfig{ // "auto" is the name to pass to MPV for autoselecting the output device diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 5c55dd7..7373a3d 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -84,6 +84,7 @@ type playbackEngine struct { // to pass to onSongChange listeners; clear once listeners have been called lastScrobbled *mediaprovider.Track + playbackCfg *PlaybackConfig scrobbleCfg *ScrobbleConfig transcodeCfg *TranscodingConfig replayGainCfg ReplayGainConfig @@ -118,6 +119,7 @@ func NewPlaybackEngine( sm: s, audiocache: c, player: p, + playbackCfg: playbackCfg, scrobbleCfg: scrobbleCfg, transcodeCfg: transcodeCfg, nowPlayingIdx: -1, @@ -820,7 +822,11 @@ func (pm *playbackEngine) invokeNoArgCallbacks(cbs []func()) { func (p *playbackEngine) startPollTimePos() { ctx, cancel := context.WithCancel(p.ctx) p.cancelPollPos = cancel - pollingTick := time.NewTicker(125 * time.Millisecond) + pollFrequency := 250 * time.Millisecond + if p.playbackCfg.UseWaveformSeekbar { + pollFrequency = 100 * time.Millisecond + } + pollingTick := time.NewTicker(pollFrequency) go func() { for { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index b1a3fa8..b40b971 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -37,7 +37,10 @@ type PlaybackManager struct { autoplay bool - lastPlayTime float64 + lastPlayTime float64 + lastPlayingID string + wfmUpdateImageCancel context.CancelFunc + wfmImageJobs [3]*WaveformImageJob } type RemotePlaybackDevice struct { @@ -75,6 +78,22 @@ func NewPlaybackManager( return pm } +func (p *PlaybackManager) findWfmImageJob(id string) (*WaveformImageJob, bool) { + for _, j := range p.wfmImageJobs { + if j != nil && j.ItemID == id { + return j, true + } + } + return nil, false +} + +func (p *PlaybackManager) addWfmImageJob(job *WaveformImageJob) { + p.wfmImageJobs[0].Cancel() + p.wfmImageJobs[0] = p.wfmImageJobs[1] + p.wfmImageJobs[1] = p.wfmImageJobs[2] + p.wfmImageJobs[2] = job +} + func (p *PlaybackManager) addOnTrackChangeHook() { // See https://github.com/dweymouth/supersonic/issues/483 // On Windows, MPV sometimes fails to start playback when switching to a track @@ -84,87 +103,21 @@ func (p *PlaybackManager) addOnTrackChangeHook() { p.lastPlayTime = curTime }) - var waveformJobs [3]*WaveformImageJob - findJob := func(id string) (*WaveformImageJob, bool) { - for _, j := range waveformJobs { - if j != nil && j.ItemID == id { - return j, true + p.engine.onBeforeSongChange = append(p.engine.onBeforeSongChange, func(item mediaprovider.MediaItem) { + if p.engine.playbackCfg.UseWaveformSeekbar { + if p.wfmGen != nil && item != nil && item.Metadata().Type == mediaprovider.MediaItemTypeTrack { + // start generating waveform image for next-up track + p.addWfmImageJob(p.wfmGen.StartWaveformGeneration(item.(*mediaprovider.Track))) } } - return nil, false - } - addJob := func(job *WaveformImageJob) { - waveformJobs[0].Cancel() - waveformJobs[0] = waveformJobs[1] - waveformJobs[1] = waveformJobs[2] - waveformJobs[2] = job - } - - p.engine.onBeforeSongChange = append(p.engine.onBeforeSongChange, func(item mediaprovider.MediaItem) { - if p.wfmGen != nil && item != nil && item.Metadata().Type == mediaprovider.MediaItemTypeTrack { - // start generating waveform image for next-up track - addJob(p.wfmGen.StartWaveformGeneration(item.(*mediaprovider.Track))) - } }) - lastPlayingID := "" - var wfmImageUpdateCancel context.CancelFunc - p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { // Autoplay if enabled and we are on the last track if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { p.enqueueAutoplayTracks() } - if wfmImageUpdateCancel != nil { - wfmImageUpdateCancel() - } - - updateUnfinishedJob := func(job *WaveformImageJob) { - ctx, c := context.WithCancel(p.cache.rootCtx) - wfmImageUpdateCancel = c - go func(ctx context.Context, job *WaveformImageJob) { - for { - time.Sleep(333 * time.Millisecond) - select { - case <-ctx.Done(): - return - default: - img := job.Get() - for _, cb := range p.onWaveformImgUpdate { - cb(img) - } - if job.Done() { - return - } - } - } - }(ctx, job) - } - - if item != nil { - // cancel possible waveform generation job for previous track - if old, ok := findJob(lastPlayingID); ok { - old.Cancel() - } - lastPlayingID = item.Metadata().ID - - var job *WaveformImageJob - if j, ok := findJob(item.Metadata().ID); ok { - job = j - } else if tr, ok := item.(*mediaprovider.Track); ok { - job = p.wfmGen.StartWaveformGeneration(tr) - addJob(job) - } - if job != nil { - img := job.Get() - for _, cb := range p.onWaveformImgUpdate { - cb(img) - } - if !job.done && job != nil { - updateUnfinishedJob(job) - } - } - } + p.handleWaveformImageSongChange(item) if runtime.GOOS != "windows" { return @@ -183,6 +136,62 @@ func (p *PlaybackManager) addOnTrackChangeHook() { }) } +func (p *PlaybackManager) handleWaveformImageSongChange(item mediaprovider.MediaItem) { + if p.wfmUpdateImageCancel != nil { + p.wfmUpdateImageCancel() + } + if !p.engine.playbackCfg.UseWaveformSeekbar { + return + } + + updateUnfinishedJob := func(job *WaveformImageJob) { + ctx, c := context.WithCancel(p.cache.rootCtx) + p.wfmUpdateImageCancel = c + go func(ctx context.Context, job *WaveformImageJob) { + for { + time.Sleep(333 * time.Millisecond) + select { + case <-ctx.Done(): + return + default: + img := job.Get() + for _, cb := range p.onWaveformImgUpdate { + cb(img) + } + if job.Done() { + return + } + } + } + }(ctx, job) + } + + if item != nil { + // cancel possible waveform generation job for previous track + if old, ok := p.findWfmImageJob(p.lastPlayingID); ok { + old.Cancel() + } + p.lastPlayingID = item.Metadata().ID + + var job *WaveformImageJob + if j, ok := p.findWfmImageJob(item.Metadata().ID); ok { + job = j + } else if tr, ok := item.(*mediaprovider.Track); ok { + job = p.wfmGen.StartWaveformGeneration(tr) + p.addWfmImageJob(job) + } + if job != nil { + img := job.Get() + for _, cb := range p.onWaveformImgUpdate { + cb(img) + } + if !job.done && job != nil { + updateUnfinishedJob(job) + } + } + } +} + func (p *PlaybackManager) ScanRemotePlayers(ctx context.Context, fastScan bool) { if fastScan { p.scanRemotePlayers(ctx, 1 /*waitSec*/) diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 699f284..ce0a2ba 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -28,7 +28,7 @@ type BottomPanel struct { var _ fyne.Widget = (*BottomPanel)(nil) -func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr *controller.Controller) *BottomPanel { +func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr *controller.Controller, useWaveformSeekbar bool) *BottomPanel { bp := &BottomPanel{} bp.ExtendBaseWidget(bp) @@ -86,7 +86,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr contr.ShowShareDialog(tr.ID) } } - bp.Controls = widgets.NewPlayerControls() + bp.Controls = widgets.NewPlayerControls(useWaveformSeekbar) bp.Controls.OnPlayPause(func() { pm.PlayPause() }) diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 8b26732..9bb0b29 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -559,6 +559,12 @@ func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabI } } + useWaveformSeekbar := widget.NewCheck(lang.L("Use waveform seekbar"), func(b bool) { + s.config.Playback.UseWaveformSeekbar = b + s.setRestartRequired() + }) + useWaveformSeekbar.Checked = s.config.Playback.UseWaveformSeekbar + return container.NewTabItem(lang.L("Appearance"), container.NewVBox( util.NewHSpace(0), // insert a theme.Padding amount of space at top container.NewBorder(nil, nil, widget.NewLabel(lang.L("Theme")), /*left*/ @@ -570,6 +576,8 @@ func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabI container.NewBorder(nil, nil, widget.NewLabel(lang.L("Grid card size")), nil, gridCardSize), disableDPI, s.newSectionSeparator(), + useWaveformSeekbar, + s.newSectionSeparator(), widget.NewRichText(&widget.TextSegment{Text: lang.L("Application font"), Style: util.BoldRichTextStyle}), container.New(layout.NewFormLayout(), widget.NewLabel(lang.L("Normal font")), container.NewBorder(nil, nil, nil, normalFontBrowse, normalFontEntry), diff --git a/ui/mainwindow.go b/ui/mainwindow.go index c77b98d..57450de 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -90,7 +90,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, )) } - m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller) + m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller, m.Controller.App.Config.Playback.UseWaveformSeekbar) app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { fyne.Do(func() { m.UpdateOnTrackChange(item) }) }) diff --git a/ui/widgets/playercontrols.go b/ui/widgets/playercontrols.go index 216bd26..adc07b1 100644 --- a/ui/widgets/playercontrols.go +++ b/ui/widgets/playercontrols.go @@ -82,6 +82,8 @@ func (t *TrackPosSlider) IsDragging() bool { type PlayerControls struct { widget.BaseWidget + UseWaveformSeekbar bool + slider *TrackPosSlider waveform *WaveformSeekbar curTimeLabel *labelMinSize @@ -112,14 +114,18 @@ func NewLabelMinSize(text string, minWidth float32) *labelMinSize { } // NewPlayerControls sets up the seek bar, and transport buttons. -func NewPlayerControls() *PlayerControls { - pc := &PlayerControls{} +func NewPlayerControls(useWaveformSeekbar bool) *PlayerControls { + pc := &PlayerControls{UseWaveformSeekbar: useWaveformSeekbar} pc.ExtendBaseWidget(pc) pc.slider = NewTrackPosSlider() pc.slider.Disable() - pc.slider.Hide() pc.waveform = NewWaveformSeekbar() + if useWaveformSeekbar { + pc.slider.Hidden = true + } else { + pc.waveform.Hidden = true + } pc.curTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55) pc.curTimeLabel.Alignment = fyne.TextAlignTrailing pc.totalTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55) @@ -221,6 +227,12 @@ func (p *PlayerControls) UpdateWaveformImg(img *backend.WaveformImage) { p.waveform.UpdateImage(img) } +func (p *PlayerControls) Refresh() { + p.waveform.Hidden = !p.UseWaveformSeekbar + p.slider.Hidden = p.UseWaveformSeekbar + p.BaseWidget.Refresh() +} + func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(p.container) }