From 17312bbe4ebc55a8d8b084585890dd526c0bc4d3 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Mon, 19 Feb 2024 21:36:14 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 5 +++++ patch.go | 16 +++++++++++--- tracker/bool.go | 35 ++++++++++++++++++++++++++++++ tracker/gioui/instrument_editor.go | 10 +++++++-- tracker/gioui/keyevent.go | 5 +++++ tracker/gioui/unit_editor.go | 34 ++++++++++++++++------------- tracker/list.go | 9 +++++++- vm/bytecode.go | 2 +- vm/featureset.go | 2 +- 9 files changed, 95 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980d1fd..9f44b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased ### 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]) - Massive rewrite of the GUI, in particular allowing better copying, pasting and 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.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 +[i116]: https://github.com/vsariola/sointu/issues/116 [i120]: https://github.com/vsariola/sointu/issues/120 [i121]: https://github.com/vsariola/sointu/issues/121 [i122]: https://github.com/vsariola/sointu/issues/122 diff --git a/patch.go b/patch.go index e6e9046..c2af2b7 100644 --- a/patch.go +++ b/patch.go @@ -44,6 +44,10 @@ type ( // unit, VarArgs is the delaytimes, in samples, of the different delaylines // in the unit. 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 @@ -220,7 +224,7 @@ func (u *Unit) Copy() Unit { } varArgs := make([]int, len(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 @@ -230,6 +234,9 @@ func (u *Unit) Copy() Unit { // unit). Effects that just change the topmost signal and will not change the // number of signals on the stack and thus return 0. func (u *Unit) StackChange() int { + if u.Disabled { + return 0 + } switch u.Type { case "addp", "mulp", "pop", "out", "outaux", "aux": 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 // signals do not care what is on the stack before and will return 0. func (u *Unit) StackNeed() int { + if u.Disabled { + return 0 + } switch u.Type { case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in": 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 // 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 -// found. +// found. FindUnit considers disabled units as non-existent. func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) { if id == 0 { return 0, 0, errors.New("FindUnit called with id 0") } for i, instr := range p { for u, unit := range instr.Units { - if unit.ID == id { + if unit.ID == id && !unit.Disabled { return i, u, nil } } diff --git a/tracker/bool.go b/tracker/bool.go index 9e27df9..bf0936b 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -19,6 +19,7 @@ type ( CommentExpanded Model NoteTracking Model UnitSearching Model + UnitDisabled Model ) 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) NoteTracking() *NoteTracking { return (*NoteTracking)(m) } func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) } +func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) } // Panic methods @@ -126,3 +128,36 @@ func (m *UnitSearching) setValue(val bool) { } } 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 +} diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index 432a54d..4c314e7 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "gioui.org/font" "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/layout" @@ -343,6 +344,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { } u := units[i] var color color.NRGBA = white + f := labelDefaultFont var stackText string stackText = strconv.FormatInt(int64(u.StackAfter), 10) @@ -353,6 +355,10 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { color = warningColor (*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} rightMargin := layout.Inset{Right: unit.Dp(10)} @@ -381,7 +387,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { editor.Color = color editor.HintColor = instrumentNameHintColor 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() 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 } 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 == "" { unitNameLabel.Text = "---" } diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index d1f2806..9276b1b 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -64,6 +64,11 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) { t.Model.Redo().Do() return } + case "D": + if e.Modifiers.Contain(key.ModShortcut) { + t.Model.UnitDisabled().Bool().Toggle() + return + } case "N": if e.Modifiers.Contain(key.ModShortcut) { t.NewSong().Do() diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 9a9e8eb..3a86786 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -21,25 +21,27 @@ import ( ) type UnitEditor struct { - sliderList *DragList - searchList *DragList - Parameters []*ParameterWidget - DeleteUnitBtn *ActionClickable - CopyUnitBtn *TipClickable - ClearUnitBtn *ActionClickable - SelectTypeBtn *widget.Clickable - tag bool - caser cases.Caser + sliderList *DragList + searchList *DragList + Parameters []*ParameterWidget + DeleteUnitBtn *ActionClickable + CopyUnitBtn *TipClickable + ClearUnitBtn *ActionClickable + DisableUnitBtn *BoolClickable + SelectTypeBtn *widget.Clickable + tag bool + caser cases.Caser } func NewUnitEditor(m *tracker.Model) *UnitEditor { ret := &UnitEditor{ - DeleteUnitBtn: NewActionClickable(m.DeleteUnit()), - ClearUnitBtn: NewActionClickable(m.ClearUnit()), - 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), } ret.caser = cases.Title(language.English) return ret @@ -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)") 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() if text == "" { 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, layout.Rigid(deleteUnitBtnStyle.Layout), layout.Rigid(copyUnitBtnStyle.Layout), + layout.Rigid(disableUnitBtnStyle.Layout), layout.Rigid(func(gtx C) D { var dims D if t.Units().SelectedType() != "" { diff --git a/tracker/list.go b/tracker/list.go index 8a45dde..644ff21 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -34,6 +34,7 @@ type ( UnitListItem struct { Type string + Disabled bool StackNeed, StackBefore, StackAfter int } @@ -323,7 +324,13 @@ func (v *Units) Iterate(yield UnitYieldFunc) { stackBefore := 0 for _, unit := range v.d.Song.Patch[v.d.InstrIndex].Units { 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 } stackBefore = stackAfter diff --git a/vm/bytecode.go b/vm/bytecode.go index de1af53..3fac621 100644 --- a/vm/bytecode.go +++ b/vm/bytecode.go @@ -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") } 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 } opcode, ok := featureSet.Opcode(unit.Type) diff --git a/vm/featureset.go b/vm/featureset.go index c94724b..59cd82c 100644 --- a/vm/featureset.go +++ b/vm/featureset.go @@ -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{}} for instrIndex, instrument := range patch { for _, unit := range instrument.Units { - if unit.Type == "" { + if unit.Type == "" || unit.Disabled { continue } if _, ok := features.opcodes[unit.Type]; !ok {