WIP: peak/rms meter visualization
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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