mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-25 00:15:00 -04:00
drafting
This commit is contained in:
parent
285f33c261
commit
c09a3f04db
135
patch.go
135
patch.go
@ -67,6 +67,13 @@ type (
|
||||
DisplayFunc UnitParameterDisplayFunc
|
||||
}
|
||||
|
||||
// StackUse documents how a unit will affect the signal stack.
|
||||
StackUse struct {
|
||||
Inputs [][]int // Inputs documents which inputs contribute to which outputs. len(Inputs) is the number of inputs. Each input can contribute to multiple outputs, so its a slice.
|
||||
Modifies []bool // Modifies documents which of the (mixed) inputs are actually modified by the unit
|
||||
NumOutputs int // NumOutputs is the number of outputs produced by the unit. This is used to determine how many outputs are needed for the unit.
|
||||
}
|
||||
|
||||
UnitParameterDisplayFunc func(int) (value string, unit string)
|
||||
)
|
||||
|
||||
@ -304,6 +311,103 @@ func (u *Unit) Copy() Unit {
|
||||
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID, Disabled: u.Disabled, Comment: u.Comment}
|
||||
}
|
||||
|
||||
var stackUseSource = [2]StackUse{
|
||||
{Inputs: [][]int{}, Modifies: []bool{true}, NumOutputs: 1}, // mono
|
||||
{Inputs: [][]int{}, Modifies: []bool{true, true}, NumOutputs: 2}, // stereo
|
||||
}
|
||||
var stackUseSink = [2]StackUse{
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 0}, // mono
|
||||
{Inputs: [][]int{{0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 0}, // stereo
|
||||
}
|
||||
var stackUseEffect = [2]StackUse{
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 1}, // mono
|
||||
{Inputs: [][]int{{0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 2}, // stereo
|
||||
}
|
||||
var stackUseMonoStereo = map[string][2]StackUse{
|
||||
"add": {
|
||||
{Inputs: [][]int{{0, 1}, {1}}, Modifies: []bool{false, true}, NumOutputs: 2},
|
||||
{Inputs: [][]int{{0, 2}, {1, 3}, {2}, {3}}, Modifies: []bool{false, false, true, true}, NumOutputs: 4},
|
||||
},
|
||||
"mul": {
|
||||
{Inputs: [][]int{{0, 1}, {1}}, Modifies: []bool{false, true}, NumOutputs: 2},
|
||||
{Inputs: [][]int{{0, 2}, {1, 3}, {2}, {3}}, Modifies: []bool{false, false, true, true}, NumOutputs: 4},
|
||||
},
|
||||
"addp": {
|
||||
{Inputs: [][]int{{0}, {0}}, Modifies: []bool{true}, NumOutputs: 1},
|
||||
{Inputs: [][]int{{0}, {1}, {0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 2},
|
||||
},
|
||||
"mulp": {
|
||||
{Inputs: [][]int{{0}, {0}}, Modifies: []bool{true}, NumOutputs: 1},
|
||||
{Inputs: [][]int{{0}, {1}, {0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 2},
|
||||
},
|
||||
"xch": {
|
||||
{Inputs: [][]int{{1}, {0}}, Modifies: []bool{false, false}, NumOutputs: 2},
|
||||
{Inputs: [][]int{{2}, {3}, {0}, {1}}, Modifies: []bool{false, false, false, false}, NumOutputs: 4},
|
||||
},
|
||||
"push": {
|
||||
{Inputs: [][]int{{0, 1}}, Modifies: []bool{false, false}, NumOutputs: 2},
|
||||
{Inputs: [][]int{{0, 2}, {1, 3}}, Modifies: []bool{false, false, false, false}, NumOutputs: 4},
|
||||
},
|
||||
"pop": stackUseSink,
|
||||
"envelope": stackUseSource,
|
||||
"oscillator": stackUseSource,
|
||||
"noise": stackUseSource,
|
||||
"loadnote": stackUseSource,
|
||||
"loadval": stackUseSource,
|
||||
"receive": stackUseSource,
|
||||
"in": stackUseSource,
|
||||
"out": stackUseSink,
|
||||
"outaux": stackUseSink,
|
||||
"aux": stackUseSink,
|
||||
"distort": stackUseEffect,
|
||||
"hold": stackUseEffect,
|
||||
"crush": stackUseEffect,
|
||||
"gain": stackUseEffect,
|
||||
"invgain": stackUseEffect,
|
||||
"dbgain": stackUseEffect,
|
||||
"filter": stackUseEffect,
|
||||
"clip": stackUseEffect,
|
||||
"delay": stackUseEffect,
|
||||
"compressor": {
|
||||
{Inputs: [][]int{{0, 1}}, Modifies: []bool{false, true}, NumOutputs: 2}, // mono
|
||||
{Inputs: [][]int{{0, 2, 3}, {1, 2, 3}}, Modifies: []bool{false, false, true, true}, NumOutputs: 4}, // stereo
|
||||
},
|
||||
"pan": {
|
||||
{Inputs: [][]int{{0, 1}}, Modifies: []bool{true, true}, NumOutputs: 2}, // mono
|
||||
{Inputs: [][]int{{0, 1}, {0, 1}}, Modifies: []bool{true, true}, NumOutputs: 2}, // mono
|
||||
},
|
||||
"speed": {
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 0},
|
||||
{},
|
||||
},
|
||||
"sync": {
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{false}, NumOutputs: 0},
|
||||
{},
|
||||
},
|
||||
}
|
||||
var stackUseSendNoPop = [2]StackUse{
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{false}, NumOutputs: 1},
|
||||
{Inputs: [][]int{{0}, {1}}, Modifies: []bool{false, false}, NumOutputs: 2},
|
||||
}
|
||||
var stackUseSendPop = [2]StackUse{
|
||||
{Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 0}, // mono
|
||||
{Inputs: [][]int{{0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 0}, // stereo
|
||||
}
|
||||
|
||||
func (u *Unit) StackUse() StackUse {
|
||||
if u.Disabled {
|
||||
return StackUse{}
|
||||
}
|
||||
if u.Type == "send" {
|
||||
// "send" unit is special, it has a different stack use depending on sendpop
|
||||
if u.Parameters["sendpop"] == 0 {
|
||||
return stackUseSendNoPop[u.Parameters["stereo"]]
|
||||
}
|
||||
return stackUseSendPop[u.Parameters["stereo"]]
|
||||
}
|
||||
return stackUseMonoStereo[u.Type][u.Parameters["stereo"]]
|
||||
}
|
||||
|
||||
// StackChange returns how this unit will affect the signal stack. "pop" and
|
||||
// "addp" and such will consume the topmost signal, and thus return -1 (or -2,
|
||||
// if the unit is a stereo unit). On the other hand, "oscillator" and "envelope"
|
||||
@ -311,40 +415,15 @@ func (u *Unit) Copy() Unit {
|
||||
// unit). Effects that just change the topmost signal and will not change the
|
||||
// number of signals on the stack and thus return 0.
|
||||
func (u *Unit) StackChange() int {
|
||||
if u.Disabled {
|
||||
return 0
|
||||
}
|
||||
switch u.Type {
|
||||
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
||||
return -1 - u.Parameters["stereo"]
|
||||
case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
||||
return 1 + u.Parameters["stereo"]
|
||||
case "pan":
|
||||
return 1 - u.Parameters["stereo"]
|
||||
case "speed":
|
||||
return -1
|
||||
case "send":
|
||||
return (-1 - u.Parameters["stereo"]) * u.Parameters["sendpop"]
|
||||
}
|
||||
return 0
|
||||
s := u.StackUse()
|
||||
return s.NumOutputs - len(s.Inputs)
|
||||
}
|
||||
|
||||
// StackNeed returns the number of signals that should be on the stack before
|
||||
// this unit is executed. Used to prevent stack underflow. Units producing
|
||||
// signals do not care what is on the stack before and will return 0.
|
||||
func (u *Unit) StackNeed() int {
|
||||
if u.Disabled {
|
||||
return 0
|
||||
}
|
||||
switch u.Type {
|
||||
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
||||
return 0
|
||||
case "mulp", "mul", "add", "addp", "xch":
|
||||
return 2 * (1 + u.Parameters["stereo"])
|
||||
case "speed":
|
||||
return 1
|
||||
}
|
||||
return 1 + u.Parameters["stereo"]
|
||||
return len(u.StackUse().Inputs)
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of an Instrument
|
||||
|
@ -17,6 +17,7 @@ type (
|
||||
forUnit map[int]derivedForUnit
|
||||
forTrack []derivedForTrack
|
||||
forPattern []derivedForPattern
|
||||
rail SignalRail // signal rail for the current patch
|
||||
}
|
||||
|
||||
derivedForUnit struct {
|
||||
@ -152,6 +153,7 @@ func (m *Model) updateDerivedScoreData() {
|
||||
}
|
||||
|
||||
func (m *Model) updateDerivedPatchData() {
|
||||
m.derived.rail.update(m.d.Song.Patch)
|
||||
clear(m.derived.forUnit)
|
||||
for i, instr := range m.d.Song.Patch {
|
||||
for u, unit := range instr.Units {
|
||||
|
207
tracker/gioui/knob.go
Normal file
207
tracker/gioui/knob.go
Normal file
@ -0,0 +1,207 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/x/stroke"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type (
|
||||
KnobState struct {
|
||||
click gesture.Click
|
||||
drag gesture.Drag
|
||||
dragStartPt f32.Point // used to calculate the drag amount
|
||||
dragStartVal int
|
||||
tipArea TipArea
|
||||
}
|
||||
|
||||
KnobStyle struct {
|
||||
Diameter unit.Dp
|
||||
StrokeWidth unit.Dp
|
||||
Bg color.NRGBA
|
||||
Pos struct {
|
||||
Color color.NRGBA
|
||||
Bg color.NRGBA
|
||||
}
|
||||
Neg struct {
|
||||
Color color.NRGBA
|
||||
Bg color.NRGBA
|
||||
}
|
||||
Indicator struct {
|
||||
Color color.NRGBA
|
||||
Width unit.Dp
|
||||
InnerDiam unit.Dp
|
||||
OuterDiam unit.Dp
|
||||
}
|
||||
Value LabelStyle
|
||||
Title LabelStyle
|
||||
}
|
||||
|
||||
KnobWidget struct {
|
||||
Theme *Theme
|
||||
Value tracker.Parameter
|
||||
State *KnobState
|
||||
Style *KnobStyle
|
||||
Hint string
|
||||
Scroll bool
|
||||
}
|
||||
)
|
||||
|
||||
func Knob(v tracker.Parameter, th *Theme, state *KnobState, hint string, scroll bool) KnobWidget {
|
||||
return KnobWidget{
|
||||
Theme: th,
|
||||
Value: v,
|
||||
State: state,
|
||||
Style: &th.Knob,
|
||||
Hint: hint,
|
||||
Scroll: scroll,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KnobWidget) Layout(gtx C) D {
|
||||
k.update(gtx)
|
||||
knob := func(gtx C) D {
|
||||
m := k.Value.Range()
|
||||
amount := float32(k.Value.Value()-m.Min) / float32(m.Max-m.Min)
|
||||
sw := gtx.Dp(k.Style.StrokeWidth)
|
||||
d := gtx.Dp(k.Style.Diameter)
|
||||
defer clip.Rect(image.Rectangle{Max: image.Pt(d, d)}).Push(gtx.Ops).Pop()
|
||||
event.Op(gtx.Ops, k.State)
|
||||
k.State.drag.Add(gtx.Ops)
|
||||
k.State.click.Add(gtx.Ops)
|
||||
k.strokeKnobArc(gtx, k.Style.Pos.Bg, sw, d, amount, 1)
|
||||
k.strokeKnobArc(gtx, k.Style.Pos.Color, sw, d, 0, amount)
|
||||
k.strokeIndicator(gtx, amount)
|
||||
return D{Size: image.Pt(d, d)}
|
||||
}
|
||||
label := Label(k.Theme, &k.Style.Value, strconv.Itoa(k.Value.Value()))
|
||||
w := func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Stacked(knob),
|
||||
layout.Stacked(label.Layout))
|
||||
}
|
||||
if k.Hint != "" {
|
||||
c := gtx.Constraints
|
||||
gtx.Constraints.Max = image.Pt(1e6, 1e6)
|
||||
return k.State.tipArea.Layout(gtx, Tooltip(k.Theme, k.Hint), func(gtx C) D {
|
||||
gtx.Constraints = c
|
||||
return w(gtx)
|
||||
})
|
||||
}
|
||||
return w(gtx)
|
||||
}
|
||||
|
||||
func (k *KnobWidget) update(gtx C) {
|
||||
for {
|
||||
p, ok := k.State.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch p.Kind {
|
||||
case pointer.Press:
|
||||
k.State.dragStartPt = p.Position
|
||||
k.State.dragStartVal = k.Value.Value()
|
||||
case pointer.Drag:
|
||||
// update the value based on the drag amount
|
||||
m := k.Value.Range()
|
||||
d := p.Position.Sub(k.State.dragStartPt)
|
||||
amount := float32(d.X-d.Y) / float32(gtx.Dp(k.Style.Diameter)) / 4
|
||||
newValue := int(float32(k.State.dragStartVal) + amount*float32(m.Max-m.Min))
|
||||
k.Value.SetValue(newValue)
|
||||
k.State.tipArea.Appear(gtx.Now)
|
||||
}
|
||||
}
|
||||
for {
|
||||
g, ok := k.State.click.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if g.Kind == gesture.KindClick && g.NumClicks > 1 {
|
||||
k.Value.Reset()
|
||||
}
|
||||
}
|
||||
for k.Scroll {
|
||||
e, ok := gtx.Event(pointer.Filter{
|
||||
Target: k.State,
|
||||
Kinds: pointer.Scroll,
|
||||
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
k.Value.SetValue(k.Value.Value() - int(delta))
|
||||
k.State.tipArea.Appear(gtx.Now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KnobWidget) strokeBg(gtx C) {
|
||||
diam := gtx.Dp(k.Style.Diameter)
|
||||
circle := clip.Ellipse{
|
||||
Min: image.Pt(0, 0),
|
||||
Max: image.Pt(diam, diam),
|
||||
}.Op(gtx.Ops)
|
||||
paint.FillShape(gtx.Ops, k.Style.Bg, circle)
|
||||
}
|
||||
|
||||
func (k *KnobWidget) strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, start, end float32) {
|
||||
rad := float32(diameter) / 2
|
||||
end = min(max(end, 0), 1)
|
||||
if end <= 0 {
|
||||
return
|
||||
}
|
||||
startAngle := float64((start*8 + 1) / 10 * 2 * math.Pi)
|
||||
deltaAngle := (end - start) * 8 * math.Pi / 5
|
||||
center := f32.Point{X: rad, Y: rad}
|
||||
r2 := rad - float32(strokeWidth)/2
|
||||
startPt := f32.Point{X: rad - r2*float32(math.Sin(startAngle)), Y: rad + r2*float32(math.Cos(startAngle))}
|
||||
segments := [...]stroke.Segment{
|
||||
stroke.MoveTo(startPt),
|
||||
stroke.ArcTo(center, deltaAngle),
|
||||
}
|
||||
s := stroke.Stroke{
|
||||
Path: stroke.Path{Segments: segments[:]},
|
||||
Width: float32(strokeWidth),
|
||||
Cap: stroke.FlatCap,
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, s.Op(gtx.Ops))
|
||||
}
|
||||
|
||||
func (k *KnobWidget) strokeIndicator(gtx C, amount float32) {
|
||||
innerRad := float32(gtx.Dp(k.Style.Indicator.InnerDiam)) / 2
|
||||
outerRad := float32(gtx.Dp(k.Style.Indicator.OuterDiam)) / 2
|
||||
center := float32(gtx.Dp(k.Style.Diameter)) / 2
|
||||
angle := (float64(amount)*8 + 1) / 10 * 2 * math.Pi
|
||||
start := f32.Point{
|
||||
X: center - innerRad*float32(math.Sin(angle)),
|
||||
Y: center + innerRad*float32(math.Cos(angle)),
|
||||
}
|
||||
end := f32.Point{
|
||||
X: center - outerRad*float32(math.Sin(angle)),
|
||||
Y: center + outerRad*float32(math.Cos(angle)),
|
||||
}
|
||||
segments := [...]stroke.Segment{
|
||||
stroke.MoveTo(start),
|
||||
stroke.LineTo(end),
|
||||
}
|
||||
s := stroke.Stroke{
|
||||
Path: stroke.Path{Segments: segments[:]},
|
||||
Width: float32(k.Style.Indicator.Width),
|
||||
Cap: stroke.FlatCap,
|
||||
}
|
||||
paint.FillShape(gtx.Ops, k.Style.Indicator.Color, s.Op(gtx.Ops))
|
||||
}
|
@ -117,18 +117,18 @@ func (s ScrollTableStyle) Layout(gtx C, element func(gtx C, x, y int) D, colTitl
|
||||
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
|
||||
s.handleEvents(gtx, p)
|
||||
|
||||
return Surface{Gray: 24, Focus: s.ScrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p, colTitle, colTitleBg)
|
||||
s.layoutRowTitles(gtx, p, rowTitle, rowTitleBg)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, element)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
})
|
||||
//return Surface{Gray: 24, Focus: s.ScrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p, colTitle, colTitleBg)
|
||||
s.layoutRowTitles(gtx, p, rowTitle, rowTitleBg)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, element)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
//})
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
|
||||
|
96
tracker/gioui/signal_rail.go
Normal file
96
tracker/gioui/signal_rail.go
Normal file
@ -0,0 +1,96 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const numSignalsDrawn = 8
|
||||
|
||||
type (
|
||||
SignalRailStyle struct {
|
||||
Color color.NRGBA
|
||||
LineWidth unit.Dp
|
||||
PortDiameter unit.Dp
|
||||
PortColor color.NRGBA
|
||||
}
|
||||
|
||||
SignalRailWidget struct {
|
||||
Style *SignalRailStyle
|
||||
Signal tracker.Signal
|
||||
Width unit.Dp
|
||||
Height unit.Dp
|
||||
}
|
||||
)
|
||||
|
||||
func SignalRail(th *Theme, signal tracker.Signal) SignalRailWidget {
|
||||
return SignalRailWidget{
|
||||
Style: &th.SignalRail,
|
||||
Signal: signal,
|
||||
Width: th.UnitEditor.Width,
|
||||
Height: th.UnitEditor.Height,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SignalRailWidget) Layout(gtx C) D {
|
||||
w := gtx.Dp(s.Width)
|
||||
h := gtx.Dp(s.Height)
|
||||
l := gtx.Dp(s.Style.LineWidth)
|
||||
d := gtx.Dp(s.Style.PortDiameter)
|
||||
c := max(l, d) / 2
|
||||
stride := (w - c*2) / numSignalsDrawn
|
||||
var path clip.Path
|
||||
path.Begin(gtx.Ops)
|
||||
// Draw pass through signals
|
||||
for i := range min(numSignalsDrawn, s.Signal.PassThrough) {
|
||||
x := float32(i*stride + c)
|
||||
path.MoveTo(f32.Pt(x, 0))
|
||||
path.LineTo(f32.Pt(x, float32(h)))
|
||||
}
|
||||
// Draw the routing of input signals
|
||||
for i := range min(len(s.Signal.StackUse.Inputs), numSignalsDrawn-s.Signal.PassThrough) {
|
||||
input := s.Signal.StackUse.Inputs[i]
|
||||
x1 := float32((i+s.Signal.PassThrough)*stride + c)
|
||||
for _, link := range input {
|
||||
x2 := float32((link+s.Signal.PassThrough)*stride + c)
|
||||
path.MoveTo(f32.Pt(x1, 0))
|
||||
path.LineTo(f32.Pt(x2, float32(h/2)))
|
||||
}
|
||||
}
|
||||
// Draw the routing of output signals
|
||||
for i := range min(s.Signal.StackUse.NumOutputs, numSignalsDrawn-s.Signal.PassThrough) {
|
||||
x := float32((i+s.Signal.PassThrough)*stride + c)
|
||||
path.MoveTo(f32.Pt(x, float32(h/2)))
|
||||
path.LineTo(f32.Pt(x, float32(h)))
|
||||
}
|
||||
paint.FillShape(gtx.Ops, s.Style.Color,
|
||||
clip.Stroke{
|
||||
Path: path.End(),
|
||||
Width: float32(l),
|
||||
}.Op())
|
||||
// Draw the circles on modified signals
|
||||
|
||||
for i := range min(len(s.Signal.StackUse.Modifies), numSignalsDrawn-s.Signal.PassThrough) {
|
||||
if !s.Signal.StackUse.Modifies[i] {
|
||||
continue
|
||||
}
|
||||
var circle clip.Path
|
||||
x := float32((i + s.Signal.PassThrough) * stride)
|
||||
circle.Begin(gtx.Ops)
|
||||
circle.MoveTo(f32.Pt(x, float32(h/2)))
|
||||
f := f32.Pt(x+float32(c), float32(h/2))
|
||||
circle.ArcTo(f, f, float32(2*math.Pi))
|
||||
p := clip.Outline{Path: circle.End()}.Op().Push(gtx.Ops)
|
||||
paint.ColorOp{Color: s.Style.PortColor}.Add(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
p.Pop()
|
||||
}
|
||||
return D{Size: image.Pt(w, h)}
|
||||
}
|
@ -106,9 +106,10 @@ type Theme struct {
|
||||
Menu PopupStyle
|
||||
Dialog PopupStyle
|
||||
}
|
||||
Split SplitStyle
|
||||
ScrollBar ScrollBarStyle
|
||||
Knob KnobStyle
|
||||
Split SplitStyle
|
||||
ScrollBar ScrollBarStyle
|
||||
Knob KnobStyle
|
||||
SignalRail SignalRailStyle
|
||||
|
||||
// iconCache is used to cache the icons created from iconvg data
|
||||
iconCache map[*byte]*widget.Icon
|
||||
|
@ -211,7 +211,7 @@ uniteditor:
|
||||
name:
|
||||
{ textsize: 12, alignment: 2, color: *highemphasis, shadowcolor: *black }
|
||||
invalidparam: { r: 120, g: 120, b: 120, a: 190 }
|
||||
sendtarget: { r: 120, g: 120, b: 210, a: 255 }
|
||||
sendtarget: { r: 120, g: 120, b: 210, a: 63 }
|
||||
width: 60
|
||||
height: 60
|
||||
rowtitlewidth: 16
|
||||
@ -221,6 +221,12 @@ knob:
|
||||
diameter: 36
|
||||
value: { textsize: 12, color: *highemphasis }
|
||||
strokewidth: 4
|
||||
bg: { r: 40, g: 40, b: 40, a: 255 }
|
||||
pos: { color: *primarycolor, bg: { r: 51, g: 36, b: 54, a: 255 } }
|
||||
neg: { color: *secondarycolor, bg: { r: 32, g: 55, b: 58, a: 255 } }
|
||||
indicator: { color: *white, width: 2, innerdiam: 24, outerdiam: 36 }
|
||||
signalrail:
|
||||
color: *primarycolor
|
||||
linewidth: 2
|
||||
portdiameter: 8
|
||||
portcolor: *secondarycolor
|
||||
|
@ -7,10 +7,8 @@ import (
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
@ -20,10 +18,8 @@ import (
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/stroke"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
@ -50,44 +46,6 @@ type (
|
||||
|
||||
searching tracker.Bool
|
||||
}
|
||||
|
||||
KnobState struct {
|
||||
click gesture.Click
|
||||
drag gesture.Drag
|
||||
dragStartPt f32.Point // used to calculate the drag amount
|
||||
dragStartVal int
|
||||
tipArea TipArea
|
||||
}
|
||||
|
||||
KnobStyle struct {
|
||||
Diameter unit.Dp
|
||||
StrokeWidth unit.Dp
|
||||
Pos struct {
|
||||
Color color.NRGBA
|
||||
Bg color.NRGBA
|
||||
}
|
||||
Neg struct {
|
||||
Color color.NRGBA
|
||||
Bg color.NRGBA
|
||||
}
|
||||
Indicator struct {
|
||||
Color color.NRGBA
|
||||
Width unit.Dp
|
||||
InnerDiam unit.Dp
|
||||
OuterDiam unit.Dp
|
||||
}
|
||||
Value LabelStyle
|
||||
Title LabelStyle
|
||||
}
|
||||
|
||||
KnobWidget struct {
|
||||
Theme *Theme
|
||||
Value tracker.Parameter
|
||||
State *KnobState
|
||||
Style *KnobStyle
|
||||
Hint string
|
||||
Scroll bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||
@ -222,6 +180,11 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
|
||||
}
|
||||
cursor := t.Model.Params().Cursor()
|
||||
cell := func(gtx C, x, y int) D {
|
||||
if x == 0 {
|
||||
sr := SignalRail(t.Theme, t.SignalRail().Item(y))
|
||||
return sr.Layout(gtx)
|
||||
}
|
||||
x--
|
||||
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if y < 0 || y >= len(pe.Parameters) || x < 0 || x >= len(pe.Parameters[y]) {
|
||||
@ -247,8 +210,57 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
|
||||
table.ColumnTitleHeight = t.Theme.UnitEditor.ColumnTitleHeight
|
||||
table.CellWidth = t.Theme.UnitEditor.Width
|
||||
table.CellHeight = t.Theme.UnitEditor.Height
|
||||
return table.Layout(gtx, cell, coltitle, rowtitle, nil, nil)
|
||||
pe.drawSignals(gtx)
|
||||
dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, nil)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) drawSignals(gtx C) {
|
||||
t := TrackerFromContext(gtx)
|
||||
units := t.Units()
|
||||
colP := pe.paramTable.ColTitleList.List.Position
|
||||
rowP := pe.paramTable.RowTitleList.List.Position
|
||||
p := image.Pt(gtx.Dp(t.Theme.UnitEditor.RowTitleWidth), gtx.Dp(t.Theme.UnitEditor.ColumnTitleHeight))
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Max = gtx.Constraints.Max.Sub(p)
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||
for i := 0; i < units.Count(); i++ {
|
||||
item := units.Item(i)
|
||||
if item.TargetUnit > 0 {
|
||||
pe.drawSignal(gtx, 3-colP.First, i-rowP.First, item.TargetPort-colP.First, item.TargetUnit-1-rowP.First)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) drawSignal(gtx C, sx, sy, ex, ey int) {
|
||||
t := TrackerFromContext(gtx)
|
||||
width := float32(gtx.Dp(t.Theme.UnitEditor.Width))
|
||||
height := float32(gtx.Dp(t.Theme.UnitEditor.Height))
|
||||
diam := gtx.Dp(t.Theme.Knob.Diameter)
|
||||
from := f32.Pt((float32(sx)+.5)*width, (float32(sy)+.6)*height)
|
||||
to := f32.Pt((float32(ex)+.5)*width, (float32(ey)+.6)*height)
|
||||
var c1, c2 f32.Point
|
||||
if sy < ey {
|
||||
from.Y += float32(diam) / 2
|
||||
to.Y -= float32(diam) / 2
|
||||
c1 = from.Add(f32.Pt(0, height/2))
|
||||
c2 = to.Sub(f32.Pt(0, height/2))
|
||||
} else {
|
||||
from.Y -= float32(diam) / 2
|
||||
to.Y += float32(diam) / 2
|
||||
c1 = from.Sub(f32.Pt(0, height/2))
|
||||
c2 = to.Add(f32.Pt(0, height/2))
|
||||
}
|
||||
var path clip.Path
|
||||
path.Begin(gtx.Ops)
|
||||
path.MoveTo(from)
|
||||
path.CubeTo(c1, c2, to)
|
||||
paint.FillShape(gtx.Ops, t.Theme.UnitEditor.SendTarget,
|
||||
clip.Stroke{
|
||||
Path: path.End(),
|
||||
Width: float32(gtx.Dp(4)),
|
||||
}.Op())
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutFooter(gtx C) D {
|
||||
@ -370,7 +382,8 @@ func (p ParameterStyle) Layout(gtx C) D {
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
|
||||
return drawCircle(gtx, gtx.Dp(p.Theme.Knob.Diameter), p.Theme.Knob.Pos.Bg)
|
||||
/*instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
|
||||
for i := range instrItems {
|
||||
i := i
|
||||
name, _, _, _ := p.tracker.Instruments().Item(i)
|
||||
@ -409,7 +422,7 @@ func (p ParameterStyle) Layout(gtx C) D {
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return unitBtn.Layout(gtx, unitItems...)
|
||||
}),
|
||||
)
|
||||
)*/
|
||||
}
|
||||
return D{}
|
||||
}
|
||||
@ -435,6 +448,12 @@ func (p ParameterStyle) Layout(gtx C) D {
|
||||
)
|
||||
}
|
||||
|
||||
func drawCircle(gtx C, i int, nRGBA color.NRGBA) D {
|
||||
defer clip.Ellipse(image.Rectangle{Max: image.Pt(i, i)}).Push(gtx.Ops).Pop()
|
||||
paint.FillShape(gtx.Ops, nRGBA, clip.Ellipse{Max: image.Pt(i, i)}.Op(gtx.Ops))
|
||||
return D{Size: image.Pt(i, i)}
|
||||
}
|
||||
|
||||
func buildUnitLabel(index int, u sointu.Unit) string {
|
||||
text := u.Type
|
||||
if u.Comment != "" {
|
||||
@ -442,141 +461,3 @@ func buildUnitLabel(index int, u sointu.Unit) string {
|
||||
}
|
||||
return fmt.Sprintf("%d: %s", index, text)
|
||||
}
|
||||
|
||||
func Knob(v tracker.Parameter, th *Theme, state *KnobState, hint string, scroll bool) KnobWidget {
|
||||
return KnobWidget{
|
||||
Theme: th,
|
||||
Value: v,
|
||||
State: state,
|
||||
Style: &th.Knob,
|
||||
Hint: hint,
|
||||
Scroll: scroll,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KnobWidget) Layout(gtx C) D {
|
||||
k.update(gtx)
|
||||
knob := func(gtx C) D {
|
||||
m := k.Value.Range()
|
||||
amount := float32(k.Value.Value()-m.Min) / float32(m.Max-m.Min)
|
||||
sw := gtx.Dp(k.Style.StrokeWidth)
|
||||
d := gtx.Dp(k.Style.Diameter)
|
||||
defer clip.Rect(image.Rectangle{Max: image.Pt(d, d)}).Push(gtx.Ops).Pop()
|
||||
event.Op(gtx.Ops, k.State)
|
||||
k.State.drag.Add(gtx.Ops)
|
||||
k.State.click.Add(gtx.Ops)
|
||||
k.strokeKnobArc(gtx, k.Style.Pos.Bg, sw, d, amount, 1)
|
||||
k.strokeKnobArc(gtx, k.Style.Pos.Color, sw, d, 0, amount)
|
||||
k.strokeIndicator(gtx, amount)
|
||||
return D{Size: image.Pt(d, d)}
|
||||
}
|
||||
label := Label(k.Theme, &k.Style.Value, strconv.Itoa(k.Value.Value()))
|
||||
w := func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Stacked(knob),
|
||||
layout.Stacked(label.Layout))
|
||||
}
|
||||
if k.Hint != "" {
|
||||
c := gtx.Constraints
|
||||
gtx.Constraints.Max = image.Pt(1e6, 1e6)
|
||||
return k.State.tipArea.Layout(gtx, Tooltip(k.Theme, k.Hint), func(gtx C) D {
|
||||
gtx.Constraints = c
|
||||
return w(gtx)
|
||||
})
|
||||
}
|
||||
return w(gtx)
|
||||
}
|
||||
|
||||
func (k *KnobWidget) update(gtx C) {
|
||||
for {
|
||||
p, ok := k.State.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch p.Kind {
|
||||
case pointer.Press:
|
||||
k.State.dragStartPt = p.Position
|
||||
k.State.dragStartVal = k.Value.Value()
|
||||
case pointer.Drag:
|
||||
// update the value based on the drag amount
|
||||
m := k.Value.Range()
|
||||
d := p.Position.Sub(k.State.dragStartPt)
|
||||
amount := float32(d.X-d.Y) / float32(gtx.Dp(k.Style.Diameter)) / 4
|
||||
newValue := int(float32(k.State.dragStartVal) + amount*float32(m.Max-m.Min))
|
||||
k.Value.SetValue(newValue)
|
||||
k.State.tipArea.Appear(gtx.Now)
|
||||
}
|
||||
}
|
||||
for {
|
||||
g, ok := k.State.click.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if g.Kind == gesture.KindClick && g.NumClicks > 1 {
|
||||
k.Value.Reset()
|
||||
}
|
||||
}
|
||||
for k.Scroll {
|
||||
e, ok := gtx.Event(pointer.Filter{
|
||||
Target: k.State,
|
||||
Kinds: pointer.Scroll,
|
||||
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
k.Value.SetValue(k.Value.Value() - int(delta))
|
||||
k.State.tipArea.Appear(gtx.Now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KnobWidget) strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, start, end float32) {
|
||||
rad := float32(diameter) / 2
|
||||
end = min(max(end, 0), 1)
|
||||
if end <= 0 {
|
||||
return
|
||||
}
|
||||
startAngle := float64((start*8 + 1) / 10 * 2 * math.Pi)
|
||||
deltaAngle := (end - start) * 8 * math.Pi / 5
|
||||
center := f32.Point{X: rad, Y: rad}
|
||||
r2 := rad - float32(strokeWidth)/2
|
||||
startPt := f32.Point{X: rad - r2*float32(math.Sin(startAngle)), Y: rad + r2*float32(math.Cos(startAngle))}
|
||||
segments := [...]stroke.Segment{
|
||||
stroke.MoveTo(startPt),
|
||||
stroke.ArcTo(center, deltaAngle),
|
||||
}
|
||||
s := stroke.Stroke{
|
||||
Path: stroke.Path{Segments: segments[:]},
|
||||
Width: float32(strokeWidth),
|
||||
Cap: stroke.FlatCap,
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, s.Op(gtx.Ops))
|
||||
}
|
||||
|
||||
func (k *KnobWidget) strokeIndicator(gtx C, amount float32) {
|
||||
innerRad := float32(gtx.Dp(k.Style.Indicator.InnerDiam)) / 2
|
||||
outerRad := float32(gtx.Dp(k.Style.Indicator.OuterDiam)) / 2
|
||||
center := float32(gtx.Dp(k.Style.Diameter)) / 2
|
||||
angle := (float64(amount)*8 + 1) / 10 * 2 * math.Pi
|
||||
start := f32.Point{
|
||||
X: center - innerRad*float32(math.Sin(angle)),
|
||||
Y: center + innerRad*float32(math.Cos(angle)),
|
||||
}
|
||||
end := f32.Point{
|
||||
X: center - outerRad*float32(math.Sin(angle)),
|
||||
Y: center + outerRad*float32(math.Cos(angle)),
|
||||
}
|
||||
segments := [...]stroke.Segment{
|
||||
stroke.MoveTo(start),
|
||||
stroke.LineTo(end),
|
||||
}
|
||||
s := stroke.Stroke{
|
||||
Path: stroke.Path{Segments: segments[:]},
|
||||
Width: float32(k.Style.Indicator.Width),
|
||||
Cap: stroke.FlatCap,
|
||||
}
|
||||
paint.FillShape(gtx.Ops, k.Style.Indicator.Color, s.Op(gtx.Ops))
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ type (
|
||||
Type, Comment string
|
||||
Disabled bool
|
||||
StackNeed, StackBefore, StackAfter int
|
||||
TargetUnit, TargetPort int
|
||||
}
|
||||
|
||||
// Range is used to represent a range [Start,End) of integers
|
||||
@ -325,11 +326,19 @@ func (v *Units) Item(index int) UnitListItem {
|
||||
return UnitListItem{}
|
||||
}
|
||||
unit := v.d.Song.Patch[v.d.InstrIndex].Units[index]
|
||||
targetUnit := 0
|
||||
if unit.Type == "send" {
|
||||
if _, tu, err := v.d.Song.Patch.FindUnit(unit.Parameters["target"]); err == nil {
|
||||
targetUnit = tu + 1
|
||||
}
|
||||
}
|
||||
return UnitListItem{
|
||||
Type: unit.Type,
|
||||
Comment: unit.Comment,
|
||||
Disabled: unit.Disabled,
|
||||
StackNeed: unit.StackNeed(),
|
||||
TargetUnit: targetUnit,
|
||||
TargetPort: unit.Parameters["port"],
|
||||
StackBefore: 0,
|
||||
StackAfter: 0,
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ type (
|
||||
ChangedSinceSave bool
|
||||
RecoveryFilePath string
|
||||
ChangedSinceRecovery bool
|
||||
SendSource int
|
||||
}
|
||||
|
||||
Model struct {
|
||||
|
84
tracker/stack.go
Normal file
84
tracker/stack.go
Normal file
@ -0,0 +1,84 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type (
|
||||
SignalRail struct {
|
||||
signals [][]Signal
|
||||
scratch []signalScratch
|
||||
}
|
||||
|
||||
signalScratch struct {
|
||||
instr, unit int
|
||||
}
|
||||
|
||||
Signal struct {
|
||||
PassThrough int
|
||||
StackUse sointu.StackUse
|
||||
}
|
||||
|
||||
SignalRailType Model
|
||||
)
|
||||
|
||||
func (m *Model) SignalRail() *SignalRailType {
|
||||
return (*SignalRailType)(m)
|
||||
}
|
||||
|
||||
func (s *SignalRailType) Item(u int) Signal {
|
||||
i := s.d.InstrIndex
|
||||
if i < 0 || u < 0 || i >= len(s.derived.rail.signals) || u >= len(s.derived.rail.signals[i]) {
|
||||
return Signal{}
|
||||
}
|
||||
return s.derived.rail.signals[i][u]
|
||||
}
|
||||
|
||||
func (s *SignalRail) update(patch sointu.Patch) (err error) {
|
||||
s.scratch = s.scratch[:0]
|
||||
for i, instr := range patch {
|
||||
for len(s.signals) <= i {
|
||||
s.signals = append(s.signals, make([]Signal, len(instr.Units)))
|
||||
}
|
||||
start := len(s.scratch)
|
||||
for u, unit := range instr.Units {
|
||||
for len(s.signals[i]) <= i {
|
||||
s.signals[i] = append(s.signals[i], Signal{})
|
||||
}
|
||||
stackUse := unit.StackUse()
|
||||
numInputs := len(stackUse.Inputs)
|
||||
if len(s.scratch) < numInputs && err != nil {
|
||||
err = fmt.Errorf("%s unit in instrument %d / %s needs %d inputs, but got only %d", unit.Type, i, instr.Name, numInputs, len(s.scratch))
|
||||
s.scratch = s.scratch[:0]
|
||||
} else {
|
||||
s.scratch = s.scratch[:len(s.scratch)-numInputs]
|
||||
}
|
||||
s.signals[i][u] = Signal{
|
||||
PassThrough: len(s.scratch),
|
||||
StackUse: stackUse,
|
||||
}
|
||||
for _ = range stackUse.NumOutputs {
|
||||
s.scratch = append(s.scratch, signalScratch{instr: i, unit: u})
|
||||
}
|
||||
}
|
||||
diff := len(s.scratch) - start
|
||||
if instr.NumVoices > 1 && diff != 0 {
|
||||
if diff < 0 {
|
||||
morepop := (instr.NumVoices - 1) * diff
|
||||
if morepop > len(s.scratch) && err != nil {
|
||||
err = fmt.Errorf("each voice of instrument %d / %s consumes %d signals, but there was not enough signals available", i, instr.Name, -diff)
|
||||
s.scratch = s.scratch[:0]
|
||||
} else {
|
||||
s.scratch = s.scratch[:len(s.scratch)-morepop]
|
||||
}
|
||||
} else {
|
||||
for range (instr.NumVoices - 1) * diff {
|
||||
s.scratch = append(s.scratch, s.scratch[len(s.scratch)-diff])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
Reference in New Issue
Block a user