Skip to content

Commit

Permalink
feat(tracker): add menu to load instrument presets
Browse files Browse the repository at this point in the history
The presets are embedded in the executable, so there's no additional files.

Closes #91
  • Loading branch information
vsariola committed Oct 1, 2023
1 parent b65d11c commit ce7c8a0
Show file tree
Hide file tree
Showing 55 changed files with 2,262 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Save the GUI state periodically to a recovery file and load it on
startup of the app, if present. The recovery file is located in the
home directory of the user.
- Instrument presets. The presets are embedded in the executable and
there's a button to open a menu to load one of the presets.

### Fixed
- The sointu-vsti-native plugin has different plugin ID and plugin name
Expand Down
36 changes: 34 additions & 2 deletions tracker/gioui/instrumenteditor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type InstrumentEditor struct {
loadInstrumentBtn *TipClickable
addUnitBtn *TipClickable
commentExpandBtn *TipClickable
presetMenuBtn *TipClickable
commentEditor *widget.Editor
nameEditor *widget.Editor
unitTypeEditor *widget.Editor
Expand All @@ -48,10 +49,12 @@ type InstrumentEditor struct {
wasFocused bool
commentExpanded bool
voiceStates [vm.MAX_VOICES]float32
presetMenuItems []MenuItem
presetMenu Menu
}

func NewInstrumentEditor() *InstrumentEditor {
return &InstrumentEditor{
ret := &InstrumentEditor{
newInstrumentBtn: new(TipClickable),
enlargeBtn: new(TipClickable),
deleteInstrumentBtn: new(TipClickable),
Expand All @@ -60,6 +63,7 @@ func NewInstrumentEditor() *InstrumentEditor {
loadInstrumentBtn: new(TipClickable),
addUnitBtn: new(TipClickable),
commentExpandBtn: new(TipClickable),
presetMenuBtn: new(TipClickable),
commentEditor: new(widget.Editor),
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
unitTypeEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
Expand All @@ -69,7 +73,12 @@ func NewInstrumentEditor() *InstrumentEditor {
unitScrollBar: &ScrollBar{Axis: layout.Vertical},
confirmInstrDelete: new(Dialog),
paramEditor: NewParamEditor(),
presetMenuItems: []MenuItem{},
}
for _, instr := range tracker.Presets {
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: instr.Name, IconBytes: icons.ImageAudiotrack})
}
return ret
}

func (t *InstrumentEditor) ExpandComment() {
Expand All @@ -85,7 +94,8 @@ func (ie *InstrumentEditor) Focused() bool {
}

func (ie *InstrumentEditor) ChildFocused() bool {
return ie.paramEditor.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.unitTypeEditor.Focused()
return ie.paramEditor.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.unitTypeEditor.Focused() ||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.Focused()
}

func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
Expand All @@ -109,6 +119,7 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
icon = icons.NavigationFullscreenExit
enlargeTip = "Shrink"
}

fullscreenBtnStyle := IconButton(t.Theme, ie.enlargeBtn, icon, true, enlargeTip)
for ie.enlargeBtn.Clickable.Clicked() {
t.SetInstrEnlarged(!t.InstrEnlarged())
Expand Down Expand Up @@ -175,11 +186,18 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
}

commentExpandBtnStyle := IconButton(t.Theme, ie.commentExpandBtn, collapseIcon, true, commentTip)
presetMenuBtnStyle := IconButton(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, true, "Load preset")
copyInstrumentBtnStyle := IconButton(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, true, "Copy instrument")
saveInstrumentBtnStyle := IconButton(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, true, "Save instrument")
loadInstrumentBtnStyle := IconButton(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, true, "Load instrument")
deleteInstrumentBtnStyle := IconButton(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument(), "Delete\ninstrument")

m := PopupMenu(t.Theme, &ie.presetMenu)

for item, clicked := ie.presetMenu.Clicked(); clicked; item, clicked = ie.presetMenu.Clicked() {
t.SetInstrument(tracker.Presets[item])
}

header := func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(Label("Voices: ", white)),
Expand All @@ -193,11 +211,24 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
}),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
layout.Rigid(commentExpandBtnStyle.Layout),
layout.Rigid(func(gtx C) D {
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
dims := presetMenuBtnStyle.Layout(gtx)
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
m.Layout(gtx, ie.presetMenuItems...)
return dims
}),
layout.Rigid(saveInstrumentBtnStyle.Layout),
layout.Rigid(loadInstrumentBtnStyle.Layout),
layout.Rigid(copyInstrumentBtnStyle.Layout),
layout.Rigid(deleteInstrumentBtnStyle.Layout))
}

for ie.presetMenuBtn.Clickable.Clicked() {
ie.presetMenu.Visible = true
}

for ie.commentExpandBtn.Clickable.Clicked() {
ie.commentExpanded = !ie.commentExpanded
if !ie.commentExpanded {
Expand Down Expand Up @@ -234,6 +265,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
}
return header(gtx)
}

for ie.copyInstrumentBtn.Clickable.Clicked() {
contents, err := yaml.Marshal(t.Instrument())
if err == nil {
Expand Down
108 changes: 53 additions & 55 deletions tracker/gioui/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Menu struct {
tags []bool
clicks []int
hover int
list layout.List
}

type MenuStyle struct {
Expand Down Expand Up @@ -54,8 +55,7 @@ func (m *Menu) Clicked() (int, bool) {

func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
contents := func(gtx C) D {
flexChildren := make([]layout.FlexChild, len(items))
for i, item := range items {
for i := range items {
// make sure we have a tag for every item
for len(m.Menu.tags) <= i {
m.Menu.tags = append(m.Menu.tags, false)
Expand All @@ -78,60 +78,58 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
}
}
}
// layout contents for this item
i2 := i // avoid loop variable getting updated in closure
item2 := item
flexChildren[i] = layout.Rigid(func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp
if i2 == m.Menu.hover-1 && !item2.Disabled {
macro = op.Record(gtx.Ops)
}
icon := widgetForIcon(item2.IconBytes)
iconColor := m.IconColor
if item2.Disabled {
iconColor = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item2.Text, FontSize: m.FontSize, Color: m.TextColor}
if item2.Disabled {
textLabel.Color = mediumEmphasisTextColor
}
shortcutLabel := LabelStyle{Text: item2.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor}
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)}
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.IconSize))
gtx.Constraints.Min = image.Pt(p, p)
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
)
if i2 == m.Menu.hover-1 && !item2.Disabled {
recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op())
recording.Add(gtx.Ops)
}
if !item2.Disabled {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops)
pointer.InputOp{Tag: &m.Menu.tags[i2],
Types: pointer.Press | pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
area.Pop()
}
return dims
})
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, flexChildren...)
m.Menu.list.Axis = layout.Vertical
return m.Menu.list.Layout(gtx, len(items), func(gtx C, i int) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp
item := &items[i]
if i == m.Menu.hover-1 && !item.Disabled {
macro = op.Record(gtx.Ops)
}
icon := widgetForIcon(item.IconBytes)
iconColor := m.IconColor
if item.Disabled {
iconColor = mediumEmphasisTextColor
}
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor}
if item.Disabled {
textLabel.Color = mediumEmphasisTextColor
}
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor}
shortcutInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(12), Bottom: unit.Dp(2), Top: unit.Dp(2)}
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return iconInset.Layout(gtx, func(gtx C) D {
p := gtx.Dp(unit.Dp(m.IconSize))
gtx.Constraints.Min = image.Pt(p, p)
return icon.Layout(gtx, iconColor)
})
}),
layout.Rigid(textLabel.Layout),
layout.Flexed(1, func(gtx C) D { return D{Size: image.Pt(gtx.Constraints.Max.X, 1)} }),
layout.Rigid(func(gtx C) D {
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
}),
)
if i == m.Menu.hover-1 && !item.Disabled {
recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op())
recording.Add(gtx.Ops)
}
if !item.Disabled {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops)
pointer.InputOp{Tag: &m.Menu.tags[i],
Types: pointer.Press | pointer.Enter | pointer.Leave,
}.Add(gtx.Ops)
area.Pop()
}
return dims
})
}
popup := Popup(&m.Menu.Visible)
popup.NE = unit.Dp(0)
Expand Down
51 changes: 51 additions & 0 deletions tracker/presets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package tracker

import (
"embed"
"io/fs"
"sort"

"github.com/vsariola/sointu"
"gopkg.in/yaml.v3"
)

type PresetList []sointu.Instrument

//go:embed presets/*
var presetFS embed.FS

var Presets PresetList

func init() {
fs.WalkDir(presetFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := fs.ReadFile(presetFS, path)
if err != nil {
return nil
}
var instr sointu.Instrument
if yaml.Unmarshal(data, &instr) != nil {
return nil
}
Presets = append(Presets, instr)
return nil
})
sort.Sort(Presets)
}

func (p PresetList) Len() int {
return len(p)
}

func (p PresetList) Less(i, j int) bool {
return p[i].Name < p[j].Name
}

func (p PresetList) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
43 changes: 43 additions & 0 deletions tracker/presets/Fairies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Fairies
numvoices: 1
units:
- type: envelope
id: 1
parameters: {attack: 80, decay: 96, gain: 128, release: 88, stereo: 0, sustain: 80}
- type: envelope
id: 2
parameters: {attack: 0, decay: 96, gain: 128, release: 88, stereo: 0, sustain: 40}
- type: distort
id: 3
parameters: {drive: 32, stereo: 0}
- type: send
id: 5
parameters: {amount: 96, port: 0, sendpop: 1, target: 12}
- type: oscillator
id: 232
parameters: {color: 3, detune: 56, gain: 64, lfo: 0, phase: 3, shape: 64, stereo: 0, transpose: 64, type: 1}
- type: oscillator
id: 233
parameters: {color: 3, detune: 72, gain: 64, lfo: 0, phase: 3, shape: 64, stereo: 0, transpose: 64, type: 1}
- type: addp
id: 234
parameters: {stereo: 0}
- type: distort
id: 10
parameters: {drive: 96, stereo: 0}
- type: filter
id: 12
parameters: {bandpass: 0, frequency: 16, highpass: 1, lowpass: 0, negbandpass: 0, neghighpass: 0, resonance: 24, stereo: 0}
- type: mulp
id: 13
parameters: {stereo: 0}
- type: pan
id: 14
parameters: {panning: 64, stereo: 0}
- type: delay
id: 15
parameters: {damp: 64, dry: 128, feedback: 64, notetracking: 2, pregain: 96, stereo: 1}
varargs: [48, 24]
- type: outaux
id: 19
parameters: {auxgain: 64, outgain: 64, stereo: 1}
Loading

0 comments on commit ce7c8a0

Please sign in to comment.