mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -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
|
## 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
|
||||||
|
16
patch.go
16
patch.go
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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 = "---"
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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() != "" {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user