mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
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.
This commit is contained in:
939
tracker/model.go
Normal file
939
tracker/model.go
Normal file
@ -0,0 +1,939 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user