feat!: display the parameters as knobs in a grid

Also removed the negbandpass & neghighpass parameters
and replaced them with bandpass & highpass set to -1, to
fit the switches better to the GUI.

Closes #51, closes #173
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-26 10:38:38 +03:00
parent c3caa8de11
commit 666af9433e
116 changed files with 3663 additions and 2049 deletions

View File

@ -8,6 +8,7 @@ import (
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
"gopkg.in/yaml.v3"
)
type (
@ -20,6 +21,7 @@ type (
up *sointu.UnitParameter
index int
vtable parameterVtable
port int
}
parameterVtable interface {
@ -29,12 +31,13 @@ type (
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)
RoundToGrid(*Parameter, int, bool) int
}
Params Model
Params Model
ParamVertList Model
// different parameter vtables to handle different types of parameters.
// Casting struct{} to interface does not cause allocations.
namedParameter struct{}
@ -54,7 +57,8 @@ type (
)
const (
IntegerParameter ParameterType = iota
NoParameter ParameterType = iota
IntegerParameter
BoolParameter
IDParameter
)
@ -67,6 +71,12 @@ func (p *Parameter) Value() int {
}
return p.vtable.Value(p)
}
func (p *Parameter) Port() (int, bool) {
if p.port <= 0 {
return 0, false
}
return p.port - 1, true
}
func (p *Parameter) SetValue(value int) bool {
if p.vtable == nil {
return false
@ -78,15 +88,35 @@ func (p *Parameter) SetValue(value int) bool {
}
return p.vtable.SetValue(p, value)
}
func (p *Parameter) Add(delta int, snapToGrid bool) bool {
if p.vtable == nil {
return false
}
newVal := p.Value() + delta
if snapToGrid && p.vtable != nil {
newVal = p.vtable.RoundToGrid(p, newVal, delta > 0)
}
return p.SetValue(newVal)
}
func (p *Parameter) Range() IntRange {
if p.vtable == nil {
return IntRange{}
}
return p.vtable.Range(p)
}
func (p *Parameter) Neutral() int {
if p.vtable == nil {
return 0
}
if p.up != nil {
return p.up.Neutral
}
return 0
}
func (p *Parameter) Type() ParameterType {
if p.vtable == nil {
return IntegerParameter
return NoParameter
}
return p.vtable.Type(p)
}
@ -102,97 +132,172 @@ func (p *Parameter) Hint() 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)
}
func (p *Parameter) UnitID() int {
if p.unit == nil {
return 0
}
return p.unit.ID
}
//
func (m *Model) ParamVertList() *ParamVertList { return (*ParamVertList)(m) }
func (pt *ParamVertList) List() List { return List{pt} }
func (pt *ParamVertList) Selected() int { return pt.d.ParamIndex }
func (pt *ParamVertList) Selected2() int { return pt.d.ParamIndex }
func (pt *ParamVertList) SetSelected(index int) { pt.d.ParamIndex = index }
func (pt *ParamVertList) SetSelected2(index int) {}
func (pt *ParamVertList) Count() int { return (*Params)(pt).Width() }
// 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) Count() int {
count := 0
for range pl.Iterate {
count++
}
return count
func (m *Model) Params() *Params { return (*Params)(m) }
func (pt *Params) Table() Table { return Table{pt} }
func (pt *Params) Cursor() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex} }
func (pt *Params) Cursor2() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex2} }
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)
}
func (pl *Params) SelectedItem() (ret Parameter) {
index := pl.Selected()
for param := range pl.Iterate {
if index == 0 {
ret = param
}
index--
}
return
func (pt *Params) SetCursor2(p Point) {
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
pt.d.UnitIndex2 = max(min(p.Y, pt.Height()-1), 0)
}
func (pl *Params) Iterate(yield ParamYieldFunc) {
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
return
func (pt *Params) Width() int {
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) {
return 0
}
if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
return
// TODO: we hack the +1 so that we always have one extra cell to draw the
// comments. Refactor the gioui side so that we can specify the width and
// height regardless of the underlying table size
return pt.derived.patch[pt.d.InstrIndex].paramsWidth + 1
}
func (pt *Params) RowWidth(y int) int {
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || y < 0 || y >= len(pt.derived.patch[pt.d.InstrIndex].params) {
return 0
}
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
unitType, ok := sointu.UnitTypes[unit.Type]
if !ok {
return
return len(pt.derived.patch[pt.d.InstrIndex].params[y])
}
func (pt *Params) Height() int { return (*Model)(pt).Units().Count() }
func (pt *Params) MoveCursor(dx, dy int) (ok bool) {
p := pt.Cursor()
p.X += dx
p.Y += dy
pt.SetCursor(p)
return p == pt.Cursor()
}
func (pt *Params) Item(p Point) Parameter {
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || p.Y < 0 || p.Y >= len(pt.derived.patch[pt.d.InstrIndex].params) || p.X < 0 || p.X >= len(pt.derived.patch[pt.d.InstrIndex].params[p.Y]) {
return Parameter{}
}
for i, up := range unitType {
if !up.CanSet {
continue
}
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(Parameter{m: (*Model)(pl), unit: unit, up: &unitType[i], vtable: &namedParameter{}}) {
return
}
}
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &gmDlsEntryParameter{}}) {
return
}
}
if unit.Type == "delay" {
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
unit.VarArgs = append(unit.VarArgs, 1)
}
if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &reverbParameter{}}) {
return
}
if !yield(Parameter{m: (*Model)(pl), unit: unit, vtable: &delayLinesParameter{}}) {
return
}
for i := range unit.VarArgs {
if !yield(Parameter{m: (*Model)(pl), unit: unit, index: i, vtable: &delayTimeParameter{}}) {
return
return pt.derived.patch[pt.d.InstrIndex].params[p.Y][p.X]
}
func (pt *Params) clear(p Point) {
q := pt.Item(p)
q.Reset()
}
func (pt *Params) set(p Point, value int) {
q := pt.Item(p)
q.SetValue(value)
}
func (pt *Params) add(rect Rect, delta int, largeStep bool) (ok bool) {
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
p := Point{x, y}
q := pt.Item(p)
if !q.Add(delta, largeStep) {
return false
}
}
}
return true
}
type paramsTable struct {
Params [][]int `yaml:",flow"`
}
func (pt *Params) marshal(rect Rect) (data []byte, ok bool) {
width := rect.BottomRight.X - rect.TopLeft.X + 1
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
var table = paramsTable{Params: make([][]int, 0, width)}
for x := 0; x < width; x++ {
table.Params = append(table.Params, make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
for y := 0; y < height; y++ {
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
table.Params[x] = append(table.Params[x], p.Value())
}
}
ret, err := yaml.Marshal(table)
if err != nil {
return nil, false
}
return ret, true
}
func (pt *Params) unmarshal(data []byte) (paramsTable, bool) {
var table paramsTable
yaml.Unmarshal(data, &table)
if len(table.Params) == 0 {
return paramsTable{}, false
}
for i := 0; i < len(table.Params); i++ {
if len(table.Params[i]) > 0 {
return table, true
}
}
return paramsTable{}, false
}
func (pt *Params) unmarshalAtCursor(data []byte) (ret bool) {
table, ok := pt.unmarshal(data)
if !ok {
return false
}
for i := 0; i < len(table.Params); i++ {
for j, q := range table.Params[i] {
x := i + pt.Cursor().X
y := j + pt.Cursor().Y
p := pt.Item(Point{x, y})
ret = p.SetValue(q) || ret
}
}
return ret
}
func (pt *Params) unmarshalRange(rect Rect, data []byte) (ret bool) {
table, ok := pt.unmarshal(data)
if !ok {
return false
}
if len(table.Params) == 0 || len(table.Params[0]) == 0 {
return false
}
width := rect.BottomRight.X - rect.TopLeft.X + 1
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
if len(table.Params) < width {
return false
}
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
if len(table.Params[0]) < height {
return false
}
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
ret = p.SetValue(table.Params[x][y]) || ret
}
}
return ret
}
func (pt *Params) change(kind string, severity ChangeSeverity) func() {
return (*Model)(pt).change(kind, PatchChange, severity)
}
func (pt *Params) cancel() {
pt.changeCancel = true
}
// namedParameter vtable
@ -207,15 +312,21 @@ func (n *namedParameter) Range(p *Parameter) IntRange {
return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue}
}
func (n *namedParameter) Type(p *Parameter) ParameterType {
if p.up == nil || !p.up.CanSet {
return NoParameter
}
if p.unit.Type == "send" && p.up.Name == "target" {
return IDParameter
}
if p.up.MinValue == 0 && p.up.MaxValue == 1 {
if p.up.MinValue >= -1 && p.up.MaxValue <= 1 {
return BoolParameter
}
return IntegerParameter
}
func (n *namedParameter) Name(p *Parameter) string {
if p.up.Name == "notetracking" {
return "tracking" // notetracking does not fit in the UI
}
return p.up.Name
}
func (n *namedParameter) Hint(p *Parameter) ParameterHint {
@ -223,39 +334,15 @@ 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)
}
if p.unit.Type == "send" {
instrIndex, targetType, ok := p.m.UnitHintInfo(p.unit.Parameters["target"])
if p.up.Name == "voice" && val == 0 {
if ok && instrIndex != p.m.d.InstrIndex {
label = "all"
} else {
label = "self"
}
}
if p.up.Name == "port" {
if !ok {
return ParameterHint{label, false}
}
portList := sointu.Ports[targetType]
if val < 0 || val >= len(portList) {
return ParameterHint{label, false}
}
label = portList[val]
}
label = fmt.Sprintf("%s %s", valueInUnits, units)
}
return ParameterHint{label, true}
}
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 {
func (n *namedParameter) RoundToGrid(p *Parameter, val int, up bool) int {
if p.up.Name == "transpose" {
return 12
return roundToGrid(val-64, 12, up) + 64
}
return 16
return roundToGrid(val, 16, up)
}
func (n *namedParameter) Reset(p *Parameter) {
v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name]
@ -301,22 +388,36 @@ 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}
}
func (g *gmDlsEntryParameter) Info(p *Parameter) (string, bool) {
return "", false
}
func (g *gmDlsEntryParameter) LargeStep(p *Parameter) int {
return 16
func (g *gmDlsEntryParameter) RoundToGrid(p *Parameter, val int, up bool) int {
return roundToGrid(val, 16, up)
}
func (g *gmDlsEntryParameter) Reset(p *Parameter) {}
// delayTimeParameter vtable
var delayNoteTrackGrid, delayBpmTrackGrid []int
func init() {
for st := -30; st <= 30; st++ {
gridVal := int(math.Exp2(float64(st)/12)*10787 + 0.5)
delayNoteTrackGrid = append(delayNoteTrackGrid, gridVal)
}
for i := 0; i < 16; i++ {
delayBpmTrackGrid = append(delayBpmTrackGrid, 1<<i)
delayBpmTrackGrid = append(delayBpmTrackGrid, 3<<i)
delayBpmTrackGrid = append(delayBpmTrackGrid, 9<<i)
}
slices.Sort(delayBpmTrackGrid)
}
func (d *delayTimeParameter) Type(p *Parameter) ParameterType { return IntegerParameter }
func (d *delayTimeParameter) Name(p *Parameter) string { return "delaytime" }
func (d *delayTimeParameter) Value(p *Parameter) int {
if p.index < 0 || p.index >= len(p.unit.VarArgs) {
return 1
@ -334,23 +435,17 @@ func (d *delayTimeParameter) Range(p *Parameter) IntRange {
}
return IntRange{Min: 1, Max: 65535}
}
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:
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
@ -372,7 +467,7 @@ func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint {
text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
}
}
text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
text = fmt.Sprintf("%.3f beats%s", float32(val)/48.0, text)
}
if p.unit.Parameters["stereo"] == 1 {
if p.index < len(p.unit.VarArgs)/2 {
@ -383,11 +478,15 @@ func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint {
}
return ParameterHint{text, true}
}
func (d *delayTimeParameter) Info(p *Parameter) (string, bool) {
return "", false
}
func (d *delayTimeParameter) LargeStep(p *Parameter) int {
return 16
func (d *delayTimeParameter) RoundToGrid(p *Parameter, val int, up bool) int {
switch p.unit.Parameters["notetracking"] {
default:
return roundToGrid(val, 16, up)
case 1:
return roundToSliceGrid(val, delayNoteTrackGrid, up)
case 2:
return roundToSliceGrid(val, delayBpmTrackGrid, up)
}
}
func (d *delayTimeParameter) Reset(p *Parameter) {}
@ -412,21 +511,13 @@ func (d *delayLinesParameter) SetValue(p *Parameter, 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) 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 (r *delayLinesParameter) RoundToGrid(p *Parameter, val int, up bool) int { return val }
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
}
@ -452,27 +543,51 @@ func (r *reverbParameter) SetValue(p *Parameter, v int) bool {
copy(p.unit.VarArgs, entry.varArgs)
return true
}
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) 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) RoundToGrid(p *Parameter, val int, up bool) int { return val }
func (r *reverbParameter) Reset(p *Parameter) {}
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}
}
func (r *reverbParameter) Info(p *Parameter) (string, bool) {
return "", false
func roundToGrid(value, grid int, up bool) int {
if up {
return value + mod(-value, grid)
}
return value - mod(value, grid)
}
func (r *reverbParameter) LargeStep(p *Parameter) int {
return 1
func mod(a, b int) int {
m := a % b
if a < 0 && b < 0 {
m -= b
}
if a < 0 && b > 0 {
m += b
}
return m
}
func roundToSliceGrid(value int, grid []int, up bool) int {
if up {
for _, v := range grid {
if value < v {
return v
}
}
} else {
for i := len(grid) - 1; i >= 0; i-- {
if value > grid[i] {
return grid[i]
}
}
}
return value
}
func (r *reverbParameter) Reset(p *Parameter) {}