mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-15 20:53:17 -05:00
drafting
This commit is contained in:
parent
c424d2b847
commit
179ebb7cc3
@ -1,16 +1,9 @@
|
|||||||
package gioui
|
package gioui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/io/pointer"
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op/clip"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
)
|
)
|
||||||
@ -21,30 +14,20 @@ type (
|
|||||||
wrapBtn *Clickable
|
wrapBtn *Clickable
|
||||||
lengthInBeatsNumber *NumericUpDownState
|
lengthInBeatsNumber *NumericUpDownState
|
||||||
triggerChannelNumber *NumericUpDownState
|
triggerChannelNumber *NumericUpDownState
|
||||||
xScale int
|
plot *Plot
|
||||||
xOffset float32
|
|
||||||
yScale float64
|
|
||||||
dragging bool
|
|
||||||
dragId pointer.ID
|
|
||||||
dragStartPoint f32.Point
|
|
||||||
}
|
|
||||||
|
|
||||||
OscilloscopeStyle struct {
|
|
||||||
CurveColors [2]color.NRGBA `yaml:",flow"`
|
|
||||||
LimitColor color.NRGBA `yaml:",flow"`
|
|
||||||
CursorColor color.NRGBA `yaml:",flow"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Oscilloscope struct {
|
Oscilloscope struct {
|
||||||
Theme *Theme
|
Theme *Theme
|
||||||
Model *tracker.ScopeModel
|
Model *tracker.ScopeModel
|
||||||
State *OscilloscopeState
|
State *OscilloscopeState
|
||||||
Style *OscilloscopeStyle
|
Style *PlotStyle
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
|
func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
|
||||||
return &OscilloscopeState{
|
return &OscilloscopeState{
|
||||||
|
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}),
|
||||||
onceBtn: new(Clickable),
|
onceBtn: new(Clickable),
|
||||||
wrapBtn: new(Clickable),
|
wrapBtn: new(Clickable),
|
||||||
lengthInBeatsNumber: NewNumericUpDownState(),
|
lengthInBeatsNumber: NewNumericUpDownState(),
|
||||||
@ -72,7 +55,41 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Flexed(1, s.layoutWave),
|
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) {
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
xticks := func(r plotRange, yield func(pos float32, label string)) {
|
||||||
|
l := s.Model.LengthInBeats().Value()
|
||||||
|
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), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yticks := func(r plotRange, yield func(pos float32, label string)) {
|
||||||
|
yield(-1, "1")
|
||||||
|
yield(1, "-1")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.State.plot.Layout(gtx, data, xticks, yticks, cx, 2)
|
||||||
|
}),
|
||||||
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,
|
||||||
layout.Rigid(leftSpacer),
|
layout.Rigid(leftSpacer),
|
||||||
@ -95,126 +112,3 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Oscilloscope) layoutWave(gtx C) D {
|
|
||||||
s.State.update(gtx, s.Model.Waveform())
|
|
||||||
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()
|
|
||||||
wave := s.Model.Waveform()
|
|
||||||
event.Op(gtx.Ops, s.State)
|
|
||||||
paint.ColorOp{Color: s.Style.CursorColor}.Add(gtx.Ops)
|
|
||||||
cursorX := int(s.State.sampleToPx(gtx, float32(wave.Cursor), wave))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(cursorX, 0), Max: image.Pt(cursorX+1, gtx.Constraints.Max.Y)})
|
|
||||||
paint.ColorOp{Color: s.Style.LimitColor}.Add(gtx.Ops)
|
|
||||||
minusOneY := int(s.State.ampToY(gtx, -1))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(0, minusOneY), Max: image.Pt(gtx.Constraints.Max.X, minusOneY+1)})
|
|
||||||
plusOneY := int(s.State.ampToY(gtx, 1))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(0, plusOneY), Max: image.Pt(gtx.Constraints.Max.X, plusOneY+1)})
|
|
||||||
leftX := int(s.State.sampleToPx(gtx, 0, wave))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(leftX, 0), Max: image.Pt(leftX+1, gtx.Constraints.Max.Y)})
|
|
||||||
rightX := int(s.State.sampleToPx(gtx, float32(len(wave.Buffer)-1), wave))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(rightX, 0), Max: image.Pt(rightX+1, gtx.Constraints.Max.Y)})
|
|
||||||
for chn := range 2 {
|
|
||||||
paint.ColorOp{Color: s.Style.CurveColors[chn]}.Add(gtx.Ops)
|
|
||||||
for px := range gtx.Constraints.Max.X {
|
|
||||||
// left and right is the sample range covered by the pixel
|
|
||||||
left := int(s.State.pxToSample(gtx, float32(px)-0.5, wave))
|
|
||||||
right := int(s.State.pxToSample(gtx, float32(px)+0.5, wave))
|
|
||||||
if right < 0 || left >= len(wave.Buffer) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
right = min(right, len(wave.Buffer)-1)
|
|
||||||
left = max(left, 0)
|
|
||||||
// smin and smax are the smallest and largest sample values in the pixel range
|
|
||||||
smax := float32(math.Inf(-1))
|
|
||||||
smin := float32(math.Inf(1))
|
|
||||||
for x := left; x <= right; x++ {
|
|
||||||
smax = max(smax, wave.Buffer[x][chn])
|
|
||||||
smin = min(smin, wave.Buffer[x][chn])
|
|
||||||
}
|
|
||||||
// y1 and y2 are the pixel range covered by the sample value
|
|
||||||
y1 := min(max(int(s.State.ampToY(gtx, smax)+0.5), 0), gtx.Constraints.Max.Y-1)
|
|
||||||
y2 := min(max(int(s.State.ampToY(gtx, smin)+0.5), 0), gtx.Constraints.Max.Y-1)
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillRect(gtx C, rect clip.Rect) {
|
|
||||||
stack := rect.Push(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
stack.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OscilloscopeState) update(gtx C, wave tracker.RingBuffer[[2]float32]) {
|
|
||||||
for {
|
|
||||||
ev, ok := gtx.Event(pointer.Filter{
|
|
||||||
Target: o,
|
|
||||||
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, wave)
|
|
||||||
o.xScale += min(max(-1, int(e.Scroll.Y)), 1)
|
|
||||||
s2 := o.pxToSample(gtx, e.Position.X, wave)
|
|
||||||
o.xOffset -= s1 - s2
|
|
||||||
case pointer.Press:
|
|
||||||
if e.Buttons&pointer.ButtonSecondary != 0 {
|
|
||||||
o.xOffset = 0
|
|
||||||
o.xScale = 0
|
|
||||||
o.yScale = 0
|
|
||||||
}
|
|
||||||
if e.Buttons&pointer.ButtonPrimary != 0 {
|
|
||||||
o.dragging = true
|
|
||||||
o.dragId = e.PointerID
|
|
||||||
o.dragStartPoint = e.Position
|
|
||||||
}
|
|
||||||
case pointer.Drag:
|
|
||||||
if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId {
|
|
||||||
deltaX := o.pxToSample(gtx, e.Position.X, wave) - o.pxToSample(gtx, o.dragStartPoint.X, wave)
|
|
||||||
o.xOffset += deltaX
|
|
||||||
num := o.yToAmp(gtx, e.Position.Y)
|
|
||||||
den := o.yToAmp(gtx, o.dragStartPoint.Y)
|
|
||||||
if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 {
|
|
||||||
o.yScale += math.Log(l)
|
|
||||||
o.yScale = min(max(o.yScale, -1e3), 1e3)
|
|
||||||
}
|
|
||||||
o.dragStartPoint = e.Position
|
|
||||||
|
|
||||||
}
|
|
||||||
case pointer.Release | pointer.Cancel:
|
|
||||||
o.dragging = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OscilloscopeState) scaleFactor() float32 {
|
|
||||||
return float32(math.Pow(1.1, float64(o.xScale)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OscilloscopeState) pxToSample(gtx C, px float32, wave tracker.RingBuffer[[2]float32]) float32 {
|
|
||||||
return px*s.scaleFactor()*float32(len(wave.Buffer))/float32(gtx.Constraints.Max.X) - s.xOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OscilloscopeState) sampleToPx(gtx C, sample float32, wave tracker.RingBuffer[[2]float32]) float32 {
|
|
||||||
return (sample + s.xOffset) * float32(gtx.Constraints.Max.X) / float32(len(wave.Buffer)) / s.scaleFactor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OscilloscopeState) ampToY(gtx C, amp float32) float32 {
|
|
||||||
scale := float32(math.Exp(s.yScale))
|
|
||||||
return (1 - amp*scale) / 2 * float32(gtx.Constraints.Max.Y-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OscilloscopeState) yToAmp(gtx C, y float32) float32 {
|
|
||||||
scale := float32(math.Exp(s.yScale))
|
|
||||||
return (1 - y/float32(gtx.Constraints.Max.Y-1)*2) / scale
|
|
||||||
}
|
|
||||||
|
|||||||
168
tracker/gioui/plot.go
Normal file
168
tracker/gioui/plot.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package gioui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/op/paint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Plot struct {
|
||||||
|
origXlim, origYlim plotRange
|
||||||
|
|
||||||
|
xScale, yScale float32
|
||||||
|
xOffset float32
|
||||||
|
dragging bool
|
||||||
|
dragId pointer.ID
|
||||||
|
dragStartPoint f32.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
PlotStyle struct {
|
||||||
|
CurveColors [2]color.NRGBA `yaml:",flow"`
|
||||||
|
LimitColor color.NRGBA `yaml:",flow"`
|
||||||
|
CursorColor color.NRGBA `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PlotDataFunc func(chn int, xr plotRange) (yr plotRange)
|
||||||
|
PlotTickFunc func(r plotRange, yield func(pos float32, label string))
|
||||||
|
plotRange struct{ a, b float32 }
|
||||||
|
plotRel float32
|
||||||
|
plotPx int
|
||||||
|
plotLogScale float32
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPlot(xlim, ylim plotRange) *Plot {
|
||||||
|
return &Plot{
|
||||||
|
origXlim: xlim,
|
||||||
|
origYlim: ylim,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cursornx float32, numchns int) D {
|
||||||
|
p.update(gtx)
|
||||||
|
t := TrackerFromContext(gtx)
|
||||||
|
style := t.Theme.Oscilloscope
|
||||||
|
s := gtx.Constraints.Max
|
||||||
|
if s.X <= 1 || s.Y <= 1 {
|
||||||
|
return D{}
|
||||||
|
}
|
||||||
|
defer clip.Rect(image.Rectangle{Max: s}).Push(gtx.Ops).Pop()
|
||||||
|
event.Op(gtx.Ops, p)
|
||||||
|
|
||||||
|
xlim := p.xlim()
|
||||||
|
ylim := p.ylim()
|
||||||
|
|
||||||
|
// draw tick marks
|
||||||
|
paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops)
|
||||||
|
|
||||||
|
xticks(xlim, func(x float32, label string) {
|
||||||
|
sx := plotPx(s.X).toScreen(xlim.toRelative(x))
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)})
|
||||||
|
})
|
||||||
|
|
||||||
|
yticks(ylim, func(y float32, label string) {
|
||||||
|
sy := plotPx(s.Y).toScreen(ylim.toRelative(y))
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)})
|
||||||
|
})
|
||||||
|
|
||||||
|
// draw cursor
|
||||||
|
paint.ColorOp{Color: style.CursorColor}.Add(gtx.Ops)
|
||||||
|
csx := plotPx(s.X).toScreen(xlim.toRelative(cursornx))
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(csx, 0), Max: image.Pt(csx+1, s.Y)})
|
||||||
|
|
||||||
|
// draw curves
|
||||||
|
for chn := range numchns {
|
||||||
|
paint.ColorOp{Color: style.CurveColors[chn]}.Add(gtx.Ops)
|
||||||
|
right := xlim.fromRelative(plotPx(s.X).fromScreen(0))
|
||||||
|
for sx := range s.X {
|
||||||
|
// 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 {
|
||||||
|
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)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return D{Size: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r plotRange) toRelative(f float32) plotRel { return plotRel((f - r.a) / (r.b - r.a)) }
|
||||||
|
func (r plotRange) fromRelative(pr plotRel) float32 { return float32(pr)*(r.b-r.a) + r.a }
|
||||||
|
func (r plotRange) offset(o float32) plotRange { return plotRange{r.a + o, r.b + o} }
|
||||||
|
func (r plotRange) scale(logScale float32) plotRange {
|
||||||
|
s := float32(math.Exp(float64(logScale)))
|
||||||
|
return plotRange{r.a * s, r.b * s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s plotPx) toScreen(pr plotRel) int { return int(float32(pr)*float32(s-1) + 0.5) }
|
||||||
|
func (s plotPx) fromScreen(px int) plotRel { return plotRel(float32(px) / float32(s-1)) }
|
||||||
|
func (s plotPx) fromScreenF32(px float32) plotRel { return plotRel(px / float32(s-1)) }
|
||||||
|
|
||||||
|
func (o *Plot) xlim() plotRange { return o.origXlim.scale(o.xScale).offset(o.xOffset) }
|
||||||
|
func (o *Plot) ylim() plotRange { return o.origYlim.scale(o.yScale) }
|
||||||
|
|
||||||
|
func fillRect(gtx C, rect clip.Rect) {
|
||||||
|
stack := rect.Push(gtx.Ops)
|
||||||
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
|
stack.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Plot) update(gtx C) {
|
||||||
|
s := gtx.Constraints.Max
|
||||||
|
for {
|
||||||
|
ev, ok := gtx.Event(pointer.Filter{
|
||||||
|
Target: o,
|
||||||
|
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:
|
||||||
|
x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
||||||
|
o.xScale += float32(min(max(-1, int(e.Scroll.Y)), 1)) * 0.1
|
||||||
|
x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
||||||
|
o.xOffset += x1 - x2
|
||||||
|
case pointer.Press:
|
||||||
|
if e.Buttons&pointer.ButtonSecondary != 0 {
|
||||||
|
o.xOffset = 0
|
||||||
|
o.xScale = 0
|
||||||
|
o.yScale = 0
|
||||||
|
}
|
||||||
|
if e.Buttons&pointer.ButtonPrimary != 0 {
|
||||||
|
o.dragging = true
|
||||||
|
o.dragId = e.PointerID
|
||||||
|
o.dragStartPoint = e.Position
|
||||||
|
}
|
||||||
|
case pointer.Drag:
|
||||||
|
if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId {
|
||||||
|
x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(o.dragStartPoint.X))
|
||||||
|
x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
||||||
|
o.xOffset += x1 - x2
|
||||||
|
|
||||||
|
num := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(e.Position.Y))
|
||||||
|
den := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(o.dragStartPoint.Y))
|
||||||
|
if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 {
|
||||||
|
o.yScale -= float32(math.Log(l))
|
||||||
|
o.yScale = min(max(o.yScale, -1e3), 1e3)
|
||||||
|
}
|
||||||
|
o.dragStartPoint = e.Position
|
||||||
|
}
|
||||||
|
case pointer.Release | pointer.Cancel:
|
||||||
|
o.dragging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,9 @@
|
|||||||
package gioui
|
package gioui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"math"
|
||||||
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op/clip"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
)
|
)
|
||||||
@ -15,11 +13,13 @@ type (
|
|||||||
resolutionNumber *NumericUpDownState
|
resolutionNumber *NumericUpDownState
|
||||||
smoothingBtn *Clickable
|
smoothingBtn *Clickable
|
||||||
chnModeBtn *Clickable
|
chnModeBtn *Clickable
|
||||||
|
plot *Plot
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSpectrumState() *SpectrumState {
|
func NewSpectrumState() *SpectrumState {
|
||||||
return &SpectrumState{
|
return &SpectrumState{
|
||||||
|
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 0}),
|
||||||
resolutionNumber: NewNumericUpDownState(),
|
resolutionNumber: NewNumericUpDownState(),
|
||||||
smoothingBtn: new(Clickable),
|
smoothingBtn: new(Clickable),
|
||||||
chnModeBtn: new(Clickable),
|
chnModeBtn: new(Clickable),
|
||||||
@ -56,8 +56,64 @@ func (s *SpectrumState) Layout(gtx C) D {
|
|||||||
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode")
|
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode")
|
||||||
smoothBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.smoothingBtn, smoothTxt, "Smoothing")
|
smoothBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.smoothingBtn, smoothTxt, "Smoothing")
|
||||||
|
|
||||||
|
numchns := 0
|
||||||
|
speclen := len(t.Model.Spectrum()[0])
|
||||||
|
if speclen > 0 {
|
||||||
|
numchns = 1
|
||||||
|
if len(t.Model.Spectrum()[1]) == speclen {
|
||||||
|
numchns = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Flexed(1, s.drawSpectrum),
|
layout.Flexed(1, func(gtx C) D {
|
||||||
|
data := func(chn int, xr plotRange) (yr plotRange) {
|
||||||
|
xr.a = softplus(xr.a*10) / 10
|
||||||
|
xr.b = softplus(xr.b*10) / 10
|
||||||
|
xr.a = float32(math.Log(float64(xr.a))) + 1
|
||||||
|
xr.b = float32(math.Log(float64(xr.b))) + 1
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
y1 := float32(math.Inf(-1))
|
||||||
|
y2 := float32(math.Inf(+1))
|
||||||
|
switch {
|
||||||
|
case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins
|
||||||
|
l := t.Model.Spectrum()[chn][x1]
|
||||||
|
r := t.Model.Spectrum()[chn][x1+1]
|
||||||
|
y1 = smoothInterpolate(l, r, float32(f1))
|
||||||
|
l = t.Model.Spectrum()[chn][x2]
|
||||||
|
r = t.Model.Spectrum()[chn][x2+1]
|
||||||
|
y2 = smoothInterpolate(l, r, float32(f2))
|
||||||
|
y1, y2 = max(y1, y2), min(y1, y2)
|
||||||
|
default:
|
||||||
|
for i := x1; i <= x2; i++ {
|
||||||
|
sample := t.Model.Spectrum()[chn][i]
|
||||||
|
y1 = max(y1, sample)
|
||||||
|
y2 = min(y2, sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y1 = (y1 / 80) + 1
|
||||||
|
y2 = (y2 / 80) + 1
|
||||||
|
y1 = softplus(y1*10) / 10
|
||||||
|
y2 = softplus(y2*10) / 10
|
||||||
|
|
||||||
|
return plotRange{-y1, -y2}
|
||||||
|
}
|
||||||
|
xticks := func(r plotRange, yield func(pos float32, label string)) {
|
||||||
|
yield(0, "")
|
||||||
|
yield(1, "")
|
||||||
|
}
|
||||||
|
yticks := func(r plotRange, yield func(pos float32, label string)) {
|
||||||
|
yield(-1, "")
|
||||||
|
yield(0, "")
|
||||||
|
}
|
||||||
|
return s.plot.Layout(gtx, data, xticks, yticks, 0, numchns)
|
||||||
|
}),
|
||||||
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,
|
||||||
layout.Rigid(leftSpacer),
|
layout.Rigid(leftSpacer),
|
||||||
@ -71,6 +127,15 @@ func (s *SpectrumState) Layout(gtx C) D {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func softplus(f float32) float32 {
|
||||||
|
return float32(math.Log(1 + math.Exp(float64(f))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func smoothInterpolate(a, b float32, t float32) float32 {
|
||||||
|
t = t * t * (3 - 2*t)
|
||||||
|
return (1-t)*a + t*b
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SpectrumState) Update(gtx C) {
|
func (s *SpectrumState) Update(gtx C) {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
for s.chnModeBtn.Clicked(gtx) {
|
for s.chnModeBtn.Clicked(gtx) {
|
||||||
@ -81,22 +146,3 @@ func (s *SpectrumState) Update(gtx C) {
|
|||||||
t.Model.SpecAnSmoothing().SetValue((t.SpecAnSmoothing().Value()+1)%(r.Max-r.Min+1) + r.Min)
|
t.Model.SpecAnSmoothing().SetValue((t.SpecAnSmoothing().Value()+1)%(r.Max-r.Min+1) + r.Min)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpectrumState) drawSpectrum(gtx C) D {
|
|
||||||
t := TrackerFromContext(gtx)
|
|
||||||
for chn := range 2 {
|
|
||||||
paint.ColorOp{Color: t.Theme.Oscilloscope.CurveColors[chn]}.Add(gtx.Ops)
|
|
||||||
p := t.Spectrum()[chn]
|
|
||||||
if len(p) <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Max.X, 1)})
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(0, gtx.Constraints.Min.Y-1), Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)})
|
|
||||||
for px := range gtx.Constraints.Max.X {
|
|
||||||
y2 := gtx.Constraints.Max.Y - 1
|
|
||||||
y1 := int(-p[px*len(p)/gtx.Constraints.Max.X] / 80 * float32(y2))
|
|
||||||
fillRect(gtx, clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ type Theme struct {
|
|||||||
Emphasis IconButtonStyle
|
Emphasis IconButtonStyle
|
||||||
Error IconButtonStyle
|
Error IconButtonStyle
|
||||||
}
|
}
|
||||||
Oscilloscope OscilloscopeStyle
|
Oscilloscope PlotStyle
|
||||||
NumericUpDown NumericUpDownStyle
|
NumericUpDown NumericUpDownStyle
|
||||||
SongPanel struct {
|
SongPanel struct {
|
||||||
RowHeader LabelStyle
|
RowHeader LabelStyle
|
||||||
|
|||||||
@ -57,9 +57,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var spectrumSmoothingMap map[SpecSmoothing]float32 = map[SpecSmoothing]float32{
|
var spectrumSmoothingMap map[SpecSmoothing]float32 = map[SpecSmoothing]float32{
|
||||||
SpecSmoothingSlow: 0.05,
|
SpecSmoothingSlow: 0.1,
|
||||||
SpecSmoothingMedium: 0.2,
|
SpecSmoothingMedium: 0.2,
|
||||||
SpecSmoothingFast: 1.0,
|
SpecSmoothingFast: 0.4,
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
|
func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
|
||||||
@ -154,8 +154,6 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
|||||||
}
|
}
|
||||||
// convert to decibels
|
// convert to decibels
|
||||||
for c := range 2 {
|
for c := range 2 {
|
||||||
vek32.MaximumNumber_Inplace(ret[c], 1e-8)
|
|
||||||
vek32.MinimumNumber_Inplace(ret[c], 1e8)
|
|
||||||
vek32.Log10_Inplace(ret[c])
|
vek32.Log10_Inplace(ret[c])
|
||||||
vek32.MulNumber_Inplace(ret[c], 10)
|
vek32.MulNumber_Inplace(ret[c], 10)
|
||||||
}
|
}
|
||||||
@ -164,7 +162,7 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
|||||||
|
|
||||||
func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) {
|
func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) {
|
||||||
for i := range buf { // de-interleave
|
for i := range buf { // de-interleave
|
||||||
sd.temp.tmp1[i] = buf[i][channel]
|
sd.temp.tmp1[i] = removeNaNsAndClamp(buf[i][channel])
|
||||||
}
|
}
|
||||||
vek32.Mul_Inplace(sd.temp.tmp1, sd.temp.window) // apply windowing
|
vek32.Mul_Inplace(sd.temp.tmp1, sd.temp.window) // apply windowing
|
||||||
vek32.Gather_Into(sd.temp.tmp2, sd.temp.tmp1, sd.temp.bitPerm) // bit-reversal permutation
|
vek32.Gather_Into(sd.temp.tmp2, sd.temp.tmp1, sd.temp.bitPerm) // bit-reversal permutation
|
||||||
|
|||||||
Reference in New Issue
Block a user