This commit is contained in:
5684185+vsariola@users.noreply.github.com
2026-01-02 02:03:05 +02:00
parent fcb9a06249
commit 06a1fb6b52
5 changed files with 145 additions and 38 deletions

View File

@ -44,6 +44,7 @@ func Scope(th *Theme, m *tracker.ScopeModel, st *OscilloscopeState) Oscilloscope
} }
func (s *Oscilloscope) Layout(gtx C) D { func (s *Oscilloscope) Layout(gtx C) D {
t := TrackerFromContext(gtx)
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
@ -58,11 +59,11 @@ func (s *Oscilloscope) Layout(gtx C) D {
w := s.Model.Waveform() w := s.Model.Waveform()
cx := float32(w.Cursor) / float32(len(w.Buffer)) cx := float32(w.Cursor) / float32(len(w.Buffer))
data := func(chn int, xr plotRange) (yr plotRange) { data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
x1 := max(int(xr.a*float32(len(w.Buffer))), 0) x1 := max(int(xr.a*float32(len(w.Buffer))), 0)
x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1) x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1)
if x1 > x2 { if x1 > x2 {
return plotRange{1, 0} return plotRange{}, false
} }
y1 := float32(math.Inf(-1)) y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1)) y2 := float32(math.Inf(+1))
@ -71,18 +72,31 @@ func (s *Oscilloscope) Layout(gtx C) D {
y1 = max(y1, sample) y1 = max(y1, sample)
y2 = min(y2, sample) y2 = min(y2, sample)
} }
return plotRange{-y1, -y2} return plotRange{-y1, -y2}, true
} }
xticks := func(r plotRange, yield func(pos float32, label string)) { rpb := max(t.Model.RowsPerBeat().Value(), 1)
l := s.Model.LengthInBeats().Value() xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
l := s.Model.LengthInBeats().Value() * rpb
a := max(int(math.Ceil(float64(r.a*float32(l)))), 0) a := max(int(math.Ceil(float64(r.a*float32(l)))), 0)
b := min(int(math.Floor(float64(r.b*float32(l)))), l) b := min(int(math.Floor(float64(r.b*float32(l)))), l)
for i := a; i <= b; i++ { step := 1
yield(float32(i)/float32(l), strconv.Itoa(i)) n := rpb
for (b-a+1)/step > count {
step *= n
n = 2
}
a = (a / step) * step
for i := a; i <= b; i += step {
if i%rpb == 0 {
beat := i / rpb
yield(float32(i)/float32(l), strconv.Itoa(beat))
} else {
yield(float32(i)/float32(l), "")
}
} }
} }
yticks := func(r plotRange, yield func(pos float32, label string)) { yticks := func(r plotRange, count int, yield func(pos float32, label string)) {
yield(-1, "") yield(-1, "")
yield(1, "") yield(1, "")
} }

View File

@ -11,6 +11,7 @@ import (
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/unit"
) )
type ( type (
@ -25,14 +26,15 @@ type (
} }
PlotStyle struct { PlotStyle struct {
CurveColors [2]color.NRGBA `yaml:",flow"` CurveColors [3]color.NRGBA `yaml:",flow"`
LimitColor color.NRGBA `yaml:",flow"` LimitColor color.NRGBA `yaml:",flow"`
CursorColor color.NRGBA `yaml:",flow"` CursorColor color.NRGBA `yaml:",flow"`
Ticks LabelStyle Ticks LabelStyle
DpPerTick unit.Dp
} }
PlotDataFunc func(chn int, xr plotRange) (yr plotRange) PlotDataFunc func(chn int, xr plotRange) (yr plotRange, ok bool)
PlotTickFunc func(r plotRange, yield func(pos float32, label string)) PlotTickFunc func(r plotRange, num int, yield func(pos float32, label string))
plotRange struct{ a, b float32 } plotRange struct{ a, b float32 }
plotRel float32 plotRel float32
plotPx int plotPx int
@ -61,8 +63,8 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur
ylim := p.ylim() ylim := p.ylim()
// draw tick marks // draw tick marks
numxticks := s.X / gtx.Dp(style.DpPerTick)
xticks(xlim, func(x float32, txt string) { xticks(xlim, numxticks, func(x float32, txt string) {
paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops) paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops)
sx := plotPx(s.X).toScreen(xlim.toRelative(x)) sx := plotPx(s.X).toScreen(xlim.toRelative(x))
fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)}) fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)})
@ -70,7 +72,8 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur
Label(t.Theme, &t.Theme.Plot.Ticks, txt).Layout(gtx) Label(t.Theme, &t.Theme.Plot.Ticks, txt).Layout(gtx)
}) })
yticks(ylim, func(y float32, txt string) { numyticks := s.Y / gtx.Dp(style.DpPerTick)
yticks(ylim, numyticks, func(y float32, txt string) {
paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops) paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops)
sy := plotPx(s.Y).toScreen(ylim.toRelative(y)) sy := plotPx(s.Y).toScreen(ylim.toRelative(y))
fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)}) fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)})
@ -91,13 +94,13 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur
// left and right is the sample range covered by the pixel // left and right is the sample range covered by the pixel
left := right left := right
right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1)) right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1))
yr := data(chn, plotRange{left, right}) yr, ok := data(chn, plotRange{left, right})
if yr.b < yr.a { if !ok {
continue continue
} }
y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a)) y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a))
y2 := plotPx(s.Y).toScreen(ylim.toRelative(yr.b)) y2 := plotPx(s.Y).toScreen(ylim.toRelative(yr.b))
fillRect(gtx, clip.Rect{Min: image.Pt(sx, y1), Max: image.Pt(sx+1, y2+1)}) fillRect(gtx, clip.Rect{Min: image.Pt(sx, min(y1, y2)), Max: image.Pt(sx+1, max(y1, y2)+1)})
} }
} }
return D{Size: s} return D{Size: s}

View File

@ -2,6 +2,7 @@ package gioui
import ( import (
"math" "math"
"strconv"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/unit" "gioui.org/unit"
@ -17,9 +18,11 @@ type (
} }
) )
const SpectrumDisplayDb = 60
func NewSpectrumState() *SpectrumState { func NewSpectrumState() *SpectrumState {
return &SpectrumState{ return &SpectrumState{
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 0}), plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDisplayDb, 0}),
resolutionNumber: NewNumericUpDownState(), resolutionNumber: NewNumericUpDownState(),
smoothingBtn: new(Clickable), smoothingBtn: new(Clickable),
chnModeBtn: new(Clickable), chnModeBtn: new(Clickable),
@ -67,15 +70,27 @@ func (s *SpectrumState) Layout(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D { layout.Flexed(1, func(gtx C) D {
data := func(chn int, xr plotRange) (yr plotRange) { biquad, biquadok := t.Model.BiquadCoeffs()
xr.a = float32(math.Exp2(float64(xr.a-1) * 8)) data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
xr.b = float32(math.Exp2(float64(xr.b-1) * 8)) 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)) w1, f1 := math.Modf(float64(xr.a) * float64(speclen))
w2, f2 := math.Modf(float64(xr.b) * float64(speclen)) w2, f2 := math.Modf(float64(xr.b) * float64(speclen))
x1 := max(int(w1), 0) x1 := max(int(w1), 0)
x2 := min(int(w2), speclen-1) x2 := min(int(w2), speclen-1)
if x1 > x2 { if x1 > x2 {
return plotRange{1, 0} return plotRange{}, false
} }
y1 := float32(math.Inf(-1)) y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1)) y2 := float32(math.Inf(+1))
@ -95,18 +110,18 @@ func (s *SpectrumState) Layout(gtx C) D {
y2 = min(y2, sample) y2 = min(y2, sample)
} }
} }
y1 = (y1 / 80) + 1 y1 = SpectrumDisplayDb + y1
y2 = (y2 / 80) + 1 y2 = SpectrumDisplayDb + y2
y1 = softplus(y1*10) / 10 y1 = softplus(y1/5) * 5 // we "squash" the low volumes so the -Inf dB becomes -SpectrumDisplayDb
y2 = softplus(y2*10) / 10 y2 = softplus(y2/5) * 5
return plotRange{-y1, -y2} return plotRange{y1, y2}, true
} }
type pair struct { xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
freq float64 type pair struct {
label string freq float64
} label string
xticks := func(r plotRange, yield func(pos float32, label string)) { }
for _, p := range []pair{ for _, p := range []pair{
{freq: 10, label: "10 Hz"}, {freq: 10, label: "10 Hz"},
{freq: 20, label: "20 Hz"}, {freq: 20, label: "20 Hz"},
@ -120,17 +135,32 @@ func (s *SpectrumState) Layout(gtx C) D {
{freq: 1e4, label: "10 kHz"}, {freq: 1e4, label: "10 kHz"},
{freq: 2e4, label: "20 kHz"}, {freq: 2e4, label: "20 kHz"},
} { } {
x := float32(math.Log2((p.freq/22050))/8 + 1) x := float32(math.Log10(p.freq / 22050))
if x >= r.a && x <= r.b { if x >= r.a && x <= r.b {
yield(x, p.label) yield(x, p.label)
} }
} }
} }
yticks := func(r plotRange, yield func(pos float32, label string)) { yticks := func(r plotRange, count int, yield func(pos float32, label string)) {
yield(-1, "0 dB") step := 3
yield(0, "-Inf dB") 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))
}
} }
return s.plot.Layout(gtx, data, xticks, yticks, 0, numchns) n := numchns
if biquadok {
n = 3
}
return s.plot.Layout(gtx, data, xticks, yticks, 0, n)
}), }),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,

View File

@ -84,10 +84,11 @@ iconbutton:
size: 24 size: 24
inset: { top: 6, bottom: 6, left: 6, right: 6 } inset: { top: 6, bottom: 6, left: 6, right: 6 }
plot: plot:
curvecolors: [*primarycolor, *secondarycolor] curvecolors: [*primarycolor, *secondarycolor,*disabled]
limitcolor: { r: 255, g: 255, b: 255, a: 8 } limitcolor: { r: 255, g: 255, b: 255, a: 8 }
cursorcolor: { r: 252, g: 186, b: 3, a: 255 } cursorcolor: { r: 252, g: 186, b: 3, a: 255 }
ticks: { textsize: 12, color: *disabled, maxlines: 1} ticks: { textsize: 12, color: *disabled, maxlines: 1}
dppertick: 50
numericupdown: numericupdown:
bgcolor: { r: 255, g: 255, b: 255, a: 3 } bgcolor: { r: 255, g: 255, b: 255, a: 3 }
textcolor: *fg textcolor: *fg

View File

@ -34,6 +34,11 @@ type (
tmpC []complex128 // temporary buffer for FFT tmpC []complex128 // temporary buffer for FFT
tmp1, tmp2 []float32 // temporary buffers for processing tmp1, tmp2 []float32 // temporary buffers for processing
} }
BiquadCoeffs struct {
b0, b1, b2 float32
a0, a1, a2 float32
}
) )
const ( const (
@ -72,6 +77,60 @@ func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
return ret return ret
} }
func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
i := m.d.InstrIndex
u := m.d.UnitIndex
if i < 0 || i >= len(m.d.Song.Patch) || u < 0 || u >= len(m.d.Song.Patch[i].Units) {
return BiquadCoeffs{}, false
}
switch m.d.Song.Patch[i].Units[u].Type {
case "filter":
p := m.d.Song.Patch[i].Units[u].Parameters
f := float32(p["frequency"]) / 128
res := float32(p["resonance"]) / 128
f2 := f * f
g := f2 / (2 - f2)
a1 := 2 * (g*g - 1)
a2 := (1 - g*(g-res))
var b0, b1, b2 float32
if p["low"] == 1 {
b0 += g * g
b1 += 2 * g * g
b2 += g * g
}
b0 += float32(p["high"])
b1 += -2 * float32(p["high"])
b2 += float32(p["high"])
b0 += g * float32(p["band"])
b2 += -g * float32(p["band"])
return BiquadCoeffs{a0: 1, a1: a1, a2: a2, b0: b0, b1: b1, b2: b2}, true
case "belleq":
f := float32(m.d.Song.Patch[i].Units[u].Parameters["frequency"]) / 128
band := float32(m.d.Song.Patch[i].Units[u].Parameters["bandwidth"]) / 128
gain := float32(m.d.Song.Patch[i].Units[u].Parameters["gain"]) / 128
omega0 := 2 * f * f
alpha := float32(math.Sin(float64(omega0))) * 2 * band
A := float32(math.Pow(2, float64(gain-.5)*6.643856189774724))
u, v := alpha*A, alpha/A
return BiquadCoeffs{
b0: 1 + u,
b1: -2 * float32(math.Cos(float64(omega0))),
b2: 1 - u,
a0: 1 + v,
a1: -2 * float32(math.Cos(float64(omega0))),
a2: 1 - v,
}, true
default:
return BiquadCoeffs{}, false
}
}
func (c *BiquadCoeffs) Gain(omega float32) float32 {
e := cmplx.Rect(1, -float64(omega))
return float32(cmplx.Abs((complex(float64(c.b0), 0) + complex(float64(c.b1), 0)*e + complex(float64(c.b2), 0)*(e*e)) /
(complex(float64(c.a0), 0) + complex(float64(c.a1), 0)*e + complex(float64(c.a2), 0)*e*e)))
}
func (s *SpecAnalyzer) Run() { func (s *SpecAnalyzer) Run() {
for { for {
select { select {