mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
195 lines
6.5 KiB
Go
195 lines
6.5 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/unit"
|
|
"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")
|
|
|
|
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
|
|
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
|
|
|
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(leftSpacer),
|
|
layout.Rigid(LabelStyle{Text: "Trigger", Color: disabledTextColor, Alignment: layout.W, FontSize: s.Theme.TextSize * 14.0 / 16.0, Shaper: s.Theme.Shaper}.Layout),
|
|
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(onceBtnStyle.Layout),
|
|
layout.Rigid(triggerChannelStyle.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(LabelStyle{Text: "Buffer", Color: disabledTextColor, Alignment: layout.W, FontSize: s.Theme.TextSize * 14.0 / 16.0, Shaper: s.Theme.Shaper}.Layout),
|
|
layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }),
|
|
layout.Rigid(wrapBtnStyle.Layout),
|
|
layout.Rigid(lengthNumberStyle.Layout),
|
|
layout.Rigid(rightSpacer),
|
|
)
|
|
}),
|
|
)
|
|
}
|
|
|
|
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()
|
|
}
|