WIP: peak/rms meter visualization
This commit is contained in:
@@ -63,6 +63,7 @@ type Player struct {
|
|||||||
prePausedState player.State
|
prePausedState player.State
|
||||||
clientName string
|
clientName string
|
||||||
equalizer Equalizer
|
equalizer Equalizer
|
||||||
|
peaksEnabled bool
|
||||||
|
|
||||||
bgCancel context.CancelFunc
|
bgCancel context.CancelFunc
|
||||||
|
|
||||||
@@ -126,6 +127,7 @@ func (p *Player) Init(maxCacheMB int) error {
|
|||||||
if err := m.Initialize(); err != nil {
|
if err := m.Initialize(); err != nil {
|
||||||
return fmt.Errorf("error initializing mpv: %s", err.Error())
|
return fmt.Errorf("error initializing mpv: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
p.mpv = m
|
p.mpv = m
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -355,20 +357,7 @@ func (p *Player) SetAudioDevice(deviceName string) error {
|
|||||||
|
|
||||||
func (p *Player) SetEqualizer(eq Equalizer) error {
|
func (p *Player) SetEqualizer(eq Equalizer) error {
|
||||||
p.equalizer = eq
|
p.equalizer = eq
|
||||||
if eq == nil || !eq.IsEnabled() {
|
return p.setAF()
|
||||||
return p.mpv.SetPropertyString("af", "")
|
|
||||||
}
|
|
||||||
af := ""
|
|
||||||
if math.Abs(eq.Preamp()) > 0.01 {
|
|
||||||
af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp())
|
|
||||||
}
|
|
||||||
eqAF := eq.Curve().String()
|
|
||||||
if af == "" {
|
|
||||||
af = eqAF
|
|
||||||
} else if eqAF != "" {
|
|
||||||
af = fmt.Sprintf("%s,%s", af, eqAF)
|
|
||||||
}
|
|
||||||
return p.mpv.SetPropertyString("af", af)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) Equalizer() Equalizer {
|
func (p *Player) Equalizer() Equalizer {
|
||||||
@@ -453,6 +442,31 @@ func (p *Player) Destroy() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) SetPeaksEnabled(enabled bool) error {
|
||||||
|
if p.peaksEnabled == enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.peaksEnabled = enabled
|
||||||
|
return p.setAF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) GetPeaks() (float64, float64, float64, float64) {
|
||||||
|
nInf := math.Inf(-1)
|
||||||
|
if p.status.State != player.Playing {
|
||||||
|
return nInf, nInf, nInf, nInf
|
||||||
|
}
|
||||||
|
prop, err := p.mpv.GetProperty("af-metadata/astats", mpv.FORMAT_NODE)
|
||||||
|
if err != nil {
|
||||||
|
return nInf, nInf, nInf, nInf
|
||||||
|
}
|
||||||
|
m := prop.(*mpv.Node).Data.(map[string]*mpv.Node)
|
||||||
|
lPeak, _ := strconv.ParseFloat(m["lavfi.astats.1.Peak_level"].Data.(string), 64)
|
||||||
|
rPeak, _ := strconv.ParseFloat(m["lavfi.astats.2.Peak_level"].Data.(string), 64)
|
||||||
|
lRMS, _ := strconv.ParseFloat(m["lavfi.astats.1.RMS_level"].Data.(string), 64)
|
||||||
|
rRMS, _ := strconv.ParseFloat(m["lavfi.astats.2.RMS_level"].Data.(string), 64)
|
||||||
|
return lPeak, rPeak, lRMS, rRMS
|
||||||
|
}
|
||||||
|
|
||||||
// sets the state and invokes callbacks, if triggered
|
// sets the state and invokes callbacks, if triggered
|
||||||
func (p *Player) setState(s player.State) {
|
func (p *Player) setState(s player.State) {
|
||||||
switch {
|
switch {
|
||||||
@@ -478,6 +492,26 @@ func (p *Player) setState(s player.State) {
|
|||||||
p.status.State = s
|
p.status.State = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) setAF() error {
|
||||||
|
af := ""
|
||||||
|
if p.peaksEnabled {
|
||||||
|
af = "@astats:astats=metadata=1:reset=1"
|
||||||
|
}
|
||||||
|
eq := p.equalizer
|
||||||
|
if eq == nil || !eq.IsEnabled() {
|
||||||
|
return p.mpv.SetPropertyString("af", af)
|
||||||
|
} else if p.peaksEnabled {
|
||||||
|
af = af + ","
|
||||||
|
}
|
||||||
|
if math.Abs(eq.Preamp()) > 0.01 {
|
||||||
|
af = fmt.Sprintf("%svolume=volume=%0.1fdB", af, eq.Preamp())
|
||||||
|
}
|
||||||
|
if eqAF := eq.Curve().String(); eqAF != "" {
|
||||||
|
af = fmt.Sprintf("%s,%s", af, eqAF)
|
||||||
|
}
|
||||||
|
return p.mpv.SetPropertyString("af", af)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Player) eventHandler(ctx context.Context) {
|
func (p *Player) eventHandler(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ func main() {
|
|||||||
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
myApp.LocalPlayer.SetPeaksEnabled(true)
|
||||||
|
|
||||||
|
peaks := fyneApp.NewWindow("Peak meter")
|
||||||
|
meter := ui.NewPeakMeter(myApp.LocalPlayer.GetPeaks)
|
||||||
|
peaks.SetContent(meter)
|
||||||
|
meter.Start()
|
||||||
|
peaks.Show()
|
||||||
|
|
||||||
mainWindow.ShowAndRun()
|
mainWindow.ShowAndRun()
|
||||||
|
|
||||||
log.Println("Running shutdown tasks...")
|
log.Println("Running shutdown tasks...")
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
meterRangeDB = 60
|
||||||
|
rmsSmoothingFactor = 0.8
|
||||||
|
peakHoldFrames = 60
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeakFN func() (float64, float64, float64, float64)
|
||||||
|
|
||||||
|
type PeakMeter struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
peakFnDB PeakFN
|
||||||
|
lPeak float64
|
||||||
|
rPeak float64
|
||||||
|
lRMS float64
|
||||||
|
rRMS float64
|
||||||
|
lPeakHold float64
|
||||||
|
rPeakHold float64
|
||||||
|
lPeakHoldFrame uint64
|
||||||
|
rPeakHoldFrame uint64
|
||||||
|
frameCounter uint64
|
||||||
|
|
||||||
|
lPeakRect canvas.Rectangle
|
||||||
|
rPeakRect canvas.Rectangle
|
||||||
|
lRMSRect canvas.Rectangle
|
||||||
|
rRMSRect canvas.Rectangle
|
||||||
|
lPeakHoldRect canvas.Rectangle
|
||||||
|
rPeakHoldRect canvas.Rectangle
|
||||||
|
anim *fyne.Animation
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeakMeter(peakFnDB PeakFN) *PeakMeter {
|
||||||
|
p := &PeakMeter{peakFnDB: peakFnDB}
|
||||||
|
p.ExtendBaseWidget(p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PeakMeter) Start() {
|
||||||
|
if p.anim != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.anim = fyne.NewAnimation(time.Duration(math.MaxInt64) /*until stopped*/, p.tick)
|
||||||
|
p.anim.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PeakMeter) Stop() {
|
||||||
|
if p.anim != nil {
|
||||||
|
p.anim.Stop()
|
||||||
|
p.anim = nil
|
||||||
|
p.frameCounter = 0
|
||||||
|
p.lPeakHoldFrame = 0
|
||||||
|
p.rPeakHoldFrame = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PeakMeter) tick(_ float32) {
|
||||||
|
lPeak, rPeak, lRMS, rRMS := p.peakFnDB()
|
||||||
|
p.lPeak = lPeak
|
||||||
|
p.rPeak = rPeak
|
||||||
|
lRMS = math.Max(-96, lRMS)
|
||||||
|
rRMS = math.Max(-96, rRMS)
|
||||||
|
p.lRMS = rmsSmoothingFactor*p.lRMS + (1-rmsSmoothingFactor)*lRMS
|
||||||
|
p.rRMS = rmsSmoothingFactor*p.rRMS + (1-rmsSmoothingFactor)*rRMS
|
||||||
|
|
||||||
|
if lPeak > p.lPeakHold || p.frameCounter-p.lPeakHoldFrame > peakHoldFrames {
|
||||||
|
p.lPeakHold = lPeak
|
||||||
|
p.lPeakHoldFrame = p.frameCounter
|
||||||
|
}
|
||||||
|
if rPeak > p.rPeakHold || p.frameCounter-p.rPeakHoldFrame > peakHoldFrames {
|
||||||
|
p.rPeakHold = rPeak
|
||||||
|
p.rPeakHoldFrame = p.frameCounter
|
||||||
|
}
|
||||||
|
|
||||||
|
p.frameCounter++
|
||||||
|
p.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PeakMeter) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
c := theme.PrimaryColor().(color.NRGBA)
|
||||||
|
c.A = 128
|
||||||
|
p.lPeakRect.FillColor = c
|
||||||
|
p.rPeakRect.FillColor = c
|
||||||
|
p.lRMSRect.FillColor = c
|
||||||
|
p.rRMSRect.FillColor = c
|
||||||
|
p.lPeakHoldRect.FillColor = theme.ForegroundColor()
|
||||||
|
p.rPeakHoldRect.FillColor = p.lPeakHoldRect.FillColor
|
||||||
|
return widget.NewSimpleRenderer(
|
||||||
|
container.New(
|
||||||
|
&peakMeterLayout{p},
|
||||||
|
&p.lPeakRect, &p.rPeakRect,
|
||||||
|
&p.lRMSRect, &p.rRMSRect,
|
||||||
|
&p.lPeakHoldRect, &p.rPeakHoldRect,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type peakMeterLayout struct {
|
||||||
|
p *PeakMeter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *peakMeterLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
|
||||||
|
return fyne.NewSize(50, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *peakMeterLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||||
|
lPeakH := float32(math.Max(0, meterRangeDB+l.p.lPeak)/meterRangeDB) * size.Height
|
||||||
|
rPeakH := float32(math.Max(0, meterRangeDB+l.p.rPeak)/meterRangeDB) * size.Height
|
||||||
|
lRMSH := float32(math.Max(0, meterRangeDB+l.p.lRMS)/meterRangeDB) * size.Height
|
||||||
|
rRMSH := float32(math.Max(0, meterRangeDB+l.p.rRMS)/meterRangeDB) * size.Height
|
||||||
|
lPeakHoldPos := float32(math.Max(0, meterRangeDB+l.p.lPeakHold)/meterRangeDB) * size.Height
|
||||||
|
rPeakHoldPos := float32(math.Max(0, meterRangeDB+l.p.rPeakHold)/meterRangeDB) * size.Height
|
||||||
|
|
||||||
|
halfW := size.Width / 2
|
||||||
|
objects[0].Move(fyne.NewPos(0, size.Height-lPeakH))
|
||||||
|
objects[0].Resize(fyne.NewSize(halfW, lPeakH))
|
||||||
|
objects[1].Move(fyne.NewPos(halfW, size.Height-rPeakH))
|
||||||
|
objects[1].Resize(fyne.NewSize(halfW, rPeakH))
|
||||||
|
objects[2].Move(fyne.NewPos(0, size.Height-lRMSH))
|
||||||
|
objects[2].Resize(fyne.NewSize(halfW, lRMSH))
|
||||||
|
objects[3].Move(fyne.NewPos(halfW, size.Height-rRMSH))
|
||||||
|
objects[3].Resize(fyne.NewSize(halfW, rRMSH))
|
||||||
|
|
||||||
|
peakHoldHt := theme.SeparatorThicknessSize() * 2
|
||||||
|
objects[4].Move(fyne.NewPos(0, size.Height-lPeakHoldPos-peakHoldHt))
|
||||||
|
objects[4].Resize(fyne.NewSize(halfW, peakHoldHt))
|
||||||
|
objects[5].Move(fyne.NewPos(halfW, size.Height-rPeakHoldPos-peakHoldHt))
|
||||||
|
objects[5].Resize(fyne.NewSize(halfW, peakHoldHt))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user