WIP: peak/rms meter visualization

This commit is contained in:
Drew Weymouth
2024-07-22 09:27:09 -07:00
parent 849d12d7a0
commit 1b5cf118e1
3 changed files with 197 additions and 14 deletions
+48 -14
View File
@@ -63,6 +63,7 @@ type Player struct {
prePausedState player.State
clientName string
equalizer Equalizer
peaksEnabled bool
bgCancel context.CancelFunc
@@ -126,6 +127,7 @@ func (p *Player) Init(maxCacheMB int) error {
if err := m.Initialize(); err != nil {
return fmt.Errorf("error initializing mpv: %s", err.Error())
}
p.mpv = m
}
ctx, cancel := context.WithCancel(context.Background())
@@ -355,20 +357,7 @@ func (p *Player) SetAudioDevice(deviceName string) error {
func (p *Player) SetEqualizer(eq Equalizer) error {
p.equalizer = eq
if eq == nil || !eq.IsEnabled() {
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)
return p.setAF()
}
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
func (p *Player) setState(s player.State) {
switch {
@@ -478,6 +492,26 @@ func (p *Player) setState(s player.State) {
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) {
for {
select {
+8
View File
@@ -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()
log.Println("Running shutdown tasks...")
+141
View File
@@ -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))
}