feat: add ability to disable units temporarily

Quite often the user wants to experiment what particular unit(s) add
to the sound. This commit adds ability to disable any set of units
temporarily, without actually deleting them. Ctrl-D disables and
re-enables the units. Disabled units are considered non-existent in
the patch.

Closes #116.
This commit is contained in:
5684185+vsariola@users.noreply.github.com 2024-02-19 21:36:14 +02:00
parent 2b3f6d8200
commit 17312bbe4e
9 changed files with 95 additions and 23 deletions

View File

@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added ### Added
- Disable units temporarily. The disabled units are shown in gray and are not
compiled into the patch and are considered for all purposes non-existent.
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
field `disabled: true` for the unit. ([#116][i116])
- Passing a file name on command line immediately tries loading that file ([#122][i122]) - Passing a file name on command line immediately tries loading that file ([#122][i122])
- Massive rewrite of the GUI, in particular allowing better copying, pasting and - Massive rewrite of the GUI, in particular allowing better copying, pasting and
scrolling of table-based data (order list and note data). scrolling of table-based data (order list and note data).
@ -125,6 +129,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0 [0.3.0]: https://github.com/vsariola/sointu/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0 [0.2.0]: https://github.com/vsariola/sointu/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0 [0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
[i116]: https://github.com/vsariola/sointu/issues/116
[i120]: https://github.com/vsariola/sointu/issues/120 [i120]: https://github.com/vsariola/sointu/issues/120
[i121]: https://github.com/vsariola/sointu/issues/121 [i121]: https://github.com/vsariola/sointu/issues/121
[i122]: https://github.com/vsariola/sointu/issues/122 [i122]: https://github.com/vsariola/sointu/issues/122

View File

@ -44,6 +44,10 @@ type (
// unit, VarArgs is the delaytimes, in samples, of the different delaylines // unit, VarArgs is the delaytimes, in samples, of the different delaylines
// in the unit. // in the unit.
VarArgs []int `yaml:",flow,omitempty"` VarArgs []int `yaml:",flow,omitempty"`
// Disabled is a flag that can be set to true to disable the unit.
// Disabled units are considered to be not present in the patch.
Disabled bool `yaml:",omitempty"`
} }
// UnitParameter documents one parameter that an unit takes // UnitParameter documents one parameter that an unit takes
@ -220,7 +224,7 @@ func (u *Unit) Copy() Unit {
} }
varArgs := make([]int, len(u.VarArgs)) varArgs := make([]int, len(u.VarArgs))
copy(varArgs, u.VarArgs) copy(varArgs, u.VarArgs)
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID} return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID, Disabled: u.Disabled}
} }
// StackChange returns how this unit will affect the signal stack. "pop" and // StackChange returns how this unit will affect the signal stack. "pop" and
@ -230,6 +234,9 @@ func (u *Unit) Copy() Unit {
// unit). Effects that just change the topmost signal and will not change the // unit). Effects that just change the topmost signal and will not change the
// number of signals on the stack and thus return 0. // number of signals on the stack and thus return 0.
func (u *Unit) StackChange() int { func (u *Unit) StackChange() int {
if u.Disabled {
return 0
}
switch u.Type { switch u.Type {
case "addp", "mulp", "pop", "out", "outaux", "aux": case "addp", "mulp", "pop", "out", "outaux", "aux":
return -1 - u.Parameters["stereo"] return -1 - u.Parameters["stereo"]
@ -249,6 +256,9 @@ func (u *Unit) StackChange() int {
// this unit is executed. Used to prevent stack underflow. Units producing // this unit is executed. Used to prevent stack underflow. Units producing
// signals do not care what is on the stack before and will return 0. // signals do not care what is on the stack before and will return 0.
func (u *Unit) StackNeed() int { func (u *Unit) StackNeed() int {
if u.Disabled {
return 0
}
switch u.Type { switch u.Type {
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in": case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
return 0 return 0
@ -350,14 +360,14 @@ func (p Patch) InstrumentForVoice(voice int) (int, error) {
// given id. Two units should never have the same id, but if they do, then the // given id. Two units should never have the same id, but if they do, then the
// first match is returned. Id 0 is interpreted as "no id", thus searching for // first match is returned. Id 0 is interpreted as "no id", thus searching for
// id 0 returns an error. Error is also returned if the searched id is not // id 0 returns an error. Error is also returned if the searched id is not
// found. // found. FindUnit considers disabled units as non-existent.
func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) { func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) {
if id == 0 { if id == 0 {
return 0, 0, errors.New("FindUnit called with id 0") return 0, 0, errors.New("FindUnit called with id 0")
} }
for i, instr := range p { for i, instr := range p {
for u, unit := range instr.Units { for u, unit := range instr.Units {
if unit.ID == id { if unit.ID == id && !unit.Disabled {
return i, u, nil return i, u, nil
} }
} }

View File

@ -19,6 +19,7 @@ type (
CommentExpanded Model CommentExpanded Model
NoteTracking Model NoteTracking Model
UnitSearching Model UnitSearching Model
UnitDisabled Model
) )
func (v Bool) Toggle() { func (v Bool) Toggle() {
@ -41,6 +42,7 @@ func (m *Model) Effect() *Effect { return (*Effect)(m) }
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) } func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
func (m *Model) NoteTracking() *NoteTracking { return (*NoteTracking)(m) } func (m *Model) NoteTracking() *NoteTracking { return (*NoteTracking)(m) }
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) } func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
// Panic methods // Panic methods
@ -126,3 +128,36 @@ func (m *UnitSearching) setValue(val bool) {
} }
} }
func (m *UnitSearching) Enabled() bool { return true } func (m *UnitSearching) Enabled() bool { return true }
// UnitDisabled methods
func (m *UnitDisabled) Bool() Bool { return Bool{m} }
func (m *UnitDisabled) Value() bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
return false
}
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Disabled
}
func (m *UnitDisabled) setValue(val bool) {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return
}
l := ((*Model)(m)).Units().List()
a, b := l.listRange()
defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)()
for i := a; i <= b; i++ {
m.d.Song.Patch[m.d.InstrIndex].Units[i].Disabled = val
}
}
func (m *UnitDisabled) Enabled() bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
if len(m.d.Song.Patch[m.d.InstrIndex].Units) == 0 {
return false
}
return true
}

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"gioui.org/font"
"gioui.org/io/clipboard" "gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/layout" "gioui.org/layout"
@ -343,6 +344,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
} }
u := units[i] u := units[i]
var color color.NRGBA = white var color color.NRGBA = white
f := labelDefaultFont
var stackText string var stackText string
stackText = strconv.FormatInt(int64(u.StackAfter), 10) stackText = strconv.FormatInt(int64(u.StackAfter), 10)
@ -353,6 +355,10 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
color = warningColor color = warningColor
(*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning) (*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning)
} }
if u.Disabled {
color = disabledTextColor
f.Style = font.Italic
}
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper} stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
rightMargin := layout.Inset{Right: unit.Dp(10)} rightMargin := layout.Inset{Right: unit.Dp(10)}
@ -381,7 +387,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
editor.Color = color editor.Color = color
editor.HintColor = instrumentNameHintColor editor.HintColor = instrumentNameHintColor
editor.TextSize = unit.Sp(12) editor.TextSize = unit.Sp(12)
editor.Font = labelDefaultFont editor.Font = f
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
key.InputOp{Tag: &ie.searchEditor, Keys: globalKeys}.Add(gtx.Ops) key.InputOp{Tag: &ie.searchEditor, Keys: globalKeys}.Add(gtx.Ops)
@ -399,7 +405,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
} }
return ret return ret
} else { } else {
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper} unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: f, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
if unitNameLabel.Text == "" { if unitNameLabel.Text == "" {
unitNameLabel.Text = "---" unitNameLabel.Text = "---"
} }

View File

@ -64,6 +64,11 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
t.Model.Redo().Do() t.Model.Redo().Do()
return return
} }
case "D":
if e.Modifiers.Contain(key.ModShortcut) {
t.Model.UnitDisabled().Bool().Toggle()
return
}
case "N": case "N":
if e.Modifiers.Contain(key.ModShortcut) { if e.Modifiers.Contain(key.ModShortcut) {
t.NewSong().Do() t.NewSong().Do()

View File

@ -27,6 +27,7 @@ type UnitEditor struct {
DeleteUnitBtn *ActionClickable DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable SelectTypeBtn *widget.Clickable
tag bool tag bool
caser cases.Caser caser cases.Caser
@ -36,6 +37,7 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{ ret := &UnitEditor{
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()), DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()), ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable), CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable), SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical), sliderList: NewDragList(m.Params().List(), layout.Vertical),
@ -114,6 +116,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
} }
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)") copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)")
deleteUnitBtnStyle := ActionIcon(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)") deleteUnitBtnStyle := ActionIcon(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
disableUnitBtnStyle := ToggleIcon(t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, "Disable unit (Ctrl-D)", "Enable unit (Ctrl-D)")
text := t.Units().SelectedType() text := t.Units().SelectedType()
if text == "" { if text == "" {
text = "Choose unit type" text = "Choose unit type"
@ -124,6 +127,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(deleteUnitBtnStyle.Layout), layout.Rigid(deleteUnitBtnStyle.Layout),
layout.Rigid(copyUnitBtnStyle.Layout), layout.Rigid(copyUnitBtnStyle.Layout),
layout.Rigid(disableUnitBtnStyle.Layout),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
var dims D var dims D
if t.Units().SelectedType() != "" { if t.Units().SelectedType() != "" {

View File

@ -34,6 +34,7 @@ type (
UnitListItem struct { UnitListItem struct {
Type string Type string
Disabled bool
StackNeed, StackBefore, StackAfter int StackNeed, StackBefore, StackAfter int
} }
@ -323,7 +324,13 @@ func (v *Units) Iterate(yield UnitYieldFunc) {
stackBefore := 0 stackBefore := 0
for _, unit := range v.d.Song.Patch[v.d.InstrIndex].Units { for _, unit := range v.d.Song.Patch[v.d.InstrIndex].Units {
stackAfter := stackBefore + unit.StackChange() stackAfter := stackBefore + unit.StackChange()
if !yield(UnitListItem{unit.Type, unit.StackNeed(), stackBefore, stackAfter}) { if !yield(UnitListItem{
Type: unit.Type,
Disabled: unit.Disabled,
StackNeed: unit.StackNeed(),
StackBefore: stackBefore,
StackAfter: stackAfter,
}) {
break break
} }
stackBefore = stackAfter stackBefore = stackAfter

View File

@ -78,7 +78,7 @@ func NewBytecode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*Bytecode,
return nil, errors.New("Each instrument must have at least 1 voice") return nil, errors.New("Each instrument must have at least 1 voice")
} }
for unitIndex, unit := range instr.Units { for unitIndex, unit := range instr.Units {
if unit.Type == "" { // empty units are just ignored & skipped if unit.Type == "" || unit.Disabled { // empty units are just ignored & skipped
continue continue
} }
opcode, ok := featureSet.Opcode(unit.Type) opcode, ok := featureSet.Opcode(unit.Type)

View File

@ -118,7 +118,7 @@ func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}} features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
for instrIndex, instrument := range patch { for instrIndex, instrument := range patch {
for _, unit := range instrument.Units { for _, unit := range instrument.Units {
if unit.Type == "" { if unit.Type == "" || unit.Disabled {
continue continue
} }
if _, ok := features.opcodes[unit.Type]; !ok { if _, ok := features.opcodes[unit.Type]; !ok {