From 4e295a3a2f808c66a6f458535529465c51e43af6 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:54:02 +0300 Subject: [PATCH] drafting --- patch.go | 14 ++--- tracker/derived.go | 8 +-- tracker/gioui/signal_rail.go | 64 +++++++++++---------- tracker/gioui/theme.go | 20 +++---- tracker/gioui/theme.yml | 13 +++-- tracker/gioui/unit_editor.go | 106 +++++++++++++++++++++-------------- tracker/list.go | 21 +++++-- tracker/stack.go | 72 +++++++++++++++++++++--- 8 files changed, 206 insertions(+), 112 deletions(-) diff --git a/patch.go b/patch.go index 84bc102..e4623a7 100644 --- a/patch.go +++ b/patch.go @@ -386,8 +386,8 @@ var stackUseMonoStereo = map[string][2]StackUse{ }, } var stackUseSendNoPop = [2]StackUse{ - {Inputs: [][]int{{0}}, Modifies: []bool{false}, NumOutputs: 1}, - {Inputs: [][]int{{0}, {1}}, Modifies: []bool{false, false}, NumOutputs: 2}, + {Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 1}, + {Inputs: [][]int{{0}, {1}}, Modifies: []bool{true, true}, NumOutputs: 2}, } var stackUseSendPop = [2]StackUse{ {Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 0}, // mono @@ -544,19 +544,19 @@ func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) { return 0, 0, fmt.Errorf("could not find a unit with id %v", id) } -func FindParamForModulationPort(unitName string, index int) (UnitParameter, bool) { +func FindParamForModulationPort(unitName string, index int) (up UnitParameter, upIndex int, ok bool) { unitType, ok := UnitTypes[unitName] if !ok { - return UnitParameter{}, false + return UnitParameter{}, 0, false } - for _, param := range unitType { + for i, param := range unitType { if !param.CanModulate { continue } if index == 0 { - return param, true + return param, i, true } index-- } - return UnitParameter{}, false + return UnitParameter{}, 0, false } diff --git a/tracker/derived.go b/tracker/derived.go index 11db21c..cd20f47 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -153,7 +153,7 @@ func (m *Model) updateDerivedScoreData() { } func (m *Model) updateDerivedPatchData() { - m.derived.rail.update(m.d.Song.Patch) + m.SignalRail().update() clear(m.derived.forUnit) for i, instr := range m.d.Song.Patch { for u, unit := range instr.Units { @@ -177,8 +177,8 @@ func (m *Model) deriveParams(unit *sointu.Unit) []Parameter { return ret } for i, up := range unitType { - if !up.CanSet { - continue + if !up.CanSet && !up.CanModulate { + continue // skip parameters that cannot be set or modulated } if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && (up.Name == "samplestart" || up.Name == "loopstart" || up.Name == "looplength") { continue // don't show the sample related params unless necessary @@ -230,7 +230,7 @@ func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[ continue } port := u.Parameters["port"] - unitParam, ok := sointu.FindParamForModulationPort(unit.Type, port) + unitParam, _, ok := sointu.FindParamForModulationPort(unit.Type, port) if !ok || unitParam.Name != paramName { continue } diff --git a/tracker/gioui/signal_rail.go b/tracker/gioui/signal_rail.go index 8aba285..8aa4073 100644 --- a/tracker/gioui/signal_rail.go +++ b/tracker/gioui/signal_rail.go @@ -12,12 +12,13 @@ import ( "github.com/vsariola/sointu/tracker" ) -const numSignalsDrawn = 8 +const maxSignalsDrawn = 16 type ( SignalRailStyle struct { Color color.NRGBA LineWidth unit.Dp + SignalWidth unit.Dp PortDiameter unit.Dp PortColor color.NRGBA } @@ -25,7 +26,6 @@ type ( SignalRailWidget struct { Style *SignalRailStyle Signal tracker.Signal - Width unit.Dp Height unit.Dp } ) @@ -34,63 +34,69 @@ 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) + sw := gtx.Dp(s.Style.SignalWidth) 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 + lw := gtx.Dp(s.Style.LineWidth) + pd := gtx.Dp(s.Style.PortDiameter) + center := sw / 2 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) + for i := range min(maxSignalsDrawn, s.Signal.PassThrough) { + x := float32(i*sw + center) 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) { + for i := range min(len(s.Signal.StackUse.Inputs), maxSignalsDrawn-s.Signal.PassThrough) { input := s.Signal.StackUse.Inputs[i] - x1 := float32((i+s.Signal.PassThrough)*stride + c) + x1 := float32((i+s.Signal.PassThrough)*sw + center) for _, link := range input { - x2 := float32((link+s.Signal.PassThrough)*stride + c) + x2 := float32((link+s.Signal.PassThrough)*sw + center) path.MoveTo(f32.Pt(x1, 0)) path.LineTo(f32.Pt(x2, float32(h/2))) } } + if s.Signal.Send { + for i := range min(len(s.Signal.StackUse.Inputs), maxSignalsDrawn-s.Signal.PassThrough) { + from := f32.Pt(float32((i+s.Signal.PassThrough)*sw+center), float32(h/2)) + to := f32.Pt(float32(gtx.Constraints.Max.X), float32(h)-float32(sw/2)) + ctrl := f32.Pt(from.X, to.Y) + path.MoveTo(from) + path.QuadTo(ctrl, to) + } + } // 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) + for i := range min(s.Signal.StackUse.NumOutputs, maxSignalsDrawn-s.Signal.PassThrough) { + x := float32((i+s.Signal.PassThrough)*sw + center) path.MoveTo(f32.Pt(x, float32(h/2))) path.LineTo(f32.Pt(x, float32(h))) } + // Signal paths finished paint.FillShape(gtx.Ops, s.Style.Color, clip.Stroke{ Path: path.End(), - Width: float32(l), + Width: float32(lw), }.Op()) - // Draw the circles on modified signals - - for i := range min(len(s.Signal.StackUse.Modifies), numSignalsDrawn-s.Signal.PassThrough) { + // Draw the circles on signals that get modified + var circle clip.Path + circle.Begin(gtx.Ops) + for i := range min(len(s.Signal.StackUse.Modifies), maxSignalsDrawn-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)) + f := f32.Pt(float32((i+s.Signal.PassThrough)*sw+center), float32(h/2)) + circle.MoveTo(f32.Pt(f.X-float32(pd/2), 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)} + 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(sw, h)} } diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 54ce47f..f52552e 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -85,16 +85,16 @@ type Theme struct { } } UnitEditor struct { - Name LabelStyle - Chooser LabelStyle - Hint LabelStyle - InvalidParam color.NRGBA - SendTarget color.NRGBA - Width unit.Dp - Height unit.Dp - RowTitleWidth unit.Dp - ColumnTitleHeight unit.Dp - RowTitle LabelStyle + Name LabelStyle + Chooser LabelStyle + Hint LabelStyle + InvalidParam color.NRGBA + SendTarget color.NRGBA + Width unit.Dp + Height unit.Dp + RowTitle LabelStyle + RowTitleWidth unit.Dp + Error color.NRGBA } Cursor CursorStyle Selection CursorStyle diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 443ca1f..8574c0d 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -211,12 +211,12 @@ 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: 63 } + sendtarget: *secondarycolor width: 60 - height: 60 - rowtitlewidth: 16 - columntitleheight: 16 + height: 70 rowtitle: { textsize: 12, color: *white, alignment: 2 } + rowtitlewidth: 16 + error: *errorcolor knob: diameter: 36 value: { textsize: 12, color: *highemphasis } @@ -226,7 +226,8 @@ knob: neg: { color: *secondarycolor, bg: { r: 32, g: 55, b: 58, a: 255 } } indicator: { color: *white, width: 2, innerdiam: 24, outerdiam: 36 } signalrail: - color: *primarycolor + color: *secondarycolor + signalwidth: 10 linewidth: 2 portdiameter: 8 - portcolor: *secondarycolor + portcolor: *primarycolor diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index a187ee5..381c96e 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -71,7 +71,7 @@ func (pe *UnitEditor) Layout(gtx C) D { t := TrackerFromContext(gtx) pe.update(gtx, t) defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - editorFunc := pe.layoutSliders + editorFunc := pe.layoutRack if pe.showingChooser() { editorFunc = pe.layoutUnitTypeChooser } @@ -152,7 +152,7 @@ func (pe *UnitEditor) ChooseUnitType(t *Tracker) { } } -func (pe *UnitEditor) layoutSliders(gtx C) D { +func (pe *UnitEditor) layoutRack(gtx C) D { t := TrackerFromContext(gtx) // create enough parameter widget to match the number of parameters width := pe.paramTable.Table.Width() @@ -161,8 +161,11 @@ func (pe *UnitEditor) layoutSliders(gtx C) D { } cellWidth := gtx.Dp(t.Theme.UnitEditor.Width) cellHeight := gtx.Dp(t.Theme.UnitEditor.Height) - rowTitleWidth := gtx.Dp(t.Theme.UnitEditor.RowTitleWidth) - columnTitleHeight := gtx.Dp(t.Theme.UnitEditor.ColumnTitleHeight) + rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.RowTitleWidth) + rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.SignalRail().MaxWidth() + rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth + signalError := t.SignalRail().Error() + columnTitleHeight := gtx.Dp(0) for i := range pe.Parameters { for len(pe.Parameters[i]) < width { pe.Parameters[i] = append(pe.Parameters[i], &ParameterWidget{}) @@ -171,20 +174,29 @@ func (pe *UnitEditor) layoutSliders(gtx C) D { coltitle := func(gtx C, x int) D { return D{Size: image.Pt(cellWidth, columnTitleHeight)} } + rowTitleBg := func(gtx C, j int) D { + paint.FillShape(gtx.Ops, t.Theme.NoteEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, 1)}.Op()) + return D{} + } rowtitle := func(gtx C, y int) D { - //defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop() - defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(cellHeight)})).Push(gtx.Ops).Pop() - gtx.Constraints = layout.Exact(image.Pt(cellHeight, rowTitleWidth)) - Label(t.Theme, &t.Theme.UnitEditor.RowTitle, t.Units().Item(y).Type).Layout(gtx) + if y < 0 || y >= len(pe.Parameters) { + return D{} + } + + sr := SignalRail(t.Theme, t.SignalRail().Item(y)) + label := Label(t.Theme, &t.Theme.UnitEditor.RowTitle, t.Units().Item(y).Type) + if signalError.Err != nil && signalError.UnitIndex == y { + label.Color = t.Theme.UnitEditor.Error + } + gtx.Constraints = layout.Exact(image.Pt(rowTitleWidth, cellHeight)) + sr.Layout(gtx) + defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: float32(rowTitleSignalWidth), Y: float32(cellHeight)})).Push(gtx.Ops).Pop() + gtx.Constraints = layout.Exact(image.Pt(cellHeight, rowTitleLabelWidth)) + label.Layout(gtx) return D{Size: image.Pt(rowTitleWidth, cellHeight)} } 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]) { @@ -206,63 +218,76 @@ func (pe *UnitEditor) layoutSliders(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)} } table := FilledScrollTable(t.Theme, pe.paramTable) - table.RowTitleWidth = t.Theme.UnitEditor.RowTitleWidth - table.ColumnTitleHeight = t.Theme.UnitEditor.ColumnTitleHeight + table.RowTitleWidth = gtx.Metric.PxToDp(rowTitleWidth) + table.ColumnTitleHeight = 0 table.CellWidth = t.Theme.UnitEditor.Width table.CellHeight = t.Theme.UnitEditor.Height - pe.drawSignals(gtx) - dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, nil) + pe.drawSignals(gtx, rowTitleWidth) + dims := table.Layout(gtx, cell, coltitle, rowtitle, nil, rowTitleBg) return dims } -func (pe *UnitEditor) drawSignals(gtx C) { +func (pe *UnitEditor) drawSignals(gtx C, rowTitleWidth int) { 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)) + p := image.Pt(rowTitleWidth, 0) 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) + if item.TargetOk { + pe.drawSignal(gtx, i-rowP.First, item.TargetX-colP.First, item.TargetY-rowP.First) } } } -func (pe *UnitEditor) drawSignal(gtx C, sx, sy, ex, ey int) { +func (pe *UnitEditor) drawSignal(gtx C, 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)) + from := f32.Pt(0, float32((sy+1)*gtx.Dp(t.Theme.UnitEditor.Height))-float32(gtx.Dp(t.Theme.SignalRail.SignalWidth)/2)) + corner := f32.Pt(1, 1) + if ex > 0 { + corner.X = -corner.X } + if sy < ey { + corner.Y = -corner.Y + } + c := float32(diam) / 2 / float32(math.Sqrt2) + topLeft := f32.Pt(float32(ex)*width, float32(ey)*height) + center := topLeft.Add(f32.Pt(width/2, height/2)) + to := mulVec(corner, f32.Pt(c, c)).Add(center) + p2 := mulVec(corner, f32.Pt(width/2, height/2)).Add(center) + p1 := f32.Pt(p2.X, float32((sy+1)*gtx.Dp(t.Theme.UnitEditor.Height))) + if sy > ey { + p1 = f32.Pt(p2.X, (float32(sy)+0.5)*float32(gtx.Dp(t.Theme.UnitEditor.Height))+float32(diam)/2) + } + k := float32(width) / 4 + //toTan := mulVec(corner, f32.Pt(-k, -k)) + p2Tan := mulVec(corner, f32.Pt(-k, -k)) + p1Tan := f32.Pt(k, p2Tan.Y) + fromTan := f32.Pt(k, 0) var path clip.Path path.Begin(gtx.Ops) path.MoveTo(from) - path.CubeTo(c1, c2, to) + path.CubeTo(from.Add(fromTan), p1.Sub(p1Tan), p1) + path.CubeTo(p1.Add(p1Tan), p2, to) paint.FillShape(gtx.Ops, t.Theme.UnitEditor.SendTarget, clip.Stroke{ Path: path.End(), - Width: float32(gtx.Dp(4)), + Width: float32(gtx.Dp(t.Theme.SignalRail.LineWidth)), }.Op()) } +func mulVec(a, b f32.Point) f32.Point { + return f32.Pt(a.X*b.X, a.Y*b.Y) +} + func (pe *UnitEditor) layoutFooter(gtx C) D { t := TrackerFromContext(gtx) st := t.Units().SelectedType() @@ -426,9 +451,9 @@ func (p ParameterStyle) Layout(gtx C) D { } return D{} } - return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(title.Layout), - layout.Flexed(1, func(gtx C) D { return layout.Center.Layout(gtx, widget) }), + title.Layout(gtx) + layout.Center.Layout(gtx, widget) + return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)} /* layout.Rigid(func(gtx C) D { if p.w.Parameter.Type() != tracker.IDParameter { hint := p.w.Parameter.Hint() @@ -445,7 +470,6 @@ func (p ParameterStyle) Layout(gtx C) D { } return D{} }),*/ - ) } func drawCircle(gtx C, i int, nRGBA color.NRGBA) D { diff --git a/tracker/list.go b/tracker/list.go index 2214b18..2c9405b 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -39,7 +39,8 @@ type ( Type, Comment string Disabled bool StackNeed, StackBefore, StackAfter int - TargetUnit, TargetPort int + TargetOk bool // TargetOk indicates if the target unit is valid + TargetX, TargetY int } // Range is used to represent a range [Start,End) of integers @@ -326,10 +327,17 @@ func (v *Units) Item(index int) UnitListItem { return UnitListItem{} } unit := v.d.Song.Patch[v.d.InstrIndex].Units[index] - targetUnit := 0 + targetOk := false + targetY := 0 + targetX := 0 if unit.Type == "send" { - if _, tu, err := v.d.Song.Patch.FindUnit(unit.Parameters["target"]); err == nil { - targetUnit = tu + 1 + if i, y, err := v.d.Song.Patch.FindUnit(unit.Parameters["target"]); err == nil { + targetUnit := v.d.Song.Patch[i].Units[y] + if _, x, ok := sointu.FindParamForModulationPort(targetUnit.Type, unit.Parameters["port"]); ok { + targetX = x + targetY = y + targetOk = true + } } } return UnitListItem{ @@ -337,8 +345,9 @@ func (v *Units) Item(index int) UnitListItem { Comment: unit.Comment, Disabled: unit.Disabled, StackNeed: unit.StackNeed(), - TargetUnit: targetUnit, - TargetPort: unit.Parameters["port"], + TargetOk: targetOk, + TargetY: targetY, + TargetX: targetX, StackBefore: 0, StackAfter: 0, } diff --git a/tracker/stack.go b/tracker/stack.go index aacb9dc..82e8699 100644 --- a/tracker/stack.go +++ b/tracker/stack.go @@ -10,17 +10,25 @@ type ( SignalRail struct { signals [][]Signal scratch []signalScratch + + error SignalError } - signalScratch struct { - instr, unit int + SignalError struct { + InstrIndex, UnitIndex int + Err error } Signal struct { PassThrough int + Send bool StackUse sointu.StackUse } + signalScratch struct { + instr, unit int + } + SignalRailType Model ) @@ -36,21 +44,51 @@ func (s *SignalRailType) Item(u int) Signal { return s.derived.rail.signals[i][u] } -func (s *SignalRail) update(patch sointu.Patch) (err error) { +func (s *SignalRailType) Error() SignalError { + i := s.d.InstrIndex + if i < 0 || i >= len(s.derived.rail.signals) { + return SignalError{} + } + if i == s.derived.rail.error.InstrIndex { + return s.derived.rail.error + } + return SignalError{} +} + +func (s *SignalRailType) MaxWidth() int { + i := s.d.InstrIndex + if i < 0 || i >= len(s.derived.rail.signals) { + return 0 + } + ret := 0 + for _, signal := range s.derived.rail.signals[i] { + ret = max(ret, signal.PassThrough+max(len(signal.StackUse.Inputs), signal.StackUse.NumOutputs)) + } + return ret +} + +func (st *SignalRailType) update() { + s := &st.derived.rail + patch := st.d.Song.Patch s.scratch = s.scratch[:0] + s.error = SignalError{} 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 { + for len(s.signals[i]) <= u { 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)) + if len(s.scratch) < numInputs { + if s.error.Err == nil { + s.error.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.error.InstrIndex = i + s.error.UnitIndex = u + } s.scratch = s.scratch[:0] } else { s.scratch = s.scratch[:len(s.scratch)-numInputs] @@ -58,6 +96,7 @@ func (s *SignalRail) update(patch sointu.Patch) (err error) { s.signals[i][u] = Signal{ PassThrough: len(s.scratch), StackUse: stackUse, + Send: unit.Type == "send", } for _ = range stackUse.NumOutputs { s.scratch = append(s.scratch, signalScratch{instr: i, unit: u}) @@ -67,8 +106,12 @@ func (s *SignalRail) update(patch sointu.Patch) (err error) { 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) + if morepop > len(s.scratch) { + if s.error.Err == nil { + s.error.Err = fmt.Errorf("each voice of instrument %d / %s consumes %d signals, but there was not enough signals available", i, instr.Name, -diff) + s.error.InstrIndex = i + s.error.UnitIndex = -1 + } s.scratch = s.scratch[:0] } else { s.scratch = s.scratch[:len(s.scratch)-morepop] @@ -80,5 +123,16 @@ func (s *SignalRail) update(patch sointu.Patch) (err error) { } } } - return err + if len(s.scratch) > 0 && s.error.Err == nil { + s.error.Err = fmt.Errorf("instrument %d / %s unit %d / %s leave a signal on stack ", s.scratch[0].instr, patch[s.scratch[0].instr].Name, s.scratch[0].unit, patch[s.scratch[0].instr].Units[s.scratch[0].unit].Type) + s.error.InstrIndex = s.scratch[0].instr + s.error.UnitIndex = s.scratch[0].unit + } + if s.error.Err != nil { + (*Model)(st).Alerts().AddNamed("SignalError", s.error.Error(), Error) + } +} + +func (e *SignalError) Error() string { + return e.Err.Error() }