Files
sointu/tracker/voices.go
2026-01-27 22:16:14 +02:00

192 lines
6.4 KiB
Go

package tracker
import (
"fmt"
"math"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
"gopkg.in/yaml.v3"
)
// VoiceSlice works similar to the Slice function, but takes a slice of
// NumVoicer:s and treats it as a "virtual slice", with element repeated by the
// number of voices it has. NumVoicer interface is implemented at least by
// sointu.Tracks and sointu.Instruments. For example, if parameter "slice" has
// three elements, returning GetNumVoices 2, 1, and 3, the VoiceSlice thinks of
// this as a virtual slice of 6 elements [0,0,1,2,2,2]. Then, the "ranges"
// parameter are slicing ranges to this virtual slice. Continuing with the
// example, if "ranges" was [2,5), the virtual slice would be [1,2,2], and the
// function would return a slice with two elements: first with NumVoices 1 and
// second with NumVoices 2. If multiple ranges are given, multiple virtual
// slices are concatenated. However, when doing so, splitting an element is not
// allowed. In the previous example, if the ranges were [1,3) and [0,1), the
// resulting concatenated virtual slice would be [0,1,0], and here the 0 element
// would be split. This is to avoid accidentally making shallow copies of
// reference types.
func VoiceSlice[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, ranges ...Range) (ret S, ok bool) {
ret = make(S, 0, len(slice))
last := -1
used := make([]bool, len(slice))
outer:
for _, r := range ranges {
left := 0
for i, elem := range slice {
right := left + (P)(&slice[i]).GetNumVoices()
if left >= r.End {
continue outer
}
if right <= r.Start {
left = right
continue
}
overlap := min(right, r.End) - max(left, r.Start)
if last == i {
(P)(&ret[len(ret)-1]).SetNumVoices(
(P)(&ret[len(ret)-1]).GetNumVoices() + overlap)
} else {
if last == math.MaxInt || used[i] {
return nil, false
}
ret = append(ret, elem)
(P)(&ret[len(ret)-1]).SetNumVoices(overlap)
used[i] = true
}
last = i
left = right
}
if left >= r.End {
continue outer
}
last = math.MaxInt // the list is closed, adding more elements causes it to fail
}
return ret, true
}
// VoiceInsert tries adding the elements "added" to the slice "orig" at the
// voice index "index". Notice that index is the index into a virtual slice
// where each element is repeated by the number of voices it has. If the index
// is between elements, the new elements are added in between the old elements.
// If the addition would cause splitting of an element, we rather increase the
// number of voices the element has, but do not split it.
func VoiceInsert[T any, S ~[]T, P sointu.NumVoicerPointer[T]](orig S, index, length int, added ...T) (ret S, retRange Range, ok bool) {
ret = make(S, 0, len(orig)+length)
left := 0
for i, elem := range orig {
right := left + (P)(&orig[i]).GetNumVoices()
if left == index { // we are between elements and it's safe to add there
if sointu.TotalVoices[T, S, P](added) < length {
return nil, Range{}, false // we are missing some elements
}
retRange = Range{len(ret), len(ret) + len(added)}
ret = append(ret, added...)
} else if left < index && index < right { // we are inside an element and would split it; just increase its voices instead of splitting
(P)(&elem).SetNumVoices((P)(&orig[i]).GetNumVoices() + sointu.TotalVoices[T, S, P](added))
retRange = Range{len(ret), len(ret)}
}
ret = append(ret, elem)
left = right
}
if left == index { // we are at the end and it's safe to add there, even if we are missing some elements
retRange = Range{len(ret), len(ret) + len(added)}
ret = append(ret, added...)
}
return ret, retRange, true
}
func VoiceRange[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, indexRange Range) (voiceRange Range) {
indexRange.Start = max(0, indexRange.Start)
indexRange.End = min(len(slice), indexRange.End)
for _, e := range slice[:indexRange.Start] {
voiceRange.Start += (P)(&e).GetNumVoices()
}
voiceRange.End = voiceRange.Start
for i := indexRange.Start; i < indexRange.End; i++ {
voiceRange.End += (P)(&slice[i]).GetNumVoices()
}
return
}
// helpers
func (m *Model) sliceInstrumentsTracks(instruments, tracks bool, ranges ...Range) (ok bool) {
defer m.change("sliceInstrumentsTracks", PatchChange, MajorChange)()
if instruments {
m.d.Song.Patch, ok = VoiceSlice(m.d.Song.Patch, ranges...)
if !ok {
goto fail
}
}
if tracks {
m.d.Song.Score.Tracks, ok = VoiceSlice(m.d.Song.Score.Tracks, ranges...)
if !ok {
goto fail
}
}
return true
fail:
(*Model)(m).Alerts().AddNamed("slicesInstrumentsTracks", "Modify prevented by Instrument-Track linking", Warning)
m.changeCancel = true
return false
}
func (m *Model) marshalVoices(r Range) (data []byte, err error) {
patch, ok := VoiceSlice(m.d.Song.Patch, r)
if !ok {
return nil, fmt.Errorf("marshalVoiceRange: slicing patch failed")
}
tracks, ok := VoiceSlice(m.d.Song.Score.Tracks, r)
if !ok {
return nil, fmt.Errorf("marshalVoiceRange: slicing tracks failed")
}
return yaml.Marshal(struct {
Patch sointu.Patch
Tracks []sointu.Track
}{patch, tracks})
}
func (m *Model) unmarshalVoices(voiceIndex int, data []byte, instruments, tracks bool) (instrRange, trackRange Range, ok bool) {
var d struct {
Patch sointu.Patch
Tracks []sointu.Track
}
if err := yaml.Unmarshal(data, &d); err != nil {
return Range{}, Range{}, false
}
return m.addVoices(voiceIndex, d.Patch, d.Tracks, instruments, tracks)
}
func (m *Model) addVoices(voiceIndex int, p sointu.Patch, t []sointu.Track, instruments, tracks bool) (instrRange Range, trackRange Range, ok bool) {
defer m.change("addVoices", PatchChange, MajorChange)()
addedLength := max(p.NumVoices(), sointu.TotalVoices(t))
if instruments {
m.assignUnitIDsForPatch(p)
m.d.Song.Patch, instrRange, ok = VoiceInsert(m.d.Song.Patch, voiceIndex, addedLength, p...)
if !ok {
goto fail
}
}
if tracks {
m.d.Song.Score.Tracks, trackRange, ok = VoiceInsert(m.d.Song.Score.Tracks, voiceIndex, addedLength, t...)
if !ok {
goto fail
}
}
return instrRange, trackRange, true
fail:
(*Model)(m).Alerts().AddNamed("addVoices", "Adding voices prevented by Instrument-Track linking", Warning)
m.changeCancel = true
return Range{}, Range{}, false
}
func (m *Model) remainingVoices(instruments, tracks bool) (ret int) {
ret = math.MaxInt
if instruments {
ret = min(ret, vm.MAX_VOICES-m.d.Song.Patch.NumVoices())
}
if tracks {
ret = min(ret, vm.MAX_VOICES-m.d.Song.Score.NumVoices())
}
return
}