mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat: keeping instruments and tracks linked & splitting them
Also includes a refactoring of the List: all methods that accepted or returned a [from, to] range now return a Range, which is non-inclusive i.e. [start,end). Also the assignUnitIds was slightly refactored & a new function called assignUnitIdsForPatch was added, to assign all unit IDs for an patch at once. Closes #157, #163.
This commit is contained in:
parent
025f8832d9
commit
216cde2365
@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Toggle button to keep instruments and tracks linked, and buttons to to split
|
||||
instruments and tracks with more than 1 voice into parallel ones
|
||||
([#163][i163], [#157][i157])
|
||||
- Mute and solo toggles for instruments ([#168][i168])
|
||||
- Compressor displays threshold and invgain in dB
|
||||
- Dragging mouse to select rectangles in the tables
|
||||
@ -263,8 +266,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
[i150]: https://github.com/vsariola/sointu/issues/150
|
||||
[i151]: https://github.com/vsariola/sointu/issues/151
|
||||
[i154]: https://github.com/vsariola/sointu/issues/154
|
||||
[i157]: https://github.com/vsariola/sointu/issues/157
|
||||
[i158]: https://github.com/vsariola/sointu/issues/158
|
||||
[i160]: https://github.com/vsariola/sointu/issues/160
|
||||
[i162]: https://github.com/vsariola/sointu/issues/162
|
||||
[i163]: https://github.com/vsariola/sointu/issues/163
|
||||
[i166]: https://github.com/vsariola/sointu/issues/166
|
||||
[i168]: https://github.com/vsariola/sointu/issues/168
|
@ -73,7 +73,12 @@ func init() {
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, NullMIDIContext{}, recoveryFile)
|
||||
|
||||
t := gioui.NewTracker(model)
|
||||
tracker.Bool{BoolData: (*tracker.InstrEnlarged)(model)}.Set(true)
|
||||
model.InstrEnlarged().Bool().Set(true)
|
||||
// since the VST is usually working without any regard for the tracks
|
||||
// until recording, disable the Instrument-Track linking by default
|
||||
// because it might just confuse the user why instrument cannot be
|
||||
// swapped/added etc.
|
||||
model.LinkInstrTrack().Bool().Set(false)
|
||||
go t.Main()
|
||||
context := VSTIProcessContext{host: h}
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
|
17
patch.go
17
patch.go
@ -356,6 +356,15 @@ func (instr *Instrument) Copy() Instrument {
|
||||
return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units, Mute: instr.Mute}
|
||||
}
|
||||
|
||||
// Implement the counter interface
|
||||
func (i *Instrument) GetNumVoices() int {
|
||||
return i.NumVoices
|
||||
}
|
||||
|
||||
func (i *Instrument) SetNumVoices(count int) {
|
||||
i.NumVoices = count
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of a Patch.
|
||||
func (p Patch) Copy() Patch {
|
||||
instruments := make([]Instrument, len(p))
|
||||
@ -409,11 +418,11 @@ func (p Patch) NumSyncs() int {
|
||||
// FirstVoiceForInstrument(1) returns 1 and FirstVoiceForInstrument(2) returns
|
||||
// 4. Essentially computes just the cumulative sum.
|
||||
func (p Patch) FirstVoiceForInstrument(instrIndex int) int {
|
||||
ret := 0
|
||||
for _, t := range p[:instrIndex] {
|
||||
ret += t.NumVoices
|
||||
if instrIndex < 0 {
|
||||
return 0
|
||||
}
|
||||
return ret
|
||||
instrIndex = min(instrIndex, len(p))
|
||||
return TotalVoices(p[:instrIndex])
|
||||
}
|
||||
|
||||
// InstrumentForVoice returns the instrument number for the given voice index.
|
||||
|
42
song.go
42
song.go
@ -75,6 +75,21 @@ type (
|
||||
OrderRow int
|
||||
PatternRow int
|
||||
}
|
||||
|
||||
// NumVoicer is used for slices where elements have NumVoices, of which
|
||||
// there are two: Tracks and Instruments.
|
||||
NumVoicer interface {
|
||||
GetNumVoices() int
|
||||
SetNumVoices(count int)
|
||||
}
|
||||
|
||||
// NumVoicerPointer is a helper interface for type constraints, as
|
||||
// SetNumVoices needs to be defined with a pointer receiver to be able to
|
||||
// actually modify the value.
|
||||
NumVoicerPointer[M any] interface {
|
||||
*M
|
||||
NumVoicer
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Score) SongPos(songRow int) SongPos {
|
||||
@ -255,11 +270,11 @@ func (l Score) NumVoices() int {
|
||||
// returns 1 and FirstVoiceForTrack(2) returns 4. Essentially computes just the
|
||||
// cumulative sum.
|
||||
func (l Score) FirstVoiceForTrack(track int) int {
|
||||
ret := 0
|
||||
for _, t := range l.Tracks[:track] {
|
||||
ret += t.NumVoices
|
||||
if track < 0 {
|
||||
return 0
|
||||
}
|
||||
return ret
|
||||
track = min(track, len(l.Tracks))
|
||||
return TotalVoices(l.Tracks[:track])
|
||||
}
|
||||
|
||||
// LengthInRows returns just RowsPerPattern * Length, as the length is the
|
||||
@ -297,3 +312,22 @@ func (s *Song) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// *Track implements NumVoicer interface
|
||||
|
||||
func (t *Track) GetNumVoices() int {
|
||||
return t.NumVoices
|
||||
}
|
||||
|
||||
func (t *Track) SetNumVoices(c int) {
|
||||
t.NumVoices = c
|
||||
}
|
||||
|
||||
// TotalVoices returns the total number of voices used in the slice; summing the
|
||||
// GetNumVoices of every element
|
||||
func TotalVoices[T any, S ~[]T, P NumVoicerPointer[T]](slice S) (ret int) {
|
||||
for _, e := range slice {
|
||||
ret += (P)(&e).GetNumVoices()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -44,21 +45,12 @@ func (m *Model) AddTrack() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("AddTrackAction", ScoreChange, MajorChange)()
|
||||
if len(m.d.Song.Score.Tracks) == 0 { // no instruments, add one
|
||||
m.d.Cursor.Track = 0
|
||||
} else {
|
||||
m.d.Cursor.Track++
|
||||
}
|
||||
m.d.Cursor.Track = max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)), 0)
|
||||
newTracks := make([]sointu.Track, len(m.d.Song.Score.Tracks)+1)
|
||||
copy(newTracks, m.d.Song.Score.Tracks[:m.d.Cursor.Track])
|
||||
copy(newTracks[m.d.Cursor.Track+1:], m.d.Song.Score.Tracks[m.d.Cursor.Track:])
|
||||
newTracks[m.d.Cursor.Track] = sointu.Track{
|
||||
NumVoices: 1,
|
||||
Patterns: []sointu.Pattern{},
|
||||
}
|
||||
m.d.Song.Score.Tracks = newTracks
|
||||
defer (*Model)(m).change("AddTrack", SongChange, MajorChange)()
|
||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||
p := sointu.Patch{defaultInstrument.Copy()}
|
||||
t := []sointu.Track{sointu.Track{NumVoices: 1}}
|
||||
_, _, ok := m.addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true)
|
||||
m.changeCancel = !ok
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -74,20 +66,12 @@ func (m *Model) AddInstrument() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("AddInstrumentAction", PatchChange, MajorChange)()
|
||||
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
||||
m.d.InstrIndex = 0
|
||||
} else {
|
||||
m.d.InstrIndex++
|
||||
}
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, sointu.Instrument{})
|
||||
copy(m.d.Song.Patch[m.d.InstrIndex+1:], m.d.Song.Patch[m.d.InstrIndex:])
|
||||
newInstr := defaultInstrument.Copy()
|
||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
m.d.UnitIndex = 0
|
||||
m.d.ParamIndex = 0
|
||||
defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)()
|
||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||
p := sointu.Patch{defaultInstrument.Copy()}
|
||||
t := []sointu.Track{sointu.Track{NumVoices: 1}}
|
||||
_, _, ok := m.addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack)
|
||||
m.changeCancel = !ok
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -99,6 +83,62 @@ func (m *Model) DeleteInstrument() Action {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SplitTrack() Action {
|
||||
return Action{
|
||||
allowed: func() bool {
|
||||
return m.d.Cursor.Track >= 0 && m.d.Cursor.Track < len(m.d.Song.Score.Tracks) && m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices > 1
|
||||
},
|
||||
do: func() {
|
||||
defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)()
|
||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||
middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2
|
||||
end := voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices
|
||||
left, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{math.MinInt, middle})
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
return
|
||||
}
|
||||
right, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{end, math.MaxInt})
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
return
|
||||
}
|
||||
newTrack := sointu.Track{NumVoices: end - middle}
|
||||
m.d.Song.Score.Tracks = append(left, newTrack)
|
||||
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SplitInstrument() Action {
|
||||
return Action{
|
||||
allowed: func() bool {
|
||||
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1
|
||||
},
|
||||
do: func() {
|
||||
defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)()
|
||||
voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex)
|
||||
middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2
|
||||
end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices
|
||||
left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle})
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
return
|
||||
}
|
||||
right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt})
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
return
|
||||
}
|
||||
newInstrument := defaultInstrument.Copy()
|
||||
m.assignUnitIDs(newInstrument.Units)
|
||||
newInstrument.NumVoices = end - middle
|
||||
m.d.Song.Patch = append(left, newInstrument)
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, right...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddUnit(before bool) Action {
|
||||
return Allow(func() {
|
||||
defer (*Model)(m).change("AddUnitAction", PatchChange, MajorChange)()
|
||||
@ -295,10 +335,10 @@ func (m *Model) PlaySelected() Action {
|
||||
m.setPanic(false)
|
||||
m.playing = true
|
||||
l := m.OrderRows().List()
|
||||
a, b := l.listRange()
|
||||
newLoop := Loop{a, b - a + 1}
|
||||
r := l.listRange()
|
||||
newLoop := Loop{r.Start, r.End - r.Start}
|
||||
m.setLoop(newLoop)
|
||||
m.send(StartPlayMsg{sointu.SongPos{OrderRow: a, PatternRow: 0}})
|
||||
m.send(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ type (
|
||||
UniquePatterns Model
|
||||
Mute Model
|
||||
Solo Model
|
||||
LinkInstrTrack Model
|
||||
)
|
||||
|
||||
func (v Bool) Toggle() {
|
||||
@ -51,6 +52,7 @@ func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
||||
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
|
||||
func (m *Model) Mute() *Mute { return (*Mute)(m) }
|
||||
func (m *Model) Solo() *Solo { return (*Solo)(m) }
|
||||
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
|
||||
|
||||
// Panic methods
|
||||
|
||||
@ -160,9 +162,9 @@ func (m *UnitDisabled) setValue(val bool) {
|
||||
return
|
||||
}
|
||||
l := ((*Model)(m)).Units().List()
|
||||
a, b := l.listRange()
|
||||
r := l.listRange()
|
||||
defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)()
|
||||
for i := a; i <= b; i++ {
|
||||
for i := r.Start; i < r.End; i++ {
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[i].Disabled = val
|
||||
}
|
||||
}
|
||||
@ -185,8 +187,8 @@ func (t *LoopToggle) setValue(val bool) {
|
||||
newLoop := Loop{}
|
||||
if val {
|
||||
l := m.OrderRows().List()
|
||||
a, b := l.listRange()
|
||||
newLoop = Loop{a, b - a + 1}
|
||||
r := l.listRange()
|
||||
newLoop = Loop{r.Start, r.End - r.Start}
|
||||
}
|
||||
m.setLoop(newLoop)
|
||||
}
|
||||
@ -249,3 +251,10 @@ func (m *Solo) setValue(val bool) {
|
||||
}
|
||||
}
|
||||
func (m *Solo) Enabled() bool { return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) }
|
||||
|
||||
// LinkInstrTrack methods
|
||||
|
||||
func (m *LinkInstrTrack) Bool() Bool { return Bool{m} }
|
||||
func (m *LinkInstrTrack) Value() bool { return m.linkInstrTrack }
|
||||
func (m *LinkInstrTrack) setValue(val bool) { m.linkInstrTrack = val }
|
||||
func (m *LinkInstrTrack) Enabled() bool { return true }
|
||||
|
@ -27,6 +27,8 @@ type InstrumentEditor struct {
|
||||
newInstrumentBtn *ActionClickable
|
||||
enlargeBtn *BoolClickable
|
||||
deleteInstrumentBtn *ActionClickable
|
||||
linkInstrTrackBtn *BoolClickable
|
||||
splitInstrumentBtn *ActionClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
@ -55,6 +57,9 @@ type InstrumentEditor struct {
|
||||
deleteInstrumentHint string
|
||||
muteHint, unmuteHint string
|
||||
soloHint, unsoloHint string
|
||||
linkDisabledHint string
|
||||
linkEnabledHint string
|
||||
splitInstrumentHint string
|
||||
}
|
||||
|
||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
@ -62,6 +67,8 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
newInstrumentBtn: NewActionClickable(model.AddInstrument()),
|
||||
enlargeBtn: NewBoolClickable(model.InstrEnlarged().Bool()),
|
||||
deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()),
|
||||
linkInstrTrackBtn: NewBoolClickable(model.LinkInstrTrack().Bool()),
|
||||
splitInstrumentBtn: NewActionClickable(model.SplitInstrument()),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
@ -95,6 +102,9 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
ret.unmuteHint = makeHint("Unmute", " (%s)", "MuteToggle")
|
||||
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
||||
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||
ret.linkDisabledHint = makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle")
|
||||
ret.linkEnabledHint = makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle")
|
||||
ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument")
|
||||
return ret
|
||||
}
|
||||
|
||||
@ -116,6 +126,7 @@ func (ie *InstrumentEditor) childFocused(gtx C) bool {
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.childFocused(gtx)
|
||||
fullscreenBtnStyle := ToggleIcon(gtx, t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, ie.enlargeHint, ie.shrinkHint)
|
||||
linkBtnStyle := ToggleIcon(gtx, t.Theme, ie.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, ie.linkDisabledHint, ie.linkEnabledHint)
|
||||
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
@ -141,6 +152,9 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, linkBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
@ -173,6 +187,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||
deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint)
|
||||
splitInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.splitInstrumentBtn, icons.CommunicationCallSplit, ie.splitInstrumentHint)
|
||||
soloBtnStyle := ToggleIcon(gtx, t.Theme, ie.soloBtn, icons.SocialGroup, icons.SocialPerson, ie.soloHint, ie.unsoloHint)
|
||||
muteBtnStyle := ToggleIcon(gtx, t.Theme, ie.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, ie.muteHint, ie.unmuteHint)
|
||||
|
||||
@ -209,6 +224,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
dims := numStyle.Layout(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(splitInstrumentBtnStyle.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(soloBtnStyle.Layout),
|
||||
|
@ -13,9 +13,12 @@
|
||||
- {key: "O", shortcut: true, action: "OpenSong"}
|
||||
- {key: "I", shortcut: true, shift: true, action: "DeleteInstrument"}
|
||||
- {key: "I", shortcut: true, action: "AddInstrument"}
|
||||
- {key: "I", shortcut: true, alt: true, action: "SplitInstrument"}
|
||||
- {key: "T", shortcut: true, shift: true, action: "DeleteTrack"}
|
||||
- {key: "T", shortcut: true, alt: true, action: "SplitTrack"}
|
||||
- {key: "T", shortcut: true, action: "AddTrack"}
|
||||
- {key: "E", shortcut: true, action: "InstrEnlargedToggle"}
|
||||
- {key: "K", shortcut: true, action: "LinkInstrTrackToggle"}
|
||||
- {key: "W", shortcut: true, action: "Quit"}
|
||||
- {key: "Space", action: "PlayingToggleUnfollow"}
|
||||
- {key: "Space", shift: true, action: "PlayingToggleFollow"}
|
||||
|
@ -199,6 +199,10 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
t.ExportFloat().Do()
|
||||
case "ExportInt16":
|
||||
t.ExportInt16().Do()
|
||||
case "SplitTrack":
|
||||
t.SplitTrack().Do()
|
||||
case "SplitInstrument":
|
||||
t.SplitInstrument().Do()
|
||||
// Booleans
|
||||
case "PanicToggle":
|
||||
t.Panic().Bool().Toggle()
|
||||
@ -212,6 +216,8 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
t.Playing().Bool().Toggle()
|
||||
case "InstrEnlargedToggle":
|
||||
t.InstrEnlarged().Bool().Toggle()
|
||||
case "LinkInstrTrackToggle":
|
||||
t.LinkInstrTrack().Bool().Toggle()
|
||||
case "CommentExpandedToggle":
|
||||
t.CommentExpanded().Bool().Toggle()
|
||||
case "FollowToggle":
|
||||
|
@ -50,9 +50,11 @@ func init() {
|
||||
}
|
||||
|
||||
type NoteEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *ActionClickable
|
||||
DeleteTrackBtn *ActionClickable
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *ActionClickable
|
||||
DeleteTrackBtn *ActionClickable
|
||||
SplitTrackBtn *ActionClickable
|
||||
|
||||
AddSemitoneBtn *ActionClickable
|
||||
SubtractSemitoneBtn *ActionClickable
|
||||
AddOctaveBtn *ActionClickable
|
||||
@ -67,6 +69,7 @@ type NoteEditor struct {
|
||||
deleteTrackHint string
|
||||
addTrackHint string
|
||||
uniqueOffTip, uniqueOnTip string
|
||||
splitTrackHint string
|
||||
}
|
||||
|
||||
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
@ -74,6 +77,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
||||
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
||||
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
||||
SplitTrackBtn: NewActionClickable(model.SplitTrack()),
|
||||
AddSemitoneBtn: NewActionClickable(model.AddSemitone()),
|
||||
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()),
|
||||
AddOctaveBtn: NewActionClickable(model.AddOctave()),
|
||||
@ -103,6 +107,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
ret.addTrackHint = makeHint("Add\ntrack", "\n(%s)", "AddTrack")
|
||||
ret.uniqueOnTip = makeHint("Duplicate non-unique patterns", " (%s)", "UniquePatternsToggle")
|
||||
ret.uniqueOffTip = makeHint("Allow editing non-unique patterns", " (%s)", "UniquePatternsToggle")
|
||||
ret.splitTrackHint = makeHint("Split track", " (%s)", "SplitTrack")
|
||||
return ret
|
||||
}
|
||||
|
||||
@ -148,6 +153,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||
splitTrackBtnStyle := ActionIcon(gtx, t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
||||
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
@ -167,6 +173,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
layout.Rigid(uniqueBtnStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Rigid(splitTrackBtnStyle.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
|
@ -2,8 +2,6 @@ package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -141,19 +139,25 @@ func (v *InstrumentVoices) Value() int {
|
||||
return max(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) setValue(value int) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
func (m *InstrumentVoices) setValue(value int) {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].NumVoices = value
|
||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices}
|
||||
ranges := MakeSetLength(voiceRange, value)
|
||||
ok := (*Model)(m).sliceInstrumentsTracks(true, m.linkInstrTrack, ranges...)
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) Range() intRange {
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Patch.NumVoices() + v.Value()}
|
||||
return intRange{1, (*Model)(v).remainingVoices(true, v.linkInstrTrack) + v.Value()}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentVoicesInt."+kind, PatchChange, MinorChange)
|
||||
return (*Model)(v).change("InstrumentVoices."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// TrackVoicesInt
|
||||
@ -170,12 +174,14 @@ func (v *TrackVoices) Value() int {
|
||||
return max(v.d.Song.Score.Tracks[t].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *TrackVoices) setValue(value int) {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return
|
||||
func (m *TrackVoices) setValue(value int) {
|
||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices}
|
||||
ranges := MakeSetLength(voiceRange, value)
|
||||
ok := (*Model)(m).sliceInstrumentsTracks(m.linkInstrTrack, true, ranges...)
|
||||
if !ok {
|
||||
m.changeCancel = true
|
||||
}
|
||||
v.d.Song.Score.Tracks[t].NumVoices = value
|
||||
}
|
||||
|
||||
func (v *TrackVoices) Range() intRange {
|
||||
@ -183,9 +189,9 @@ func (v *TrackVoices) Range() intRange {
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return intRange{1, 1}
|
||||
}
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Score.NumVoices() + v.d.Song.Score.Tracks[t].NumVoices}
|
||||
return intRange{1, (*Model)(v).remainingVoices(v.linkInstrTrack, true) + v.d.Song.Score.Tracks[t].NumVoices}
|
||||
}
|
||||
|
||||
func (v *TrackVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("TrackVoicesInt."+kind, ScoreChange, MinorChange)
|
||||
return (*Model)(v).change("TrackVoices."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
638
tracker/list.go
638
tracker/list.go
@ -3,6 +3,9 @@ package tracker
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"math"
|
||||
"math/bits"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -26,10 +29,10 @@ type (
|
||||
MutableListData interface {
|
||||
change(kind string, severity ChangeSeverity) func()
|
||||
cancel()
|
||||
swap(i, j int) (ok bool)
|
||||
delete(i int) (ok bool)
|
||||
marshal(from, to int) ([]byte, error)
|
||||
unmarshal([]byte) (from, to int, err error)
|
||||
move(r Range, delta int) (ok bool)
|
||||
delete(r Range) (ok bool)
|
||||
marshal(r Range) ([]byte, error)
|
||||
unmarshal([]byte) (r Range, err error)
|
||||
}
|
||||
|
||||
UnitListItem struct {
|
||||
@ -38,6 +41,11 @@ type (
|
||||
StackNeed, StackBefore, StackAfter int
|
||||
}
|
||||
|
||||
// Range is used to represent a range [Start,End) of integers
|
||||
Range struct {
|
||||
Start, End int
|
||||
}
|
||||
|
||||
UnitYieldFunc func(index int, item UnitListItem) (ok bool)
|
||||
UnitSearchYieldFunc func(index int, item string) (ok bool)
|
||||
|
||||
@ -59,39 +67,21 @@ func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) }
|
||||
func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) }
|
||||
func (m *Model) SearchResults() *SearchResults { return (*SearchResults)(m) }
|
||||
|
||||
// MoveElements moves the selected elements in a list by delta. If delta is
|
||||
// negative, the elements move up, otherwise down. The list must implement the
|
||||
// MutableListData interface.
|
||||
func (v List) MoveElements(delta int) (ok bool) {
|
||||
if delta == 0 {
|
||||
return false
|
||||
}
|
||||
// MoveElements moves the selected elements in a list by delta. The list must
|
||||
// implement the MutableListData interface.
|
||||
func (v List) MoveElements(delta int) bool {
|
||||
s, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
return false
|
||||
}
|
||||
r := v.listRange()
|
||||
if delta == 0 || r.Start+delta < 0 || r.End+delta > v.Count() {
|
||||
return false
|
||||
}
|
||||
defer s.change("MoveElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
if a+delta < 0 {
|
||||
delta = -a
|
||||
}
|
||||
if b+delta >= v.Count() {
|
||||
delta = v.Count() - 1 - b
|
||||
}
|
||||
if delta < 0 {
|
||||
for i := a; i <= b; i++ {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !s.move(r, delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
v.SetSelected(v.Selected() + delta)
|
||||
v.SetSelected2(v.Selected2() + delta)
|
||||
@ -100,24 +90,25 @@ func (v List) MoveElements(delta int) (ok bool) {
|
||||
|
||||
// DeleteElements deletes the selected elements in a list. The list must
|
||||
// implement the MutableListData interface.
|
||||
func (v List) DeleteElements(backwards bool) (ok bool) {
|
||||
func (v List) DeleteElements(backwards bool) bool {
|
||||
d, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
return false
|
||||
}
|
||||
r := v.listRange()
|
||||
if r.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
defer d.change("DeleteElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
for i := b; i >= a; i-- {
|
||||
if !d.delete(i) {
|
||||
d.cancel()
|
||||
return false
|
||||
}
|
||||
if !d.delete(r) {
|
||||
d.cancel()
|
||||
return false
|
||||
}
|
||||
if backwards && a > 0 {
|
||||
a--
|
||||
if backwards && r.Start > 0 {
|
||||
r.Start--
|
||||
}
|
||||
v.SetSelected(a)
|
||||
v.SetSelected2(a)
|
||||
v.SetSelected(r.Start)
|
||||
v.SetSelected2(r.Start)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -125,12 +116,15 @@ func (v List) DeleteElements(backwards bool) (ok bool) {
|
||||
// the MutableListData interface. Returns the copied data, marshaled into byte
|
||||
// slice, and true if successful.
|
||||
func (v List) CopyElements() ([]byte, bool) {
|
||||
a, b := v.listRange()
|
||||
m, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ret, err := m.marshal(a, b)
|
||||
r := v.listRange()
|
||||
if r.Len() == 0 {
|
||||
return nil, false
|
||||
}
|
||||
ret, err := m.marshal(r)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
@ -146,19 +140,19 @@ func (v List) PasteElements(data []byte) (ok bool) {
|
||||
return false
|
||||
}
|
||||
defer m.change("PasteElements", MajorChange)()
|
||||
from, to, err := m.unmarshal(data)
|
||||
r, err := m.unmarshal(data)
|
||||
if err != nil {
|
||||
m.cancel()
|
||||
return false
|
||||
}
|
||||
v.SetSelected(from)
|
||||
v.SetSelected2(to)
|
||||
v.SetSelected(r.Start)
|
||||
v.SetSelected2(r.End - 1)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *List) listRange() (lower, higher int) {
|
||||
lower = min(v.Selected(), v.Selected2())
|
||||
higher = max(v.Selected(), v.Selected2())
|
||||
func (v *List) listRange() (r Range) {
|
||||
r.Start = max(min(v.Selected(), v.Selected2()), 0)
|
||||
r.End = min(max(v.Selected(), v.Selected2())+1, v.Count())
|
||||
return
|
||||
}
|
||||
|
||||
@ -219,25 +213,27 @@ func (v *Instruments) SetSelected2(value int) {
|
||||
v.d.InstrIndex2 = max(min(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) swap(i, j int) (ok bool) {
|
||||
if i < 0 || j < 0 || i >= len(v.d.Song.Patch) || j >= len(v.d.Song.Patch) || i == j {
|
||||
func (v *Instruments) move(r Range, delta int) (ok bool) {
|
||||
voiceDelta := 0
|
||||
if delta < 0 {
|
||||
voiceDelta = -VoiceRange(v.d.Song.Patch, Range{r.Start + delta, r.Start}).Len()
|
||||
} else if delta > 0 {
|
||||
voiceDelta = VoiceRange(v.d.Song.Patch, Range{r.End, r.End + delta}).Len()
|
||||
}
|
||||
if voiceDelta == 0 {
|
||||
return false
|
||||
}
|
||||
instr := v.d.Song.Patch
|
||||
instr[i], instr[j] = instr[j], instr[i]
|
||||
return true
|
||||
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Patch, r), voiceDelta)
|
||||
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
||||
}
|
||||
|
||||
func (v *Instruments) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
v.d.Song.Patch = append(v.d.Song.Patch[:i], v.d.Song.Patch[i+1:]...)
|
||||
return true
|
||||
func (v *Instruments) delete(r Range) (ok bool) {
|
||||
ranges := Complement(VoiceRange(v.d.Song.Patch, r))
|
||||
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
||||
}
|
||||
|
||||
func (v *Instruments) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("InstrumentListView."+n, PatchChange, severity)
|
||||
return (*Model)(v).change("Instruments."+n, SongChange, severity)
|
||||
}
|
||||
|
||||
func (v *Instruments) cancel() {
|
||||
@ -248,38 +244,17 @@ func (v *Instruments) Count() int {
|
||||
return len(v.d.Song.Patch)
|
||||
}
|
||||
|
||||
func (v *Instruments) marshal(from, to int) ([]byte, error) {
|
||||
if from < 0 || to >= len(v.d.Song.Patch) || from > to {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Patch sointu.Patch }{v.d.Song.Patch[from : to+1]})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
func (v *Instruments) marshal(r Range) ([]byte, error) {
|
||||
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Patch, r))
|
||||
}
|
||||
|
||||
func (v *Instruments) unmarshal(data []byte) (from, to int, err error) {
|
||||
var newInstr struct{ Patch sointu.Patch }
|
||||
if err := yaml.Unmarshal(data, &newInstr); err != nil {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: %v", err)
|
||||
func (m *Instruments) unmarshal(data []byte) (r Range, err error) {
|
||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||
r, _, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, true, m.linkInstrTrack)
|
||||
if !ok {
|
||||
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
||||
}
|
||||
if len(newInstr.Patch) == 0 {
|
||||
return 0, 0, errors.New("InstrumentListView.unmarshal: no instruments")
|
||||
}
|
||||
if v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices())
|
||||
}
|
||||
v.d.Song.Patch = append(v.d.Song.Patch, make([]sointu.Instrument, len(newInstr.Patch))...)
|
||||
sel := v.Selected()
|
||||
copy(v.d.Song.Patch[sel+len(newInstr.Patch):], v.d.Song.Patch[sel:])
|
||||
for i := 0; i < len(newInstr.Patch); i++ {
|
||||
(*Model)(v).assignUnitIDs(newInstr.Patch[i].Units)
|
||||
v.d.Song.Patch[sel+i] = newInstr.Patch[i]
|
||||
}
|
||||
from = sel
|
||||
to = sel + len(newInstr.Patch) - 1
|
||||
return
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Units methods
|
||||
@ -373,30 +348,25 @@ func (v *Units) Count() int {
|
||||
return len(m.d.Song.Patch[(*Model)(v).d.InstrIndex].Units)
|
||||
}
|
||||
|
||||
func (v *Units) swap(i, j int) (ok bool) {
|
||||
func (v *Units) move(r Range, delta int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || j < 0 || i >= len(units) || j >= len(units) || i == j {
|
||||
return false
|
||||
for i, j := range r.Swaps(delta) {
|
||||
units[i], units[j] = units[j], units[i]
|
||||
}
|
||||
units[i], units[j] = units[j], units[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Units) delete(i int) (ok bool) {
|
||||
func (v *Units) delete(r Range) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || i >= len(units) {
|
||||
return false
|
||||
}
|
||||
units = append(units[:i], units[i+1:]...)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
u := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = append(u[:r.Start], u[r.End:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -408,42 +378,39 @@ func (v *Units) cancel() {
|
||||
(*Model)(v).changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Units) marshal(from, to int) ([]byte, error) {
|
||||
func (v *Units) marshal(r Range) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return nil, errors.New("UnitListView.marshal: no instruments")
|
||||
}
|
||||
if from < 0 || to >= len(m.d.Song.Patch[m.d.InstrIndex].Units) || from > to {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{m.d.Song.Patch[m.d.InstrIndex].Units[from : to+1]})
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units[r.Start:r.End]
|
||||
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{units})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Units) unmarshal(data []byte) (from, to int, err error) {
|
||||
func (v *Units) unmarshal(data []byte) (r Range, err error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no instruments")
|
||||
return Range{}, errors.New("UnitListView.unmarshal: no instruments")
|
||||
}
|
||||
var pastedUnits struct{ Units []sointu.Unit }
|
||||
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
|
||||
return 0, 0, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
||||
return Range{}, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(pastedUnits.Units) == 0 {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no units")
|
||||
return Range{}, errors.New("UnitListView.unmarshal: no units")
|
||||
}
|
||||
m.assignUnitIDs(pastedUnits.Units)
|
||||
sel := v.Selected()
|
||||
units := append(m.d.Song.Patch[m.d.InstrIndex].Units, make([]sointu.Unit, len(pastedUnits.Units))...)
|
||||
copy(units[sel+len(pastedUnits.Units):], units[sel:])
|
||||
copy(units[sel:], pastedUnits.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
from = sel
|
||||
to = sel + len(pastedUnits.Units) - 1
|
||||
return
|
||||
var ok bool
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units, ok = Insert(m.d.Song.Patch[m.d.InstrIndex].Units, sel, pastedUnits.Units...)
|
||||
if !ok {
|
||||
return Range{}, errors.New("UnitListView.unmarshal: insert failed")
|
||||
}
|
||||
return Range{sel, sel + len(pastedUnits.Units)}, nil
|
||||
}
|
||||
|
||||
// Tracks methods
|
||||
@ -468,27 +435,27 @@ func (v *Tracks) SetSelected2(value int) {
|
||||
v.d.Cursor2.Track = max(min(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) swap(i, j int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || j < 0 || i >= len(m.d.Song.Score.Tracks) || j >= len(m.d.Song.Score.Tracks) || i == j {
|
||||
func (v *Tracks) move(r Range, delta int) (ok bool) {
|
||||
voiceDelta := 0
|
||||
if delta < 0 {
|
||||
voiceDelta = -VoiceRange(v.d.Song.Score.Tracks, Range{r.Start + delta, r.Start}).Len()
|
||||
} else if delta > 0 {
|
||||
voiceDelta = VoiceRange(v.d.Song.Score.Tracks, Range{r.End, r.End + delta}).Len()
|
||||
}
|
||||
if voiceDelta == 0 {
|
||||
return false
|
||||
}
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
tracks[i], tracks[j] = tracks[j], tracks[i]
|
||||
return true
|
||||
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Score.Tracks, r), voiceDelta)
|
||||
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
||||
}
|
||||
|
||||
func (v *Tracks) delete(i int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || i >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks[:i], m.d.Song.Score.Tracks[i+1:]...)
|
||||
return true
|
||||
func (v *Tracks) delete(r Range) (ok bool) {
|
||||
ranges := Complement(VoiceRange(v.d.Song.Score.Tracks, r))
|
||||
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
||||
}
|
||||
|
||||
func (v *Tracks) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("TrackList."+n, ScoreChange, severity)
|
||||
return (*Model)(v).change("TrackList."+n, SongChange, severity)
|
||||
}
|
||||
|
||||
func (v *Tracks) cancel() {
|
||||
@ -499,37 +466,17 @@ func (v *Tracks) Count() int {
|
||||
return len((*Model)(v).d.Song.Score.Tracks)
|
||||
}
|
||||
|
||||
func (v *Tracks) marshal(from, to int) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if from < 0 || to >= len(m.d.Song.Score.Tracks) || from > to {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Score sointu.Score }{sointu.Score{Tracks: m.d.Song.Score.Tracks[from : to+1]}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
func (v *Tracks) marshal(r Range) ([]byte, error) {
|
||||
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Score.Tracks, r))
|
||||
}
|
||||
|
||||
func (v *Tracks) unmarshal(data []byte) (from, to int, err error) {
|
||||
m := (*Model)(v)
|
||||
var newTracks struct{ Score sointu.Score }
|
||||
if err := yaml.Unmarshal(data, &newTracks); err != nil {
|
||||
return 0, 0, fmt.Errorf("TrackListView.unmarshal: %v", err)
|
||||
func (m *Tracks) unmarshal(data []byte) (r Range, err error) {
|
||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||
_, r, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, m.linkInstrTrack, true)
|
||||
if !ok {
|
||||
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
||||
}
|
||||
if len(newTracks.Score.Tracks) == 0 {
|
||||
return 0, 0, errors.New("TrackListView.unmarshal: no tracks")
|
||||
}
|
||||
if v.d.Song.Score.NumVoices()+newTracks.Score.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newTracks.Score.NumVoices())
|
||||
}
|
||||
from = m.d.Cursor.Track
|
||||
to = m.d.Cursor.Track + len(newTracks.Score.Tracks) - 1
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
newTracks.Score.Tracks = append(newTracks.Score.Tracks, tracks[m.d.Cursor.Track:]...)
|
||||
tracks = append(tracks[:m.d.Cursor.Track], newTracks.Score.Tracks...)
|
||||
m.d.Song.Score.Tracks = tracks
|
||||
return
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// OrderRows methods
|
||||
@ -562,21 +509,22 @@ func (v *OrderRows) SetSelected2(value int) {
|
||||
v.d.Cursor2.OrderRow = max(min(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *OrderRows) swap(x, y int) (ok bool) {
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
track := &v.d.Song.Score.Tracks[i]
|
||||
a, b := track.Order.Get(x), track.Order.Get(y)
|
||||
track.Order.Set(x, b)
|
||||
track.Order.Set(y, a)
|
||||
func (v *OrderRows) move(r Range, delta int) (ok bool) {
|
||||
swaps := r.Swaps(delta)
|
||||
for i, t := range v.d.Song.Score.Tracks {
|
||||
for a, b := range swaps {
|
||||
ea, eb := t.Order.Get(a), t.Order.Get(b)
|
||||
v.d.Song.Score.Tracks[i].Order.Set(a, eb)
|
||||
v.d.Song.Score.Tracks[i].Order.Set(b, ea)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *OrderRows) delete(i int) (ok bool) {
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
if i < len(track.Order) {
|
||||
track.Order = append(track.Order[:i], track.Order[i+1:]...)
|
||||
}
|
||||
func (v *OrderRows) delete(r Range) (ok bool) {
|
||||
for i, t := range v.d.Song.Score.Tracks {
|
||||
r2 := r.Intersect(Range{0, len(t.Order)})
|
||||
v.d.Song.Score.Tracks[i].Order = append(t.Order[:r2.Start], t.Order[r2.End:]...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -597,18 +545,18 @@ type marshalOrderRows struct {
|
||||
Columns [][]int `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *OrderRows) marshal(from, to int) ([]byte, error) {
|
||||
func (v *OrderRows) marshal(r Range) ([]byte, error) {
|
||||
var table marshalOrderRows
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
table.Columns = append(table.Columns, make([]int, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(from + j)
|
||||
table.Columns = append(table.Columns, make([]int, r.Len()))
|
||||
for j := 0; j < r.Len(); j++ {
|
||||
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(r.Start + j)
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *OrderRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
func (v *OrderRows) unmarshal(data []byte) (r Range, err error) {
|
||||
var table marshalOrderRows
|
||||
err = yaml.Unmarshal(data, &table)
|
||||
if err != nil {
|
||||
@ -618,19 +566,19 @@ func (v *OrderRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
err = errors.New("OrderRowList.unmarshal: no rows")
|
||||
return
|
||||
}
|
||||
from = v.d.Cursor.OrderRow
|
||||
to = v.d.Cursor.OrderRow + len(table.Columns[0]) - 1
|
||||
r.Start = v.d.Cursor.OrderRow
|
||||
r.End = v.d.Cursor.OrderRow + len(table.Columns[0])
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
if i >= len(table.Columns) {
|
||||
break
|
||||
}
|
||||
order := &v.d.Song.Score.Tracks[i].Order
|
||||
for j := 0; j < from-len(*order); j++ {
|
||||
for j := 0; j < r.Start-len(*order); j++ {
|
||||
*order = append(*order, -1)
|
||||
}
|
||||
if len(*order) > from {
|
||||
table.Columns[i] = append(table.Columns[i], (*order)[from:]...)
|
||||
*order = (*order)[:from]
|
||||
if len(*order) > r.Start {
|
||||
table.Columns[i] = append(table.Columns[i], (*order)[r.Start:]...)
|
||||
*order = (*order)[:r.Start]
|
||||
}
|
||||
*order = append(*order, table.Columns[i]...)
|
||||
}
|
||||
@ -663,25 +611,26 @@ func (v *NoteRows) SetSelected2(value int) {
|
||||
|
||||
}
|
||||
|
||||
func (v *NoteRows) swap(i, j int) (ok bool) {
|
||||
ipos := v.d.Song.Score.SongPos(i)
|
||||
jpos := v.d.Song.Score.SongPos(j)
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
n1 := track.Note(ipos)
|
||||
n2 := track.Note(jpos)
|
||||
track.SetNote(ipos, n2, v.uniquePatterns)
|
||||
track.SetNote(jpos, n1, v.uniquePatterns)
|
||||
func (v *NoteRows) move(r Range, delta int) (ok bool) {
|
||||
for a, b := range r.Swaps(delta) {
|
||||
apos := v.d.Song.Score.SongPos(a)
|
||||
bpos := v.d.Song.Score.SongPos(b)
|
||||
for _, t := range v.d.Song.Score.Tracks {
|
||||
n1 := t.Note(apos)
|
||||
n2 := t.Note(bpos)
|
||||
t.SetNote(apos, n2, v.uniquePatterns)
|
||||
t.SetNote(bpos, n1, v.uniquePatterns)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *NoteRows) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= v.Count() {
|
||||
return
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(i)
|
||||
func (v *NoteRows) delete(r Range) (ok bool) {
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
track.SetNote(pos, 1, v.uniquePatterns)
|
||||
for i := r.Start; i < r.End; i++ {
|
||||
pos := v.d.Song.Score.SongPos(i)
|
||||
track.SetNote(pos, 1, v.uniquePatterns)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -702,12 +651,12 @@ type marshalNoteRows struct {
|
||||
NoteRows [][]byte `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *NoteRows) marshal(from, to int) ([]byte, error) {
|
||||
func (v *NoteRows) marshal(r Range) ([]byte, error) {
|
||||
var table marshalNoteRows
|
||||
for i, track := range v.d.Song.Score.Tracks {
|
||||
table.NoteRows = append(table.NoteRows, make([]byte, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
row := from + j
|
||||
table.NoteRows = append(table.NoteRows, make([]byte, r.Len()))
|
||||
for j := 0; j < r.Len(); j++ {
|
||||
row := r.Start + j
|
||||
pos := v.d.Song.Score.SongPos(row)
|
||||
table.NoteRows[i][j] = track.Note(pos)
|
||||
}
|
||||
@ -715,22 +664,22 @@ func (v *NoteRows) marshal(from, to int) ([]byte, error) {
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *NoteRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
func (v *NoteRows) unmarshal(data []byte) (r Range, err error) {
|
||||
var table marshalNoteRows
|
||||
if err := yaml.Unmarshal(data, &table); err != nil {
|
||||
return 0, 0, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
||||
return Range{}, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
||||
}
|
||||
if len(table.NoteRows) < 1 {
|
||||
return 0, 0, errors.New("NoteRowList.unmarshal: no tracks")
|
||||
return Range{}, errors.New("NoteRowList.unmarshal: no tracks")
|
||||
}
|
||||
from = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
||||
r.Start = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
||||
for i, arr := range table.NoteRows {
|
||||
if i >= len(v.d.Song.Score.Tracks) {
|
||||
continue
|
||||
}
|
||||
to = from + len(arr) - 1
|
||||
r.End = r.Start + len(arr)
|
||||
for j, note := range arr {
|
||||
y := j + from
|
||||
y := j + r.Start
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
v.d.Song.Score.Tracks[i].SetNote(pos, note, v.uniquePatterns)
|
||||
}
|
||||
@ -780,3 +729,274 @@ func (l *SearchResults) Count() (count int) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r Range) Len() int { return r.End - r.Start }
|
||||
|
||||
func (r Range) Swaps(delta int) iter.Seq2[int, int] {
|
||||
if delta > 0 {
|
||||
return func(yield func(int, int) bool) {
|
||||
for i := r.End - 1; i >= r.Start; i-- {
|
||||
if !yield(i, i+delta) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return func(yield func(int, int) bool) {
|
||||
for i := r.Start; i < r.End; i++ {
|
||||
if !yield(i, i+delta) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r Range) Intersect(s Range) (ret Range) {
|
||||
ret.Start = max(r.Start, s.Start)
|
||||
ret.End = max(min(r.End, s.End), ret.Start)
|
||||
if ret.Len() == 0 {
|
||||
return Range{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func MakeMoveRanges(a Range, delta int) [4]Range {
|
||||
if delta < 0 {
|
||||
return [4]Range{
|
||||
{math.MinInt, a.Start + delta},
|
||||
{a.Start, a.End},
|
||||
{a.Start + delta, a.Start},
|
||||
{a.End, math.MaxInt},
|
||||
}
|
||||
}
|
||||
return [4]Range{
|
||||
{math.MinInt, a.Start},
|
||||
{a.End, a.End + delta},
|
||||
{a.Start, a.End},
|
||||
{a.End + delta, math.MaxInt},
|
||||
}
|
||||
}
|
||||
|
||||
// MakeSetLength takes a range and a length, and returns a slice of ranges that
|
||||
// can be used with VoiceSlice to expand or shrink the range to the given
|
||||
// length, by either duplicating or removing elements. The function tries to
|
||||
// duplicate elements so all elements are equally spaced, and tries to remove
|
||||
// elements from the middle of the range.
|
||||
func MakeSetLength(a Range, length int) []Range {
|
||||
ret := make([]Range, a.Len(), max(a.Len(), length)+2)
|
||||
for i := 0; i < a.Len(); i++ {
|
||||
ret[i] = Range{a.Start + i, a.Start + i + 1}
|
||||
}
|
||||
for x := len(ret); x < length; x++ {
|
||||
e := (x << 1) ^ (1 << bits.Len((uint)(x)))
|
||||
ret = append(ret[0:e+1], ret[e:]...)
|
||||
}
|
||||
for x := len(ret); x > length; x-- {
|
||||
e := (((x << 1) ^ (1 << bits.Len((uint)(x)))) + x - 1) % x
|
||||
ret = append(ret[0:e], ret[e+1:]...)
|
||||
}
|
||||
ret = append([]Range{{math.MinInt, a.Start}}, ret...)
|
||||
ret = append(ret, Range{a.End, math.MaxInt})
|
||||
return ret
|
||||
}
|
||||
|
||||
func Complement(a Range) [2]Range {
|
||||
return [2]Range{
|
||||
{math.MinInt, a.Start},
|
||||
{a.End, math.MaxInt},
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts elements into a slice at the given index. If the index is out
|
||||
// of bounds, the function returns false.
|
||||
func Insert[T any, S ~[]T](slice S, index int, inserted ...T) (ret S, ok bool) {
|
||||
if index < 0 || index > len(slice) {
|
||||
return nil, false
|
||||
}
|
||||
ret = make(S, 0, len(slice)+len(inserted))
|
||||
ret = append(ret, slice[:index]...)
|
||||
ret = append(ret, inserted...)
|
||||
ret = append(ret, slice[index:]...)
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -62,6 +62,10 @@ type (
|
||||
follow bool
|
||||
quitted bool
|
||||
uniquePatterns bool
|
||||
// when linkInstrTrack is false, editing an instrument does not change
|
||||
// the track. when true, editing an instrument changes the tracks (e.g.
|
||||
// reordering or deleting instrument can delete track)
|
||||
linkInstrTrack bool
|
||||
|
||||
cachePatternUseCount [][]int
|
||||
|
||||
@ -184,6 +188,7 @@ func NewModelPlayer(synther sointu.Synther, midiContext MIDIContext, recoveryFil
|
||||
m.modelMessages = modelMessages
|
||||
m.PlayerMessages = playerMessages
|
||||
m.d.Octave = 4
|
||||
m.linkInstrTrack = true
|
||||
m.d.RecoveryFilePath = recoveryFilePath
|
||||
m.resetSong()
|
||||
if recoveryFilePath != "" {
|
||||
@ -431,33 +436,56 @@ func (m *Model) maxID() int {
|
||||
return maxID
|
||||
}
|
||||
|
||||
func (m *Model) assignUnitIDs(units []sointu.Unit) {
|
||||
maxId := 0
|
||||
usedIds := make(map[int]bool)
|
||||
func (m *Model) maxIDandUsed() (maxID int, usedIDs map[int]bool) {
|
||||
usedIDs = make(map[int]bool)
|
||||
for _, instr := range m.d.Song.Patch {
|
||||
for _, unit := range instr.Units {
|
||||
usedIds[unit.ID] = true
|
||||
if maxId < unit.ID {
|
||||
maxId = unit.ID
|
||||
usedIDs[unit.ID] = true
|
||||
if maxID < unit.ID {
|
||||
maxID = unit.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Model) assignUnitIDsForPatch(patch sointu.Patch) {
|
||||
maxId, usedIds := m.maxIDandUsed()
|
||||
rewrites := map[int]int{}
|
||||
for _, instr := range patch {
|
||||
rewriteUnitIds(instr.Units, &maxId, usedIds, rewrites)
|
||||
}
|
||||
for _, instr := range patch {
|
||||
rewriteSendTargets(instr.Units, rewrites)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) assignUnitIDs(units []sointu.Unit) {
|
||||
maxID, usedIds := m.maxIDandUsed()
|
||||
rewrites := map[int]int{}
|
||||
rewriteUnitIds(units, &maxID, usedIds, rewrites)
|
||||
rewriteSendTargets(units, rewrites)
|
||||
}
|
||||
|
||||
func rewriteUnitIds(units []sointu.Unit, maxId *int, usedIds map[int]bool, rewrites map[int]int) {
|
||||
for i := range units {
|
||||
if id := units[i].ID; id == 0 || usedIds[id] {
|
||||
maxId++
|
||||
*maxId++
|
||||
if id > 0 {
|
||||
rewrites[id] = maxId
|
||||
rewrites[id] = *maxId
|
||||
}
|
||||
units[i].ID = maxId
|
||||
units[i].ID = *maxId
|
||||
}
|
||||
usedIds[units[i].ID] = true
|
||||
if maxId < units[i].ID {
|
||||
maxId = units[i].ID
|
||||
if *maxId < units[i].ID {
|
||||
*maxId = units[i].ID
|
||||
}
|
||||
}
|
||||
for i, u := range units {
|
||||
if target, ok := u.Parameters["target"]; u.Type == "send" && ok {
|
||||
}
|
||||
|
||||
func rewriteSendTargets(units []sointu.Unit, rewrites map[int]int) {
|
||||
for i := range units {
|
||||
if target, ok := units[i].Parameters["target"]; units[i].Type == "send" && ok {
|
||||
if newId, ok := rewrites[target]; ok {
|
||||
units[i].Parameters["target"] = newId
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
|
||||
s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed)
|
||||
s.IterateBool("Follow", s.model.Follow().Bool(), yield, seed)
|
||||
s.IterateBool("UniquePatterns", s.model.UniquePatterns().Bool(), yield, seed)
|
||||
s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack().Bool(), yield, seed)
|
||||
// Strings
|
||||
s.IterateString("FilePath", s.model.FilePath().String(), yield, seed)
|
||||
s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed)
|
||||
@ -92,6 +93,8 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
|
||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
||||
s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed)
|
||||
// just test loading one of the presets
|
||||
s.IterateAction("LoadPreset", s.model.LoadPreset(seed%tracker.NumPresets()), yield, seed)
|
||||
// Tables
|
||||
|
Loading…
x
Reference in New Issue
Block a user