Files
sointu/tracker/params.go
5684185+vsariola@users.noreply.github.com 0182d004b1 drafting
2025-06-26 19:26:52 +03:00

484 lines
12 KiB
Go

package tracker
import (
"fmt"
"math"
"slices"
"strconv"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
)
type (
// 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
}
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)
}
Params Model
ParamVertList 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
ParameterType int
ParameterHint struct {
Label string
Valid bool
}
)
const (
NoParameter ParameterType = iota
IntegerParameter
BoolParameter
IDParameter
)
// Parameter methods
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 NoParameter
}
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)
}
//
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 (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 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 {
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.d.Song.Patch) {
return 0
}
ret := 0
for _, unit := range pt.d.Song.Patch[pt.d.InstrIndex].Units {
ret = max(ret, len(pt.derived.forUnit[unit.ID].params))
}
return ret
}
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.d.Song.Patch) ||
p.Y < 0 || p.Y >= len(pt.d.Song.Patch[pt.d.InstrIndex].Units) {
return Parameter{}
}
id := pt.d.Song.Patch[pt.d.InstrIndex].Units[p.Y].ID
if p.X < 0 || p.X >= len(pt.derived.forUnit[id].params) {
return Parameter{}
}
return pt.derived.forUnit[id].params[p.X]
}
func (pt *Params) clear(p Point) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) set(p Point, value int) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) add(rect Rect, delta int) (ok bool) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) marshal(rect Rect) (data []byte, ok bool) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) unmarshalAtCursor(data []byte) (ok bool) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) unmarshalRange(rect Rect, data []byte) (ok bool) {
panic("NOT IMPLEMENTED")
}
func (pt *Params) change(kind string, severity ChangeSeverity) func() {
panic("NOT IMPLEMENTED")
}
func (pt *Params) cancel() {
panic("NOT IMPLEMENTED")
}
// namedParameter vtable
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 (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.unit.Type == "send" && p.up.Name == "target" {
return IDParameter
}
if p.up.MinValue == 0 && p.up.MaxValue == 1 {
return BoolParameter
}
return IntegerParameter
}
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 {
valueInUnits, units := p.up.DisplayFunc(val)
label = fmt.Sprintf("%s %s", 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]
}
}
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 {
if p.up.Name == "transpose" {
return 12
}
return 16
}
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 vtable
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 (g *gmDlsEntryParameter) SetValue(p *Parameter, v int) bool {
if v < 1 || v > len(GmDlsEntries) {
return false
}
defer p.m.change("GmDlsEntryParameter", PatchChange, MinorChange)()
e := GmDlsEntries[v-1]
p.unit.Parameters["samplestart"] = e.Start
p.unit.Parameters["loopstart"] = e.LoopStart
p.unit.Parameters["looplength"] = e.LoopLength
p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
return true
}
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 := "custom"
if v := g.Value(p); v > 0 {
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) Reset(p *Parameter) {}
// delayTimeParameter vtable
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 (d *delayTimeParameter) SetValue(p *Parameter, v int) bool {
defer p.m.change("DelayTimeParameter", PatchChange, MinorChange)()
p.unit.VarArgs[p.index] = v
return true
}
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 (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("%.3f rows", float32(val)/float32(p.m.d.Song.SamplesPerRow()))
case 1:
relPitch := float64(val) / 10787
semitones := -math.Log2(relPitch) * 12
text = fmt.Sprintf("%.3f st", semitones)
case 2:
k := 0
v := val
for v&1 == 0 {
v >>= 1
k++
}
switch v {
case 1:
if k <= 7 {
text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k))
}
case 3:
if k <= 6 {
text = fmt.Sprintf(" (1/%d)", 1<<(6-k))
}
case 9:
if k <= 5 {
text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
}
}
text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
}
if p.unit.Parameters["stereo"] == 1 {
if p.index < len(p.unit.VarArgs)/2 {
text += " R"
} else {
text += " L"
}
}
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) Reset(p *Parameter) {}
// 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 (d *delayLinesParameter) SetValue(p *Parameter, v int) bool {
defer p.m.change("DelayLinesParameter", PatchChange, MinorChange)()
targetLines := v
if p.unit.Parameters["stereo"] == 1 {
targetLines *= 2
}
for len(p.unit.VarArgs) < targetLines {
p.unit.VarArgs = append(p.unit.VarArgs, 1)
}
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 vtable
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 (r *reverbParameter) SetValue(p *Parameter, v int) bool {
if v < 1 || v > len(reverbs) {
return false
}
defer p.m.change("ReverbParameter", PatchChange, MinorChange)()
entry := reverbs[v-1]
p.unit.Parameters["stereo"] = entry.stereo
p.unit.Parameters["notetracking"] = 0
p.unit.VarArgs = make([]int, len(entry.varArgs))
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) Hint(p *Parameter) ParameterHint {
i := r.Value(p)
label := "custom"
if i > 0 {
label = 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) {}