mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
410 lines
11 KiB
Go
410 lines
11 KiB
Go
package tracker
|
|
|
|
import (
|
|
"os"
|
|
|
|
"github.com/vsariola/sointu"
|
|
"github.com/vsariola/sointu/vm"
|
|
)
|
|
|
|
type (
|
|
// Action describes a user action that can be performed on the model. It is
|
|
// usually a button press or a menu item. Action advertises whether it is
|
|
// allowed to be performed or not.
|
|
Action struct {
|
|
do func()
|
|
allowed func() bool
|
|
}
|
|
)
|
|
|
|
// Action methods
|
|
|
|
func (e Action) Do() {
|
|
if e.allowed != nil && e.allowed() {
|
|
e.do()
|
|
}
|
|
}
|
|
|
|
func (e Action) Allowed() bool {
|
|
return e.allowed != nil && e.allowed()
|
|
}
|
|
|
|
func Allow(do func()) Action {
|
|
return Action{do: do, allowed: func() bool { return true }}
|
|
}
|
|
|
|
func Check(do func(), allowed func() bool) Action {
|
|
return Action{do: do, allowed: allowed}
|
|
}
|
|
|
|
// Model methods
|
|
|
|
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 = intMax(intMin(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
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) DeleteTrack() Action {
|
|
return Action{
|
|
allowed: func() bool { return len(m.d.Song.Score.Tracks) > 0 },
|
|
do: func() {
|
|
defer (*Model)(m).change("DeleteTrackAction", ScoreChange, MajorChange)()
|
|
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 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:], m.d.Song.Score.Tracks[m.d.Cursor.Track+1:])
|
|
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
|
m.d.Song.Score.Tracks = newTracks
|
|
m.d.Cursor2 = m.d.Cursor
|
|
},
|
|
}
|
|
}
|
|
|
|
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
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) DeleteInstrument() Action {
|
|
return Action{
|
|
allowed: func() bool { return len((*Model)(m).d.Song.Patch) > 0 },
|
|
do: func() {
|
|
defer (*Model)(m).change("DeleteInstrumentAction", PatchChange, MajorChange)()
|
|
m.d.Song.Patch = append(m.d.Song.Patch[:m.d.InstrIndex], m.d.Song.Patch[m.d.InstrIndex+1:]...)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) AddUnit(before bool) Action {
|
|
return Allow(func() {
|
|
defer (*Model)(m).change("AddUnitAction", PatchChange, MajorChange)()
|
|
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
|
instr := sointu.Instrument{NumVoices: 1}
|
|
instr.Units = make([]sointu.Unit, 0, 1)
|
|
m.d.Song.Patch = append(m.d.Song.Patch, instr)
|
|
m.d.UnitIndex = 0
|
|
} else {
|
|
if !before {
|
|
m.d.UnitIndex++
|
|
}
|
|
}
|
|
m.d.InstrIndex = intMax(intMin(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0)
|
|
instr := m.d.Song.Patch[m.d.InstrIndex]
|
|
newUnits := make([]sointu.Unit, len(instr.Units)+1)
|
|
m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1)
|
|
m.d.UnitIndex2 = m.d.UnitIndex
|
|
copy(newUnits, instr.Units[:m.d.UnitIndex])
|
|
copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:])
|
|
(*Model)(m).assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
|
|
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
|
|
m.d.ParamIndex = 0
|
|
})
|
|
}
|
|
|
|
func (m *Model) DeleteUnit() Action {
|
|
return Action{
|
|
allowed: func() bool {
|
|
return len((*Model)(m).d.Song.Patch) > 0 && len((*Model)(m).d.Song.Patch[(*Model)(m).d.InstrIndex].Units) > 1
|
|
},
|
|
do: func() {
|
|
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
|
m.Units().List().DeleteElements(true)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) ClearUnit() Action {
|
|
return Action{
|
|
do: func() {
|
|
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
|
m.d.UnitIndex = intMax(intMin(m.d.UnitIndex, len(m.d.Song.Patch[m.d.InstrIndex].Units)-1), 0)
|
|
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = sointu.Unit{}
|
|
},
|
|
allowed: func() bool {
|
|
return m.d.InstrIndex >= 0 &&
|
|
m.d.InstrIndex < len(m.d.Song.Patch) &&
|
|
len(m.d.Song.Patch[m.d.InstrIndex].Units) > 0
|
|
},
|
|
}
|
|
}
|
|
func (m *Model) Undo() Action {
|
|
return Action{
|
|
allowed: func() bool { return len((*Model)(m).undoStack) > 0 },
|
|
do: func() {
|
|
m.redoStack = append(m.redoStack, m.d.Copy())
|
|
if len(m.redoStack) >= maxUndo {
|
|
copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:])
|
|
m.redoStack = m.redoStack[:maxUndo]
|
|
}
|
|
m.d = m.undoStack[len(m.undoStack)-1]
|
|
m.undoStack = m.undoStack[:len(m.undoStack)-1]
|
|
m.prevUndoKind = ""
|
|
(*Model)(m).send(m.d.Song.Copy())
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) Redo() Action {
|
|
return Action{
|
|
allowed: func() bool { return len((*Model)(m).redoStack) > 0 },
|
|
do: func() {
|
|
m.undoStack = append(m.undoStack, m.d.Copy())
|
|
if len(m.undoStack) >= maxUndo {
|
|
copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:])
|
|
m.undoStack = m.undoStack[:maxUndo]
|
|
}
|
|
m.d = m.redoStack[len(m.redoStack)-1]
|
|
m.redoStack = m.redoStack[:len(m.redoStack)-1]
|
|
m.prevUndoKind = ""
|
|
(*Model)(m).send(m.d.Song.Copy())
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) AddSemitone() Action {
|
|
return Allow(func() { Table{(*Notes)(m)}.Add(1) })
|
|
}
|
|
|
|
func (m *Model) SubtractSemitone() Action {
|
|
return Allow(func() { Table{(*Notes)(m)}.Add(-1) })
|
|
}
|
|
|
|
func (m *Model) AddOctave() Action {
|
|
return Allow(func() { Table{(*Notes)(m)}.Add(12) })
|
|
}
|
|
|
|
func (m *Model) SubtractOctave() Action {
|
|
return Allow(func() { Table{(*Notes)(m)}.Add(-12) })
|
|
}
|
|
|
|
func (m *Model) EditNoteOff() Action {
|
|
return Allow(func() { Table{(*Notes)(m)}.Fill(0) })
|
|
}
|
|
|
|
func (m *Model) RemoveUnused() Action {
|
|
return Allow(func() {
|
|
defer m.change("RemoveUnusedAction", ScoreChange, MajorChange)()
|
|
for trkIndex, trk := range m.d.Song.Score.Tracks {
|
|
// assign new indices to patterns
|
|
newIndex := map[int]int{}
|
|
runningIndex := 0
|
|
length := 0
|
|
if len(trk.Order) > m.d.Song.Score.Length {
|
|
trk.Order = trk.Order[:m.d.Song.Score.Length]
|
|
}
|
|
for i, p := range trk.Order {
|
|
// if the pattern hasn't been considered and is within limits
|
|
if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) {
|
|
pat := trk.Patterns[p]
|
|
useful := false
|
|
for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept
|
|
if n != 1 {
|
|
useful = true
|
|
break
|
|
}
|
|
}
|
|
if useful {
|
|
newIndex[p] = runningIndex
|
|
runningIndex++
|
|
} else {
|
|
newIndex[p] = -1
|
|
}
|
|
}
|
|
if ind, ok := newIndex[p]; ok && ind > -1 {
|
|
length = i + 1
|
|
trk.Order[i] = ind
|
|
} else {
|
|
trk.Order[i] = -1
|
|
}
|
|
}
|
|
trk.Order = trk.Order[:length]
|
|
newPatterns := make([]sointu.Pattern, runningIndex)
|
|
for i, pat := range trk.Patterns {
|
|
if ind, ok := newIndex[i]; ok && ind > -1 {
|
|
patLength := 0
|
|
for j, note := range pat { // find last note that is something else that hold
|
|
if note != 1 {
|
|
patLength = j + 1
|
|
}
|
|
}
|
|
if patLength > m.d.Song.Score.RowsPerPattern {
|
|
patLength = m.d.Song.Score.RowsPerPattern
|
|
}
|
|
newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold
|
|
}
|
|
}
|
|
trk.Patterns = newPatterns
|
|
m.d.Song.Score.Tracks[trkIndex] = trk
|
|
}
|
|
})
|
|
}
|
|
|
|
func (m *Model) Rewind() Action {
|
|
return Action{
|
|
allowed: func() bool {
|
|
return m.playing || !m.instrEnlarged
|
|
},
|
|
do: func() {
|
|
m.playing = true
|
|
m.send(StartPlayMsg{})
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Model) AddOrderRow(before bool) Action {
|
|
return Allow(func() {
|
|
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
|
if before {
|
|
m.d.Cursor.OrderRow++
|
|
}
|
|
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
|
from := m.d.Cursor.OrderRow
|
|
m.d.Song.Score.Length++
|
|
for i := range m.d.Song.Score.Tracks {
|
|
order := &m.d.Song.Score.Tracks[i].Order
|
|
if len(*order) > from {
|
|
*order = append(*order, -1)
|
|
copy((*order)[from+1:], (*order)[from:])
|
|
(*order)[from] = -1
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (m *Model) DeleteOrderRow(backwards bool) Action {
|
|
return Allow(func() {
|
|
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
|
from := m.d.Cursor.OrderRow
|
|
m.d.Song.Score.Length--
|
|
for i := range m.d.Song.Score.Tracks {
|
|
order := &m.d.Song.Score.Tracks[i].Order
|
|
if len(*order) > from {
|
|
copy((*order)[from:], (*order)[from+1:])
|
|
*order = (*order)[:len(*order)-1]
|
|
}
|
|
}
|
|
if backwards {
|
|
if m.d.Cursor.OrderRow > 0 {
|
|
m.d.Cursor.OrderRow--
|
|
}
|
|
}
|
|
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
|
return
|
|
})
|
|
}
|
|
|
|
func (m *Model) NewSong() Action {
|
|
return Allow(func() {
|
|
m.dialog = NewSongChanges
|
|
m.completeAction(true)
|
|
})
|
|
}
|
|
|
|
func (m *Model) OpenSong() Action {
|
|
return Allow(func() {
|
|
m.dialog = OpenSongChanges
|
|
m.completeAction(true)
|
|
})
|
|
}
|
|
|
|
func (m *Model) Quit() Action {
|
|
return Allow(func() {
|
|
m.dialog = QuitChanges
|
|
m.completeAction(true)
|
|
})
|
|
}
|
|
|
|
func (m *Model) ForceQuit() Action {
|
|
return Allow(func() {
|
|
m.quitted = true
|
|
})
|
|
}
|
|
|
|
func (m *Model) SaveSong() Action {
|
|
return Allow(func() {
|
|
if m.d.FilePath == "" {
|
|
switch m.dialog {
|
|
case NoDialog:
|
|
m.dialog = SaveAsExplorer
|
|
case NewSongChanges:
|
|
m.dialog = NewSongSaveExplorer
|
|
case OpenSongChanges:
|
|
m.dialog = OpenSongSaveExplorer
|
|
case QuitChanges:
|
|
m.dialog = QuitSaveExplorer
|
|
}
|
|
return
|
|
}
|
|
f, err := os.Create(m.d.FilePath)
|
|
if err != nil {
|
|
m.Alerts().Add("Error creating file: "+err.Error(), Error)
|
|
return
|
|
}
|
|
m.WriteSong(f)
|
|
m.d.ChangedSinceSave = false
|
|
})
|
|
}
|
|
|
|
func (m *Model) DiscardSong() Action { return Allow(func() { m.completeAction(false) }) }
|
|
func (m *Model) SaveSongAs() Action { return Allow(func() { m.dialog = SaveAsExplorer }) }
|
|
func (m *Model) Cancel() Action { return Allow(func() { m.dialog = NoDialog }) }
|
|
func (m *Model) Export() Action { return Allow(func() { m.dialog = Export }) }
|
|
func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) }
|
|
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
|
|
|
|
func (m *Model) completeAction(checkSave bool) {
|
|
if checkSave && m.d.ChangedSinceSave {
|
|
return
|
|
}
|
|
switch m.dialog {
|
|
case NewSongChanges, NewSongSaveExplorer:
|
|
c := m.change("NewSong", SongChange|LoopChange, MajorChange)
|
|
m.resetSong()
|
|
c()
|
|
m.d.ChangedSinceSave = false
|
|
m.dialog = NoDialog
|
|
case OpenSongChanges, OpenSongSaveExplorer:
|
|
m.dialog = OpenSongOpenExplorer
|
|
case QuitChanges, QuitSaveExplorer:
|
|
m.quitted = true
|
|
m.dialog = NoDialog
|
|
default:
|
|
m.dialog = NoDialog
|
|
}
|
|
}
|