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) } }