Files
sointu/tracker/gioui/specanalyzer.go
5684185+vsariola@users.noreply.github.com 06a1fb6b52 drafting
2026-01-02 02:03:05 +02:00

197 lines
5.7 KiB
Go

package gioui
import (
"math"
"strconv"
"gioui.org/layout"
"gioui.org/unit"
"github.com/vsariola/sointu/tracker"
)
type (
SpectrumState struct {
resolutionNumber *NumericUpDownState
smoothingBtn *Clickable
chnModeBtn *Clickable
plot *Plot
}
)
const SpectrumDisplayDb = 60
func NewSpectrumState() *SpectrumState {
return &SpectrumState{
plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDisplayDb, 0}),
resolutionNumber: NewNumericUpDownState(),
smoothingBtn: new(Clickable),
chnModeBtn: new(Clickable),
}
}
func (s *SpectrumState) Layout(gtx C) D {
s.Update(gtx)
t := TrackerFromContext(gtx)
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
var chnModeTxt string = "???"
switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) {
case tracker.SpecChnModeCombine:
chnModeTxt = "Sum"
case tracker.SpecChnModeSeparate:
chnModeTxt = "Separate"
case tracker.SpecChnModeOff:
chnModeTxt = "Off"
}
var smoothTxt string = "???"
switch tracker.SpecSmoothing(t.Model.SpecAnSmoothing().Value()) {
case tracker.SpecSmoothingSlow:
smoothTxt = "Slow"
case tracker.SpecSmoothingMedium:
smoothTxt = "Medium"
case tracker.SpecSmoothingFast:
smoothTxt = "Fast"
}
resolution := NumUpDown(t.Model.SpecAnResolution(), t.Theme, s.resolutionNumber, "Resolution")
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode")
smoothBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.smoothingBtn, smoothTxt, "Smoothing")
numchns := 0
speclen := len(t.Model.Spectrum()[0])
if speclen > 0 {
numchns = 1
if len(t.Model.Spectrum()[1]) == speclen {
numchns = 2
}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
biquad, biquadok := t.Model.BiquadCoeffs()
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
if chn == 2 {
if xr.a >= 0 {
return plotRange{}, false
}
ya := math.Log10(float64(biquad.Gain(float32(math.Pi*math.Pow(10, float64(xr.a)))))) * 20
yb := math.Log10(float64(biquad.Gain(float32(math.Pi*math.Pow(10, float64(xr.b)))))) * 20
return plotRange{float32(ya) + SpectrumDisplayDb, float32(yb) + SpectrumDisplayDb}, true
}
if chn >= numchns {
return plotRange{}, false
}
xr.a = float32(math.Pow(10, float64(xr.a)))
xr.b = float32(math.Pow(10, float64(xr.b)))
w1, f1 := math.Modf(float64(xr.a) * float64(speclen))
w2, f2 := math.Modf(float64(xr.b) * float64(speclen))
x1 := max(int(w1), 0)
x2 := min(int(w2), speclen-1)
if x1 > x2 {
return plotRange{}, false
}
y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1))
switch {
case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins
l := t.Model.Spectrum()[chn][x1]
r := t.Model.Spectrum()[chn][x1+1]
y1 = smoothInterpolate(l, r, float32(f1))
l = t.Model.Spectrum()[chn][x2]
r = t.Model.Spectrum()[chn][x2+1]
y2 = smoothInterpolate(l, r, float32(f2))
y1, y2 = max(y1, y2), min(y1, y2)
default:
for i := x1; i <= x2; i++ {
sample := t.Model.Spectrum()[chn][i]
y1 = max(y1, sample)
y2 = min(y2, sample)
}
}
y1 = SpectrumDisplayDb + y1
y2 = SpectrumDisplayDb + y2
y1 = softplus(y1/5) * 5 // we "squash" the low volumes so the -Inf dB becomes -SpectrumDisplayDb
y2 = softplus(y2/5) * 5
return plotRange{y1, y2}, true
}
xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
type pair struct {
freq float64
label string
}
for _, p := range []pair{
{freq: 10, label: "10 Hz"},
{freq: 20, label: "20 Hz"},
{freq: 50, label: "50 Hz"},
{freq: 100, label: "100 Hz"},
{freq: 200, label: "200 Hz"},
{freq: 500, label: "500 Hz"},
{freq: 1e3, label: "1 kHz"},
{freq: 2e3, label: "2 kHz"},
{freq: 5e3, label: "5 kHz"},
{freq: 1e4, label: "10 kHz"},
{freq: 2e4, label: "20 kHz"},
} {
x := float32(math.Log10(p.freq / 22050))
if x >= r.a && x <= r.b {
yield(x, p.label)
}
}
}
yticks := func(r plotRange, count int, yield func(pos float32, label string)) {
step := 3
var start, end int
for {
start = int(math.Ceil(float64(r.b-SpectrumDisplayDb) / float64(step)))
end = int(math.Floor(float64(r.a-SpectrumDisplayDb) / float64(step)))
step *= 2
if end-start+1 <= count*4 { // we use 4x density for the y-lines in the spectrum
break
}
}
for i := start; i <= end; i++ {
yield(float32(i*step)+SpectrumDisplayDb, strconv.Itoa(i*step))
}
}
n := numchns
if biquadok {
n = 3
}
return s.plot.Layout(gtx, data, xticks, yticks, 0, n)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(leftSpacer),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
layout.Rigid(chnModeBtn.Layout),
layout.Rigid(smoothBtn.Layout),
layout.Rigid(resolution.Layout),
layout.Rigid(rightSpacer),
)
}),
)
}
func softplus(f float32) float32 {
return float32(math.Log(1 + math.Exp(float64(f))))
}
func smoothInterpolate(a, b float32, t float32) float32 {
t = t * t * (3 - 2*t)
return (1-t)*a + t*b
}
func (s *SpectrumState) Update(gtx C) {
t := TrackerFromContext(gtx)
for s.chnModeBtn.Clicked(gtx) {
t.Model.SpecAnChannelsInt().SetValue((t.SpecAnChannelsInt().Value() + 1) % int(tracker.NumSpecChnModes))
}
for s.smoothingBtn.Clicked(gtx) {
r := t.Model.SpecAnSmoothing().Range()
t.Model.SpecAnSmoothing().SetValue((t.SpecAnSmoothing().Value()+1)%(r.Max-r.Min+1) + r.Min)
}
}