sointu/tracker/derived.go
2024-11-10 00:02:13 +02:00

354 lines
8.5 KiB
Go

package tracker
import (
"fmt"
"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 (
derivedModelData struct {
// map Unit by ID, other entities by their respective index
forUnit map[int]derivedForUnit
forTrack []derivedForTrack
forPattern []derivedForPattern
}
derivedForUnit struct {
unit sointu.Unit
instrument sointu.Instrument
instrumentIndex int
unitIndex int
// map param by Name
forParameter map[string]derivedForParameter
}
derivedForParameter struct {
sendTooltip string
sendSources []sendSourceData
}
sendSourceData struct {
unitId int
paramName string
amount int
instrumentIndex int
instrumentName string
}
derivedForTrack struct {
instrumentRange []int
tracksWithSameInstrument []int
title string
}
derivedForPattern struct {
useCount []int
}
)
// public access functions
func (m *Model) InstrumentForUnit(id int) (sointu.Instrument, int, bool) {
forUnit, ok := m.derived.forUnit[id]
if !ok {
return sointu.Instrument{}, -1, false
}
return forUnit.instrument, forUnit.instrumentIndex, true
}
func (m *Model) UnitInfo(id int) (instrName string, units []sointu.Unit, unitIndex int, ok bool) {
fu, ok := m.derived.forUnit[id]
return fu.instrument.Name, fu.instrument.Units, fu.unitIndex, ok
}
func (m *Model) UnitHintInfo(id int) (instrIndex int, unitType string, ok bool) {
fu, ok := m.derived.forUnit[id]
return fu.instrumentIndex, fu.unit.Type, ok
}
func (m *Model) ParameterInfo(unitId int, paramName string) (isSendTarget bool, tooltip string, exists bool) {
du, ok1 := m.derived.forUnit[unitId]
if !ok1 {
return false, "", false
}
dp, ok2 := du.forParameter[paramName]
if !ok2 {
return false, "", false
}
return len(dp.sendSources) > 0, dp.sendTooltip, true
}
func (m *Model) TrackTitle(index int) string {
if index < 0 || index > len(m.derived.forTrack) {
return ""
}
return m.derived.forTrack[index].title
}
func (m *Model) PatternUseCount(index int) []int {
if index < 0 || index > len(m.derived.forPattern) {
return nil
}
return m.derived.forPattern[index].useCount
}
func (m *Model) PatternUnique(t, p int) bool {
if t < 0 || t > len(m.derived.forPattern) {
return false
}
forPattern := m.derived.forPattern[t]
if p < 0 || p >= len(forPattern.useCount) {
return false
}
return forPattern.useCount[p] == 1
}
// public getters with further model information
func (m *Model) TracksWithSameInstrumentAsCurrent() []int {
currentTrack := m.d.Cursor.Track
if currentTrack > len(m.derived.forTrack) {
return nil
}
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([]derivedForTrack, 0),
forPattern: make([]derivedForPattern, 0),
}
m.updateDerivedScoreData()
m.updateDerivedPatchData()
}
func (m *Model) updateDerivedScoreData() {
m.derived.forTrack = m.derived.forTrack[:0]
m.derived.forPattern = m.derived.forPattern[:0]
for index, track := range m.d.Song.Score.Tracks {
firstInstr, lastInstr, _ := m.instrumentRangeFor(index)
m.derived.forTrack = append(
m.derived.forTrack,
derivedForTrack{
instrumentRange: []int{firstInstr, lastInstr},
tracksWithSameInstrument: slices.Collect(m.tracksWithSameInstrument(index)),
title: m.buildTrackTitle(index),
},
)
m.derived.forPattern = append(
m.derived.forPattern,
derivedForPattern{
useCount: m.calcPatternUseCounts(track),
},
)
}
}
func (m *Model) updateDerivedPatchData() {
clear(m.derived.forUnit)
for i, instr := range m.d.Song.Patch {
for u, unit := range instr.Units {
m.derived.forUnit[unit.ID] = derivedForUnit{
unit: unit,
unitIndex: u,
instrument: instr,
instrumentIndex: i,
forParameter: make(map[string]derivedForParameter),
}
m.updateDerivedParameterData(unit)
}
}
}
func (m *Model) updateDerivedParameterData(unit sointu.Unit) {
fu, _ := m.derived.forUnit[unit.ID]
for name := range fu.unit.Parameters {
sendSources := slices.Collect(m.collectSendSources(unit, name))
fu.forParameter[name] = derivedForParameter{
sendSources: sendSources,
sendTooltip: m.buildSendTargetTooltip(fu.instrumentIndex, sendSources),
}
}
}
// internals...
func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] {
return func(yield func(sendSourceData) bool) {
for i, 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
}
port := u.Parameters["port"]
unitParam, ok := sointu.FindParamForModulationPort(unit.Type, port)
if !ok || unitParam.Name != paramName {
continue
}
sourceData := sendSourceData{
unitId: u.ID,
paramName: paramName,
instrumentIndex: i,
instrumentName: instr.Name,
amount: u.Parameters["amount"],
}
if !yield(sourceData) {
return
}
}
}
}
}
func (m *Model) buildSendTargetTooltip(ownInstrIndex int, sendSources []sendSourceData) string {
if len(sendSources) == 0 {
return ""
}
amounts := ""
for _, sendSource := range sendSources {
sourceInfo := ""
if sendSource.instrumentIndex != ownInstrIndex {
sourceInfo = fmt.Sprintf(" from \"%s\"", sendSource.instrumentName)
}
if amounts == "" {
amounts = fmt.Sprintf("x %d%s", sendSource.amount, sourceInfo)
} else {
amounts = fmt.Sprintf("%s, x %d%s", amounts, sendSource.amount, sourceInfo)
}
}
count := "1 send"
if len(sendSources) > 1 {
count = fmt.Sprintf("%d sends", len(sendSources))
}
return fmt.Sprintf("%s [%s]", count, amounts)
}
func (m *Model) instrumentRangeFor(trackIndex int) (int, int, error) {
track := m.d.Song.Score.Tracks[trackIndex]
if track.NumVoices <= 0 {
return 0, 0, fmt.Errorf("track %d has no voices", 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
}
}
}
}
func (m *Model) calcPatternUseCounts(track sointu.Track) []int {
result := make([]int, len(m.d.Song.Score.Tracks))
for j, _ := range result {
result[j] = 0
}
for j := 0; j < m.d.Song.Score.Length; j++ {
if j >= len(track.Order) {
break
}
p := track.Order[j]
for len(result) <= p {
result = append(result, 0)
}
if p < 0 {
continue
}
result[p]++
}
return result
}