mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-08 00:30:18 -05:00
218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
package gioui
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
|
|
"gioui.org/layout"
|
|
"gioui.org/unit"
|
|
"github.com/vsariola/sointu/tracker"
|
|
)
|
|
|
|
type (
|
|
SpectrumState struct {
|
|
resolutionNumber *NumericUpDownState
|
|
speed *NumericUpDownState
|
|
chnModeBtn *Clickable
|
|
plot *Plot
|
|
}
|
|
)
|
|
|
|
const (
|
|
SpectrumDbMin = -60
|
|
SpectrumDbMax = 12
|
|
)
|
|
|
|
func NewSpectrumState() *SpectrumState {
|
|
return &SpectrumState{
|
|
plot: NewPlot(plotRange{-3.7, 0}, plotRange{SpectrumDbMax, SpectrumDbMin}, SpectrumDbMin),
|
|
resolutionNumber: NewNumericUpDownState(),
|
|
speed: NewNumericUpDownState(),
|
|
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(36)}.Layout
|
|
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
|
|
|
var chnModeTxt string = "???"
|
|
switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) {
|
|
case tracker.SpecChnModeSum:
|
|
chnModeTxt = "Sum"
|
|
case tracker.SpecChnModeSeparate:
|
|
chnModeTxt = "Separate"
|
|
}
|
|
|
|
resolution := NumUpDown(t.Model.SpecAnResolution(), t.Theme, s.resolutionNumber, "Resolution")
|
|
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Text, s.chnModeBtn, chnModeTxt, "Channel mode")
|
|
speed := NumUpDown(t.Model.SpecAnSpeed(), t.Theme, s.speed, "Speed")
|
|
|
|
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), float32(yb)}, 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) - 1) // -1 cause we don't have the DC bin there
|
|
w2, f2 := math.Modf(float64(xr.b)*float64(speclen) - 1) // -1 cause we don't have the DC bin there
|
|
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 = softplus((y1-SpectrumDbMin)/5)*5 + SpectrumDbMin // we "squash" the low volumes so the -Inf dB becomes -SpectrumDbMin
|
|
y2 = softplus((y2-SpectrumDbMin)/5)*5 + SpectrumDbMin
|
|
|
|
return plotRange{y1, y2}, true
|
|
}
|
|
xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
|
|
type pair struct {
|
|
freq float64
|
|
label string
|
|
}
|
|
const offset = 0.343408593803857 // log10(22050/10000)
|
|
const startdiv = 3 * (1 << 8)
|
|
step := nextPowerOfTwo(int(float64(r.b-r.a)*startdiv/float64(count)) + 1)
|
|
start := int(math.Floor(float64(r.a+offset) * startdiv / float64(step)))
|
|
end := int(math.Ceil(float64(r.b+offset) * startdiv / float64(step)))
|
|
for i := start; i <= end; i++ {
|
|
lognormfreq := float32(i*step)/startdiv - offset
|
|
freq := math.Pow(10, float64(lognormfreq)) * 22050
|
|
df := freq * math.Log(10) * float64(step) / startdiv // this is roughly the difference in Hz between the ticks currently
|
|
rounding := int(math.Floor(math.Log10(df)))
|
|
r := math.Pow(10, float64(rounding))
|
|
freq = math.Round(freq/r) * r
|
|
tickpos := float32(math.Log10(freq / 22050))
|
|
if rounding >= 3 {
|
|
yield(tickpos, fmt.Sprintf("%.0f kHz", freq/1000))
|
|
} else {
|
|
yield(tickpos, fmt.Sprintf("%s Hz", strconv.FormatFloat(freq, 'f', -rounding, 64)))
|
|
}
|
|
}
|
|
}
|
|
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) / float64(step)))
|
|
end = int(math.Floor(float64(r.a) / float64(step)))
|
|
if end-start+1 <= count*4 { // we use 4x density for the y-lines in the spectrum
|
|
break
|
|
}
|
|
step *= 2
|
|
}
|
|
for i := start; i <= end; i++ {
|
|
yield(float32(i*step), strconv.Itoa(i*step))
|
|
}
|
|
}
|
|
n := numchns
|
|
if biquadok {
|
|
n = 3
|
|
}
|
|
return s.plot.Layout(gtx, data, xticks, yticks, float32(math.NaN()), n)
|
|
}),
|
|
layout.Rigid(func(gtx C) D {
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(leftSpacer),
|
|
layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Resolution").Layout),
|
|
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(resolution.Layout),
|
|
layout.Rigid(rightSpacer),
|
|
)
|
|
}),
|
|
layout.Rigid(func(gtx C) D {
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(leftSpacer),
|
|
layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Speed").Layout),
|
|
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(speed.Layout),
|
|
layout.Rigid(rightSpacer),
|
|
)
|
|
}),
|
|
layout.Rigid(func(gtx C) D {
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(leftSpacer),
|
|
layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Channels").Layout),
|
|
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(chnModeBtn.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 nextPowerOfTwo(v int) int {
|
|
if v <= 0 {
|
|
return 1
|
|
}
|
|
v--
|
|
v |= v >> 1
|
|
v |= v >> 2
|
|
v |= v >> 4
|
|
v |= v >> 8
|
|
v |= v >> 16
|
|
v |= v >> 32
|
|
v++
|
|
return v
|
|
}
|
|
|
|
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))
|
|
}
|
|
s.resolutionNumber.Update(gtx, t.Model.SpecAnResolution())
|
|
s.speed.Update(gtx, t.Model.SpecAnSpeed())
|
|
}
|