feat: introduce "cache" for derived model information

This commit is contained in:
qm210 2024-11-09 02:19:52 +01:00
parent c266db17d9
commit 6f1cb5e7ea
7 changed files with 271 additions and 155 deletions

30
song.go
View File

@ -2,7 +2,6 @@ package sointu
import (
"errors"
"iter"
)
type (
@ -336,32 +335,3 @@ func TotalVoices[T any, S ~[]T, P NumVoicerPointer[T]](slice S) (ret int) {
}
return
}
func (s *Song) InstrumentForTrack(trackIndex int) (int, bool) {
voiceIndex := s.Score.FirstVoiceForTrack(trackIndex)
instrument, err := s.Patch.InstrumentForVoice(voiceIndex)
return instrument, err == nil
}
func (s *Song) AllTracksWithSameInstrument(trackIndex int) iter.Seq[int] {
return func(yield func(int) bool) {
currentInstrument, currentExists := s.InstrumentForTrack(trackIndex)
if !currentExists {
return
}
for i := 0; i < len(s.Score.Tracks); i++ {
instrument, exists := s.InstrumentForTrack(i)
if !exists {
return
}
if instrument != currentInstrument {
continue
}
if !yield(i) {
return
}
}
}
}

235
tracker/derived.go Normal file
View File

@ -0,0 +1,235 @@
package tracker
import (
"github.com/vsariola/sointu"
"iter"
"slices"
)
/*
from modelData we can derive useful information that can be cached for performance
or easy access, because of nested iterations over the score or patch data.
i.e. this needs to update when the model changes, and only then.
*/
type (
derivedForUnit struct {
unit *sointu.Unit
instrument *sointu.Instrument
sends []*sointu.Unit
}
derivedForTrack struct {
instrumentRange []int
tracksWithSameInstrument []int
title string
}
derivedModelData struct {
// map unit by ID
forUnit map[int]derivedForUnit
// map track by index
forTrack map[int]derivedForTrack
}
)
// public access functions
func (m *Model) forUnitById(id int) *derivedForUnit {
forUnit, ok := m.derived.forUnit[id]
if !ok {
return nil
}
return &forUnit
}
func (m *Model) InstrumentForUnit(id int) *sointu.Instrument {
fu := m.forUnitById(id)
if fu == nil {
return nil
}
return fu.instrument
}
func (m *Model) UnitById(id int) *sointu.Unit {
fu := m.forUnitById(id)
if fu == nil {
return nil
}
return fu.unit
}
func (m *Model) SendTargetsForUnit(id int) []*sointu.Unit {
fu := m.forUnitById(id)
if fu == nil {
return nil
}
return fu.sends
}
func (m *Model) forTrackByIndex(index int) *derivedForTrack {
forTrack, ok := m.derived.forTrack[index]
if !ok {
return nil
}
return &forTrack
}
func (m *Model) TrackTitle(index int) string {
ft := m.forTrackByIndex(index)
if ft == nil {
return ""
}
return ft.title
}
// public getters with further model information
func (m *Model) TracksWithSameInstrumentAsCurrent() []int {
currentTrack := m.d.Cursor.Track
return m.derived.forTrack[currentTrack].tracksWithSameInstrument
}
func (m *Model) CountNextTracksForCurrentInstrument() int {
currentTrack := m.d.Cursor.Track
count := 0
for t := range m.TracksWithSameInstrumentAsCurrent() {
if t > currentTrack {
count++
}
}
return count
}
// init / update methods
func (m *Model) initDerivedData() {
m.derived = derivedModelData{
forUnit: make(map[int]derivedForUnit),
forTrack: make(map[int]derivedForTrack),
}
m.updateDerivedScoreData()
m.updateDerivedPatchData()
}
func (m *Model) updateDerivedScoreData() {
for index, _ := range m.d.Song.Score.Tracks {
firstInstr, lastInstr, _ := m.instrumentRangeFor(index)
m.derived.forTrack[index] = derivedForTrack{
instrumentRange: []int{firstInstr, lastInstr},
tracksWithSameInstrument: slices.Collect(m.tracksWithSameInstrument(index)),
title: m.buildTrackTitle(index),
}
}
}
func (m *Model) updateDerivedPatchData() {
for _, instr := range m.d.Song.Patch {
for _, unit := range instr.Units {
m.derived.forUnit[unit.ID] = derivedForUnit{
unit: &unit,
instrument: &instr,
sends: slices.Collect(m.collectSendsTo(unit)),
}
}
}
}
// internals...
func (m *Model) collectSendsTo(unit sointu.Unit) iter.Seq[*sointu.Unit] {
return func(yield func(*sointu.Unit) bool) {
for _, instr := range m.d.Song.Patch {
for _, u := range instr.Units {
if u.Type != "send" {
continue
}
targetId, ok := u.Parameters["target"]
if !ok || targetId != unit.ID {
continue
}
if !yield(&u) {
return
}
}
}
}
}
func (m *Model) instrumentRangeFor(trackIndex int) (int, int, error) {
track := m.d.Song.Score.Tracks[trackIndex]
firstVoice := m.d.Song.Score.FirstVoiceForTrack(trackIndex)
lastVoice := firstVoice + track.NumVoices - 1
firstIndex, err1 := m.d.Song.Patch.InstrumentForVoice(firstVoice)
if err1 != nil {
return trackIndex, trackIndex, err1
}
lastIndex, err2 := m.d.Song.Patch.InstrumentForVoice(lastVoice)
if err2 != nil {
return trackIndex, trackIndex, err2
}
return firstIndex, lastIndex, nil
}
func (m *Model) buildTrackTitle(x int) (title string) {
title = "?"
if x < 0 || x >= len(m.d.Song.Score.Tracks) {
return
}
firstIndex, lastIndex, err := m.instrumentRangeFor(x)
if err != nil {
return
}
switch diff := lastIndex - firstIndex; diff {
case 0:
title = m.d.Song.Patch[firstIndex].Name
default:
n1 := m.d.Song.Patch[firstIndex].Name
n2 := m.d.Song.Patch[firstIndex+1].Name
if len(n1) > 0 {
n1 = string(n1[0])
} else {
n1 = "?"
}
if len(n2) > 0 {
n2 = string(n2[0])
} else {
n2 = "?"
}
if diff > 1 {
title = n1 + "/" + n2 + "..."
} else {
title = n1 + "/" + n2
}
}
return
}
func (m *Model) instrumentForTrack(trackIndex int) (int, bool) {
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(trackIndex)
instrument, err := m.d.Song.Patch.InstrumentForVoice(voiceIndex)
return instrument, err == nil
}
func (m *Model) tracksWithSameInstrument(trackIndex int) iter.Seq[int] {
return func(yield func(int) bool) {
currentInstrument, currentExists := m.instrumentForTrack(trackIndex)
if !currentExists {
return
}
for i := 0; i < len(m.d.Song.Score.Tracks); i++ {
instrument, exists := m.instrumentForTrack(i)
if !exists {
return
}
if instrument != currentInstrument {
continue
}
if !yield(i) {
return
}
}
}
}

View File

@ -220,12 +220,11 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
pxRowMarkWidth := gtx.Dp(trackRowMarkWidth)
colTitle := func(gtx C, i int) D {
h := gtx.Dp(unit.Dp(trackColTitleHeight))
title := ((*tracker.Order)(t.Model)).Title(i)
h := gtx.Dp(trackColTitleHeight)
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
LabelStyle{
Alignment: layout.N,
Text: title,
Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor,
Shaper: t.Theme.Shaper,
@ -294,7 +293,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
}
// draw the corresponding "fake cursors" for instrument-track-groups (for polyphony)
if hasTrackMidiIn {
for trackIndex := range ((*tracker.Order)(t.Model)).TrackIndicesForCurrentInstrument() {
for trackIndex := range t.Model.TracksWithSameInstrumentAsCurrent() {
if x == trackIndex && y == cursor.Y {
te.paintColumnCell(gtx, x, t, cursorNeighborForTrackMidiInColor)
}
@ -414,7 +413,7 @@ func (te *NoteEditor) HandleMidiInput(t *Tracker) {
return
}
te.scrollTable.Table.SetCursor2(te.scrollTable.Table.Cursor())
remaining := (*tracker.Order)(t.Model).CountNextTracksForCurrentInstrument()
remaining := t.Model.CountNextTracksForCurrentInstrument()
for i, note := range t.MidiNotePlaying {
t.Model.Notes().Table().Set(note)
te.scrollTable.Table.MoveCursor(1, 0)

View File

@ -68,8 +68,13 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
title := t.Model.Order().Title(i)
LabelStyle{Alignment: layout.NW, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
LabelStyle{
Alignment: layout.NW,
Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor,
Shaper: t.Theme.Shaper,
}.Layout(gtx)
return D{Size: image.Pt(gtx.Dp(patternCellWidth), h)}
}

View File

@ -247,20 +247,26 @@ type ParameterWidget struct {
}
type ParameterStyle struct {
tracker *Tracker
w *ParameterWidget
Theme *material.Theme
Focus bool
sends []sointu.Unit
tracker *Tracker
w *ParameterWidget
Theme *material.Theme
SendTargetTheme *material.Theme
Focus bool
sends []sointu.Unit
}
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
sends := slices.Collect(t.Model.CollectSendsTo(paramWidget.Parameter))
sendTargetTheme := th.WithPalette(material.Palette{
Bg: th.Bg,
Fg: paramIsSendTargetColor,
ContrastBg: th.ContrastBg,
ContrastFg: th.ContrastFg,
})
return ParameterStyle{
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
Theme: th,
w: paramWidget,
sends: sends,
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
Theme: th,
SendTargetTheme: &sendTargetTheme,
w: paramWidget,
}
}
@ -296,7 +302,6 @@ func (p ParameterStyle) Layout(gtx C) D {
}
sliderStyle := material.Slider(p.Theme, &p.w.floatWidget)
sliderStyle.Color = p.Theme.Fg
if len(sends) > 0 {
sliderStyle.Color = paramIsSendTargetColor
}
@ -374,11 +379,11 @@ func (p ParameterStyle) Layout(gtx C) D {
layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
label := Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper)
info := p.buildTooltip(sends)
info := p.buildSendTargetTooltip(sends)
if info == "" {
return label(gtx)
}
tooltip := component.PlatformTooltip(p.Theme, info)
tooltip := component.PlatformTooltip(p.SendTargetTheme, info)
return p.w.tipArea.Layout(gtx, tooltip, label)
}
return D{}
@ -413,7 +418,7 @@ func (p ParameterStyle) findSends() iter.Seq[sointu.Unit] {
}
}
func (p ParameterStyle) buildTooltip(sends []sointu.Unit) string {
func (p ParameterStyle) buildSendTargetTooltip(sends []sointu.Unit) string {
if len(sends) == 0 {
return ""
}
@ -424,7 +429,7 @@ func (p ParameterStyle) buildTooltip(sends []sointu.Unit) string {
sourceInstr := p.tracker.Model.InstrumentForUnit(sends[0].ID)
sourceInfo := ""
if sourceInstr != targetInstr {
sourceInfo = fmt.Sprintf(" (%s)", sourceInstr.Name)
sourceInfo = fmt.Sprintf(" from \"%s\"", sourceInstr.Name)
}
if amounts == "" {
amounts = fmt.Sprintf("x %d%s", sends[i].Parameters["amount"], sourceInfo)

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"iter"
"os"
"path/filepath"
@ -40,7 +39,8 @@ type (
}
Model struct {
d modelData
d modelData
derived derivedModelData
instrEnlarged bool
commentExpanded bool
@ -202,6 +202,7 @@ func NewModel(broker *Broker, synther sointu.Synther, midiContext MIDIContext, r
}
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.initDerivedData()
return m
}
@ -236,6 +237,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
m.updatePatternUseCount()
m.d.Cursor.SongPos = m.d.Song.Score.Clamp(m.d.Cursor.SongPos)
m.d.Cursor2.SongPos = m.d.Song.Score.Clamp(m.d.Cursor2.SongPos)
m.updateDerivedScoreData()
trySend(m.broker.ToPlayer, any(m.d.Song.Score.Copy()))
}
if m.changeType&PatchChange != 0 {
@ -251,6 +253,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
m.d.UnitIndex2 = clamp(m.d.UnitIndex2, 0, unitCount-1)
m.d.UnitSearching = false // if we change anything in the patch, reset the unit searching
m.d.UnitSearchString = ""
m.updateDerivedPatchData()
trySend(m.broker.ToPlayer, any(m.d.Song.Patch.Copy()))
}
if m.changeType&BPMChange != 0 {
@ -598,38 +601,3 @@ func clamp(a, min, max int) int {
}
return a
}
func (m *Model) CollectSendsTo(param Parameter) iter.Seq[sointu.Unit] {
return func(yield func(sointu.Unit) bool) {
p, ok := param.(NamedParameter)
if !ok {
return
}
for _, instr := range m.d.Song.Patch {
for _, unit := range instr.Units {
if unit.Type != "send" {
continue
}
targetId, ok := unit.Parameters["target"]
if !ok || targetId != p.Unit().ID {
continue
}
if !yield(unit) {
return
}
}
}
}
}
func (m *Model) InstrumentForUnit(id int) *sointu.Instrument {
for _, instr := range m.d.Song.Patch {
for _, unit := range instr.Units {
if unit.ID == id {
return &instr
}
}
}
// ID does not exist
return nil
}

View File

@ -1,7 +1,6 @@
package tracker
import (
"iter"
"math"
"github.com/vsariola/sointu"
@ -367,71 +366,6 @@ func (m *Order) SetValue(p Point, val int) {
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
}
func (e *Order) Title(x int) (title string) {
title = "?"
if x < 0 || x >= len(e.d.Song.Score.Tracks) {
return
}
firstIndex, lastIndex, err := e.instrumentListFor(x)
if err != nil {
return
}
switch diff := lastIndex - firstIndex; diff {
case 0:
title = e.d.Song.Patch[firstIndex].Name
default:
n1 := e.d.Song.Patch[firstIndex].Name
n2 := e.d.Song.Patch[firstIndex+1].Name
if len(n1) > 0 {
n1 = string(n1[0])
} else {
n1 = "?"
}
if len(n2) > 0 {
n2 = string(n2[0])
} else {
n2 = "?"
}
if diff > 1 {
title = n1 + "/" + n2 + "..."
} else {
title = n1 + "/" + n2
}
}
return
}
func (e *Order) instrumentListFor(trackIndex int) (int, int, error) {
track := e.d.Song.Score.Tracks[trackIndex]
firstVoice := e.d.Song.Score.FirstVoiceForTrack(trackIndex)
lastVoice := firstVoice + track.NumVoices - 1
firstIndex, err1 := e.d.Song.Patch.InstrumentForVoice(firstVoice)
if err1 != nil {
return trackIndex, trackIndex, err1
}
lastIndex, err2 := e.d.Song.Patch.InstrumentForVoice(lastVoice)
if err2 != nil {
return trackIndex, trackIndex, err2
}
return firstIndex, lastIndex, nil
}
func (e *Order) TrackIndicesForCurrentInstrument() iter.Seq[int] {
currentTrack := e.d.Cursor.Track
return e.d.Song.AllTracksWithSameInstrument(currentTrack)
}
func (e *Order) CountNextTracksForCurrentInstrument() int {
currentTrack := e.d.Cursor.Track
count := 0
for t := range e.TrackIndicesForCurrentInstrument() {
if t > currentTrack {
count++
}
}
return count
}
// NoteTable
func (v *Notes) Table() Table {