diff --git a/patch.go b/patch.go index c30f7c6..84bc102 100644 --- a/patch.go +++ b/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 diff --git a/tracker/derived.go b/tracker/derived.go index 24f4894..11db21c 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -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 { diff --git a/tracker/gioui/knob.go b/tracker/gioui/knob.go new file mode 100644 index 0000000..8710711 --- /dev/null +++ b/tracker/gioui/knob.go @@ -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)) +} diff --git a/tracker/gioui/scroll_table.go b/tracker/gioui/scroll_table.go index 695e9d4..529153d 100644 --- a/tracker/gioui/scroll_table.go +++ b/tracker/gioui/scroll_table.go @@ -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) { diff --git a/tracker/gioui/signal_rail.go b/tracker/gioui/signal_rail.go new file mode 100644 index 0000000..8aba285 --- /dev/null +++ b/tracker/gioui/signal_rail.go @@ -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)} +} diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index b3dbcc6..54ce47f 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -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 diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 9865e2b..443ca1f 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -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 diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 25eec57..a187ee5 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -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)) -} diff --git a/tracker/list.go b/tracker/list.go index ed2ac3c..2214b18 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -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, } diff --git a/tracker/model.go b/tracker/model.go index 0f8d493..7766b9d 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -35,6 +35,7 @@ type ( ChangedSinceSave bool RecoveryFilePath string ChangedSinceRecovery bool + SendSource int } Model struct { diff --git a/tracker/stack.go b/tracker/stack.go new file mode 100644 index 0000000..aacb9dc --- /dev/null +++ b/tracker/stack.go @@ -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 +}