mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
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:
parent
2b3f6d8200
commit
17312bbe4e
@ -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
|
||||
|
16
patch.go
16
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 = "---"
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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() != "" {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user