This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-07-03 18:54:02 +03:00
parent c09a3f04db
commit 4e295a3a2f
8 changed files with 206 additions and 112 deletions

View File

@ -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
}

View File

@ -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)}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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,
}

View File

@ -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()
}