Files
sointu/tracker/gioui/oscilloscope.go
5684185+vsariola@users.noreply.github.com 655d736149 drafting
2026-01-17 14:54:40 +02:00

128 lines
3.8 KiB
Go

package gioui
import (
"math"
"strconv"
"gioui.org/layout"
"gioui.org/unit"
"github.com/vsariola/sointu/tracker"
)
type (
OscilloscopeState struct {
onceBtn *Clickable
wrapBtn *Clickable
lengthInBeatsNumber *NumericUpDownState
triggerChannelNumber *NumericUpDownState
plot *Plot
}
Oscilloscope struct {
Theme *Theme
Model *tracker.ScopeModel
State *OscilloscopeState
}
)
func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
return &OscilloscopeState{
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}, 0),
onceBtn: new(Clickable),
wrapBtn: new(Clickable),
lengthInBeatsNumber: NewNumericUpDownState(),
triggerChannelNumber: NewNumericUpDownState(),
}
}
func Scope(th *Theme, m *tracker.ScopeModel, st *OscilloscopeState) Oscilloscope {
return Oscilloscope{
Theme: th,
Model: m,
State: st,
}
}
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
triggerChannel := NumUpDown(s.Model.TriggerChannel(), s.Theme, s.State.triggerChannelNumber, "Trigger channel")
lengthInBeats := NumUpDown(s.Model.LengthInBeats(), s.Theme, s.State.lengthInBeatsNumber, "Buffer length in beats")
onceBtn := ToggleBtn(s.Model.Once(), s.Theme, s.State.onceBtn, "Once", "Trigger once on next event")
wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
w := s.Model.Waveform()
cx := float32(w.Cursor) / float32(len(w.Buffer))
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{}, false
}
y1 := float32(math.Inf(-1))
y2 := float32(math.Inf(+1))
for i := x1; i <= x2; i++ {
sample := w.Buffer[i][chn]
y1 = max(y1, sample)
y2 = min(y2, sample)
}
return plotRange{-y1, -y2}, true
}
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)
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, count int, yield func(pos float32, label string)) {
yield(-1, "")
yield(1, "")
}
return s.State.plot.Layout(gtx, data, xticks, yticks, cx, 2)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(leftSpacer),
layout.Rigid(Label(s.Theme, &s.Theme.SongPanel.RowHeader, "Trigger").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
layout.Rigid(onceBtn.Layout),
layout.Rigid(triggerChannel.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(s.Theme, &s.Theme.SongPanel.RowHeader, "Buffer").Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
layout.Rigid(wrapBtn.Layout),
layout.Rigid(lengthInBeats.Layout),
layout.Rigid(rightSpacer),
)
}),
)
}