mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-01 21:30:19 -05:00
192 lines
6.4 KiB
Go
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
|
|
}
|