sointu/tracker/params.go
5684185+vsariola@users.noreply.github.com d92426a100 feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data.

This rewrite closes #72, #106 and #120.
2024-02-17 18:16:06 +02:00

346 lines
8.8 KiB
Go

package tracker
import (
"fmt"
"math"
"slices"
"strconv"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
)
type (
Parameter interface {
IntData
Type() ParameterType
Name() string
Hint() string
LargeStep() int
Reset()
}
parameter struct {
m *Model
unit *sointu.Unit
}
NamedParameter struct {
parameter
up *sointu.UnitParameter
}
DelayTimeParameter struct {
parameter
index int
}
DelayLinesParameter struct{ parameter }
GmDlsEntryParameter struct{ parameter }
ReverbParameter struct{ parameter }
Params Model
ParamYieldFunc func(Parameter)
ParameterType int
)
const (
IntegerParameter ParameterType = iota
BoolParameter
IDParameter
)
// Model methods
func (m *Model) Params() *Params { return (*Params)(m) }
// parameter methods
func (p parameter) change(kind string) func() {
return p.m.change("Parameter."+kind, PatchChange, MinorChange)
}
// ParamList
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 = intMax(intMin(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
pl.Iterate(func(p Parameter) {
count++
})
return count
}
func (pl *Params) SelectedItem() (ret Parameter) {
index := pl.Selected()
pl.Iterate(func(param Parameter) {
if index == 0 {
ret = param
}
index--
})
return
}
func (pl *Params) Iterate(yield ParamYieldFunc) {
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
return
}
if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
return
}
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
unitType, ok := sointu.UnitTypes[unit.Type]
if !ok {
return
}
for i := range unitType {
if !unitType[i].CanSet {
continue
}
if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
break // don't show the sample related params unless necessary
}
yield(NamedParameter{
parameter: parameter{m: (*Model)(pl), unit: unit},
up: &unitType[i],
})
}
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
}
switch {
case unit.Type == "delay":
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
unit.VarArgs = append(unit.VarArgs, 1)
}
yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
for i := range unit.VarArgs {
yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i})
}
}
}
// NamedParameter
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) { p.unit.Parameters[p.up.Name] = value }
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.parameter.change("Reset")()
p.unit.Parameters[p.up.Name] = v
}
func (p NamedParameter) Type() 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 (p NamedParameter) Hint() string {
val := p.Value()
text := p.m.d.Song.Patch.ParamHintString(p.m.d.InstrIndex, p.m.d.UnitIndex, p.up.Name)
if text != "" {
text = fmt.Sprintf("%v / %v", val, text)
} else {
text = strconv.Itoa(val)
}
return text
}
func (p NamedParameter) LargeStep() int {
if p.up.Name == "transpose" {
return 12
}
return 16
}
// GmDlsEntryParameter
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"])}
if v, ok := gmDlsEntryMap[key]; ok {
return v + 1
}
return 0
}
func (p GmDlsEntryParameter) setValue(v int) {
if v < 1 || v > len(GmDlsEntries) {
return
}
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
}
func (p GmDlsEntryParameter) Hint() string {
if v := p.Value(); v > 0 {
return fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name)
}
return "0 / custom"
}
// DelayTimeParameter
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 {
if p.index < 0 || p.index >= len(p.unit.VarArgs) {
return 1
}
return p.unit.VarArgs[p.index]
}
func (p DelayTimeParameter) setValue(v int) {
p.unit.VarArgs[p.index] = v
}
func (p DelayTimeParameter) Range() intRange {
if p.unit.Parameters["notetracking"] == 2 {
return intRange{Min: 1, Max: 576}
}
return intRange{Min: 1, Max: 65535}
}
func (p DelayTimeParameter) Hint() string {
val := p.Value()
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()))
case 1:
relPitch := float64(val) / 10787
semitones := -math.Log2(relPitch) * 12
text = fmt.Sprintf("%v / %.3f st", val, semitones)
case 2:
k := 0
v := val
for v&1 == 0 { // divide val by 2 until it is odd
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))
}
break
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 text
}
// 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() string { return strconv.Itoa(p.Value()) }
func (p DelayLinesParameter) Value() int {
val := len(p.unit.VarArgs)
if p.unit.Parameters["stereo"] == 1 {
val /= 2
}
return val
}
func (p DelayLinesParameter) setValue(v int) {
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]
}
// ReverbParameter
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 {
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) {
if v < 1 || v > len(reverbs) {
return
}
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)
}
func (p ReverbParameter) Hint() string {
i := p.Value()
if i > 0 {
return fmt.Sprintf("%v / %v", i, reverbs[i-1].name)
}
return "0 / custom"
}