4 Commits

10 changed files with 327 additions and 194 deletions

View File

@ -28,6 +28,14 @@ func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
func (NullContext) Params() (ret tracker.ExtValueArray, ok bool) {
return tracker.ExtValueArray{}, false
}
func (NullContext) SetParams(params tracker.ExtParamArray) bool {
return false
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

View File

@ -9,6 +9,7 @@ import (
"math"
"os"
"path/filepath"
"strconv"
"time"
"github.com/vsariola/sointu"
@ -22,6 +23,7 @@ type VSTIProcessContext struct {
events []vst2.MIDIEvent
eventIndex int
host vst2.Host
parameters []*vst2.Parameter
}
func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
@ -52,6 +54,45 @@ func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {
return timeInfo.Tempo, true
}
func (c *VSTIProcessContext) Params() (ret tracker.ExtValueArray, ok bool) {
for i, p := range c.parameters {
ret[i] = p.Value
}
return ret, true
}
func (c *VSTIProcessContext) SetParams(a tracker.ExtParamArray) bool {
changed := false
for i, p := range c.parameters {
i := i
name := a[i].Param.Name
if name == "" {
name = fmt.Sprintf("P%d", i)
}
if p.Value != a[i].Val || p.Name != name {
p.Value = a[i].Val
p.Name = name
p.GetValueFunc = func(value float32) float32 {
return float32(a[i].Param.MinValue) + value*float32(a[i].Param.MaxValue-a[i].Param.MinValue)
}
p.GetValueLabelFunc = func(v float32) string {
if f := a[i].Param.DisplayFunc; f != nil {
s, u := a[i].Param.DisplayFunc(int(v + .5))
c.parameters[i].Unit = u
return s
}
c.parameters[i].Unit = ""
return strconv.Itoa(int(v + .5))
}
changed = true
}
}
if changed {
c.host.UpdateDisplay()
}
return true
}
func init() {
var (
version = int32(100)
@ -74,7 +115,15 @@ func init() {
})
}
go t.Main()
context := VSTIProcessContext{host: h}
parameters := make([]*vst2.Parameter, 0, tracker.ExtParamCount)
for i := 0; i < tracker.ExtParamCount; i++ {
parameters = append(parameters,
&vst2.Parameter{
Name: fmt.Sprintf("P%d", i),
NotAutomated: true,
})
}
context := VSTIProcessContext{host: h, parameters: parameters}
buf := make(sointu.AudioBuffer, 1024)
return vst2.Plugin{
UniqueID: PLUGIN_ID,
@ -85,6 +134,7 @@ func init() {
Vendor: "vsariola/sointu",
Category: vst2.PluginCategorySynth,
Flags: vst2.PluginIsSynth,
Parameters: parameters,
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
left := out.Channel(0)
right := out.Channel(1)

2
go.mod
View File

@ -2,6 +2,8 @@ module github.com/vsariola/sointu
go 1.21
replace pipelined.dev/audio/vst2 => ../vst2
require (
gioui.org v0.5.0
gioui.org/x v0.5.0

233
patch.go
View File

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"sort"
"strconv"
)
type (
@ -57,7 +58,10 @@ type (
MaxValue int // maximum value of the parameter, inclusive
CanSet bool // if this parameter can be set before hand i.e. through the gui
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
DisplayFunc UnitParameterDisplayFunc
}
UnitParameterDisplayFunc func(int) (value string, unit string)
)
// UnitTypes documents all the available unit types and if they support stereo variant
@ -79,7 +83,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"crush": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(24 * float64(v) / 128), "bits" }}},
"gain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
@ -88,7 +92,7 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"dbgain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "decibels", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "decibels", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(40 * (float64(v)/64 - 1)), "dB" }}},
"filter": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
@ -108,15 +112,15 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false},
{Name: "notetracking", MinValue: 0, MaxValue: 2, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(noteTrackingNames[:])},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(1 - float64(v)/128), "" }}},
"speed": []UnitParameter{},
"out": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -128,20 +132,20 @@ var UnitTypes = map[string]([]UnitParameter){
"aux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(channelNames[:])}},
"send": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v)/64 - 1), "" }},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "target", MinValue: 0, MaxValue: math.MaxInt32, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"envelope": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"noise": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -149,14 +153,14 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"oscillator": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: oscillatorTransposeDispFunc},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v-64) / 64), "st" }},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "frequency", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(oscTypes[:])},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
{Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false},
@ -164,17 +168,59 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v)/64 - 1), "" }}},
"receive": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"in": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false, DisplayFunc: arrDispFunc(channelNames[:])}},
"sync": []UnitParameter{},
}
var channelNames = [...]string{"left", "right", "aux1 left", "aux1 right", "aux2 left", "aux2 right", "aux3 left", "aux3 right"}
var noteTrackingNames = [...]string{"fixed", "pitch", "BPM"}
var oscTypes = [...]string{"sine", "trisaw", "pulse", "gate", "sample"}
func arrDispFunc(arr []string) UnitParameterDisplayFunc {
return func(v int) (string, string) {
if v < 0 || v >= len(arr) {
return "???", ""
}
return arr[v], ""
}
}
func compressorTimeDispFunc(v int) (string, string) {
alpha := math.Pow(2, -24*float64(v)/128) // alpha is the "smoothing factor" of first order low pass iir
sec := -1 / (44100 * math.Log(1-alpha)) // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
return engineeringTime(sec)
}
func oscillatorTransposeDispFunc(v int) (string, string) {
relvalue := v - 64
octaves := relvalue / 12
semitones := relvalue % 12
if semitones == 0 {
return strconv.Itoa(octaves), "oct"
}
return strconv.Itoa(semitones), "st"
}
func engineeringTime(sec float64) (string, string) {
if sec < 1e-3 {
return fmt.Sprintf("%.2f", sec*1e6), "us"
} else if sec < 1 {
return fmt.Sprintf("%.2f", sec*1e3), "ms"
}
return fmt.Sprintf("%.2f", sec), "s"
}
func formatFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
// When unit.Type = "oscillator", its unit.Parameter["Type"] tells the type of
// the oscillator. There is five different oscillator types, so these consts
// just enumerate them.
@ -374,158 +420,3 @@ func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) {
}
return 0, 0, fmt.Errorf("could not find a unit with id %v", id)
}
// ParamHintString returns a human readable string representing the current
// value of a given unit parameter.
func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
if instrIndex < 0 || instrIndex >= len(p) {
return ""
}
instr := p[instrIndex]
if unitIndex < 0 || unitIndex >= len(instr.Units) {
return ""
}
unit := instr.Units[unitIndex]
value := unit.Parameters[param]
switch unit.Type {
case "envelope":
switch param {
case "attack":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100)
case "decay":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * (1 - float64(unit.Parameters["sustain"])/128))
case "release":
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * float64(unit.Parameters["sustain"]) / 128)
}
case "oscillator":
switch param {
case "type":
switch value {
case Sine:
return "Sine"
case Trisaw:
return "Trisaw"
case Pulse:
return "Pulse"
case Gate:
return "Gate"
case Sample:
return "Sample"
default:
return "Unknown"
}
case "transpose":
relvalue := value - 64
octaves := relvalue / 12
semitones := relvalue % 12
if octaves != 0 {
return fmt.Sprintf("%v oct, %v st", octaves, semitones)
}
return fmt.Sprintf("%v st", semitones)
case "detune":
return fmt.Sprintf("%v st", float32(value-64)/64.0)
}
case "compressor":
switch param {
case "attack":
fallthrough
case "release":
alpha := math.Pow(2, -24*float64(value)/128) // alpha is the "smoothing factor" of first order low pass iir
sec := -1 / (44100 * math.Log(1-alpha)) // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
return engineeringTime(sec)
case "ratio":
return fmt.Sprintf("1 : %.3f", 1-float64(value)/128)
}
case "loadval":
switch param {
case "value":
return fmt.Sprintf("%.2f", float32(value)/64-1)
}
case "send":
switch param {
case "amount":
return fmt.Sprintf("%.2f", float32(value)/64-1)
case "voice":
if value == 0 {
targetIndex, _, err := p.FindUnit(unit.Parameters["target"])
if err == nil && targetIndex != instrIndex {
return "all"
}
return "self"
}
return fmt.Sprintf("%v", value)
case "target":
instrIndex, unitIndex, err := p.FindUnit(unit.Parameters["target"])
if err != nil {
return "invalid target"
}
instr := p[instrIndex]
unit := instr.Units[unitIndex]
return fmt.Sprintf("%v / %v%v", instr.Name, unit.Type, unitIndex)
case "port":
instrIndex, unitIndex, err := p.FindUnit(unit.Parameters["target"])
if err != nil {
return fmt.Sprintf("%v ???", value)
}
portList := Ports[p[instrIndex].Units[unitIndex].Type]
if value < 0 || value >= len(portList) {
return fmt.Sprintf("%v ???", value)
}
return fmt.Sprintf(portList[value])
}
case "delay":
switch param {
case "notetracking":
switch value {
case 0:
return "fixed"
case 1:
return "tracks pitch"
case 2:
return "tracks BPM"
}
}
case "in", "aux":
switch param {
case "channel":
switch value {
case 0:
return "left"
case 1:
return "right"
case 2:
return "aux1 left"
case 3:
return "aux1 right"
case 4:
return "aux2 left"
case 5:
return "aux2 right"
case 6:
return "aux3 left"
case 7:
return "aux3 right"
}
}
case "dbgain":
switch param {
case "decibels":
return fmt.Sprintf("%.2f dB", 40*(float32(value)/64-1))
}
case "crush":
switch param {
case "resolution":
return fmt.Sprintf("%v bits", 24*float32(value)/128)
}
}
return ""
}
func engineeringTime(sec float64) string {
if sec < 1e-3 {
return fmt.Sprintf("%.2f us", sec*1e6)
} else if sec < 1 {
return fmt.Sprintf("%.2f ms", sec*1e3)
}
return fmt.Sprintf("%.2f s", sec)
}

View File

@ -176,6 +176,7 @@ func (m *Model) Undo() Action {
m.undoStack = m.undoStack[:len(m.undoStack)-1]
m.prevUndoKind = ""
(*Model)(m).send(m.d.Song.Copy())
(*Model)(m).send(m.ExtParams())
},
}
}
@ -193,6 +194,7 @@ func (m *Model) Redo() Action {
m.redoStack = m.redoStack[:len(m.redoStack)-1]
m.prevUndoKind = ""
(*Model)(m).send(m.d.Song.Copy())
(*Model)(m).send(m.ExtParams())
},
}
}
@ -408,3 +410,30 @@ func (m *Model) completeAction(checkSave bool) {
m.dialog = NoDialog
}
}
func (m *Model) SetExtLink(index int) Action {
return Action{
do: func() {
defer m.change("SetExtLink", ExtParamLinkChange, MajorChange)()
p, _ := m.Params().SelectedItem().(NamedParameter)
for i := range p.m.d.ExtParamLinks {
if i == index {
continue
}
if p.m.d.ExtParamLinks[i].UnitID == p.unit.ID && p.m.d.ExtParamLinks[i].ParamName == p.up.Name {
p.m.d.ExtParamLinks[i] = ExtParamLink{}
}
}
if index > -1 {
p.m.d.ExtParamLinks[index] = ExtParamLink{UnitID: p.unit.ID, ParamName: p.up.Name}
}
},
allowed: func() bool {
if index < -1 || index >= len(m.d.ExtParamLinks) {
return false
}
_, ok := m.Params().SelectedItem().(NamedParameter)
return ok
},
}
}

View File

@ -24,27 +24,39 @@ import (
)
type UnitEditor struct {
sliderList *DragList
searchList *DragList
Parameters []*ParameterWidget
DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
caser cases.Caser
sliderList *DragList
searchList *DragList
Parameters []*ParameterWidget
DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
ExtLinkMenuItems []MenuItem
ExtLinkMenu Menu
caser cases.Caser
}
func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
ExtLinkMenu: Menu{},
ExtLinkMenuItems: []MenuItem{{Text: "None", IconBytes: icons.HardwarePhoneLinkOff, Doer: m.SetExtLink(-1)}},
}
for i := 0; i < tracker.ExtParamCount; i++ {
ret.ExtLinkMenuItems = append(ret.ExtLinkMenuItems, MenuItem{
Text: fmt.Sprintf("P%v", i),
IconBytes: icons.HardwarePhoneLink,
Doer: m.SetExtLink(i),
})
}
ret.caser = cases.Title(language.English)
return ret
}
@ -104,6 +116,9 @@ func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D {
}
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
if m, ok := pe.Parameters[index].Parameter.(tracker.NamedParameter); ok {
paramStyle.Linked = m.Linked() == index
}
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
}
@ -132,6 +147,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
text = pe.caser.String(text)
}
hintText := Label(text, white, t.Theme.Shaper)
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtnStyle.Layout),
layout.Rigid(copyUnitBtnStyle.Layout),
@ -214,6 +230,7 @@ type ParameterWidget struct {
unitBtn widget.Clickable
unitMenu Menu
Parameter tracker.Parameter
extLinkBtn TipClickable
}
type ParameterStyle struct {
@ -221,6 +238,7 @@ type ParameterStyle struct {
w *ParameterWidget
Theme *material.Theme
Focus bool
Linked bool
}
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
@ -233,6 +251,29 @@ func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) P
func (p ParameterStyle) Layout(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
ue := p.tracker.InstrumentEditor.unitEditor
extLinkMenuBtnStyle := TipIcon(p.Theme, &p.w.extLinkBtn, icons.ContentLink, "Link to external parameter")
if !p.Linked {
extLinkMenuBtnStyle.IconButtonStyle.Color = disabledTextColor
if !p.Focus {
extLinkMenuBtnStyle.IconButtonStyle.Color = transparent
}
}
dims := extLinkMenuBtnStyle.Layout(gtx)
if p.Focus {
m := PopupMenu(&ue.ExtLinkMenu, p.Theme.Shaper)
for p.w.extLinkBtn.Clickable.Clicked(gtx) {
m.Menu.Visible = true
}
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(200))
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
m.Layout(gtx, ue.ExtLinkMenuItems...)
}
return dims
}),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path/filepath"
@ -11,6 +12,8 @@ import (
"github.com/vsariola/sointu/vm"
)
const ExtParamCount = 16
// Model implements the mutable state for the tracker program GUI.
//
// Go does not have immutable slices, so there's no efficient way to guarantee
@ -37,6 +40,7 @@ type (
RecoveryFilePath string
ChangedSinceRecovery bool
Loop Loop
ExtParamLinks [ExtParamCount]ExtParamLink
}
Model struct {
@ -106,6 +110,20 @@ type (
model *Model
}
// ExtParamLink is for linking VST parameters to the patch parameters. There's
// fixed number of parameters in the VST plugin and these are linked to
// particular parameters of a unit.
ExtParamLink struct {
UnitID int
ParamName string
}
ExtValueArray [ExtParamCount]float32
ExtParamArray [ExtParamCount]struct {
Val float32
Param sointu.UnitParameter
}
IsPlayingMsg struct{ bool }
StartPlayMsg struct{ sointu.SongPos }
BPMMsg struct{ int }
@ -133,6 +151,7 @@ const (
BPMChange
RowsPerBeatChange
LoopChange
ExtParamLinkChange
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
)
@ -247,6 +266,9 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
if m.changeType&LoopChange != 0 {
m.send(m.d.Loop)
}
if m.changeType&ExtParamLinkChange != 0 || m.changeType&PatchChange != 0 {
m.send(m.ExtParams())
}
m.undoSkipCounter++
var limit int
switch m.changeSeverity {
@ -322,6 +344,7 @@ func (m *Model) UnmarshalRecovery(bytes []byte) {
}
m.d.ChangedSinceRecovery = false
m.send(m.d.Song.Copy())
m.send(m.ExtParams())
m.send(m.d.Loop)
m.updatePatternUseCount()
}
@ -355,6 +378,8 @@ func (m *Model) ProcessPlayerMessage(msg PlayerMsg) {
m.Alerts().AddAlert(e)
case IsPlayingMsg:
m.playing = e.bool
case ExtValueArray:
m.SetExtValues(e)
default:
}
}
@ -389,6 +414,51 @@ func (m *Model) Instrument(index int) sointu.Instrument {
return m.d.Song.Patch[index].Copy()
}
func (m *Model) ExtParams() (params ExtParamArray) {
for i, l := range m.d.ExtParamLinks {
instrIndex, unitIndex, err := m.d.Song.Patch.FindUnit(l.UnitID)
if err != nil {
continue
}
unit := m.d.Song.Patch[instrIndex].Units[unitIndex]
if up, ok := sointu.UnitTypes[unit.Type]; ok {
for _, p := range up {
if p.Name == l.ParamName && p.CanSet && p.MaxValue > p.MinValue {
params[i].Val = float32(unit.Parameters[l.ParamName]-p.MinValue) / float32(p.MaxValue-p.MinValue)
params[i].Param = p
}
}
}
}
return
}
func (m *Model) SetExtValues(vals ExtValueArray) {
defer m.change("SetExtValues", PatchChange, MinorChange)()
changed := false
for i, l := range m.d.ExtParamLinks {
instrIndex, unitIndex, err := m.d.Song.Patch.FindUnit(l.UnitID)
if err != nil {
continue
}
unit := m.d.Song.Patch[instrIndex].Units[unitIndex]
if up, ok := sointu.UnitTypes[unit.Type]; ok {
for _, p := range up {
if p.Name == l.ParamName && p.CanSet && p.MaxValue > p.MinValue {
newVal := int(math.Round(float64(vals[i])*float64(p.MaxValue-p.MinValue))) + p.MinValue
if unit.Parameters[l.ParamName] != newVal {
unit.Parameters[l.ParamName] = newVal
changed = true
}
}
}
}
}
if !changed {
m.changeCancel = true
}
}
func (d *modelData) Copy() modelData {
ret := *d
ret.Song = d.Song.Copy()

View File

@ -21,6 +21,14 @@ func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
func (NullContext) Params() (params tracker.ExtValueArray, ok bool) {
return tracker.ExtValueArray{}, false
}
func (NullContext) SetParams(params tracker.ExtParamArray) bool {
return false
}
type modelFuzzState struct {
model *tracker.Model
clipboard []byte

View File

@ -162,13 +162,29 @@ func (p NamedParameter) Type() ParameterType {
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)
if p.up.DisplayFunc != nil {
valueInUnits, units := p.up.DisplayFunc(val)
return fmt.Sprintf("%d / %s %s", val, valueInUnits, units)
}
return text
if p.unit.Type == "send" && p.up.Name == "voice" && val == 0 {
targetIndex, _, err := p.m.FindUnit(p.unit.Parameters["target"])
if err == nil && targetIndex != p.m.d.InstrIndex {
return "all"
}
return "self"
}
if p.unit.Type == "send" && p.up.Name == "port" {
instrIndex, unitIndex, err := p.m.FindUnit(p.unit.Parameters["target"])
if err != nil {
return strconv.Itoa(val)
}
portList := sointu.Ports[p.m.d.Song.Patch[instrIndex].Units[unitIndex].Type]
if val < 0 || val >= len(portList) {
return strconv.Itoa(val)
}
return fmt.Sprintf(portList[val])
}
return strconv.Itoa(val)
}
func (p NamedParameter) LargeStep() int {
@ -178,6 +194,15 @@ func (p NamedParameter) LargeStep() int {
return 16
}
func (p NamedParameter) Linked() int {
for i, l := range p.m.d.ExtParamLinks {
if l.UnitID == p.unit.ID && l.ParamName == p.up.Name {
return i
}
}
return -1
}
// GmDlsEntryParameter
func (p GmDlsEntryParameter) Name() string { return "sample" }

View File

@ -25,6 +25,7 @@ type (
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
voices [vm.MAX_VOICES]voice
loop Loop
extParamValues ExtValueArray
recState recState // is the recording off; are we waiting for a note; or are we recording
recording Recording // the recorded MIDI events and BPM
@ -39,6 +40,8 @@ type (
PlayerProcessContext interface {
NextEvent() (event MIDINoteEvent, ok bool)
BPM() (bpm float64, ok bool)
Params() (params ExtValueArray, ok bool)
SetParams(params ExtParamArray) bool
}
// MIDINoteEvent is a MIDI event triggering or releasing a note. In
@ -225,6 +228,10 @@ func (p *Player) advanceRow() {
}
func (p *Player) processMessages(context PlayerProcessContext) {
if value, ok := context.Params(); ok && value != p.extParamValues {
p.extParamValues = value
p.send(p.extParamValues)
}
loop:
for { // process new message
select {
@ -293,6 +300,8 @@ loop:
}
p.recState = recStateNone
}
case ExtParamArray:
context.SetParams(m)
default:
// ignore unknown messages
}