mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-19 14:43:29 -05:00
refactor(tracker): group Model methods, with each group in one source file
This commit is contained in:
parent
b93304adab
commit
86ca3fb300
191
tracker/voices.go
Normal file
191
tracker/voices.go
Normal file
@ -0,0 +1,191 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user