mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-27 19:00:25 -04:00
354 lines
8.5 KiB
Go
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
|
|
}
|