Skip to content

Commit

Permalink
feat: keeping instruments and tracks linked & splitting them
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vsariola committed Oct 20, 2024
1 parent 025f883 commit 216cde2
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 285 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion cmd/sointu-vsti/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 13 additions & 4 deletions patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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.
Expand Down
42 changes: 38 additions & 4 deletions song.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
104 changes: 72 additions & 32 deletions tracker/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tracker

import (
"fmt"
"math"
"os"

"github.com/vsariola/sointu"
Expand Down Expand Up @@ -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
},
}
}
Expand All @@ -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
},
}
}
Expand All @@ -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)()
Expand Down Expand Up @@ -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}})
},
}
}
Expand Down
17 changes: 13 additions & 4 deletions tracker/bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type (
UniquePatterns Model
Mute Model
Solo Model
LinkInstrTrack Model
)

func (v Bool) Toggle() {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 }
16 changes: 16 additions & 0 deletions tracker/gioui/instrument_editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type InstrumentEditor struct {
newInstrumentBtn *ActionClickable
enlargeBtn *BoolClickable
deleteInstrumentBtn *ActionClickable
linkInstrTrackBtn *BoolClickable
splitInstrumentBtn *ActionClickable
copyInstrumentBtn *TipClickable
saveInstrumentBtn *TipClickable
loadInstrumentBtn *TipClickable
Expand Down Expand Up @@ -55,13 +57,18 @@ type InstrumentEditor struct {
deleteInstrumentHint string
muteHint, unmuteHint string
soloHint, unsoloHint string
linkDisabledHint string
linkEnabledHint string
splitInstrumentHint string
}

func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret := &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),
Expand Down Expand Up @@ -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
}

Expand All @@ -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))
Expand All @@ -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)
}),
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 216cde2

Please sign in to comment.