diff --git a/tracker/derived.go b/tracker/derived.go index b37f75f..61fd177 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -76,16 +76,16 @@ func (m *Model) UnitHintInfo(id int) (instrIndex int, unitType string, ok bool) return fu.instrumentIndex, fu.unit.Type, ok } -func (m *Model) ParameterInfo(unitId int, paramName string) (isSendTarget bool, tooltip string, exists bool) { +func (m *Model) ParameterInfo(unitId int, paramName string) (tooltip string, ok bool) { du, ok1 := m.derived.forUnit[unitId] if !ok1 { - return false, "", false + return "", false } dp, ok2 := du.forParameter[paramName] if !ok2 { - return false, "", false + return "", false } - return len(dp.sendSources) > 0, dp.sendTooltip, true + return dp.sendTooltip, len(dp.sendSources) > 0 } func (m *Model) TrackTitle(index int) string { diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 6199b1d..0a88807 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -199,29 +199,21 @@ func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D { } func (pe *UnitEditor) command(e key.Event, t *Tracker) { - params := (*tracker.Params)(t.Model) + params := t.Model.Params() switch e.State { case key.Press: switch e.Name { case key.NameLeftArrow: - sel := params.SelectedItem() - if sel == nil { - return - } - i := tracker.MakeInt(sel) + i := params.SelectedItem() if e.Modifiers.Contain(key.ModShift) { - i.SetValue(i.Value() - sel.LargeStep()) + i.SetValue(i.Value() - i.LargeStep()) } else { i.SetValue(i.Value() - 1) } case key.NameRightArrow: - sel := params.SelectedItem() - if sel == nil { - return - } - i := tracker.MakeInt(sel) + i := params.SelectedItem() if e.Modifiers.Contain(key.ModShift) { - i.SetValue(i.Value() + sel.LargeStep()) + i.SetValue(i.Value() + i.LargeStep()) } else { i.SetValue(i.Value() + 1) } @@ -266,7 +258,7 @@ func (t *Tracker) ParamStyle(th *Theme, paramWidget *ParameterWidget) ParameterS } func (p ParameterStyle) Layout(gtx C) D { - isSendTarget, info := p.tryDerivedParameterInfo() + info, infoOk := p.w.Parameter.Info() return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110)) @@ -286,7 +278,7 @@ func (p ParameterStyle) Layout(gtx C) D { } if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll { delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1) - tracker.MakeInt(p.w.Parameter).Add(-int(delta)) + p.w.Parameter.SetValue(p.w.Parameter.Value() - int(delta)) } } gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200)) @@ -296,7 +288,7 @@ func (p ParameterStyle) Layout(gtx C) D { p.w.floatWidget.Value = (float32(p.w.Parameter.Value()) - float32(ra.Min)) / float32(ra.Max-ra.Min) } sliderStyle := material.Slider(&p.Theme.Material, &p.w.floatWidget) - if isSendTarget { + if infoOk { sliderStyle.Color = p.Theme.UnitEditor.SendTarget } r := image.Rectangle{Max: gtx.Constraints.Min} @@ -306,7 +298,7 @@ func (p ParameterStyle) Layout(gtx C) D { event.Op(gtx.Ops, &p.w.floatWidget) } dims := sliderStyle.Layout(gtx) - tracker.MakeInt(p.w.Parameter).SetValue(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5)) + p.w.Parameter.SetValue(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5)) return dims case tracker.BoolParameter: gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60)) @@ -318,9 +310,9 @@ func (p ParameterStyle) Layout(gtx C) D { defer pointer.PassOp{}.Push(gtx.Ops).Pop() dims := layout.Center.Layout(gtx, boolStyle.Layout) if p.w.boolWidget.Value { - tracker.MakeInt(p.w.Parameter).SetValue(ra.Max) + p.w.Parameter.SetValue(ra.Max) } else { - tracker.MakeInt(p.w.Parameter).SetValue(ra.Min) + p.w.Parameter.SetValue(ra.Min) } return dims case tracker.IDParameter: @@ -334,7 +326,7 @@ func (p ParameterStyle) Layout(gtx C) D { instrItems[i].IconBytes = icons.NavigationChevronRight instrItems[i].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { if id, ok := p.tracker.Instruments().FirstID(i); ok { - tracker.MakeInt(p.w.Parameter).SetValue(id) + p.w.Parameter.SetValue(id) } })) } @@ -351,7 +343,7 @@ func (p ParameterStyle) Layout(gtx C) D { unitItems[j].Text = buildUnitLabel(j, unit) unitItems[j].IconBytes = icons.NavigationChevronRight unitItems[j].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { - tracker.MakeInt(p.w.Parameter).SetValue(id) + p.w.Parameter.SetValue(id) })) } } @@ -392,12 +384,3 @@ func buildUnitLabel(index int, u sointu.Unit) string { } return fmt.Sprintf("%d: %s", index, text) } - -func (p ParameterStyle) tryDerivedParameterInfo() (isSendTarget bool, sendInfo string) { - param, ok := (p.w.Parameter).(tracker.NamedParameter) - if !ok { - return false, "" - } - isSendTarget, sendInfo, _ = p.tracker.ParameterInfo(param.Unit().ID, param.Name()) - return isSendTarget, sendInfo -} diff --git a/tracker/params.go b/tracker/params.go index 950b9ad..adb005a 100644 --- a/tracker/params.go +++ b/tracker/params.go @@ -11,35 +11,37 @@ import ( ) type ( - Parameter interface { - IntValue - Type() ParameterType - Name() string - Hint() ParameterHint - LargeStep() int - Reset() + // Parameter represents a parameter of a unit. To support polymorphism + // without causing allocations, it has a vtable that defines the methods for + // the specific parameter type, to which all the method calls are delegated. + Parameter struct { + m *Model + unit *sointu.Unit + up *sointu.UnitParameter + index int + vtable parameterVtable } - parameter struct { - m *Model - unit *sointu.Unit + parameterVtable interface { + Value(*Parameter) int + SetValue(*Parameter, int) bool + Range(*Parameter) IntRange + Type(*Parameter) ParameterType + Name(*Parameter) string + Hint(*Parameter) ParameterHint + Info(*Parameter) (string, bool) // additional info for the parameter, used to display send targets + LargeStep(*Parameter) int + Reset(*Parameter) } - NamedParameter struct { - parameter - up *sointu.UnitParameter - } - - DelayTimeParameter struct { - parameter - index int - } - - DelayLinesParameter struct{ parameter } - GmDlsEntryParameter struct{ parameter } - ReverbParameter struct{ parameter } - Params Model + // different parameter vtables to handle different types of parameters. + // Casting struct{} to interface does not cause allocations. + namedParameter struct{} + delayTimeParameter struct{} + delayLinesParameter struct{} + gmDlsEntryParameter struct{} + reverbParameter struct{} ParamYieldFunc func(param Parameter) bool @@ -57,22 +59,76 @@ const ( IDParameter ) -// Model methods +// Parameter methods -func (m *Model) Params() *Params { return (*Params)(m) } +func (p *Parameter) Value() int { + if p.vtable == nil { + return 0 + } + return p.vtable.Value(p) +} +func (p *Parameter) SetValue(value int) bool { + if p.vtable == nil { + return false + } + r := p.Range() + value = r.Clamp(value) + if value == p.Value() || value < r.Min || value > r.Max { + return false + } + return p.vtable.SetValue(p, value) +} +func (p *Parameter) Range() IntRange { + if p.vtable == nil { + return IntRange{} + } + return p.vtable.Range(p) +} +func (p *Parameter) Type() ParameterType { + if p.vtable == nil { + return IntegerParameter + } + return p.vtable.Type(p) +} +func (p *Parameter) Name() string { + if p.vtable == nil { + return "" + } + return p.vtable.Name(p) +} +func (p *Parameter) Hint() ParameterHint { + if p.vtable == nil { + return ParameterHint{} + } + return p.vtable.Hint(p) +} +func (p *Parameter) Info() (string, bool) { + if p.vtable == nil { + return "", false + } + return p.vtable.Info(p) +} +func (p *Parameter) LargeStep() int { + if p.vtable == nil { + return 1 + } + return p.vtable.LargeStep(p) +} +func (p *Parameter) Reset() { + if p.vtable == nil { + return + } + p.vtable.Reset(p) +} -// ParamList +// Model and Params methods +func (m *Model) Params() *Params { return (*Params)(m) } func (pl *Params) List() List { return List{pl} } func (pl *Params) Selected() int { return pl.d.ParamIndex } func (pl *Params) Selected2() int { return pl.Selected() } func (pl *Params) SetSelected(value int) { pl.d.ParamIndex = max(min(value, pl.Count()-1), 0) } func (pl *Params) SetSelected2(value int) {} -func (pl *Params) cancel() { (*Model)(pl).changeCancel = true } - -func (pl *Params) change(n string, severity ChangeSeverity) func() { - return (*Model)(pl).change("ParamList."+n, PatchChange, severity) -} func (pl *Params) Count() int { count := 0 @@ -105,65 +161,52 @@ func (pl *Params) Iterate(yield ParamYieldFunc) { if !ok { return } - for i := range unitType { - if !unitType[i].CanSet { + for i, up := range unitType { + if !up.CanSet { continue } - if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 { - break // don't show the sample related params unless necessary + 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 } - if !yield(NamedParameter{ - parameter: parameter{m: (*Model)(pl), unit: unit}, - up: &unitType[i], - }) { + if !yield(Parameter{m: (*Model)(pl), unit: unit, up: &unitType[i], vtable: &namedParameter{}}) { return } } if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample { - if !yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { + if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &gmDlsEntryParameter{}}) { return } } - switch { - case unit.Type == "delay": + if unit.Type == "delay" { if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 { unit.VarArgs = append(unit.VarArgs, 1) } - if !yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { + if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &reverbParameter{}}) { return } - if !yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { + if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &delayLinesParameter{}}) { return } for i := range unit.VarArgs { - if !yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i}) { + if !yield(Parameter{m: (*Model)(pl), unit: unit, index: i, vtable: &delayTimeParameter{}}) { return } } } } -// NamedParameter +// namedParameter vtable -func (p NamedParameter) Name() string { return p.up.Name } -func (p NamedParameter) Range() IntRange { return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue} } -func (p NamedParameter) Value() int { return p.unit.Parameters[p.up.Name] } -func (p NamedParameter) SetValue(value int) bool { +func (n *namedParameter) Value(p *Parameter) int { return p.unit.Parameters[p.up.Name] } +func (n *namedParameter) SetValue(p *Parameter, value int) bool { defer p.m.change("Parameter"+p.Name(), PatchChange, MinorChange)() p.unit.Parameters[p.up.Name] = value return true } - -func (p NamedParameter) Reset() { - v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name] - if !ok || p.unit.Parameters[p.up.Name] == v { - return - } - defer p.m.change("Reset"+p.Name(), PatchChange, MinorChange)() - p.unit.Parameters[p.up.Name] = v +func (n *namedParameter) Range(p *Parameter) IntRange { + return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue} } - -func (p NamedParameter) Type() ParameterType { +func (n *namedParameter) Type(p *Parameter) ParameterType { if p.unit.Type == "send" && p.up.Name == "target" { return IDParameter } @@ -172,8 +215,10 @@ func (p NamedParameter) Type() ParameterType { } return IntegerParameter } - -func (p NamedParameter) Hint() ParameterHint { +func (n *namedParameter) Name(p *Parameter) string { + return p.up.Name +} +func (n *namedParameter) Hint(p *Parameter) ParameterHint { val := p.Value() label := strconv.Itoa(val) if p.up.DisplayFunc != nil { @@ -197,40 +242,44 @@ func (p NamedParameter) Hint() ParameterHint { if val < 0 || val >= len(portList) { return ParameterHint{label, false} } - label = fmt.Sprintf(portList[val]) + label = portList[val] } } return ParameterHint{label, true} } - -func (p NamedParameter) LargeStep() int { +func (n *namedParameter) Info(p *Parameter) (string, bool) { + sendInfo, ok := p.m.ParameterInfo(p.unit.ID, p.up.Name) + return sendInfo, ok +} +func (n *namedParameter) LargeStep(p *Parameter) int { if p.up.Name == "transpose" { return 12 } return 16 } - -func (p NamedParameter) Unit() sointu.Unit { - return *p.parameter.unit +func (n *namedParameter) Reset(p *Parameter) { + v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name] + if !ok || p.unit.Parameters[p.up.Name] == v { + return + } + defer p.m.change("Reset"+p.Name(), PatchChange, MinorChange)() + p.unit.Parameters[p.up.Name] = v } -// GmDlsEntryParameter +// gmDlsEntryParameter vtable -func (p GmDlsEntryParameter) Name() string { return "sample" } -func (p GmDlsEntryParameter) Type() ParameterType { return IntegerParameter } -func (p GmDlsEntryParameter) Range() IntRange { return IntRange{Min: 0, Max: len(GmDlsEntries)} } -func (p GmDlsEntryParameter) LargeStep() int { return 16 } -func (p GmDlsEntryParameter) Reset() { return } - -func (p GmDlsEntryParameter) Value() int { - key := vm.SampleOffset{Start: uint32(p.unit.Parameters["samplestart"]), LoopStart: uint16(p.unit.Parameters["loopstart"]), LoopLength: uint16(p.unit.Parameters["looplength"])} +func (g *gmDlsEntryParameter) Value(p *Parameter) int { + key := vm.SampleOffset{ + Start: uint32(p.unit.Parameters["samplestart"]), + LoopStart: uint16(p.unit.Parameters["loopstart"]), + LoopLength: uint16(p.unit.Parameters["looplength"]), + } if v, ok := gmDlsEntryMap[key]; ok { return v + 1 } return 0 } - -func (p GmDlsEntryParameter) SetValue(v int) bool { +func (g *gmDlsEntryParameter) SetValue(p *Parameter, v int) bool { if v < 1 || v > len(GmDlsEntries) { return false } @@ -242,44 +291,57 @@ func (p GmDlsEntryParameter) SetValue(v int) bool { p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose return true } - -func (p GmDlsEntryParameter) Hint() ParameterHint { +func (g *gmDlsEntryParameter) Range(p *Parameter) IntRange { + return IntRange{Min: 0, Max: len(GmDlsEntries)} +} +func (g *gmDlsEntryParameter) Type(p *Parameter) ParameterType { + return IntegerParameter +} +func (g *gmDlsEntryParameter) Name(p *Parameter) string { + return "sample" +} +func (g *gmDlsEntryParameter) Hint(p *Parameter) ParameterHint { label := "0 / custom" - if v := p.Value(); v > 0 { + if v := g.Value(p); v > 0 { label = fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name) } return ParameterHint{label, true} } +func (g *gmDlsEntryParameter) Info(p *Parameter) (string, bool) { + return "", false +} +func (g *gmDlsEntryParameter) LargeStep(p *Parameter) int { + return 16 +} +func (g *gmDlsEntryParameter) Reset(p *Parameter) {} -// DelayTimeParameter +// delayTimeParameter vtable -func (p DelayTimeParameter) Name() string { return "delaytime" } -func (p DelayTimeParameter) Type() ParameterType { return IntegerParameter } -func (p DelayTimeParameter) LargeStep() int { return 16 } -func (p DelayTimeParameter) Reset() { return } - -func (p DelayTimeParameter) Value() int { +func (d *delayTimeParameter) Value(p *Parameter) int { if p.index < 0 || p.index >= len(p.unit.VarArgs) { return 1 } return p.unit.VarArgs[p.index] } - -func (p DelayTimeParameter) SetValue(v int) bool { +func (d *delayTimeParameter) SetValue(p *Parameter, v int) bool { defer p.m.change("DelayTimeParameter", PatchChange, MinorChange)() p.unit.VarArgs[p.index] = v return true } - -func (p DelayTimeParameter) Range() IntRange { +func (d *delayTimeParameter) Range(p *Parameter) IntRange { if p.unit.Parameters["notetracking"] == 2 { return IntRange{Min: 1, Max: 576} } return IntRange{Min: 1, Max: 65535} } - -func (p DelayTimeParameter) Hint() ParameterHint { - val := p.Value() +func (d *delayTimeParameter) Type(p *Parameter) ParameterType { + return IntegerParameter +} +func (d *delayTimeParameter) Name(p *Parameter) string { + return "delaytime" +} +func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint { + val := d.Value(p) var text string switch p.unit.Parameters["notetracking"] { default: @@ -292,7 +354,7 @@ func (p DelayTimeParameter) Hint() ParameterHint { case 2: k := 0 v := val - for v&1 == 0 { // divide val by 2 until it is odd + for v&1 == 0 { v >>= 1 k++ } @@ -305,7 +367,6 @@ func (p DelayTimeParameter) Hint() ParameterHint { if k <= 6 { text = fmt.Sprintf(" (1/%d)", 1<<(6-k)) } - break case 9: if k <= 5 { text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k)) @@ -322,28 +383,24 @@ func (p DelayTimeParameter) Hint() ParameterHint { } return ParameterHint{text, true} } - -// DelayLinesParameter - -func (p DelayLinesParameter) Name() string { return "delaylines" } -func (p DelayLinesParameter) Type() ParameterType { return IntegerParameter } -func (p DelayLinesParameter) Range() IntRange { return IntRange{Min: 1, Max: 32} } -func (p DelayLinesParameter) LargeStep() int { return 4 } -func (p DelayLinesParameter) Reset() { return } - -func (p DelayLinesParameter) Hint() ParameterHint { - return ParameterHint{strconv.Itoa(p.Value()), true} +func (d *delayTimeParameter) Info(p *Parameter) (string, bool) { + return "", false } +func (d *delayTimeParameter) LargeStep(p *Parameter) int { + return 16 +} +func (d *delayTimeParameter) Reset(p *Parameter) {} -func (p DelayLinesParameter) Value() int { +// delayLinesParameter vtable + +func (d *delayLinesParameter) Value(p *Parameter) int { val := len(p.unit.VarArgs) if p.unit.Parameters["stereo"] == 1 { val /= 2 } return val } - -func (p DelayLinesParameter) SetValue(v int) bool { +func (d *delayLinesParameter) SetValue(p *Parameter, v int) bool { defer p.m.change("DelayLinesParameter", PatchChange, MinorChange)() targetLines := v if p.unit.Parameters["stereo"] == 1 { @@ -355,23 +412,35 @@ func (p DelayLinesParameter) SetValue(v int) bool { p.unit.VarArgs = p.unit.VarArgs[:targetLines] return true } +func (d *delayLinesParameter) Range(p *Parameter) IntRange { + return IntRange{Min: 1, Max: 32} +} +func (d *delayLinesParameter) Type(p *Parameter) ParameterType { + return IntegerParameter +} +func (d *delayLinesParameter) Name(p *Parameter) string { + return "delaylines" +} +func (d *delayLinesParameter) Hint(p *Parameter) ParameterHint { + return ParameterHint{strconv.Itoa(d.Value(p)), true} +} +func (d *delayLinesParameter) Info(p *Parameter) (string, bool) { + return "", false +} +func (d *delayLinesParameter) LargeStep(p *Parameter) int { + return 4 +} +func (d *delayLinesParameter) Reset(p *Parameter) {} -// ReverbParameter +// reverbParameter vtable -func (p ReverbParameter) Name() string { return "reverb" } -func (p ReverbParameter) Type() ParameterType { return IntegerParameter } -func (p ReverbParameter) Range() IntRange { return IntRange{Min: 0, Max: len(reverbs)} } -func (p ReverbParameter) LargeStep() int { return 1 } -func (p ReverbParameter) Reset() { return } - -func (p ReverbParameter) Value() int { +func (r *reverbParameter) Value(p *Parameter) int { i := slices.IndexFunc(reverbs, func(d delayPreset) bool { return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs) }) return i + 1 } - -func (p ReverbParameter) SetValue(v int) bool { +func (r *reverbParameter) SetValue(p *Parameter, v int) bool { if v < 1 || v > len(reverbs) { return false } @@ -383,12 +452,27 @@ func (p ReverbParameter) SetValue(v int) bool { copy(p.unit.VarArgs, entry.varArgs) return true } - -func (p ReverbParameter) Hint() ParameterHint { - i := p.Value() +func (r *reverbParameter) Range(p *Parameter) IntRange { + return IntRange{Min: 0, Max: len(reverbs)} +} +func (r *reverbParameter) Type(p *Parameter) ParameterType { + return IntegerParameter +} +func (r *reverbParameter) Name(p *Parameter) string { + return "reverb" +} +func (r *reverbParameter) Hint(p *Parameter) ParameterHint { + i := r.Value(p) label := "0 / custom" if i > 0 { label = fmt.Sprintf("%v / %v", i, reverbs[i-1].name) } return ParameterHint{label, true} } +func (r *reverbParameter) Info(p *Parameter) (string, bool) { + return "", false +} +func (r *reverbParameter) LargeStep(p *Parameter) int { + return 1 +} +func (r *reverbParameter) Reset(p *Parameter) {}