From 1b5cf118e1fac958f19f1db01db5b0ac53393e03 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 19 Jul 2024 16:52:08 -0700 Subject: [PATCH] WIP: peak/rms meter visualization --- backend/player/mpv/player.go | 62 +++++++++++---- main.go | 8 ++ ui/peakmeter.go | 141 +++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 ui/peakmeter.go diff --git a/backend/player/mpv/player.go b/backend/player/mpv/player.go index 41a31b6..1a6999b 100644 --- a/backend/player/mpv/player.go +++ b/backend/player/mpv/player.go @@ -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 { diff --git a/main.go b/main.go index 07dda4b..40371ac 100644 --- a/main.go +++ b/main.go @@ -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...") diff --git a/ui/peakmeter.go b/ui/peakmeter.go new file mode 100644 index 0000000..ac9b327 --- /dev/null +++ b/ui/peakmeter.go @@ -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)) +}