sointu/tracker/gioui/oscilloscope.go
5684185+vsariola@users.noreply.github.com 76322bb541 fix(tracker): the scope length is in beats, not in rows
Already the oscilloscope calculated its length in beats, but
everywhere the variable was called "lengthInRows." Renamed the
variable to lengthInBeats and also changed the tooltip to be correct
2024-11-02 23:13:48 +02:00

184 lines
5.9 KiB
Go

package gioui
import (
"image"
"image/color"
"math"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/widget/material"
"github.com/vsariola/sointu/tracker"
)
type (
Oscilloscope struct {
onceBtn *BoolClickable
wrapBtn *BoolClickable
lengthInBeatsNumber *NumberInput
triggerChannelNumber *NumberInput
xScale int
xOffset float32
dragging bool
dragId pointer.ID
dragStartPx float32
}
OscilloscopeStyle struct {
Oscilloscope *Oscilloscope
Wave tracker.RingBuffer[[2]float32]
Colors [2]color.NRGBA
ClippedColor color.NRGBA
Theme *material.Theme
}
)
func NewOscilloscope(model *tracker.Model) *Oscilloscope {
return &Oscilloscope{
onceBtn: NewBoolClickable(model.SignalAnalyzer().Once().Bool()),
wrapBtn: NewBoolClickable(model.SignalAnalyzer().Wrap().Bool()),
lengthInBeatsNumber: NewNumberInput(model.SignalAnalyzer().LengthInBeats().Int()),
triggerChannelNumber: NewNumberInput(model.SignalAnalyzer().TriggerChannel().Int()),
}
}
func LineOscilloscope(s *Oscilloscope, wave tracker.RingBuffer[[2]float32], th *material.Theme) *OscilloscopeStyle {
return &OscilloscopeStyle{Oscilloscope: s, Wave: wave, Colors: [2]color.NRGBA{primaryColor, secondaryColor}, Theme: th, ClippedColor: errorColor}
}
func (s *OscilloscopeStyle) Layout(gtx C) D {
wrapBtnStyle := ToggleButton(gtx, s.Theme, s.Oscilloscope.wrapBtn, "Wrap")
onceBtnStyle := ToggleButton(gtx, s.Theme, s.Oscilloscope.onceBtn, "Once")
triggerChannelStyle := NumericUpDown(s.Theme, s.Oscilloscope.triggerChannelNumber, "Trigger channel")
lengthNumberStyle := NumericUpDown(s.Theme, s.Oscilloscope.lengthInBeatsNumber, "Buffer length in beats")
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D { return s.layoutWave(gtx) }),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("TRG:", white, s.Theme.Shaper)),
layout.Rigid(triggerChannelStyle.Layout),
layout.Rigid(onceBtnStyle.Layout),
)
}),
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("BUF:", white, s.Theme.Shaper)),
layout.Rigid(lengthNumberStyle.Layout),
layout.Rigid(wrapBtnStyle.Layout),
)
}),
)
}
func (s *OscilloscopeStyle) layoutWave(gtx C) D {
s.update(gtx)
if gtx.Constraints.Max.X == 0 || gtx.Constraints.Max.Y == 0 {
return D{}
}
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
event.Op(gtx.Ops, s.Oscilloscope)
paint.ColorOp{Color: disabledTextColor}.Add(gtx.Ops)
cursorX := int(s.sampleToPx(gtx, float32(s.Wave.Cursor)))
stack := clip.Rect{Min: image.Pt(cursorX, 0), Max: image.Pt(cursorX+1, gtx.Constraints.Max.Y)}.Push(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stack.Pop()
for chn := 0; chn < 2; chn++ {
paint.ColorOp{Color: s.Colors[chn]}.Add(gtx.Ops)
clippedColorSet := false
yprev := int((s.Wave.Buffer[0][chn] + 1) / 2 * float32(gtx.Constraints.Max.Y))
for px := 0; px < gtx.Constraints.Max.X; px++ {
x := int(s.pxToSample(gtx, float32(px)))
if x < 0 || x >= len(s.Wave.Buffer) {
continue
}
y := int((s.Wave.Buffer[x][chn] + 1) / 2 * float32(gtx.Constraints.Max.Y))
if y < 0 {
y = 0
} else if y >= gtx.Constraints.Max.Y {
y = gtx.Constraints.Max.Y - 1
}
y1, y2 := yprev, y
if y < yprev {
y1, y2 = y, yprev-1
} else if y > yprev {
y1++
}
clipped := false
if y1 == y2 && y1 == 0 {
clipped = true
}
if y1 == y2 && y1 == gtx.Constraints.Max.Y-1 {
clipped = true
}
if clippedColorSet != clipped {
if clipped {
paint.ColorOp{Color: s.ClippedColor}.Add(gtx.Ops)
} else {
paint.ColorOp{Color: s.Colors[chn]}.Add(gtx.Ops)
}
clippedColorSet = clipped
}
stack := clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)}.Push(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stack.Pop()
yprev = y
}
}
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
}
func (o *OscilloscopeStyle) update(gtx C) {
for {
ev, ok := gtx.Event(pointer.Filter{
Target: o.Oscilloscope,
Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
})
if !ok {
break
}
if e, ok := ev.(pointer.Event); ok {
switch e.Kind {
case pointer.Scroll:
s1 := o.pxToSample(gtx, e.Position.X)
o.Oscilloscope.xScale += min(max(-1, int(e.Scroll.Y)), 1)
s2 := o.pxToSample(gtx, e.Position.X)
o.Oscilloscope.xOffset -= s1 - s2
case pointer.Press:
if e.Buttons&pointer.ButtonSecondary != 0 {
o.Oscilloscope.xOffset = 0
o.Oscilloscope.xScale = 0
}
if e.Buttons&pointer.ButtonPrimary != 0 {
o.Oscilloscope.dragging = true
o.Oscilloscope.dragId = e.PointerID
o.Oscilloscope.dragStartPx = e.Position.X
}
case pointer.Drag:
if e.Buttons&pointer.ButtonPrimary != 0 && o.Oscilloscope.dragging && e.PointerID == o.Oscilloscope.dragId {
delta := o.pxToSample(gtx, e.Position.X) - o.pxToSample(gtx, o.Oscilloscope.dragStartPx)
o.Oscilloscope.xOffset += delta
o.Oscilloscope.dragStartPx = e.Position.X
}
case pointer.Release | pointer.Cancel:
o.Oscilloscope.dragging = false
}
}
}
}
func (o *OscilloscopeStyle) scaleFactor() float32 {
return float32(math.Pow(1.1, float64(o.Oscilloscope.xScale)))
}
func (s *OscilloscopeStyle) pxToSample(gtx C, px float32) float32 {
return px*s.scaleFactor()*float32(len(s.Wave.Buffer))/float32(gtx.Constraints.Max.X) - s.Oscilloscope.xOffset
}
func (s *OscilloscopeStyle) sampleToPx(gtx C, sample float32) float32 {
return (sample + s.Oscilloscope.xOffset) * float32(gtx.Constraints.Max.X) / float32(len(s.Wave.Buffer)) / s.scaleFactor()
}