This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-26 19:26:52 +03:00
parent 083ccc58b5
commit 0182d004b1
4 changed files with 298 additions and 165 deletions

View File

@ -5,6 +5,7 @@ import (
"image/color"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
@ -84,11 +85,16 @@ type Theme struct {
}
}
UnitEditor struct {
Hint LabelStyle
Chooser LabelStyle
ParameterName LabelStyle
InvalidParam color.NRGBA
SendTarget color.NRGBA
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
}
Cursor CursorStyle
Selection CursorStyle
@ -102,6 +108,7 @@ type Theme struct {
}
Split SplitStyle
ScrollBar ScrollBarStyle
Knob KnobStyle
// iconCache is used to cache the icons created from iconvg data
iconCache map[*byte]*widget.Icon

View File

@ -178,12 +178,6 @@ instrumenteditor:
disabled: { textsize: 12, color: *disabled }
warning: *warningcolor
error: *errorcolor
uniteditor:
hint: { textsize: 16, color: *highemphasis, shadowcolor: *black }
chooser: { textsize: 12, color: *white, shadowcolor: *black }
parametername: { textsize: 16, color: *white, shadowcolor: *black }
invalidparam: { r: 120, g: 120, b: 120, a: 190 }
sendtarget: { r: 120, g: 120, b: 210, a: 255 }
cursor:
active: { r: 100, g: 140, b: 255, a: 48 }
activealt: { r: 255, g: 100, b: 140, a: 48 }
@ -211,3 +205,21 @@ dialog:
textinset: { top: 12, bottom: 12, left: 20, right: 20 }
buttons: *textbutton
split: { bar: 10, minsize1: 180, minsize2: 180 }
uniteditor:
hint: { textsize: 16, color: *highemphasis, shadowcolor: *black }
chooser: { textsize: 12, color: *white, shadowcolor: *black }
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 }
width: 60
height: 60
rowtitlewidth: 16
columntitleheight: 16
rowtitle: { textsize: 12, color: *white, alignment: 2 }
knob:
diameter: 36
strokewidth: 4
color: *primarycolor
trackcolor: { r: 0, g: 0, b: 0, a: 255 }
value: { textsize: 12, color: *highemphasis }

View File

@ -4,10 +4,13 @@ import (
"bytes"
"fmt"
"image"
"image/color"
"io"
"math"
"strconv"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
@ -17,9 +20,9 @@ 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/component"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"golang.org/x/exp/shiny/materialdesign/icons"
@ -27,24 +30,52 @@ import (
"golang.org/x/text/language"
)
type UnitEditor struct {
paramTable *ScrollTable
searchList *DragList
Parameters [][]*ParameterWidget
DeleteUnitBtn *Clickable
CopyUnitBtn *Clickable
ClearUnitBtn *Clickable
DisableUnitBtn *Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
type (
UnitEditor struct {
paramTable *ScrollTable
searchList *DragList
Parameters [][]*ParameterWidget
DeleteUnitBtn *Clickable
CopyUnitBtn *Clickable
ClearUnitBtn *Clickable
DisableUnitBtn *Clickable
SelectTypeBtn *Clickable
commentEditor *Editor
caser cases.Caser
copyHint string
disableUnitHint string
enableUnitHint string
copyHint string
disableUnitHint string
enableUnitHint string
searching tracker.Bool
}
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
Color color.NRGBA
TrackColor color.NRGBA
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 {
ret := &UnitEditor{
@ -157,25 +188,28 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
for len(pe.Parameters) < pe.paramTable.Table.Height() {
pe.Parameters = append(pe.Parameters, make([]*ParameterWidget, 0))
}
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)
for i := range pe.Parameters {
for len(pe.Parameters[i]) < width {
pe.Parameters[i] = append(pe.Parameters[i], &ParameterWidget{})
}
}
coltitle := func(gtx C, x int) D {
return D{Size: image.Pt(gtx.Dp(100), gtx.Dp(10))}
return D{Size: image.Pt(cellWidth, columnTitleHeight)}
}
rowtitle := func(gtx C, y int) D {
h := gtx.Dp(100)
//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(h)})).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
Label(t.Theme, &t.Theme.OrderEditor.TrackTitle, t.Units().Item(y).Type).Layout(gtx)
return D{Size: image.Pt(gtx.Dp(10), h)}
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)
return D{Size: image.Pt(rowTitleWidth, cellHeight)}
}
cursor := t.Model.Params().Cursor()
cell := func(gtx C, x, y int) D {
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(100), gtx.Dp(100)))
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]) {
return D{}
@ -192,14 +226,14 @@ func (pe *UnitEditor) layoutSliders(gtx C) D {
pe.Parameters[y][x].Parameter = param
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[y][x])
paramStyle.Focus = pe.paramTable.Table.Cursor() == tracker.Point{X: x, Y: y}
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
}
table := FilledScrollTable(t.Theme, pe.paramTable)
table.RowTitleWidth = 10
table.ColumnTitleHeight = 10
table.CellWidth = 100
table.CellHeight = 100
table.RowTitleWidth = t.Theme.UnitEditor.RowTitleWidth
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)
}
@ -268,14 +302,14 @@ func (t *UnitEditor) Tags(level int, yield TagYieldFunc) bool {
}
type ParameterWidget struct {
floatWidget widget.Float
boolWidget widget.Bool
instrBtn Clickable
instrMenu MenuState
unitBtn Clickable
unitMenu MenuState
Parameter tracker.Parameter
tipArea TipArea
knobState KnobState
boolWidget widget.Bool
instrBtn Clickable
instrMenu MenuState
unitBtn Clickable
unitMenu MenuState
Parameter tracker.Parameter
tipArea TipArea
}
type ParameterStyle struct {
@ -302,120 +336,89 @@ func (t *Tracker) ParamStyle(th *Theme, paramWidget *ParameterWidget) ParameterS
}
func (p ParameterStyle) Layout(gtx C) D {
info, infoOk := p.w.Parameter.Info()
title := Label(p.Theme, &p.Theme.UnitEditor.ParameterName, p.w.Parameter.Name())
title.Alignment = text.Middle
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(title.Layout),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(100)
gtx.Constraints.Min.Y = gtx.Dp(50)
switch p.w.Parameter.Type() {
case tracker.IntegerParameter:
for p.Focus {
e, ok := gtx.Event(pointer.Filter{
Target: &p.w.floatWidget,
Kinds: pointer.Scroll,
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
})
if !ok {
break
//_, _ := p.w.Parameter.Info()
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.w.Parameter.Name())
widget := func(gtx C) D {
switch p.w.Parameter.Type() {
case tracker.IntegerParameter:
k := Knob(p.w.Parameter, p.Theme, &p.w.knobState, p.w.Parameter.Hint().Label, p.Focus)
return k.Layout(gtx)
case tracker.BoolParameter:
ra := p.w.Parameter.Range()
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
boolStyle := material.Switch(&p.Theme.Material, &p.w.boolWidget, "Toggle boolean parameter")
boolStyle.Color.Disabled = p.Theme.Material.Fg
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := layout.Center.Layout(gtx, boolStyle.Layout)
if p.w.boolWidget.Value {
p.w.Parameter.SetValue(ra.Max)
} else {
p.w.Parameter.SetValue(ra.Min)
}
return dims
case tracker.IDParameter:
instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
for i := range instrItems {
i := i
name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name
instrItems[i].Icon = icons.NavigationChevronRight
instrItems[i].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
if id, ok := p.tracker.Instruments().FirstID(i); ok {
p.w.Parameter.SetValue(id)
}
if ev, ok := e.(pointer.Event); ok && ev.Kind == pointer.Scroll {
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
p.w.Parameter.SetValue(p.w.Parameter.Value() - int(delta))
}
}
ra := p.w.Parameter.Range()
if !p.w.floatWidget.Dragging() {
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 infoOk {
sliderStyle.Color = p.Theme.UnitEditor.SendTarget
}
r := image.Rectangle{Max: gtx.Constraints.Min}
defer clip.Rect(r).Push(gtx.Ops).Pop()
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
if p.Focus {
event.Op(gtx.Ops, &p.w.floatWidget)
}
dims := sliderStyle.Layout(gtx)
p.w.Parameter.SetValue(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5))
return dims
case tracker.BoolParameter:
ra := p.w.Parameter.Range()
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
boolStyle := material.Switch(&p.Theme.Material, &p.w.boolWidget, "Toggle boolean parameter")
boolStyle.Color.Disabled = p.Theme.Material.Fg
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := layout.Center.Layout(gtx, boolStyle.Layout)
if p.w.boolWidget.Value {
p.w.Parameter.SetValue(ra.Max)
} else {
p.w.Parameter.SetValue(ra.Min)
}
return dims
case tracker.IDParameter:
instrItems := make([]ActionMenuItem, p.tracker.Instruments().Count())
for i := range instrItems {
i := i
name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name
instrItems[i].Icon = icons.NavigationChevronRight
instrItems[i].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
if id, ok := p.tracker.Instruments().FirstID(i); ok {
p.w.Parameter.SetValue(id)
}
}))
}
var unitItems []ActionMenuItem
instrName := "<instr>"
unitName := "<unit>"
targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value())
if ok {
instrName = targetInstrName
unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex])
unitItems = make([]ActionMenuItem, len(units))
for j, unit := range units {
id := unit.ID
unitItems[j].Text = buildUnitLabel(j, unit)
unitItems[j].Icon = icons.NavigationChevronRight
unitItems[j].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
p.w.Parameter.SetValue(id)
}))
}
var unitItems []ActionMenuItem
instrName := "<instr>"
unitName := "<unit>"
targetInstrName, units, targetUnitIndex, ok := p.tracker.UnitInfo(p.w.Parameter.Value())
if ok {
instrName = targetInstrName
unitName = buildUnitLabel(targetUnitIndex, units[targetUnitIndex])
unitItems = make([]ActionMenuItem, len(units))
for j, unit := range units {
id := unit.ID
unitItems[j].Text = buildUnitLabel(j, unit)
unitItems[j].Icon = icons.NavigationChevronRight
unitItems[j].Action = tracker.MakeEnabledAction((tracker.DoFunc)(func() {
p.w.Parameter.SetValue(id)
}))
}
}
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
instrBtn := MenuBtn(p.tracker.Theme, &p.w.instrMenu, &p.w.instrBtn, instrName)
unitBtn := MenuBtn(p.tracker.Theme, &p.w.unitMenu, &p.w.unitBtn, unitName)
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return instrBtn.Layout(gtx, instrItems...)
}),
layout.Rigid(func(gtx C) D {
return unitBtn.Layout(gtx, unitItems...)
}),
)
}
return D{}
}),
layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
hint := p.w.Parameter.Hint()
label := Label(p.tracker.Theme, &p.tracker.Theme.UnitEditor.Hint, hint.Label)
label.Alignment = text.Middle
if !hint.Valid {
label.Color = p.tracker.Theme.UnitEditor.InvalidParam
}
if info == "" {
return label.Layout(gtx)
}
tooltip := component.PlatformTooltip(p.SendTargetTheme, info)
return p.w.tipArea.Layout(gtx, tooltip, label.Layout)
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
instrBtn := MenuBtn(p.tracker.Theme, &p.w.instrMenu, &p.w.instrBtn, instrName)
unitBtn := MenuBtn(p.tracker.Theme, &p.w.unitMenu, &p.w.unitBtn, unitName)
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return instrBtn.Layout(gtx, instrItems...)
}),
layout.Rigid(func(gtx C) D {
return unitBtn.Layout(gtx, unitItems...)
}),
)
}
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) }),
/* layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
hint := p.w.Parameter.Hint()
label := Label(p.tracker.Theme, &p.tracker.Theme.UnitEditor.Hint, hint.Label)
label.Alignment = text.Middle
if !hint.Valid {
label.Color = p.tracker.Theme.UnitEditor.InvalidParam
}
return D{}
}),
if info == "" {
return label.Layout(gtx)
}
tooltip := component.PlatformTooltip(p.SendTargetTheme, info)
return p.w.tipArea.Layout(gtx, tooltip, label.Layout)
}
return D{}
}),*/
)
}
@ -426,3 +429,113 @@ 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)
strokeKnobArc(gtx, k.Style.TrackColor, sw, d, 1)
strokeKnobArc(gtx, k.Style.Color, sw, d, 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 strokeKnobArc(gtx C, color color.NRGBA, strokeWidth, diameter int, amount float32) {
var path clip.Path
rad := float32(diameter) / 2
amount = min(max(amount, 0), 1)
if amount <= 0 {
return
}
angle := amount * 8 * math.Pi / 5
center := f32.Point{X: rad, Y: rad}
r2 := rad - float32(strokeWidth)/2
start := f32.Point{X: rad - r2*float32(math.Sin(math.Pi/5)), Y: rad + r2*float32(math.Cos(math.Pi/5))}
path.Begin(gtx.Ops)
path.MoveTo(start)
path.ArcTo(center, center, angle)
paint.FillShape(gtx.Ops, color,
clip.Stroke{
Path: path.End(),
Width: float32(strokeWidth),
}.Op())
}

View File

@ -143,6 +143,7 @@ func (pt *Params) Cursor2() Point { return pt.Cursor() }
func (pt *Params) SetCursor(p Point) {
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
pt.d.UnitIndex = max(min(p.Y, pt.Height()-1), 0)
pt.d.UnitIndex2 = pt.d.UnitIndex
}
func (pt *Params) SetCursor2(p Point) {}
func (pt *Params) Width() int {
@ -227,7 +228,7 @@ func (n *namedParameter) Hint(p *Parameter) ParameterHint {
label := strconv.Itoa(val)
if p.up.DisplayFunc != nil {
valueInUnits, units := p.up.DisplayFunc(val)
label = fmt.Sprintf("%d / %s %s", val, valueInUnits, units)
label = fmt.Sprintf("%s %s", valueInUnits, units)
}
if p.unit.Type == "send" {
instrIndex, targetType, ok := p.m.UnitHintInfo(p.unit.Parameters["target"])
@ -305,9 +306,9 @@ func (g *gmDlsEntryParameter) Name(p *Parameter) string {
return "sample"
}
func (g *gmDlsEntryParameter) Hint(p *Parameter) ParameterHint {
label := "0 / custom"
label := "custom"
if v := g.Value(p); v > 0 {
label = fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name)
label = GmDlsEntries[v-1].Name
}
return ParameterHint{label, true}
}
@ -350,11 +351,11 @@ func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint {
switch p.unit.Parameters["notetracking"] {
default:
case 0:
text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(p.m.d.Song.SamplesPerRow()))
text = fmt.Sprintf("%.3f rows", float32(val)/float32(p.m.d.Song.SamplesPerRow()))
case 1:
relPitch := float64(val) / 10787
semitones := -math.Log2(relPitch) * 12
text = fmt.Sprintf("%v / %.3f st", val, semitones)
text = fmt.Sprintf("%.3f st", semitones)
case 2:
k := 0
v := val
@ -467,9 +468,9 @@ func (r *reverbParameter) Name(p *Parameter) string {
}
func (r *reverbParameter) Hint(p *Parameter) ParameterHint {
i := r.Value(p)
label := "0 / custom"
label := "custom"
if i > 0 {
label = fmt.Sprintf("%v / %v", i, reverbs[i-1].name)
label = reverbs[i-1].name
}
return ParameterHint{label, true}
}