diff --git a/CHANGELOG.md b/CHANGELOG.md index 03decdc..5282fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/cmd/sointu-vsti/main.go b/cmd/sointu-vsti/main.go index bfa216a..8f38aa7 100644 --- a/cmd/sointu-vsti/main.go +++ b/cmd/sointu-vsti/main.go @@ -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) diff --git a/patch.go b/patch.go index cca1fbd..f9b7cd2 100644 --- a/patch.go +++ b/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. diff --git a/song.go b/song.go index fbe2ad0..1f33aa1 100644 --- a/song.go +++ b/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 +} diff --git a/tracker/action.go b/tracker/action.go index 97b8a36..e228faf 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -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}}) }, } } diff --git a/tracker/bool.go b/tracker/bool.go index bc9df9a..e7d8603 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -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 } diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index ababc68..f01fc54 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -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), diff --git a/tracker/gioui/keybindings.yml b/tracker/gioui/keybindings.yml index d570721..2570151 100644 --- a/tracker/gioui/keybindings.yml +++ b/tracker/gioui/keybindings.yml @@ -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"} diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index cdce2f1..3ae19d5 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -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": diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index e7a494f..2715d77 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -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)) diff --git a/tracker/int.go b/tracker/int.go index 469f2dd..bb08564 100644 --- a/tracker/int.go +++ b/tracker/int.go @@ -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) } diff --git a/tracker/list.go b/tracker/list.go index aaefc02..f9bb51f 100644 --- a/tracker/list.go +++ b/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 +} diff --git a/tracker/model.go b/tracker/model.go index 05eec27..e4f093c 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -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 } diff --git a/tracker/model_test.go b/tracker/model_test.go index ffa2a0a..b42ffdf 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -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