package tracker import ( "math" "time" "github.com/vsariola/sointu" "gopkg.in/yaml.v3" ) type ( Table struct { TableData } TableData interface { Cursor() Point Cursor2() Point SetCursor(Point) SetCursor2(Point) Width() int Height() int MoveCursor(dx, dy int) (ok bool) clear(p Point) set(p Point, value int) add(rect Rect, delta int, largestep bool) (ok bool) marshal(rect Rect) (data []byte, ok bool) unmarshalAtCursor(data []byte) (ok bool) unmarshalRange(rect Rect, data []byte) (ok bool) change(kind string, severity ChangeSeverity) func() cancel() } Point struct { X, Y int } Rect struct { TopLeft, BottomRight Point } Order Model Notes Model ) // Model methods func (m *Model) Order() *Order { return (*Order)(m) } func (m *Model) Notes() *Notes { return (*Notes)(m) } // Rect methods func (r *Rect) Contains(p Point) bool { return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X && r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y } func (r *Rect) Width() int { return r.BottomRight.X - r.TopLeft.X + 1 } func (r *Rect) Height() int { return r.BottomRight.Y - r.TopLeft.Y + 1 } func (r *Rect) Limit(width, height int) { if r.TopLeft.X < 0 { r.TopLeft.X = 0 } if r.TopLeft.Y < 0 { r.TopLeft.Y = 0 } if r.BottomRight.X >= width { r.BottomRight.X = width - 1 } if r.BottomRight.Y >= height { r.BottomRight.Y = height - 1 } } // Table methods func (v Table) Range() (rect Rect) { rect.TopLeft.X = min(v.Cursor().X, v.Cursor2().X) rect.TopLeft.Y = min(v.Cursor().Y, v.Cursor2().Y) rect.BottomRight.X = max(v.Cursor().X, v.Cursor2().X) rect.BottomRight.Y = max(v.Cursor().Y, v.Cursor2().Y) return } func (v Table) Copy() ([]byte, bool) { ret, ok := v.marshal(v.Range()) if !ok { return nil, false } return ret, true } func (v Table) Paste(data []byte) bool { defer v.change("Paste", MajorChange)() if v.Cursor() == v.Cursor2() { return v.unmarshalAtCursor(data) } else { return v.unmarshalRange(v.Range(), data) } } func (v Table) Clear() { defer v.change("Clear", MajorChange)() rect := v.Range() rect.Limit(v.Width(), v.Height()) for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ { for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ { v.clear(Point{x, y}) } } } func (v Table) Set(value byte) { defer v.change("Set", MajorChange)() cursor := v.Cursor() // TODO: might check for visibility v.set(cursor, int(value)) } func (v Table) Fill(value int) { defer v.change("Fill", MajorChange)() rect := v.Range() rect.Limit(v.Width(), v.Height()) for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ { for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ { v.set(Point{x, y}, value) } } } func (v Table) Add(delta int, largeStep bool) { defer v.change("Add", MinorChange)() if !v.add(v.Range(), delta, largeStep) { v.cancel() } } func (v Table) SetCursorX(x int) { p := v.Cursor() p.X = x v.SetCursor(p) } func (v Table) SetCursorY(y int) { p := v.Cursor() p.Y = y v.SetCursor(p) } // Order methods func (v *Order) Table() Table { return Table{v} } func (m *Order) Cursor() Point { t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0) p := max(min(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0) return Point{t, p} } func (m *Order) Cursor2() Point { t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0) p := max(min(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0) return Point{t, p} } func (m *Order) SetCursor(p Point) { m.d.Cursor.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0) y := max(min(p.Y, m.d.Song.Score.Length-1), 0) if y != m.d.Cursor.OrderRow { m.follow = false } m.d.Cursor.OrderRow = y m.updateCursorRows() } func (m *Order) SetCursor2(p Point) { m.d.Cursor2.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0) m.d.Cursor2.OrderRow = max(min(p.Y, m.d.Song.Score.Length-1), 0) m.updateCursorRows() } func (v *Order) updateCursorRows() { if v.Cursor() == v.Cursor2() { v.d.Cursor.PatternRow = 0 v.d.Cursor2.PatternRow = 0 return } if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow { v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1 v.d.Cursor2.PatternRow = 0 } else { v.d.Cursor.PatternRow = 0 v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1 } } func (v *Order) Width() int { return len((*Model)(v).d.Song.Score.Tracks) } func (v *Order) Height() int { return (*Model)(v).d.Song.Score.Length } func (v *Order) MoveCursor(dx, dy int) (ok bool) { p := v.Cursor() p.X += dx p.Y += dy v.SetCursor(p) return p == v.Cursor() } func (m *Order) clear(p Point) { m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1) } func (m *Order) set(p Point, value int) { m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value) } func (v *Order) add(rect Rect, delta int, largeStep bool) (ok bool) { if largeStep { delta *= 8 } for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ { for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ { if !v.add1(Point{x, y}, delta) { return false } } } return true } func (v *Order) add1(p Point, delta int) (ok bool) { if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) { return true } val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y) if val < 0 { return true } val += delta if val < 0 || val > 36 { return false } v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val) return true } type marshalOrder struct { Order []int `yaml:",flow"` } type marshalTracks struct { Tracks []marshalOrder } func (m *Order) marshal(rect Rect) (data []byte, ok bool) { width := rect.BottomRight.X - rect.TopLeft.X + 1 height := rect.BottomRight.Y - rect.TopLeft.Y + 1 var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)} for x := 0; x < width; x++ { ax := x + rect.TopLeft.X if ax < 0 || ax >= len(m.d.Song.Score.Tracks) { continue } table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)}) for y := 0; y < height; y++ { table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y)) } } ret, err := yaml.Marshal(table) if err != nil { return nil, false } return ret, true } func (m *Order) unmarshal(data []byte) (marshalTracks, bool) { var table marshalTracks yaml.Unmarshal(data, &table) if len(table.Tracks) == 0 { return marshalTracks{}, false } for i := 0; i < len(table.Tracks); i++ { if len(table.Tracks[i].Order) > 0 { return table, true } } return marshalTracks{}, false } func (v *Order) unmarshalAtCursor(data []byte) bool { table, ok := v.unmarshal(data) if !ok { return false } for i := 0; i < len(table.Tracks); i++ { for j, q := range table.Tracks[i].Order { if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 { continue } x := i + v.Cursor().X y := j + v.Cursor().Y if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length { continue } v.d.Song.Score.Tracks[x].Order.Set(y, q) } } return true } func (v *Order) unmarshalRange(rect Rect, data []byte) bool { table, ok := v.unmarshal(data) if !ok { return false } for i := 0; i < rect.Width(); i++ { for j := 0; j < rect.Height(); j++ { k := i % len(table.Tracks) l := j % len(table.Tracks[k].Order) a := table.Tracks[k].Order[l] if a < -1 || a > 36 { continue } x := i + rect.TopLeft.X y := j + rect.TopLeft.Y if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length { continue } v.d.Song.Score.Tracks[x].Order.Set(y, a) } } return true } func (v *Order) change(kind string, severity ChangeSeverity) func() { return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity) } func (v *Order) cancel() { v.changeCancel = true } func (m *Order) Value(p Point) int { if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) { return -1 } return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y) } func (m *Order) SetValue(p Point, val int) { defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)() m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val) } // NoteTable func (v *Notes) Table() Table { return Table{v} } func (m *Notes) Cursor() Point { t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0) p := max(min(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0) return Point{t, p} } func (m *Notes) Cursor2() Point { t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0) p := max(min(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0) return Point{t, p} } func (v *Notes) SetCursor(p Point) { v.d.Cursor.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0) newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y}) if newPos != v.d.Cursor.SongPos { v.follow = false } v.d.Cursor.SongPos = newPos } func (v *Notes) SetCursor2(p Point) { v.d.Cursor2.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0) v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y}) } func (m *Notes) SetCursorFloat(x, y float32) { m.SetCursor(Point{int(x), int(y)}) m.d.LowNibble = math.Mod(float64(x), 1.0) > 0.5 } func (v *Notes) Width() int { return len((*Model)(v).d.Song.Score.Tracks) } func (v *Notes) Height() int { return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern } func (v *Notes) MoveCursor(dx, dy int) (ok bool) { p := v.Cursor() for dx < 0 { if v.Effect(p.X) && v.d.LowNibble { v.d.LowNibble = false } else { p.X-- v.d.LowNibble = true } dx++ } for dx > 0 { if v.Effect(p.X) && !v.d.LowNibble { v.d.LowNibble = true } else { p.X++ v.d.LowNibble = false } dx-- } p.Y += dy v.SetCursor(p) return p == v.Cursor() } func (v *Notes) clear(p Point) { v.Input(1) } func (v *Notes) set(p Point, value int) { v.SetValue(p, byte(value)) } func (v *Notes) add(rect Rect, delta int, largeStep bool) (ok bool) { if largeStep { delta *= 12 } for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- { for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- { if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() { continue } pos := v.d.Song.Score.SongPos(y) note := v.d.Song.Score.Tracks[x].Note(pos) if note <= 1 { continue } newVal := int(note) + delta if newVal < 2 { newVal = 2 } else if newVal > 255 { newVal = 255 } // only do all sets after all gets, so we don't accidentally adjust single note multiple times defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal), v.uniquePatterns) } } return true } type noteTable struct { Notes [][]byte `yaml:",flow"` } func (m *Notes) marshal(rect Rect) (data []byte, ok bool) { width := rect.BottomRight.X - rect.TopLeft.X + 1 height := rect.BottomRight.Y - rect.TopLeft.Y + 1 var table = noteTable{Notes: make([][]byte, 0, width)} for x := 0; x < width; x++ { table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)) for y := 0; y < height; y++ { pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y) ax := x + rect.TopLeft.X if ax < 0 || ax >= len(m.d.Song.Score.Tracks) { continue } table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos)) } } ret, err := yaml.Marshal(table) if err != nil { return nil, false } return ret, true } func (v *Notes) unmarshal(data []byte) (noteTable, bool) { var table noteTable yaml.Unmarshal(data, &table) if len(table.Notes) == 0 { return noteTable{}, false } for i := 0; i < len(table.Notes); i++ { if len(table.Notes[i]) > 0 { return table, true } } return noteTable{}, false } func (v *Notes) unmarshalAtCursor(data []byte) bool { table, ok := v.unmarshal(data) if !ok { return false } for i := 0; i < len(table.Notes); i++ { for j, q := range table.Notes[i] { x := i + v.Cursor().X y := j + v.Cursor().Y if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() { continue } pos := v.d.Song.Score.SongPos(y) v.d.Song.Score.Tracks[x].SetNote(pos, q, v.uniquePatterns) } } return true } func (v *Notes) unmarshalRange(rect Rect, data []byte) bool { table, ok := v.unmarshal(data) if !ok { return false } for i := 0; i < rect.Width(); i++ { for j := 0; j < rect.Height(); j++ { k := i % len(table.Notes) l := j % len(table.Notes[k]) a := table.Notes[k][l] x := i + rect.TopLeft.X y := j + rect.TopLeft.Y if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() { continue } pos := v.d.Song.Score.SongPos(y) v.d.Song.Score.Tracks[x].SetNote(pos, a, v.uniquePatterns) } } return true } func (v *Notes) change(kind string, severity ChangeSeverity) func() { return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity) } func (v *Notes) cancel() { v.changeCancel = true } func (m *Notes) Value(p Point) byte { if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) { return 1 } pos := m.d.Song.Score.SongPos(p.Y) return m.d.Song.Score.Tracks[p.X].Note(pos) } func (m *Notes) Effect(x int) bool { if x < 0 || x >= len(m.d.Song.Score.Tracks) { return false } return m.d.Song.Score.Tracks[x].Effect } func (m *Notes) LowNibble() bool { return m.d.LowNibble } func (m *Notes) SetValue(p Point, val byte) { defer m.change("SetValue", MinorChange)() if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) { return } track := &(m.d.Song.Score.Tracks[p.X]) pos := m.d.Song.Score.SongPos(p.Y) (*track).SetNote(pos, val, m.uniquePatterns) } func (v *Notes) Input(note byte) NoteEvent { v.Table().Fill(int(note)) return v.finishInput(note) } func (v *Notes) InputNibble(nibble byte) NoteEvent { defer v.change("FillNibble", MajorChange)() rect := Table{v}.Range() for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ { for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ { val := v.Value(Point{x, y}) if val == 1 { val = 0 // treat hold also as 0 } if v.d.LowNibble { val = (val & 0xf0) | byte(nibble&15) } else { val = (val & 0x0f) | byte((nibble&15)<<4) } v.SetValue(Point{x, y}, val) } } return v.finishInput(v.Value(v.Cursor())) } func (v *Notes) finishInput(note byte) NoteEvent { if step := v.d.Step; step > 0 { v.Table().MoveCursor(0, step) v.Table().SetCursor2(v.Table().Cursor()) } TrySend(v.broker.ToGUI, any(MsgToGUI{Kind: GUIMessageEnsureCursorVisible, Param: v.Table().Cursor().Y})) track := v.Cursor().X ts := time.Now().UnixMilli() * 441 / 10 // convert to 44100Hz frames return NoteEvent{IsTrack: true, Channel: track, Note: note, On: true, Timestamp: ts} }