This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-10-16 23:55:02 +03:00
parent bdd729efc1
commit f80e71d2ec
5 changed files with 187 additions and 114 deletions

View File

@ -34,7 +34,7 @@ func NewInstrumentPresets(m *tracker.Model) *InstrumentPresets {
builtinPresetsBtn: new(Clickable), builtinPresetsBtn: new(Clickable),
dirBtn: new(Clickable), dirBtn: new(Clickable),
dirList: NewDragList(m.PresetDirList().List(), layout.Vertical), dirList: NewDragList(m.PresetDirList().List(), layout.Vertical),
resultList: NewDragList(m.PresetDirList().List(), layout.Vertical), resultList: NewDragList(m.PresetResultList().List(), layout.Vertical),
} }
} }
@ -48,12 +48,25 @@ func (ip *InstrumentPresets) layout(gtx C) D {
return Label(tr.Theme, &tr.Theme.Dialog.Text, tr.Model.PresetDirList().Value(i)).Layout(gtx) return Label(tr.Theme, &tr.Theme.Dialog.Text, tr.Model.PresetDirList().Value(i)).Layout(gtx)
} }
dirs := func(gtx C) D { dirs := func(gtx C) D {
return FilledDragList(tr.Theme, ip.dirList).Layout(gtx, dirElem, nil) gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
style := FilledDragList(tr.Theme, ip.dirList)
dims := style.Layout(gtx, dirElem, nil)
style.LayoutScrollBar(gtx)
return dims
}
dirSurface := func(gtx C) D {
return Surface{Gray: 30, Focus: tr.PatchPanel.TreeFocused(gtx)}.Layout(gtx, dirs)
}
resultElem := func(gtx C, i int) D {
return Label(tr.Theme, &tr.Theme.Dialog.Text, tr.Model.PresetResultList().Value(i)).Layout(gtx)
}
results := func(gtx C) D {
return FilledDragList(tr.Theme, ip.resultList).Layout(gtx, resultElem, nil)
} }
bottom := func(gtx C) D { bottom := func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(dirs), layout.Rigid(dirSurface),
layout.Rigid(dirs), layout.Flexed(1, results),
) )
} }
// layout // layout

View File

@ -138,9 +138,6 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
shrinkHint: makeHint("Shrink", " (%s)", "InstrEnlargedToggle"), shrinkHint: makeHint("Shrink", " (%s)", "InstrEnlargedToggle"),
addInstrumentHint: makeHint("Add\ninstrument", "\n(%s)", "AddInstrument"), addInstrumentHint: makeHint("Add\ninstrument", "\n(%s)", "AddInstrument"),
} }
for index, name := range m.IterateInstrumentPresets {
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem(m.LoadPreset(index), name, "", icons.ImageAudiotrack))
}
return ret return ret
} }

View File

@ -85,6 +85,9 @@ type (
broker *Broker broker *Broker
MIDI MIDIContext MIDI MIDIContext
presets PresetSlice
presetIndex int
} }
// Cursor identifies a row and a track in a song score. // Cursor identifies a row and a track in a song score.
@ -202,6 +205,8 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
m.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM) m.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM)
m.updateDeriveData(SongChange) m.updateDeriveData(SongChange)
m.updateDerivedPresetSearch()
m.loadPresets()
return m return m
} }

View File

@ -50,6 +50,9 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed) s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed)
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed) s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed) s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed)
s.IterateList("PresetDirs", s.model.PresetDirList().List(), yield, seed)
s.IterateList("PresetResults", s.model.PresetResultList().List(), yield, seed)
// Bools
s.IterateBool("Panic", s.model.Panic(), yield, seed) s.IterateBool("Panic", s.model.Panic(), yield, seed)
s.IterateBool("Recording", s.model.IsRecording(), yield, seed) s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
s.IterateBool("Playing", s.model.Playing(), yield, seed) s.IterateBool("Playing", s.model.Playing(), yield, seed)
@ -87,8 +90,6 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed) s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed) s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed) s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed)
// just test loading one of the presets
s.IterateAction("LoadPreset", s.model.LoadPreset(seed%tracker.NumPresets()), yield, seed)
// Tables // Tables
s.IterateTable("Order", s.model.Order().Table(), yield, seed) s.IterateTable("Order", s.model.Order().Table(), yield, seed)
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed) s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)

View File

@ -17,6 +17,9 @@ import (
//go:generate go run generate/gmdls_entries.go //go:generate go run generate/gmdls_entries.go
//go:generate go run generate/clean_presets.go //go:generate go run generate/clean_presets.go
//go:embed presets/*
var instrumentPresetFS embed.FS
type ( type (
// GmDlsEntry is a single sample entry from the gm.dls file // GmDlsEntry is a single sample entry from the gm.dls file
GmDlsEntry struct { GmDlsEntry struct {
@ -27,6 +30,15 @@ type (
Name string // sample Name Name string // sample Name
} }
Preset struct {
Directory string
User bool
NeedsGmDls bool
Instr sointu.Instrument
}
PresetSlice []Preset
InstrumentPresetYieldFunc func(index int, item string) (ok bool) InstrumentPresetYieldFunc func(index int, item string) (ok bool)
LoadPreset struct { LoadPreset struct {
Index int Index int
@ -41,13 +53,15 @@ type (
PresetKind Model PresetKind Model
ClearPresetSearch Model ClearPresetSearch Model
PresetDirList Model PresetDirList Model
PresetResultList Model
derivedPresetSearch struct { derivedPresetSearch struct {
dirIndex int dirIndex int
noGmDls bool noGmDls bool
kind PresetKindEnum kind PresetKindEnum
searchStrings []string
dirs []string dirs []string
results []int results []Preset
} }
PresetKindEnum int PresetKindEnum int
@ -62,15 +76,15 @@ const (
func (m *Model) updateDerivedPresetSearch() { func (m *Model) updateDerivedPresetSearch() {
// parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all // parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all
search := strings.TrimSpace(m.d.PresetSearchString) search := strings.TrimSpace(m.d.PresetSearchString)
lower := strings.ToLower(search) parts := strings.Fields(search)
parts := strings.Fields(lower)
// parse parts to see if they contain : // parse parts to see if they contain :
m.derived.presetSearch.dirs = []string{"All"} m.derived.presetSearch.dirIndex = 0
m.derived.presetSearch.noGmDls = false m.derived.presetSearch.noGmDls = false
m.derived.presetSearch.kind = AllPresets m.derived.presetSearch.kind = AllPresets
m.derived.presetSearch.searchStrings = m.derived.presetSearch.searchStrings[:0]
for _, part := range parts { for _, part := range parts {
if strings.HasPrefix(part, "d:") && len(part) > 3 { if strings.HasPrefix(part, "d:") && len(part) > 2 {
dir := strings.TrimSpace(part[3:]) dir := strings.TrimSpace(part[2:])
ind := slices.IndexFunc(m.derived.presetSearch.dirs, func(c string) bool { return c == dir }) ind := slices.IndexFunc(m.derived.presetSearch.dirs, func(c string) bool { return c == dir })
m.derived.presetSearch.dirIndex = max(ind, 0) m.derived.presetSearch.dirIndex = max(ind, 0)
} else if strings.HasPrefix(part, "g:n") { } else if strings.HasPrefix(part, "g:n") {
@ -83,8 +97,100 @@ func (m *Model) updateDerivedPresetSearch() {
case "u": case "u":
m.derived.presetSearch.kind = UserPresets m.derived.presetSearch.kind = UserPresets
} }
} else {
m.derived.presetSearch.searchStrings = append(m.derived.presetSearch.searchStrings, strings.ToLower(part))
} }
} }
// update results
m.derived.presetSearch.results = m.derived.presetSearch.results[:0]
for _, p := range m.presets {
if m.derived.presetSearch.kind == BuiltinPresets && p.User {
continue
}
if m.derived.presetSearch.kind == UserPresets && !p.User {
continue
}
if m.derived.presetSearch.dirIndex > 0 && m.derived.presetSearch.dirIndex < len(m.derived.presetSearch.dirs) && p.Directory != m.derived.presetSearch.dirs[m.derived.presetSearch.dirIndex] {
continue
}
if m.derived.presetSearch.noGmDls && p.NeedsGmDls {
continue
}
if len(m.derived.presetSearch.searchStrings) == 0 {
goto found
}
for _, s := range m.derived.presetSearch.searchStrings {
if strings.Contains(strings.ToLower(p.Instr.Name), s) {
goto found
}
}
continue
found:
m.derived.presetSearch.results = append(m.derived.presetSearch.results, p)
}
}
func (m *Model) loadPresets() {
m.presets = nil
m.loadPresetsFromFs(instrumentPresetFS, false)
if configDir, err := os.UserConfigDir(); err == nil {
userPresets := filepath.Join(configDir, "sointu", "presets")
m.loadPresetsFromFs(os.DirFS(userPresets), true)
}
sort.Sort(m.presets)
seenDir := make(map[string]bool)
for _, p := range m.presets {
seenDir[p.Directory] = true
}
dirs := make([]string, 0, len(seenDir))
for k := range seenDir {
dirs = append(dirs, k)
}
sort.Strings(dirs)
m.derived.presetSearch.dirs = make([]string, 0, len(dirs)+1)
m.derived.presetSearch.dirs = append(m.derived.presetSearch.dirs, "---")
m.derived.presetSearch.dirs = append(m.derived.presetSearch.dirs, dirs...)
}
func (m *Model) loadPresetsFromFs(fsys fs.FS, userDefined bool) {
fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := fs.ReadFile(instrumentPresetFS, path)
if err != nil {
return nil
}
var instr sointu.Instrument
if yaml.UnmarshalStrict(data, &instr) == nil {
noExt := path[:len(path)-len(filepath.Ext(path))]
splitted := splitPath(noExt)
splitted = splitted[1:] // remove "presets" from the path
instr.Name = splitted[len(splitted)-1]
preset := Preset{
Directory: strings.Join(splitted[:len(splitted)-1], "/"),
User: userDefined,
Instr: instr,
NeedsGmDls: checkNeedsGmDls(instr),
}
m.presets = append(m.presets, preset)
}
return nil
})
}
func checkNeedsGmDls(instr sointu.Instrument) bool {
for _, u := range instr.Units {
if u.Type == "oscillator" {
if u.Parameters["type"] == sointu.Sample {
return true
}
}
}
return false
} }
func (m *Model) PresetSearchString() String { return MakeString((*PresetSearchString)(m)) } func (m *Model) PresetSearchString() String { return MakeString((*PresetSearchString)(m)) }
@ -190,6 +296,40 @@ func (m *PresetDirList) SetSelected(i int) {
(*Model)(m).updateDerivedPresetSearch() (*Model)(m).updateDerivedPresetSearch()
} }
func (m *Model) PresetResultList() *PresetResultList { return (*PresetResultList)(m) }
func (v *PresetResultList) List() List { return List{v} }
func (m *PresetResultList) Count() int { return len(m.derived.presetSearch.results) }
func (m *PresetResultList) Selected() int {
return min(max(m.presetIndex, 0), len(m.derived.presetSearch.results)-1)
}
func (m *PresetResultList) Selected2() int { return m.Selected() }
func (m *PresetResultList) SetSelected2(i int) {}
func (m *PresetResultList) Value(i int) string {
if i < 0 || i >= len(m.derived.presetSearch.results) {
return ""
}
return m.derived.presetSearch.results[i].Instr.Name
}
func (m *PresetResultList) SetSelected(i int) {
i = min(max(i, 0), len(m.derived.presetSearch.results)-1)
if i < 0 || i >= len(m.derived.presetSearch.results) {
return
}
m.presetIndex = i
defer (*Model)(m).change("LoadPreset", PatchChange, MinorChange)()
if m.d.InstrIndex < 0 {
m.d.InstrIndex = 0
}
m.d.InstrIndex2 = m.d.InstrIndex
for m.d.InstrIndex >= len(m.d.Song.Patch) {
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
}
newInstr := m.derived.presetSearch.results[i].Instr.Copy()
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
(*Model)(m).assignUnitIDs(newInstr.Units)
m.d.Song.Patch[m.d.InstrIndex] = newInstr
}
func removeFilters(str string, prefix string) string { func removeFilters(str string, prefix string) string {
parts := strings.Fields(str) parts := strings.Fields(str)
newParts := make([]string, 0, len(parts)) newParts := make([]string, 0, len(parts))
@ -297,97 +437,6 @@ type delayPreset struct {
varArgs []int varArgs []int
} }
func (m *Model) IterateInstrumentPresets(yield InstrumentPresetYieldFunc) {
for index, instr := range instrumentPresets {
if !yield(index, instr.Name) {
return
}
}
}
func NumPresets() int {
return len(instrumentPresets)
}
// LoadPreset loads a preset from the list of instrument presets. The index
// should be within the range of 0 to NumPresets()-1.
func (m *Model) LoadPreset(index int) Action {
return MakeEnabledAction(LoadPreset{Index: index, Model: m})
}
func (m LoadPreset) Do() {
defer m.change("LoadPreset", PatchChange, MajorChange)()
if m.d.InstrIndex < 0 {
m.d.InstrIndex = 0
}
m.d.InstrIndex2 = m.d.InstrIndex
for m.d.InstrIndex >= len(m.d.Song.Patch) {
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
}
newInstr := instrumentPresets[m.Index].Copy()
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
m.Model.assignUnitIDs(newInstr.Units)
m.d.Song.Patch[m.d.InstrIndex] = newInstr
}
type instrumentPresetsSlice []sointu.Instrument
//go:embed presets/*
var instrumentPresetFS embed.FS
var instrumentPresets instrumentPresetsSlice
func init() {
fs.WalkDir(instrumentPresetFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := fs.ReadFile(instrumentPresetFS, path)
if err != nil {
return nil
}
var instr sointu.Instrument
if yaml.UnmarshalStrict(data, &instr) == nil {
noExt := path[:len(path)-len(filepath.Ext(path))]
splitted := splitPath(noExt)
splitted = splitted[1:] // remove "presets" from the path
instr.Name = strings.Join(splitted, " ")
instrumentPresets = append(instrumentPresets, instr)
}
return nil
})
if configDir, err := os.UserConfigDir(); err == nil {
userPresets := filepath.Join(configDir, "sointu", "presets")
filepath.WalkDir(userPresets, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var instr sointu.Instrument
if yaml.Unmarshal(data, &instr) == nil {
if len(userPresets)+1 > len(path) {
return nil
}
subPath := path[len(userPresets)+1:]
noExt := subPath[:len(subPath)-len(filepath.Ext(subPath))]
splitted := splitPath(noExt)
instr.Name = strings.Join(splitted, " ")
instrumentPresets = append(instrumentPresets, instr)
}
return nil
})
}
sort.Sort(instrumentPresets)
}
func splitPath(path string) []string { func splitPath(path string) []string {
subPath := path subPath := path
var result []string var result []string
@ -413,6 +462,14 @@ func splitPath(path string) []string {
return result return result
} }
func (p instrumentPresetsSlice) Len() int { return len(p) } func (p PresetSlice) Len() int { return len(p) }
func (p instrumentPresetsSlice) Less(i, j int) bool { return p[i].Name < p[j].Name } func (p PresetSlice) Less(i, j int) bool {
func (p instrumentPresetsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } if p[i].Directory == p[j].Directory {
if p[i].Instr.Name == p[j].Instr.Name {
return p[i].User && !p[j].User
}
return p[i].Instr.Name < p[j].Instr.Name
}
return p[i].Directory < p[j].Directory
}
func (p PresetSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }