package tracker import ( "errors" "fmt" "math" "strconv" "strings" "github.com/vsariola/sointu" "github.com/vsariola/sointu/vm" ) // 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. // It is owned by the GUI thread (goroutine), while the player is owned by // by the audioprocessing thread. They communicate using the two channels type ( Model struct { song sointu.Song selectionCorner SongPoint cursor SongPoint lowNibble bool instrIndex int unitIndex int paramIndex int octave int noteTracking bool usedIDs map[int]bool maxID int filePath string changedSinceSave bool patternUseCount [][]int panic bool playing bool recording bool playPosition SongRow instrEnlarged bool prevUndoType string undoSkipCounter int undoStack []sointu.Song redoStack []sointu.Song PlayerMessages <-chan PlayerMessage modelMessages chan<- interface{} } ModelPatchChangedMessage struct { sointu.Patch } ModelScoreChangedMessage struct { sointu.Score } ModelPlayingChangedMessage struct { bool } ModelPlayFromPositionMessage struct { SongRow } ModelSamplesPerRowChangedMessage struct { BPM, RowsPerBeat int } ModelPanicMessage struct { bool } ModelRecordingMessage struct { bool } ModelNoteOnMessage struct { id NoteID } ModelNoteOffMessage struct { id NoteID } ) type Parameter struct { Type ParameterType Name string Hint string Value int Min int Max int LargeStep int } type ParameterType int const ( IntegerParameter ParameterType = iota BoolParameter IDParameter ) const maxUndo = 256 func NewModel(modelMessages chan<- interface{}, playerMessages <-chan PlayerMessage) *Model { ret := new(Model) ret.modelMessages = modelMessages ret.PlayerMessages = playerMessages ret.setSongNoUndo(defaultSong.Copy()) return ret } func (m *Model) FilePath() string { return m.filePath } func (m *Model) SetFilePath(value string) { m.filePath = value } func (m *Model) ChangedSinceSave() bool { return m.changedSinceSave } func (m *Model) SetChangedSinceSave(value bool) { m.changedSinceSave = value } func (m *Model) ResetSong() { m.SetSong(defaultSong.Copy()) m.filePath = "" m.changedSinceSave = false } func (m *Model) SetSong(song sointu.Song) { // guard for malformed songs if len(song.Score.Tracks) == 0 || song.Score.Length <= 0 || len(song.Patch) == 0 { return } 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) ProcessPlayerMessage(msg PlayerMessage) { m.playPosition = msg.SongRow switch e := msg.Inner.(type) { case PlayerCrashMessage: m.panic = true case PlayerRecordedMessage: if e.BPM == 0 { e.BPM = float64(m.song.BPM) } song := RecordingToSong(m.song.Patch, m.song.RowsPerBeat, m.song.Score.RowsPerPattern, e) m.SetSong(song) m.instrEnlarged = false default: } } 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) SetInstrumentComment(comment string) { if m.Instrument().Comment == comment { return } m.saveUndo("SetInstrumentComment", 10) m.song.Patch[m.instrIndex].Comment = comment } 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: []sointu.Pattern{}, } m.song.Score.Tracks = newTracks m.clampPositions() m.notifyScoreChange() } func (m *Model) CanAddTrack() bool { return m.song.Score.NumVoices() < 32 } func (m *Model) DeleteTrack(forward bool) { if !m.CanDeleteTrack() { return } m.saveUndo("DeleteTrack", 0) newTracks := make([]sointu.Track, len(m.song.Score.Tracks)-1) copy(newTracks, m.song.Score.Tracks[:m.cursor.Track]) copy(newTracks[m.cursor.Track:], m.song.Score.Tracks[m.cursor.Track+1:]) m.song.Score.Tracks = newTracks if !forward { m.cursor.Track-- } m.selectionCorner = m.cursor m.clampPositions() m.computePatternUseCounts() m.notifyScoreChange() } func (m *Model) CanDeleteTrack() bool { return len(m.song.Score.Tracks) > 1 } func (m *Model) SwapTracks(i, j int) { if i < 0 || j < 0 || i >= len(m.song.Score.Tracks) || j >= len(m.song.Score.Tracks) || i == j { return } m.saveUndo("SwapTracks", 10) tracks := m.song.Score.Tracks tracks[i], tracks[j] = tracks[j], tracks[i] m.clampPositions() m.notifyScoreChange() } 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) NoteOn(id NoteID) { m.modelMessages <- ModelNoteOnMessage{id} } func (m *Model) NoteOff(id NoteID) { m.modelMessages <- ModelNoteOffMessage{id} } func (m *Model) Playing() bool { return m.playing } func (m *Model) SetPlaying(val bool) { if m.playing != val { m.playing = val m.modelMessages <- ModelPlayingChangedMessage{val} } } func (m *Model) PlayPosition() SongRow { return m.playPosition } 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] pat := trk.Order.Get(m.cursor.Pattern) if pat < 0 || pat >= len(trk.Patterns) { return 1 } return trk.Patterns[pat].Get(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 if m.cursor.Pattern < 0 || m.cursor.Row < 0 { return } patIndex := tracks[m.cursor.Track].Order.Get(m.cursor.Pattern) if patIndex < 0 { patIndex = len(tracks[m.cursor.Track].Patterns) for _, pi := range tracks[m.cursor.Track].Order { if pi >= patIndex { patIndex = pi + 1 // we find a pattern that is not in the pattern table nor in the order list i.e. completely new pattern } } tracks[m.cursor.Track].Order.Set(m.cursor.Pattern, patIndex) } for len(tracks[m.cursor.Track].Patterns) <= patIndex { tracks[m.cursor.Track].Patterns = append(tracks[m.cursor.Track].Patterns, nil) } tracks[m.cursor.Track].Patterns[patIndex].Set(m.cursor.Row, iv) m.notifyScoreChange() } func (m *Model) AdjustPatternNumber(delta int, swap bool) { r1, r2 := m.cursor.Pattern, m.selectionCorner.Pattern if r1 > r2 { r1, r2 = r2, r1 } t1, t2 := m.cursor.Track, m.selectionCorner.Track if t1 > t2 { t1, t2 = t2, t1 } type k = struct { track int pat int } newIds := map[k]int{} usedIds := map[k]bool{} for t := t1; t <= t2; t++ { for r := r1; r <= r2; r++ { p := m.song.Score.Tracks[t].Order.Get(r) if p < 0 { continue } if p+delta < 0 || p+delta > 35 { return // if any of the patterns would go out of range, abort } newIds[k{t, p}] = p + delta usedIds[k{t, p + delta}] = true } } m.saveUndo("AdjustPatternNumber", 10) for t := t1; t <= t2; t++ { if swap { maxId := len(m.song.Score.Tracks[t].Patterns) - 1 // check if song uses patterns that are not in the table yet for _, o := range m.song.Score.Tracks[t].Order { if maxId < o { maxId = o } } for p := 0; p <= maxId; p++ { j := p if delta > 0 { j = maxId - p } if _, ok := newIds[k{t, j}]; ok { continue } nextId := j for used := usedIds[k{t, nextId}]; used; used = usedIds[k{t, nextId}] { if delta < 0 { nextId++ } else { nextId-- } } newIds[k{t, j}] = nextId usedIds[k{t, nextId}] = true } for i, o := range m.song.Score.Tracks[t].Order { if o < 0 { continue } m.song.Score.Tracks[t].Order[i] = newIds[k{t, o}] } newPatterns := make([]sointu.Pattern, len(m.song.Score.Tracks[t].Patterns)) for p, pat := range m.song.Score.Tracks[t].Patterns { id := newIds[k{t, p}] for len(newPatterns) <= id { newPatterns = append(newPatterns, nil) } newPatterns[id] = pat } m.song.Score.Tracks[t].Patterns = newPatterns } else { for r := r1; r <= r2; r++ { p := m.song.Score.Tracks[t].Order.Get(r) if p < 0 { continue } m.song.Score.Tracks[t].Order.Set(r, p+delta) } } } m.computePatternUseCounts() m.notifyScoreChange() } func (m *Model) SetRecording(val bool) { if m.recording != val { m.recording = val m.instrEnlarged = val m.modelMessages <- ModelRecordingMessage{val} } } func (m *Model) Recording() bool { return m.recording } func (m *Model) SetPanic(val bool) { if m.panic != val { m.panic = val m.modelMessages <- ModelPanicMessage{val} } } func (m *Model) Panic() bool { return m.panic } func (m *Model) SetInstrEnlarged(val bool) { m.instrEnlarged = val } func (m *Model) InstrEnlarged() bool { return m.instrEnlarged } func (m *Model) PlayFromPosition(sr SongRow) { m.playing = true m.modelMessages <- ModelPlayFromPositionMessage{sr} } func (m *Model) SetCurrentPattern(pat int) { m.saveUndo("SetCurrentPattern", 0) m.song.Score.Tracks[m.cursor.Track].Order.Set(m.cursor.Pattern, pat) m.computePatternUseCounts() m.notifyScoreChange() } func (m *Model) IsPatternUnique(track, pattern int) bool { if track < 0 || track >= len(m.patternUseCount) { return false } p := m.patternUseCount[track] if pattern < 0 || pattern >= len(p) { return false } return p[pattern] <= 1 } 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.computePatternUseCounts() 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) PasteUnits(units []sointu.Unit) { m.saveUndo("PasteUnits", 0) newUnits := make([]sointu.Unit, len(m.Instrument().Units)+len(units)) m.unitIndex++ copy(newUnits, m.Instrument().Units[:m.unitIndex]) copy(newUnits[m.unitIndex+len(units):], m.Instrument().Units[m.unitIndex:]) for _, unit := range units { if _, ok := m.usedIDs[unit.ID]; ok { m.maxID++ unit.ID = m.maxID } m.usedIDs[unit.ID] = true } copy(newUnits[m.unitIndex:m.unitIndex+len(units)], units) m.song.Patch[m.instrIndex].Units = newUnits m.paramIndex = 0 m.clampPositions() 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.computePatternUseCounts() 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.computePatternUseCounts() m.notifyScoreChange() } func (m *Model) DeleteUnits(forward bool, a, b int) []sointu.Unit { instr := m.Instrument() m.saveUndo("DeleteUnits", 0) a, b = intMin(a, b), intMax(a, b) if a < 0 { a = 0 } if b > len(instr.Units)-1 { b = len(instr.Units) - 1 } for i := a; i <= b; i++ { delete(m.usedIDs, instr.Units[i].ID) } var newUnits []sointu.Unit if a == 0 && b == len(instr.Units)-1 { newUnits = make([]sointu.Unit, 1) m.unitIndex = 0 } else { newUnits = make([]sointu.Unit, len(instr.Units)-(b-a+1)) copy(newUnits, instr.Units[:a]) copy(newUnits[a:], instr.Units[b+1:]) m.unitIndex = a if forward { m.unitIndex-- } } deletedUnits := instr.Units[a : b+1] m.song.Patch[m.instrIndex].Units = newUnits m.paramIndex = 0 m.clampPositions() m.notifyPatchChange() return deletedUnits } 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++ { if len(m.song.Score.Tracks[c].Order) <= s.Pattern { continue } 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.computePatternUseCounts() m.notifyScoreChange() } 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) ClearUndoHistory() { if len(m.undoStack) > 0 { m.undoStack = m.undoStack[:0] } if len(m.redoStack) > 0 { m.redoStack = m.redoStack[: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) 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.Wrap(m.song.Score) m.selectionCorner = m.selectionCorner.Wrap(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) m.paramIndex = clamp(m.paramIndex, 0, m.NumParams()-1) } 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 } if unit.Type == "delay" { numSettableParams += 1 + len(unit.VarArgs) if len(unit.VarArgs)%2 == 1 && unit.Parameters["stereo"] == 1 { numSettableParams++ } } 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 } } largeStep := 16 if unit.Type == "oscillator" && t.Name == "transpose" { largeStep = 12 } return Parameter{Type: typ, Min: min, Max: max, Name: name, Hint: text, Value: val, LargeStep: largeStep}, nil } if unit.Type == "oscillator" && index == 0 { key := vm.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 } if unit.Type == "delay" { if index == 0 { l := len(unit.VarArgs) if unit.Parameters["stereo"] == 1 { l = (l + 1) / 2 } return Parameter{Type: IntegerParameter, Min: 1, Max: 32, Name: "delaylines", Hint: strconv.Itoa(l), Value: l}, nil } index-- if index < len(unit.VarArgs) { val := unit.VarArgs[index] var text string switch unit.Parameters["notetracking"] { default: case 0: text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(m.song.SamplesPerRow())) return Parameter{Type: IntegerParameter, Min: 1, Max: 65535, Name: "delaytime", Hint: text, Value: val, LargeStep: 256}, nil case 1: relPitch := float64(val) / 10787 semitones := -math.Log2(relPitch) * 12 text = fmt.Sprintf("%v / %.3f st", val, semitones) return Parameter{Type: IntegerParameter, Min: 1, Max: 65535, Name: "delaytime", Hint: text, Value: val, LargeStep: 256}, nil case 2: k := 0 v := val for v&1 == 0 { // divide val by 2 until it is odd v >>= 1 k++ } text := "" switch v { case 1: if k <= 7 { text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k)) } case 3: if k <= 6 { text = fmt.Sprintf(" (1/%d)", 1<<(6-k)) } break case 9: if k <= 5 { text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k)) } } text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text) return Parameter{Type: IntegerParameter, Min: 1, Max: 576, Name: "delaytime", Hint: text, Value: val, LargeStep: 16}, nil } } } return Parameter{}, errors.New("invalid parameter") } func (m *Model) RemoveUnusedData() { m.saveUndo("RemoveUnusedData", 0) for trkIndex, trk := range m.song.Score.Tracks { // assign new indices to patterns newIndex := map[int]int{} runningIndex := 0 length := 0 if len(trk.Order) > m.song.Score.Length { trk.Order = trk.Order[:m.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.song.Score.RowsPerPattern { patLength = m.song.Score.RowsPerPattern } newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold } } trk.Patterns = newPatterns m.song.Score.Tracks[trkIndex] = trk } m.computePatternUseCounts() m.notifyScoreChange() } 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 p.Name == "delaylines" { m.saveUndo("SetParam", 20) targetLines := value if unit.Parameters["stereo"] == 1 { targetLines *= 2 } for len(m.Instrument().Units[m.unitIndex].VarArgs) < targetLines { m.Instrument().Units[m.unitIndex].VarArgs = append(m.Instrument().Units[m.unitIndex].VarArgs, 1) } m.Instrument().Units[m.unitIndex].VarArgs = m.Instrument().Units[m.unitIndex].VarArgs[:targetLines] } else if p.Name == "delaytime" { m.saveUndo("SetParam", 20) index := m.paramIndex - 7 for len(m.Instrument().Units[m.unitIndex].VarArgs) <= index { m.Instrument().Units[m.unitIndex].VarArgs = append(m.Instrument().Units[m.unitIndex].VarArgs, 1) } m.Instrument().Units[m.unitIndex].VarArgs[index] = value } else { if unit.Parameters[p.Name] == value { return } m.saveUndo("SetParam", 20) unit.Parameters[p.Name] = value } m.clampPositions() m.notifyPatchChange() } 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.computePatternUseCounts() m.notifySamplesPerRowChange() m.notifyPatchChange() m.notifyScoreChange() } func (m *Model) notifyPatchChange() { m.panic = false select { case m.modelMessages <- ModelPatchChangedMessage{m.song.Patch.Copy()}: default: } } func (m *Model) notifyScoreChange() { select { case m.modelMessages <- ModelScoreChangedMessage{m.song.Score.Copy()}: default: } } func (m *Model) notifySamplesPerRowChange() { select { case m.modelMessages <- ModelSamplesPerRowChangedMessage{m.song.BPM, m.song.RowsPerBeat}: default: } } func (m *Model) saveUndo(undoType string, undoSkipping int) { if m.prevUndoType == undoType && m.undoSkipCounter < undoSkipping { m.undoSkipCounter++ return } m.changedSinceSave = true 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) { rewrites := map[int]int{} for i := range units { if id := units[i].ID; id == 0 || m.usedIDs[id] { m.maxID++ if id > 0 { rewrites[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 } } for i, u := range units { if target, ok := u.Parameters["target"]; u.Type == "send" && ok { if newId, ok := rewrites[target]; ok { units[i].Parameters["target"] = newId } } } } func (m *Model) computePatternUseCounts() { for i, track := range m.song.Score.Tracks { for len(m.patternUseCount) <= i { m.patternUseCount = append(m.patternUseCount, nil) } for j := range m.patternUseCount[i] { m.patternUseCount[i][j] = 0 } for j := 0; j < m.song.Score.Length; j++ { if j >= len(track.Order) { break } p := track.Order[j] for len(m.patternUseCount[i]) <= p { m.patternUseCount[i] = append(m.patternUseCount[i], 0) } if p < 0 { continue } m.patternUseCount[i][p]++ } } } func clamp(a, min, max int) int { if a < min { return min } if a > max { return max } return a } func intMax(a, b int) int { if a > b { return a } return b } func intMin(a, b int) int { if a < b { return a } return b }