add setting to disable waveform seekbar

This commit is contained in:
Drew Weymouth
2025-08-01 09:07:52 -07:00
parent e0cb2a57ac
commit 5209c08444
7 changed files with 121 additions and 84 deletions
+6 -4
View File
@@ -110,8 +110,9 @@ type NowPlayingPageConfig struct {
} }
type PlaybackConfig struct { type PlaybackConfig struct {
Autoplay bool Autoplay bool
RepeatMode string RepeatMode string
UseWaveformSeekbar bool
} }
type LocalPlaybackConfig struct { type LocalPlaybackConfig struct {
@@ -239,8 +240,9 @@ func DefaultConfig(appVersionTag string) *Config {
TracklistColumns: []string{"Album", "Time", "Plays"}, TracklistColumns: []string{"Album", "Time", "Plays"},
}, },
Playback: PlaybackConfig{ Playback: PlaybackConfig{
Autoplay: false, Autoplay: false,
RepeatMode: "None", RepeatMode: "None",
UseWaveformSeekbar: true,
}, },
LocalPlayback: LocalPlaybackConfig{ LocalPlayback: LocalPlaybackConfig{
// "auto" is the name to pass to MPV for autoselecting the output device // "auto" is the name to pass to MPV for autoselecting the output device
+7 -1
View File
@@ -84,6 +84,7 @@ type playbackEngine struct {
// to pass to onSongChange listeners; clear once listeners have been called // to pass to onSongChange listeners; clear once listeners have been called
lastScrobbled *mediaprovider.Track lastScrobbled *mediaprovider.Track
playbackCfg *PlaybackConfig
scrobbleCfg *ScrobbleConfig scrobbleCfg *ScrobbleConfig
transcodeCfg *TranscodingConfig transcodeCfg *TranscodingConfig
replayGainCfg ReplayGainConfig replayGainCfg ReplayGainConfig
@@ -118,6 +119,7 @@ func NewPlaybackEngine(
sm: s, sm: s,
audiocache: c, audiocache: c,
player: p, player: p,
playbackCfg: playbackCfg,
scrobbleCfg: scrobbleCfg, scrobbleCfg: scrobbleCfg,
transcodeCfg: transcodeCfg, transcodeCfg: transcodeCfg,
nowPlayingIdx: -1, nowPlayingIdx: -1,
@@ -820,7 +822,11 @@ func (pm *playbackEngine) invokeNoArgCallbacks(cbs []func()) {
func (p *playbackEngine) startPollTimePos() { func (p *playbackEngine) startPollTimePos() {
ctx, cancel := context.WithCancel(p.ctx) ctx, cancel := context.WithCancel(p.ctx)
p.cancelPollPos = cancel 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() { go func() {
for { for {
+82 -73
View File
@@ -37,7 +37,10 @@ type PlaybackManager struct {
autoplay bool autoplay bool
lastPlayTime float64 lastPlayTime float64
lastPlayingID string
wfmUpdateImageCancel context.CancelFunc
wfmImageJobs [3]*WaveformImageJob
} }
type RemotePlaybackDevice struct { type RemotePlaybackDevice struct {
@@ -75,6 +78,22 @@ func NewPlaybackManager(
return pm 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() { func (p *PlaybackManager) addOnTrackChangeHook() {
// See https://github.com/dweymouth/supersonic/issues/483 // See https://github.com/dweymouth/supersonic/issues/483
// On Windows, MPV sometimes fails to start playback when switching to a track // On Windows, MPV sometimes fails to start playback when switching to a track
@@ -84,87 +103,21 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
p.lastPlayTime = curTime p.lastPlayTime = curTime
}) })
var waveformJobs [3]*WaveformImageJob p.engine.onBeforeSongChange = append(p.engine.onBeforeSongChange, func(item mediaprovider.MediaItem) {
findJob := func(id string) (*WaveformImageJob, bool) { if p.engine.playbackCfg.UseWaveformSeekbar {
for _, j := range waveformJobs { if p.wfmGen != nil && item != nil && item.Metadata().Type == mediaprovider.MediaItemTypeTrack {
if j != nil && j.ItemID == id { // start generating waveform image for next-up track
return j, true 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) { p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
// Autoplay if enabled and we are on the last track // Autoplay if enabled and we are on the last track
if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
p.enqueueAutoplayTracks() p.enqueueAutoplayTracks()
} }
if wfmImageUpdateCancel != nil { p.handleWaveformImageSongChange(item)
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)
}
}
}
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
return 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) { func (p *PlaybackManager) ScanRemotePlayers(ctx context.Context, fastScan bool) {
if fastScan { if fastScan {
p.scanRemotePlayers(ctx, 1 /*waitSec*/) p.scanRemotePlayers(ctx, 1 /*waitSec*/)
+2 -2
View File
@@ -28,7 +28,7 @@ type BottomPanel struct {
var _ fyne.Widget = (*BottomPanel)(nil) 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 := &BottomPanel{}
bp.ExtendBaseWidget(bp) bp.ExtendBaseWidget(bp)
@@ -86,7 +86,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
contr.ShowShareDialog(tr.ID) contr.ShowShareDialog(tr.ID)
} }
} }
bp.Controls = widgets.NewPlayerControls() bp.Controls = widgets.NewPlayerControls(useWaveformSeekbar)
bp.Controls.OnPlayPause(func() { bp.Controls.OnPlayPause(func() {
pm.PlayPause() pm.PlayPause()
}) })
+8
View File
@@ -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( return container.NewTabItem(lang.L("Appearance"), container.NewVBox(
util.NewHSpace(0), // insert a theme.Padding amount of space at top util.NewHSpace(0), // insert a theme.Padding amount of space at top
container.NewBorder(nil, nil, widget.NewLabel(lang.L("Theme")), /*left*/ 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), container.NewBorder(nil, nil, widget.NewLabel(lang.L("Grid card size")), nil, gridCardSize),
disableDPI, disableDPI,
s.newSectionSeparator(), s.newSectionSeparator(),
useWaveformSeekbar,
s.newSectionSeparator(),
widget.NewRichText(&widget.TextSegment{Text: lang.L("Application font"), Style: util.BoldRichTextStyle}), widget.NewRichText(&widget.TextSegment{Text: lang.L("Application font"), Style: util.BoldRichTextStyle}),
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel(lang.L("Normal font")), container.NewBorder(nil, nil, nil, normalFontBrowse, normalFontEntry), widget.NewLabel(lang.L("Normal font")), container.NewBorder(nil, nil, nil, normalFontBrowse, normalFontEntry),
+1 -1
View File
@@ -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) { app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
fyne.Do(func() { m.UpdateOnTrackChange(item) }) fyne.Do(func() { m.UpdateOnTrackChange(item) })
}) })
+15 -3
View File
@@ -82,6 +82,8 @@ func (t *TrackPosSlider) IsDragging() bool {
type PlayerControls struct { type PlayerControls struct {
widget.BaseWidget widget.BaseWidget
UseWaveformSeekbar bool
slider *TrackPosSlider slider *TrackPosSlider
waveform *WaveformSeekbar waveform *WaveformSeekbar
curTimeLabel *labelMinSize curTimeLabel *labelMinSize
@@ -112,14 +114,18 @@ func NewLabelMinSize(text string, minWidth float32) *labelMinSize {
} }
// NewPlayerControls sets up the seek bar, and transport buttons. // NewPlayerControls sets up the seek bar, and transport buttons.
func NewPlayerControls() *PlayerControls { func NewPlayerControls(useWaveformSeekbar bool) *PlayerControls {
pc := &PlayerControls{} pc := &PlayerControls{UseWaveformSeekbar: useWaveformSeekbar}
pc.ExtendBaseWidget(pc) pc.ExtendBaseWidget(pc)
pc.slider = NewTrackPosSlider() pc.slider = NewTrackPosSlider()
pc.slider.Disable() pc.slider.Disable()
pc.slider.Hide()
pc.waveform = NewWaveformSeekbar() pc.waveform = NewWaveformSeekbar()
if useWaveformSeekbar {
pc.slider.Hidden = true
} else {
pc.waveform.Hidden = true
}
pc.curTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55) pc.curTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55)
pc.curTimeLabel.Alignment = fyne.TextAlignTrailing pc.curTimeLabel.Alignment = fyne.TextAlignTrailing
pc.totalTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55) pc.totalTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55)
@@ -221,6 +227,12 @@ func (p *PlayerControls) UpdateWaveformImg(img *backend.WaveformImage) {
p.waveform.UpdateImage(img) 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 { func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(p.container) return widget.NewSimpleRenderer(p.container)
} }