diff --git a/song.go b/song.go index ab1ad95..11b3cdd 100644 --- a/song.go +++ b/song.go @@ -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 - } - } - } -} diff --git a/tracker/derived.go b/tracker/derived.go new file mode 100644 index 0000000..e57c8b6 --- /dev/null +++ b/tracker/derived.go @@ -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 + } + } + } +} diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index 086a622..f03dea0 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -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) diff --git a/tracker/gioui/order_editor.go b/tracker/gioui/order_editor.go index 79f8529..06b853d 100644 --- a/tracker/gioui/order_editor.go +++ b/tracker/gioui/order_editor.go @@ -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)} } diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index f0adee4..0e711f7 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -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) diff --git a/tracker/model.go b/tracker/model.go index 5d759cc..02dba9e 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -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 -} diff --git a/tracker/table.go b/tracker/table.go index d598d10..a270218 100644 --- a/tracker/table.go +++ b/tracker/table.go @@ -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 {