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 {