create waveform seekbar widget; still more to do
This commit is contained in:
+10
-25
@@ -11,9 +11,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/backend/player"
|
"github.com/dweymouth/supersonic/backend/player"
|
||||||
"github.com/dweymouth/supersonic/backend/player/dlna"
|
"github.com/dweymouth/supersonic/backend/player/dlna"
|
||||||
@@ -36,6 +33,8 @@ type PlaybackManager struct {
|
|||||||
remotePlayers []RemotePlaybackDevice
|
remotePlayers []RemotePlaybackDevice
|
||||||
currentRemotePlayer *RemotePlaybackDevice
|
currentRemotePlayer *RemotePlaybackDevice
|
||||||
|
|
||||||
|
onWaveformImgUpdate []func(*WaveformImage)
|
||||||
|
|
||||||
autoplay bool
|
autoplay bool
|
||||||
|
|
||||||
lastPlayTime float64
|
lastPlayTime float64
|
||||||
@@ -91,34 +90,16 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
|
|||||||
// TODO: make more permanent
|
// TODO: make more permanent
|
||||||
if p.cache != nil && item != nil {
|
if p.cache != nil && item != nil {
|
||||||
go func() {
|
go func() {
|
||||||
log.Println("begin generating waveform image")
|
|
||||||
if path := p.cache.PathForCachedFile(item.Metadata().ID); path != "" {
|
if path := p.cache.PathForCachedFile(item.Metadata().ID); path != "" {
|
||||||
t := time.Now()
|
|
||||||
wd, err := GetWaveformDataForFile(context.Background(), path)
|
wd, err := GetWaveformDataForFile(context.Background(), path)
|
||||||
log.Printf("generate data took %0.3f milliseconds", float64(time.Since(t).Nanoseconds())/1000000)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
} else {
|
} else {
|
||||||
log.Println("have waveform data")
|
|
||||||
im := NewWaveformImage()
|
im := NewWaveformImage()
|
||||||
var c color.Color
|
GenerateWaveformImage(wd, im, color.White)
|
||||||
fyne.DoAndWait(func() {
|
for _, cb := range p.onWaveformImgUpdate {
|
||||||
c = fyne.CurrentApp().Settings().Theme().Color(
|
cb(im)
|
||||||
theme.ColorNamePrimary,
|
}
|
||||||
fyne.CurrentApp().Settings().ThemeVariant(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
log.Println("generating image...")
|
|
||||||
t := time.Now()
|
|
||||||
GenerateWaveformImage(wd, im, c)
|
|
||||||
log.Printf("generate image took %0.3f milliseconds", float64(time.Since(t).Nanoseconds())/1000000)
|
|
||||||
fyne.Do(func() {
|
|
||||||
w := fyne.CurrentApp().NewWindow("waveform")
|
|
||||||
w.SetPadded(false)
|
|
||||||
w.SetContent(canvas.NewImageFromImage(im))
|
|
||||||
w.Resize(fyne.NewSize(1024, 32))
|
|
||||||
w.Show()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Println("no cached file for waveform image")
|
log.Println("no cached file for waveform image")
|
||||||
@@ -213,6 +194,10 @@ func (p *PlaybackManager) OnPlayerChange(cb func()) {
|
|||||||
p.engine.onPlayerChange = append(p.engine.onPlayerChange, cb)
|
p.engine.onPlayerChange = append(p.engine.onPlayerChange, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) OnWaveformImgUpdate(cb func(*WaveformImage)) {
|
||||||
|
p.onWaveformImgUpdate = append(p.onWaveformImgUpdate, cb)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) IsSeeking() bool {
|
func (p *PlaybackManager) IsSeeking() bool {
|
||||||
return p.engine.IsSeeking()
|
return p.engine.IsSeeking()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,9 +204,6 @@ func convertToWav(ctx context.Context, inPath, outPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setPixel(img *image.NRGBA, x, y int, c color.NRGBA) {
|
func setPixel(img *image.NRGBA, x, y int, c color.NRGBA) {
|
||||||
if x < 0 || x >= img.Bounds().Dx() || y < 0 || y >= img.Bounds().Dy() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offset := img.PixOffset(x, y)
|
offset := img.PixOffset(x, y)
|
||||||
img.Pix[offset+0] = c.R
|
img.Pix[offset+0] = c.R
|
||||||
img.Pix[offset+1] = c.G
|
img.Pix[offset+1] = c.G
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
|||||||
bp.ExtendBaseWidget(bp)
|
bp.ExtendBaseWidget(bp)
|
||||||
|
|
||||||
pm.OnSongChange(bp.onSongChange)
|
pm.OnSongChange(bp.onSongChange)
|
||||||
|
pm.OnWaveformImgUpdate(bp.updateWaveformImg)
|
||||||
pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) {
|
pm.OnPlayTimeUpdate(func(cur, total float64, _ bool) {
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
if !pm.IsSeeking() {
|
if !pm.IsSeeking() {
|
||||||
@@ -141,6 +142,12 @@ func (bp *BottomPanel) onSongChange(song mediaprovider.MediaItem, _ *mediaprovid
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bp *BottomPanel) updateWaveformImg(img *backend.WaveformImage) {
|
||||||
|
fyne.Do(func() {
|
||||||
|
bp.Controls.UpdateWaveformImg(img)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (bp *BottomPanel) CreateRenderer() fyne.WidgetRenderer {
|
func (bp *BottomPanel) CreateRenderer() fyne.WidgetRenderer {
|
||||||
bp.ExtendBaseWidget(bp)
|
bp.ExtendBaseWidget(bp)
|
||||||
return widget.NewSimpleRenderer(bp.container)
|
return widget.NewSimpleRenderer(bp.container)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package widgets
|
package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/dweymouth/supersonic/backend"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@@ -82,6 +83,7 @@ type PlayerControls struct {
|
|||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
slider *TrackPosSlider
|
slider *TrackPosSlider
|
||||||
|
waveform *WaveformSeekbar
|
||||||
curTimeLabel *labelMinSize
|
curTimeLabel *labelMinSize
|
||||||
totalTimeLabel *labelMinSize
|
totalTimeLabel *labelMinSize
|
||||||
prev *IconButton
|
prev *IconButton
|
||||||
@@ -116,6 +118,8 @@ func NewPlayerControls() *PlayerControls {
|
|||||||
|
|
||||||
pc.slider = NewTrackPosSlider()
|
pc.slider = NewTrackPosSlider()
|
||||||
pc.slider.Disable()
|
pc.slider.Disable()
|
||||||
|
pc.slider.Hide()
|
||||||
|
pc.waveform = NewWaveformSeekbar()
|
||||||
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)
|
||||||
@@ -138,7 +142,11 @@ func NewPlayerControls() *PlayerControls {
|
|||||||
|
|
||||||
buttons := container.NewHBox(layout.NewSpacer(), pc.prev, pc.playpause, pc.next, layout.NewSpacer())
|
buttons := container.NewHBox(layout.NewSpacer(), pc.prev, pc.playpause, pc.next, layout.NewSpacer())
|
||||||
|
|
||||||
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider)
|
seekCtrl := container.NewStack(
|
||||||
|
pc.slider,
|
||||||
|
pc.waveform,
|
||||||
|
)
|
||||||
|
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, seekCtrl)
|
||||||
pc.container = container.New(layout.NewCustomPaddedVBoxLayout(0), c, buttons)
|
pc.container = container.New(layout.NewCustomPaddedVBoxLayout(0), c, buttons)
|
||||||
|
|
||||||
return pc
|
return pc
|
||||||
@@ -203,10 +211,15 @@ func (pc *PlayerControls) UpdatePlayTime(curTime, totalTime float64) {
|
|||||||
if updated {
|
if updated {
|
||||||
// Only update slider once a second when time label changes
|
// Only update slider once a second when time label changes
|
||||||
pc.slider.SetValue(v)
|
pc.slider.SetValue(v)
|
||||||
|
pc.waveform.SetProgress(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlayerControls) UpdateWaveformImg(img *backend.WaveformImage) {
|
||||||
|
p.waveform.UpdateImage(img)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer {
|
func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(p.container)
|
return widget.NewSimpleRenderer(p.container)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"github.com/dweymouth/supersonic/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WaveformSeekbar struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
imgColorL color.Color
|
||||||
|
imgColorR color.Color
|
||||||
|
imgProgress float64
|
||||||
|
|
||||||
|
img *canvas.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWaveformSeekbar() *WaveformSeekbar {
|
||||||
|
w := &WaveformSeekbar{
|
||||||
|
img: canvas.NewImageFromImage(nil),
|
||||||
|
}
|
||||||
|
w.ExtendBaseWidget(w)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WaveformSeekbar) UpdateImage(img *backend.WaveformImage) {
|
||||||
|
w.img.Image = img
|
||||||
|
w.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WaveformSeekbar) Refresh() {
|
||||||
|
prm, fg := w.getThemeColors()
|
||||||
|
w.recolorImage(prm, fg, w.imgProgress)
|
||||||
|
w.img.Refresh()
|
||||||
|
//w.BaseWidget.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WaveformSeekbar) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(
|
||||||
|
container.New(layout.NewCustomPaddedLayout(4, 4, 0, 0), w.img),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProgress sets how much of the seekbar has been played
|
||||||
|
// (ratio from 0 to 1)
|
||||||
|
func (w *WaveformSeekbar) SetProgress(v float64) {
|
||||||
|
prm, fg := w.getThemeColors()
|
||||||
|
w.recolorImage(prm, fg, v)
|
||||||
|
w.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WaveformSeekbar) getThemeColors() (primary, foreground color.Color) {
|
||||||
|
th := w.Theme()
|
||||||
|
vnt := fyne.CurrentApp().Settings().ThemeVariant()
|
||||||
|
primary = th.Color(theme.ColorNamePrimary, vnt)
|
||||||
|
foreground = th.Color(theme.ColorNameForeground, vnt)
|
||||||
|
return primary, foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WaveformSeekbar) recolorImage(cL, cR color.Color, progress float64) {
|
||||||
|
if w.img.Image == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.imgColorL == cL && w.imgColorR == cR && w.imgProgress == progress {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_r, _g, _b, _ := cL.RGBA()
|
||||||
|
rL, gL, bL := byte(_r>>8), byte(_g>>8), byte(_b>>8)
|
||||||
|
_r, _g, _b, _ = cR.RGBA()
|
||||||
|
rR, gR, bR := byte(_r>>8), byte(_g>>8), byte(_b>>8)
|
||||||
|
|
||||||
|
// TODO- smartly figure out which pixels we need
|
||||||
|
// to update for different scenarios (e.g. progress update only)
|
||||||
|
// and not iterate the whole thing every time
|
||||||
|
img := w.img.Image.(*image.NRGBA)
|
||||||
|
bnds := img.Rect.Bounds()
|
||||||
|
thresholdPixel := int(float64(bnds.Dx()) * progress)
|
||||||
|
log.Println("progress = ", progress, " bounds Dx = ", bnds.Dx())
|
||||||
|
log.Println("thresholdPixel = ", thresholdPixel)
|
||||||
|
for x := 0; x < bnds.Dx(); x++ {
|
||||||
|
for y := 0; y < bnds.Dy(); y++ {
|
||||||
|
if x < thresholdPixel {
|
||||||
|
setPixelRGB(img, x, y, rL, gL, bL)
|
||||||
|
} else {
|
||||||
|
setPixelRGB(img, x, y, rR, gR, bR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.imgColorL, w.imgColorR = cL, cR
|
||||||
|
w.imgProgress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPixelRGB(img *image.NRGBA, x, y int, r, g, b byte) {
|
||||||
|
offset := img.PixOffset(x, y)
|
||||||
|
img.Pix[offset+0] = r
|
||||||
|
img.Pix[offset+1] = g
|
||||||
|
img.Pix[offset+2] = b
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user