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 {
t := TrackerFromContext(gtx)
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
@ -58,11 +59,11 @@ func (s *Oscilloscope) Layout(gtx C) D {
w := s.Model.Waveform()
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)
x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1)
if x1 > x2 {
return plotRange{1, 0}
return plotRange{}, false
}
y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1))
@ -71,18 +72,31 @@ func (s *Oscilloscope) Layout(gtx C) D {
y1 = max(y1, sample)
y2 = min(y2, sample)
}
return plotRange{-y1, -y2}
return plotRange{-y1, -y2}, true
}
xticks := func(r plotRange, yield func(pos float32, label string)) {
l := s.Model.LengthInBeats().Value()
rpb := max(t.Model.RowsPerBeat().Value(), 1)
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)
b := min(int(math.Floor(float64(r.b*float32(l)))), l)
for i := a; i <= b; i++ {
yield(float32(i)/float32(l), strconv.Itoa(i))
step := 1
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, "")
}

View File

@ -11,6 +11,7 @@ import (
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
)
type (
@ -25,14 +26,15 @@ type (
}
PlotStyle struct {
CurveColors [2]color.NRGBA `yaml:",flow"`
CurveColors [3]color.NRGBA `yaml:",flow"`
LimitColor color.NRGBA `yaml:",flow"`
CursorColor color.NRGBA `yaml:",flow"`
Ticks LabelStyle
DpPerTick unit.Dp
}
PlotDataFunc func(chn int, xr plotRange) (yr plotRange)
PlotTickFunc func(r plotRange, yield func(pos float32, label string))
PlotDataFunc func(chn int, xr plotRange) (yr plotRange, ok bool)
PlotTickFunc func(r plotRange, num int, yield func(pos float32, label string))
plotRange struct{ a, b float32 }
plotRel float32
plotPx int
@ -61,8 +63,8 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur
ylim := p.ylim()
// draw tick marks
xticks(xlim, func(x float32, txt string) {
numxticks := s.X / gtx.Dp(style.DpPerTick)
xticks(xlim, numxticks, func(x float32, txt string) {
paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops)
sx := plotPx(s.X).toScreen(xlim.toRelative(x))
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)
})
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)
sy := plotPx(s.Y).toScreen(ylim.toRelative(y))
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 := right
right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1))
yr := data(chn, plotRange{left, right})
if yr.b < yr.a {
yr, ok := data(chn, plotRange{left, right})
if !ok {
continue
}
y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a))
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}

View File

@ -2,6 +2,7 @@ package gioui
import (
"math"
"strconv"
"gioui.org/layout"
"gioui.org/unit"
@ -17,9 +18,11 @@ type (
}
)
const SpectrumDisplayDb = 60
func NewSpectrumState() *SpectrumState {
return &SpectrumState{
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 0}),
plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDisplayDb, 0}),
resolutionNumber: NewNumericUpDownState(),
smoothingBtn: new(Clickable),
chnModeBtn: new(Clickable),
@ -67,15 +70,27 @@ func (s *SpectrumState) Layout(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
data := func(chn int, xr plotRange) (yr plotRange) {
xr.a = float32(math.Exp2(float64(xr.a-1) * 8))
xr.b = float32(math.Exp2(float64(xr.b-1) * 8))
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{1, 0}
return plotRange{}, false
}
y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1))
@ -95,18 +110,18 @@ func (s *SpectrumState) Layout(gtx C) D {
y2 = min(y2, sample)
}
}
y1 = (y1 / 80) + 1
y2 = (y2 / 80) + 1
y1 = softplus(y1*10) / 10
y2 = softplus(y2*10) / 10
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}
return plotRange{y1, y2}, true
}
type pair struct {
freq float64
label string
}
xticks := func(r plotRange, yield func(pos float32, label string)) {
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"},
@ -120,17 +135,32 @@ func (s *SpectrumState) Layout(gtx C) D {
{freq: 1e4, label: "10 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 {
yield(x, p.label)
}
}
}
yticks := func(r plotRange, yield func(pos float32, label string)) {
yield(-1, "0 dB")
yield(0, "-Inf dB")
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))
}
}
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 {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,

View File

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

View File

@ -34,6 +34,11 @@ type (
tmpC []complex128 // temporary buffer for FFT
tmp1, tmp2 []float32 // temporary buffers for processing
}
BiquadCoeffs struct {
b0, b1, b2 float32
a0, a1, a2 float32
}
)
const (
@ -72,6 +77,60 @@ func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
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() {
for {
select {