sointu/tracker/model.go
vsariola adcf3ebce8 feat(sointu, tracker,...): restructure domain & tracker models
send targets are now by ID and Song has "Score" part, which is the notes for it. also, moved the model part separate of the actual gioui dependend stuff.

sorry to my future self about the code bomb; ended up too far and did not find an easy way to rewrite the history to make the steps smaller, so in the end, just squashed everything.
2021-02-28 14:24:54 +02:00

940 lines
21 KiB
Go

package tracker
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/compiler"
)
// Model implements the mutable state for the tracker program GUI.
//
// Go does not have immutable slices, so there's no efficient way to guarantee
// accidental mutations in the song. But at least the value members are
// protected.
type Model struct {
song sointu.Song
editMode EditMode
selectionCorner SongPoint
cursor SongPoint
lowNibble bool
instrIndex int
unitIndex int
paramIndex int
octave int
noteTracking bool
usedIDs map[int]bool
maxID int
prevUndoType string
undoSkipCounter int
undoStack []sointu.Song
redoStack []sointu.Song
samplesPerRowObservers []chan<- int
patchObservers []chan<- sointu.Patch
scoreObservers []chan<- sointu.Score
playingObservers []chan<- bool
}
type Parameter struct {
Type ParameterType
Name string
Hint string
Value int
Min int
Max int
}
type EditMode int
type ParameterType int
const (
EditPatterns EditMode = iota
EditTracks
EditUnits
EditParameters
)
const (
IntegerParameter ParameterType = iota
BoolParameter
IDParameter
)
const maxUndo = 256
func NewModel() *Model {
ret := new(Model)
ret.setSongNoUndo(defaultSong.Copy())
return ret
}
func (m *Model) ResetSong() {
m.SetSong(defaultSong.Copy())
}
func (m *Model) SetSong(song sointu.Song) {
m.saveUndo("SetSong", 0)
m.setSongNoUndo(song)
}
func (m *Model) SetOctave(value int) bool {
if value < 0 {
value = 0
}
if value > 9 {
value = 9
}
if m.octave == value {
return false
}
m.octave = value
return true
}
func (m *Model) SetInstrument(instrument sointu.Instrument) bool {
if len(instrument.Units) == 0 {
return false
}
m.saveUndo("SetInstrument", 0)
m.freeUnitIDs(m.song.Patch[m.instrIndex].Units)
m.assignUnitIDs(instrument.Units)
m.song.Patch[m.instrIndex] = instrument
m.clampPositions()
m.notifyPatchChange()
return true
}
func (m *Model) SetInstrIndex(value int) {
m.instrIndex = value
m.clampPositions()
}
func (m *Model) SetInstrumentVoices(value int) {
if value < 1 {
value = 1
}
maxRemain := m.MaxInstrumentVoices()
if value > maxRemain {
value = maxRemain
}
if m.Instrument().NumVoices == value {
return
}
m.saveUndo("SetInstrumentVoices", 10)
m.song.Patch[m.instrIndex].NumVoices = value
m.notifyPatchChange()
}
func (m *Model) MaxInstrumentVoices() int {
maxRemain := 32 - m.song.Patch.NumVoices() + m.Instrument().NumVoices
if maxRemain < 1 {
return 1
}
return maxRemain
}
func (m *Model) SetInstrumentName(name string) {
name = strings.TrimSpace(name)
if m.Instrument().Name == name {
return
}
m.saveUndo("SetInstrumentName", 10)
m.song.Patch[m.instrIndex].Name = name
}
func (m *Model) SetBPM(value int) {
if value < 1 {
value = 1
}
if value > 999 {
value = 999
}
if m.song.BPM == value {
return
}
m.saveUndo("SetBPM", 100)
m.song.BPM = value
m.notifySamplesPerRowChange()
}
func (m *Model) SetRowsPerBeat(value int) {
if value < 1 {
value = 1
}
if value > 32 {
value = 32
}
if m.song.RowsPerBeat == value {
return
}
m.saveUndo("SetRowsPerBeat", 10)
m.song.RowsPerBeat = value
m.notifySamplesPerRowChange()
}
func (m *Model) AddTrack(after bool) {
if !m.CanAddTrack() {
return
}
m.saveUndo("AddTrack", 0)
newTracks := make([]sointu.Track, len(m.song.Score.Tracks)+1)
if after {
m.cursor.Track++
}
copy(newTracks, m.song.Score.Tracks[:m.cursor.Track])
copy(newTracks[m.cursor.Track+1:], m.song.Score.Tracks[m.cursor.Track:])
newTracks[m.cursor.Track] = sointu.Track{
NumVoices: 1,
Patterns: [][]byte{make([]byte, m.song.Score.RowsPerPattern)},
}
m.song.Score.Tracks = newTracks
m.clampPositions()
m.notifyScoreChange()
}
func (m *Model) CanAddTrack() bool {
return m.song.Score.NumVoices() < 32
}
func (m *Model) SetTrackVoices(value int) {
if value < 1 {
value = 1
}
maxRemain := m.MaxTrackVoices()
if value > maxRemain {
value = maxRemain
}
if m.song.Score.Tracks[m.cursor.Track].NumVoices == value {
return
}
m.saveUndo("SetTrackVoices", 10)
m.song.Score.Tracks[m.cursor.Track].NumVoices = value
m.notifyScoreChange()
}
func (m *Model) MaxTrackVoices() int {
maxRemain := 32 - m.song.Score.NumVoices() + m.song.Score.Tracks[m.cursor.Track].NumVoices
if maxRemain < 1 {
maxRemain = 1
}
return maxRemain
}
func (m *Model) AddInstrument(after bool) {
if !m.CanAddInstrument() {
return
}
m.saveUndo("AddInstrument", 0)
newInstruments := make([]sointu.Instrument, len(m.song.Patch)+1)
if after {
m.instrIndex++
}
copy(newInstruments, m.song.Patch[:m.instrIndex])
copy(newInstruments[m.instrIndex+1:], m.song.Patch[m.instrIndex:])
newInstr := defaultInstrument.Copy()
m.assignUnitIDs(newInstr.Units)
newInstruments[m.instrIndex] = newInstr
m.unitIndex = 0
m.paramIndex = 0
m.song.Patch = newInstruments
m.notifyPatchChange()
}
func (m *Model) CanAddInstrument() bool {
return m.song.Patch.NumVoices() < 32
}
func (m *Model) SwapInstruments(i, j int) {
if i < 0 || j < 0 || i >= len(m.song.Patch) || j >= len(m.song.Patch) || i == j {
return
}
m.saveUndo("SwapInstruments", 10)
instruments := m.song.Patch
instruments[i], instruments[j] = instruments[j], instruments[i]
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) DeleteInstrument(forward bool) {
if !m.CanDeleteInstrument() {
return
}
m.saveUndo("DeleteInstrument", 0)
m.freeUnitIDs(m.song.Patch[m.instrIndex].Units)
m.song.Patch = append(m.song.Patch[:m.instrIndex], m.song.Patch[m.instrIndex+1:]...)
if (!forward && m.instrIndex > 0) || m.instrIndex >= len(m.song.Patch) {
m.instrIndex--
}
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) CanDeleteInstrument() bool {
return len(m.song.Patch) > 1
}
func (m *Model) Note() byte {
trk := m.song.Score.Tracks[m.cursor.Track]
if m.cursor.Pattern < 0 || m.cursor.Pattern >= len(trk.Order) {
return 1
}
p := trk.Order[m.cursor.Pattern]
if p < 0 || p >= len(trk.Patterns) {
return 1
}
pat := trk.Patterns[p]
if m.cursor.Row < 0 || m.cursor.Row >= len(pat) {
return 1
}
return pat[m.cursor.Row]
}
// SetCurrentNote sets the (note) value in current pattern under cursor to iv
func (m *Model) SetNote(iv byte) {
m.saveUndo("SetNote", 10)
tracks := m.song.Score.Tracks
order := tracks[m.cursor.Track].Order
if m.cursor.Pattern < 0 || m.cursor.Pattern >= len(order) || m.cursor.Row < 0 {
return
}
patIndex := order[m.cursor.Pattern]
if patIndex < 0 {
return
}
for len(tracks[m.cursor.Track].Patterns) <= patIndex {
tracks[m.cursor.Track].Patterns = append(tracks[m.cursor.Track].Patterns, nil)
}
patterns := tracks[m.cursor.Track].Patterns
for len(patterns[patIndex]) <= m.cursor.Row {
patterns[patIndex] = append(patterns[patIndex], 1)
}
patterns[patIndex][m.cursor.Row] = iv
m.notifyScoreChange()
}
func (m *Model) SetCurrentPattern(pat int) {
m.saveUndo("SetCurrentPattern", 0)
track := &m.song.Score.Tracks[m.cursor.Track]
for len(track.Order) <= m.cursor.Pattern {
track.Order = append(track.Order, -1)
}
track.Order[m.cursor.Pattern] = pat
m.notifyScoreChange()
}
func (m *Model) SetSongLength(value int) {
if value < 1 {
value = 1
}
if value == m.song.Score.Length {
return
}
m.saveUndo("SetSongLength", 10)
m.song.Score.Length = value
m.clampPositions()
m.notifyScoreChange()
}
func (m *Model) SetRowsPerPattern(value int) {
if value < 1 {
value = 1
}
if value > 255 {
value = 255
}
if value == m.song.Score.RowsPerPattern {
return
}
m.saveUndo("SetRowsPerPattern", 10)
m.song.Score.RowsPerPattern = value
m.clampPositions()
m.notifyScoreChange()
}
func (m *Model) SetUnitType(t string) {
unit, ok := defaultUnits[t]
if !ok { // if the type is invalid, we just set it to empty unit
unit = sointu.Unit{Parameters: make(map[string]int)}
} else {
unit = unit.Copy()
}
if m.Unit().Type == unit.Type {
return
}
m.saveUndo("SetUnitType", 0)
oldID := m.Unit().ID
m.Instrument().Units[m.unitIndex] = unit
m.Instrument().Units[m.unitIndex].ID = oldID // keep the ID of the replaced unit
m.notifyPatchChange()
}
func (m *Model) SetUnitIndex(value int) {
m.unitIndex = value
m.paramIndex = 0
m.clampPositions()
}
func (m *Model) AddUnit(after bool) {
m.saveUndo("AddUnit", 10)
newUnits := make([]sointu.Unit, len(m.Instrument().Units)+1)
if after {
m.unitIndex++
}
copy(newUnits, m.Instrument().Units[:m.unitIndex])
copy(newUnits[m.unitIndex+1:], m.Instrument().Units[m.unitIndex:])
m.assignUnitIDs(newUnits[m.unitIndex : m.unitIndex+1])
m.song.Patch[m.instrIndex].Units = newUnits
m.paramIndex = 0
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) AddOrderRow(after bool) {
m.saveUndo("AddOrderRow", 10)
if after {
m.cursor.Pattern++
}
for i, trk := range m.song.Score.Tracks {
if l := len(trk.Order); l > m.cursor.Pattern {
newOrder := make([]int, l+1)
copy(newOrder, trk.Order[:m.cursor.Pattern])
copy(newOrder[m.cursor.Pattern+1:], trk.Order[m.cursor.Pattern:])
newOrder[m.cursor.Pattern] = -1
m.song.Score.Tracks[i].Order = newOrder
}
}
m.song.Score.Length++
m.selectionCorner = m.cursor
m.clampPositions()
m.notifyScoreChange()
}
func (m *Model) DeleteOrderRow(forward bool) {
if m.song.Score.Length <= 1 {
return
}
m.saveUndo("DeleteOrderRow", 0)
for i, trk := range m.song.Score.Tracks {
if l := len(trk.Order); l > m.cursor.Pattern {
newOrder := make([]int, l-1)
copy(newOrder, trk.Order[:m.cursor.Pattern])
copy(newOrder[m.cursor.Pattern:], trk.Order[m.cursor.Pattern+1:])
m.song.Score.Tracks[i].Order = newOrder
}
}
if !forward && m.cursor.Pattern > 0 {
m.cursor.Pattern--
}
m.song.Score.Length--
m.selectionCorner = m.cursor
m.clampPositions()
m.notifyScoreChange()
}
func (m *Model) DeleteUnit(forward bool) {
if !m.CanDeleteUnit() {
return
}
instr := m.Instrument()
m.saveUndo("DeleteUnit", 0)
delete(m.usedIDs, instr.Units[m.unitIndex].ID)
newUnits := make([]sointu.Unit, len(instr.Units)-1)
copy(newUnits, instr.Units[:m.unitIndex])
copy(newUnits[m.unitIndex:], instr.Units[m.unitIndex+1:])
m.song.Patch[m.instrIndex].Units = newUnits
if !forward && m.unitIndex > 0 {
m.unitIndex--
}
m.paramIndex = 0
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) CanDeleteUnit() bool {
return len(m.Instrument().Units) > 1
}
func (m *Model) ResetParam() {
p, err := m.Param(m.paramIndex)
if err != nil {
return
}
unit := m.Unit()
paramList, ok := sointu.UnitTypes[unit.Type]
if !ok || m.paramIndex < 0 || m.paramIndex >= len(paramList) {
return
}
paramType := paramList[m.paramIndex]
defaultValue, ok := defaultUnits[unit.Type].Parameters[paramType.Name]
if unit.Parameters[p.Name] == defaultValue {
return
}
m.saveUndo("ResetParam", 0)
unit.Parameters[paramType.Name] = defaultValue
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) SetParamIndex(value int) {
m.paramIndex = value
m.clampPositions()
}
func (m *Model) setGmDlsEntry(index int) {
if index < 0 || index >= len(GmDlsEntries) {
return
}
entry := GmDlsEntries[index]
unit := m.Unit()
if unit.Type != "oscillator" || unit.Parameters["type"] != sointu.Sample {
return
}
if unit.Parameters["samplestart"] == entry.Start && unit.Parameters["loopstart"] == entry.LoopStart && unit.Parameters["looplength"] == entry.LoopLength {
return
}
m.saveUndo("SetGmDlsEntry", 20)
unit.Parameters["samplestart"] = entry.Start
unit.Parameters["loopstart"] = entry.LoopStart
unit.Parameters["looplength"] = entry.LoopLength
unit.Parameters["transpose"] = 64 + entry.SuggestedTranspose
m.notifyPatchChange()
}
func (m *Model) SwapUnits(i, j int) {
units := m.Instrument().Units
if i < 0 || j < 0 || i >= len(units) || j >= len(units) || i == j {
return
}
m.saveUndo("SwapUnits", 10)
units[i], units[j] = units[j], units[i]
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) getSelectionRange() (int, int, int, int) {
r1 := m.cursor.Pattern*m.song.Score.RowsPerPattern + m.cursor.Row
r2 := m.selectionCorner.Pattern*m.song.Score.RowsPerPattern + m.selectionCorner.Row
if r2 < r1 {
r1, r2 = r2, r1
}
t1 := m.cursor.Track
t2 := m.selectionCorner.Track
if t2 < t1 {
t1, t2 = t2, t1
}
return r1, r2, t1, t2
}
func (m *Model) AdjustSelectionPitch(delta int) {
m.saveUndo("AdjustSelectionPitch", 10)
r1, r2, t1, t2 := m.getSelectionRange()
for c := t1; c <= t2; c++ {
adjustedNotes := map[struct {
Pat int
Row int
}]bool{}
for r := r1; r <= r2; r++ {
s := SongRow{Row: r}.Wrap(m.song.Score)
if s.Pattern >= len(m.song.Score.Tracks[c].Order) {
break
}
p := m.song.Score.Tracks[c].Order[s.Pattern]
if p < 0 {
continue
}
noteIndex := struct {
Pat int
Row int
}{p, s.Row}
if !adjustedNotes[noteIndex] {
patterns := m.song.Score.Tracks[c].Patterns
if p >= len(patterns) {
continue
}
pattern := patterns[p]
if s.Row >= len(pattern) {
continue
}
if val := pattern[s.Row]; val > 1 {
newVal := int(val) + delta
if newVal < 2 {
newVal = 2
} else if newVal > 255 {
newVal = 255
}
pattern[s.Row] = byte(newVal)
}
adjustedNotes[noteIndex] = true
}
}
}
m.notifyScoreChange()
}
func (m *Model) DeleteSelection() {
m.saveUndo("DeleteSelection", 0)
r1, r2, t1, t2 := m.getSelectionRange()
for r := r1; r <= r2; r++ {
s := SongRow{Row: r}.Wrap(m.song.Score)
for c := t1; c <= t2; c++ {
p := m.song.Score.Tracks[c].Order[s.Pattern]
if p < 0 {
continue
}
patterns := m.song.Score.Tracks[c].Patterns
if p >= len(patterns) {
continue
}
pattern := patterns[p]
if s.Row >= len(pattern) {
continue
}
m.song.Score.Tracks[c].Patterns[p][s.Row] = 1
}
}
m.notifyScoreChange()
}
func (m *Model) DeletePatternSelection() {
m.saveUndo("DeletePatternSelection", 0)
r1, r2, t1, t2 := m.getSelectionRange()
p1 := SongRow{Row: r1}.Wrap(m.song.Score).Pattern
p2 := SongRow{Row: r2}.Wrap(m.song.Score).Pattern
for p := p1; p <= p2; p++ {
for c := t1; c <= t2; c++ {
if p < len(m.song.Score.Tracks[c].Order) {
m.song.Score.Tracks[c].Order[p] = -1
}
}
}
m.notifyScoreChange()
}
func (m *Model) SetEditMode(value EditMode) {
m.editMode = value
}
func (m *Model) Undo() {
if !m.CanUndo() {
return
}
if len(m.redoStack) >= maxUndo {
m.redoStack = m.redoStack[1:]
}
m.redoStack = append(m.redoStack, m.song.Copy())
m.setSongNoUndo(m.undoStack[len(m.undoStack)-1])
m.undoStack = m.undoStack[:len(m.undoStack)-1]
}
func (m *Model) CanUndo() bool {
return len(m.undoStack) > 0
}
func (m *Model) Redo() {
if !m.CanRedo() {
return
}
if len(m.undoStack) >= maxUndo {
m.undoStack = m.undoStack[1:]
}
m.undoStack = append(m.undoStack, m.song.Copy())
m.setSongNoUndo(m.redoStack[len(m.redoStack)-1])
m.redoStack = m.redoStack[:len(m.redoStack)-1]
}
func (m *Model) CanRedo() bool {
return len(m.redoStack) > 0
}
func (m *Model) SetNoteTracking(value bool) {
m.noteTracking = value
}
func (m *Model) NoteTracking() bool {
return m.noteTracking
}
func (m *Model) Octave() int {
return m.octave
}
func (m *Model) Song() sointu.Song {
return m.song
}
func (m *Model) EditMode() EditMode {
return m.editMode
}
func (m *Model) SelectionCorner() SongPoint {
return m.selectionCorner
}
func (m *Model) SetSelectionCorner(value SongPoint) {
m.selectionCorner = value
m.clampPositions()
}
func (m *Model) Cursor() SongPoint {
return m.cursor
}
func (m *Model) SetCursor(value SongPoint) {
m.cursor = value
m.clampPositions()
}
func (m *Model) LowNibble() bool {
return m.lowNibble
}
func (m *Model) SetLowNibble(value bool) {
m.lowNibble = value
}
func (m *Model) InstrIndex() int {
return m.instrIndex
}
func (m *Model) Track() sointu.Track {
return m.song.Score.Tracks[m.cursor.Track]
}
func (m *Model) Instrument() sointu.Instrument {
return m.song.Patch[m.instrIndex]
}
func (m *Model) Unit() sointu.Unit {
return m.song.Patch[m.instrIndex].Units[m.unitIndex]
}
func (m *Model) UnitIndex() int {
return m.unitIndex
}
func (m *Model) ParamIndex() int {
return m.paramIndex
}
func (m *Model) clampPositions() {
m.cursor = m.cursor.Clamp(m.song.Score)
m.selectionCorner = m.selectionCorner.Clamp(m.song.Score)
if !m.Track().Effect {
m.lowNibble = false
}
m.instrIndex = clamp(m.instrIndex, 0, len(m.song.Patch)-1)
m.unitIndex = clamp(m.unitIndex, 0, len(m.Instrument().Units)-1)
for m.paramIndex < 0 {
if m.unitIndex == 0 {
m.paramIndex = 0
break
}
m.unitIndex--
m.paramIndex += m.NumParams()
}
for n := m.NumParams(); m.paramIndex >= n; n = m.NumParams() {
if m.unitIndex == len(m.Instrument().Units)-1 {
m.paramIndex = n - 1
break
}
m.paramIndex -= n
m.unitIndex++
}
}
func (m *Model) NumParams() int {
unit := m.Unit()
if unit.Type == "oscillator" {
if unit.Parameters["type"] != sointu.Sample {
return 10
}
return 14
}
numSettableParams := 0
for _, t := range sointu.UnitTypes[m.Unit().Type] {
if t.CanSet {
numSettableParams++
}
}
if numSettableParams == 0 {
numSettableParams = 1
}
return numSettableParams
}
func (m *Model) Param(index int) (Parameter, error) {
unit := m.Unit()
for _, t := range sointu.UnitTypes[unit.Type] {
if !t.CanSet {
continue
}
if index != 0 {
index--
continue
}
typ := IntegerParameter
if t.MaxValue == t.MinValue+1 {
typ = BoolParameter
}
val := m.Unit().Parameters[t.Name]
name := t.Name
hint := m.song.Patch.ParamHintString(m.instrIndex, m.unitIndex, name)
var text string
if hint != "" {
text = fmt.Sprintf("%v / %v", val, hint)
} else {
text = strconv.Itoa(val)
}
min, max := t.MinValue, t.MaxValue
if unit.Type == "send" {
if t.Name == "voice" {
i, _, err := m.song.Patch.FindSendTarget(unit.Parameters["target"])
if err == nil {
max = m.song.Patch[i].NumVoices
}
} else if t.Name == "target" {
typ = IDParameter
}
}
return Parameter{Type: typ, Min: min, Max: max, Name: name, Hint: text, Value: val}, nil
}
if unit.Type == "oscillator" && index == 0 {
key := compiler.SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
val := 0
hint := "0 / custom"
if v, ok := GmDlsEntryMap[key]; ok {
val = v + 1
hint = fmt.Sprintf("%v / %v", val, GmDlsEntries[v].Name)
}
return Parameter{Type: IntegerParameter, Min: 0, Max: len(GmDlsEntries), Name: "sample", Hint: hint, Value: val}, nil
}
return Parameter{}, errors.New("invalid parameter")
}
func (m *Model) SetParam(value int) {
p, err := m.Param(m.paramIndex)
if err != nil {
return
}
if value < p.Min {
value = p.Min
} else if value > p.Max {
value = p.Max
}
if p.Name == "sample" {
m.setGmDlsEntry(value - 1)
return
}
unit := m.Unit()
if unit.Parameters[p.Name] == value {
return
}
m.saveUndo("SetParam", 20)
unit.Parameters[p.Name] = value
m.clampPositions()
m.notifyPatchChange()
}
func (m *Model) AddPatchObserver(observer chan<- sointu.Patch) {
m.patchObservers = append(m.patchObservers, observer)
}
func (m *Model) AddScoreObserver(observer chan<- sointu.Score) {
m.scoreObservers = append(m.scoreObservers, observer)
}
func (m *Model) AddSamplesPerRowObserver(observer chan<- int) {
m.samplesPerRowObservers = append(m.samplesPerRowObservers, observer)
}
func (m *Model) AddPlayingObserver(observer chan<- bool) {
m.playingObservers = append(m.playingObservers, observer)
}
func (m *Model) setSongNoUndo(song sointu.Song) {
m.song = song
m.usedIDs = make(map[int]bool)
m.maxID = 0
for _, instr := range m.song.Patch {
for _, unit := range instr.Units {
if m.maxID < unit.ID {
m.maxID = unit.ID
}
}
}
for _, instr := range m.song.Patch {
m.assignUnitIDs(instr.Units)
}
m.clampPositions()
m.notifySamplesPerRowChange()
m.notifyPatchChange()
m.notifyScoreChange()
}
func (m *Model) notifyPatchChange() {
for _, channel := range m.patchObservers {
channel <- m.song.Patch.Copy()
}
}
func (m *Model) notifyScoreChange() {
for _, channel := range m.scoreObservers {
channel <- m.song.Score.Copy()
}
}
func (m *Model) notifySamplesPerRowChange() {
for _, channel := range m.samplesPerRowObservers {
channel <- m.song.SamplesPerRow()
}
}
func (m *Model) saveUndo(undoType string, undoSkipping int) {
if m.prevUndoType == undoType && m.undoSkipCounter < undoSkipping {
m.undoSkipCounter++
return
}
m.prevUndoType = undoType
m.undoSkipCounter = 0
if len(m.undoStack) >= maxUndo {
m.undoStack = m.undoStack[1:]
}
m.undoStack = append(m.undoStack, m.song.Copy())
m.redoStack = m.redoStack[:0]
}
func (m *Model) freeUnitIDs(units []sointu.Unit) {
for _, u := range units {
delete(m.usedIDs, u.ID)
}
}
func (m *Model) assignUnitIDs(units []sointu.Unit) {
for i := range units {
if units[i].ID == 0 || m.usedIDs[units[i].ID] {
m.maxID++
units[i].ID = m.maxID
}
m.usedIDs[units[i].ID] = true
if m.maxID < units[i].ID {
m.maxID = units[i].ID
}
}
}
func clamp(a, min, max int) int {
if a < min {
return min
}
if a > max {
return max
}
return a
}