mirror of
				https://github.com/vsariola/sointu.git
				synced 2025-10-30 23:45:53 -04:00 
			
		
		
		
	feat(tracker): implement selecting a range in the tracker
also refactored a location in song to SongPoint, and a particular row in song into SongRow
This commit is contained in:
		| @ -60,7 +60,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool { | ||||
| 			t.SetCurrentNote(0) | ||||
| 			return true | ||||
| 		case key.NameDeleteForward: | ||||
| 			t.SetCurrentNote(1) | ||||
| 			t.DeleteSelection() | ||||
| 			return true | ||||
| 		case key.NameEscape: | ||||
| 			os.Exit(0) | ||||
| @ -77,7 +77,11 @@ func (t *Tracker) KeyEvent(e key.Event) bool { | ||||
| 			if e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				delta = -t.song.PatternRows() | ||||
| 			} | ||||
| 			t.moveCursor(delta) | ||||
| 			t.Cursor.Row += delta | ||||
| 			t.Cursor.Clamp(t.song) | ||||
| 			if !e.Modifiers.Contain(key.ModShift) { | ||||
| 				t.SelectionCorner = t.Cursor | ||||
| 			} | ||||
| 			t.NoteTracking = false | ||||
| 			return true | ||||
| 		case key.NameDownArrow: | ||||
| @ -85,37 +89,41 @@ func (t *Tracker) KeyEvent(e key.Event) bool { | ||||
| 			if e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				delta = t.song.PatternRows() | ||||
| 			} | ||||
| 			t.moveCursor(delta) | ||||
| 			t.Cursor.Row += delta | ||||
| 			t.Cursor.Clamp(t.song) | ||||
| 			if !e.Modifiers.Contain(key.ModShift) { | ||||
| 				t.SelectionCorner = t.Cursor | ||||
| 			} | ||||
| 			t.NoteTracking = false | ||||
| 			return true | ||||
| 		case key.NameLeftArrow: | ||||
| 			if t.CursorColumn == 0 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				t.ActiveTrack = (t.ActiveTrack + len(t.song.Tracks) - 1) % len(t.song.Tracks) | ||||
| 				if t.TrackShowHex[t.ActiveTrack] { | ||||
| 			if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				t.Cursor.Track-- | ||||
| 				t.Cursor.Clamp(t.song) | ||||
| 				if t.TrackShowHex[t.Cursor.Track] { | ||||
| 					t.CursorColumn = 1 | ||||
| 				} else { | ||||
| 					t.CursorColumn = 0 | ||||
| 				} | ||||
| 				if !e.Modifiers.Contain(key.ModShift) { | ||||
| 					t.SelectionCorner = t.Cursor | ||||
| 				} | ||||
| 			} else { | ||||
| 				t.CursorColumn-- | ||||
| 			} | ||||
| 			return true | ||||
| 		case key.NameRightArrow: | ||||
| 			if t.CursorColumn == 1 || !t.TrackShowHex[t.ActiveTrack] || e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				t.ActiveTrack = (t.ActiveTrack + 1) % len(t.song.Tracks) | ||||
| 			if t.CursorColumn == 1 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) { | ||||
| 				t.Cursor.Track++ | ||||
| 				t.Cursor.Clamp(t.song) | ||||
| 				if !e.Modifiers.Contain(key.ModShift) { | ||||
| 					t.SelectionCorner = t.Cursor | ||||
| 				} | ||||
| 				t.CursorColumn = 0 | ||||
| 			} else { | ||||
| 				t.CursorColumn++ | ||||
| 			} | ||||
| 			return true | ||||
| 		case key.NameTab: | ||||
| 			if e.Modifiers.Contain(key.ModShift) { | ||||
| 				t.ActiveTrack = (t.ActiveTrack + len(t.song.Tracks) - 1) % len(t.song.Tracks) | ||||
| 			} else { | ||||
| 				t.ActiveTrack = (t.ActiveTrack + 1) % len(t.song.Tracks) | ||||
| 			} | ||||
| 			t.CursorColumn = 0 | ||||
| 			return true | ||||
| 		} | ||||
| 		if e.Modifiers.Contain(key.ModCtrl) { | ||||
| 			if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { | ||||
| @ -123,7 +131,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool { | ||||
| 				return true | ||||
| 			} | ||||
| 		} else { | ||||
| 			if !t.TrackShowHex[t.ActiveTrack] { | ||||
| 			if !t.TrackShowHex[t.Cursor.Track] { | ||||
| 				if val, ok := noteMap[e.Name]; ok { | ||||
| 					t.NotePressed(val) | ||||
| 					return true | ||||
| @ -139,24 +147,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (t *Tracker) moveCursor(delta int) { | ||||
| 	newRow := t.CursorRow + delta | ||||
| 	remainder := (newRow + t.song.PatternRows()) % t.song.PatternRows() | ||||
| 	t.DisplayPattern += (newRow - remainder) / t.song.PatternRows() | ||||
| 	if t.DisplayPattern < 0 { | ||||
| 		t.CursorRow = 0 | ||||
| 		t.DisplayPattern = 0 | ||||
| 	} else if t.DisplayPattern >= t.song.SequenceLength() { | ||||
| 		t.CursorRow = t.song.PatternRows() - 1 | ||||
| 		t.DisplayPattern = t.song.SequenceLength() - 1 | ||||
| 	} else { | ||||
| 		t.CursorRow = remainder | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getCurrent returns the current (note) value in current pattern under the cursor | ||||
| func (t *Tracker) getCurrent() byte { | ||||
| 	return t.song.Tracks[t.ActiveTrack].Patterns[t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern]][t.CursorRow] | ||||
| 	return t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] | ||||
| } | ||||
|  | ||||
| // NotePressed handles incoming key presses while in the note column | ||||
|  | ||||
| @ -83,18 +83,8 @@ func (t *Tracker) Layout(gtx layout.Context) { | ||||
| } | ||||
|  | ||||
| func (t *Tracker) layoutTracksAndPatterns(gtx layout.Context) layout.Dimensions { | ||||
| 	playPat := t.PlayPattern | ||||
| 	if !t.Playing { | ||||
| 		playPat = -1 | ||||
| 	} | ||||
| 	return t.BottomHorizontalSplit.Layout(gtx, | ||||
| 		t.layoutPatterns( | ||||
| 			t.song.Tracks, | ||||
| 			t.ActiveTrack, | ||||
| 			t.DisplayPattern, | ||||
| 			t.CursorColumn, | ||||
| 			playPat, | ||||
| 		), | ||||
| 		t.layoutPatterns, | ||||
| 		t.layoutTracks, | ||||
| 	) | ||||
| } | ||||
| @ -106,7 +96,7 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { | ||||
| 	t.playRowPatMutex.RLock() | ||||
| 	defer t.playRowPatMutex.RUnlock() | ||||
|  | ||||
| 	playPat := t.PlayPattern | ||||
| 	playPat := t.PlayPosition.Pattern | ||||
| 	if !t.Playing { | ||||
| 		playPat = -1 | ||||
| 	} | ||||
| @ -114,16 +104,15 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { | ||||
| 	rowMarkers := layout.Rigid(t.layoutRowMarkers( | ||||
| 		len(t.song.Tracks[0].Patterns[0]), | ||||
| 		len(t.song.Tracks[0].Sequence), | ||||
| 		t.CursorRow, | ||||
| 		t.DisplayPattern, | ||||
| 		t.Cursor.Row, | ||||
| 		t.Cursor.Pattern, | ||||
| 		t.CursorColumn, | ||||
| 		t.PlayRow, | ||||
| 		t.PlayPosition.Row, | ||||
| 		playPat, | ||||
| 	)) | ||||
| 	leftInset := layout.Inset{Left: unit.Dp(4)} | ||||
| 	for i, trk := range t.song.Tracks { | ||||
| 		i2 := i     // avoids i being updated in the closure | ||||
| 		trk2 := trk // avoids trk being updated in the closure | ||||
| 	for i := range t.song.Tracks { | ||||
| 		i2 := i // avoids i being updated in the closure | ||||
| 		if len(t.TrackHexCheckBoxes) <= i { | ||||
| 			t.TrackHexCheckBoxes = append(t.TrackHexCheckBoxes, new(widget.Bool)) | ||||
| 		} | ||||
| @ -136,17 +125,7 @@ func (t *Tracker) layoutTracks(gtx layout.Context) layout.Dimensions { | ||||
| 			cbStyle.Color = white | ||||
| 			ret := layout.Stack{}.Layout(gtx, | ||||
| 				layout.Stacked(func(gtx layout.Context) D { | ||||
| 					return leftInset.Layout(gtx, t.layoutTrack( | ||||
| 						trk2.Patterns, | ||||
| 						trk2.Sequence, | ||||
| 						t.ActiveTrack == i2, | ||||
| 						t.TrackShowHex[i2], | ||||
| 						t.CursorRow, | ||||
| 						t.DisplayPattern, | ||||
| 						t.CursorColumn, | ||||
| 						t.PlayRow, | ||||
| 						playPat, | ||||
| 					)) | ||||
| 					return leftInset.Layout(gtx, t.layoutTrack(i2)) | ||||
| 				}), | ||||
| 				layout.Stacked(cbStyle.Layout), | ||||
| 			) | ||||
|  | ||||
| @ -11,7 +11,6 @@ import ( | ||||
| 	"gioui.org/op/clip" | ||||
| 	"gioui.org/op/paint" | ||||
| 	"gioui.org/widget" | ||||
| 	"github.com/vsariola/sointu" | ||||
| ) | ||||
|  | ||||
| const patternCellHeight = 16 | ||||
| @ -19,34 +18,41 @@ const patternCellWidth = 16 | ||||
| const patternVisibleTracks = 8 | ||||
| const patternRowMarkerWidth = 30 | ||||
|  | ||||
| func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget { | ||||
| 	return func(gtx layout.Context) layout.Dimensions { | ||||
| 		gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth | ||||
| 		gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth | ||||
| 		defer op.Push(gtx.Ops).Pop() | ||||
| 		clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) | ||||
| 		paint.FillShape(gtx.Ops, patternSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) | ||||
| 		for j := range tracks[0].Sequence { | ||||
| 			if j == playingPattern { | ||||
| 				paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op()) | ||||
| 			} | ||||
| 			paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) | ||||
| 			widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j))) | ||||
| 			stack := op.Push(gtx.Ops) | ||||
| 			op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) | ||||
| 			for i, track := range tracks { | ||||
| 				paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops) | ||||
| 				widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, fmt.Sprintf("%d", track.Sequence[j])) | ||||
| 				if activeTrack == i && j == cursorPattern { | ||||
| 					paint.FillShape(gtx.Ops, patternCursorColor, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op()) | ||||
| 				} | ||||
| 				op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) | ||||
| 			} | ||||
| 			stack.Pop() | ||||
| 			op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops) | ||||
| 		} | ||||
| 		return layout.Dimensions{Size: gtx.Constraints.Max} | ||||
| func (t *Tracker) layoutPatterns(gtx C) D { | ||||
| 	gtx.Constraints.Min.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth | ||||
| 	gtx.Constraints.Max.X = patternCellWidth*patternVisibleTracks + patternRowMarkerWidth | ||||
| 	defer op.Push(gtx.Ops).Pop() | ||||
| 	clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) | ||||
| 	paint.FillShape(gtx.Ops, patternSurfaceColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}.Op()) | ||||
| 	patternRect := SongRect{ | ||||
| 		Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track}, | ||||
| 		Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track}, | ||||
| 	} | ||||
| 	for j := 0; j < t.song.SequenceLength(); j++ { | ||||
| 		if j == t.PlayPosition.Pattern && t.Playing { | ||||
| 			paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op()) | ||||
| 		} | ||||
| 		paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) | ||||
| 		widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j))) | ||||
| 		stack := op.Push(gtx.Ops) | ||||
| 		op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) | ||||
| 		for i, track := range t.song.Tracks { | ||||
| 			paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops) | ||||
| 			widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, fmt.Sprintf("%d", track.Sequence[j])) | ||||
| 			point := SongPoint{Track: i, SongRow: SongRow{Pattern: j}} | ||||
| 			if patternRect.Contains(point) { | ||||
| 				color := patternSelectionColor | ||||
| 				if point.Pattern == t.Cursor.Pattern && point.Track == t.Cursor.Track { | ||||
| 					color = patternCursorColor | ||||
| 				} | ||||
| 				paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op()) | ||||
| 			} | ||||
| 			op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) | ||||
| 		} | ||||
| 		stack.Pop() | ||||
| 		op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops) | ||||
| 	} | ||||
| 	return layout.Dimensions{Size: gtx.Constraints.Max} | ||||
| } | ||||
|  | ||||
| func patternIndexToString(index byte) string { | ||||
|  | ||||
							
								
								
									
										85
									
								
								tracker/songpoint.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								tracker/songpoint.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| package tracker | ||||
|  | ||||
| import "github.com/vsariola/sointu" | ||||
|  | ||||
| type SongRow struct { | ||||
| 	Pattern int | ||||
| 	Row     int | ||||
| } | ||||
|  | ||||
| type SongPoint struct { | ||||
| 	Track int | ||||
| 	SongRow | ||||
| } | ||||
|  | ||||
| type SongRect struct { | ||||
| 	Corner1 SongPoint | ||||
| 	Corner2 SongPoint | ||||
| } | ||||
|  | ||||
| func (r *SongRow) Wrap(song sointu.Song) { | ||||
| 	totalRow := r.Pattern*song.PatternRows() + r.Row | ||||
| 	r.Row = mod(totalRow, song.PatternRows()) | ||||
| 	r.Pattern = mod((totalRow-r.Row)/song.PatternRows(), song.SequenceLength()) | ||||
| } | ||||
|  | ||||
| func (r *SongRow) Clamp(song sointu.Song) { | ||||
| 	totalRow := r.Pattern*song.PatternRows() + r.Row | ||||
| 	if totalRow < 0 { | ||||
| 		totalRow = 0 | ||||
| 	} | ||||
| 	if totalRow >= song.TotalRows() { | ||||
| 		totalRow = song.TotalRows() - 1 | ||||
| 	} | ||||
| 	r.Row = totalRow % song.PatternRows() | ||||
| 	r.Pattern = ((totalRow - r.Row) / song.PatternRows()) % song.SequenceLength() | ||||
| } | ||||
|  | ||||
| func (p *SongPoint) Wrap(song sointu.Song) { | ||||
| 	p.Track = mod(p.Track, len(song.Tracks)) | ||||
| 	p.SongRow.Wrap(song) | ||||
| } | ||||
|  | ||||
| func (p *SongPoint) Clamp(song sointu.Song) { | ||||
| 	if p.Track < 0 { | ||||
| 		p.Track = 0 | ||||
| 	} else if l := len(song.Tracks); p.Track >= l { | ||||
| 		p.Track = l - 1 | ||||
| 	} | ||||
| 	p.SongRow.Clamp(song) | ||||
| } | ||||
|  | ||||
| func (r *SongRect) Contains(p SongPoint) bool { | ||||
| 	track1, track2 := r.Corner1.Track, r.Corner2.Track | ||||
| 	if track2 < track1 { | ||||
| 		track1, track2 = track2, track1 | ||||
| 	} | ||||
| 	if p.Track < track1 || p.Track > track2 { | ||||
| 		return false | ||||
| 	} | ||||
| 	pattern1, row1, pattern2, row2 := r.Corner1.Pattern, r.Corner1.Row, r.Corner2.Pattern, r.Corner2.Row | ||||
| 	if pattern2 < pattern1 || (pattern1 == pattern2 && row2 < row1) { | ||||
| 		pattern1, row1, pattern2, row2 = pattern2, row2, pattern1, row1 | ||||
| 	} | ||||
| 	if p.Pattern < pattern1 || p.Pattern > pattern2 { | ||||
| 		return false | ||||
| 	} | ||||
| 	if p.Pattern == pattern1 && p.Row < row1 { | ||||
| 		return false | ||||
| 	} | ||||
| 	if p.Pattern == pattern2 && p.Row > row2 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func mod(a, b int) int { | ||||
| 	m := a % b | ||||
| 	if a < 0 && b < 0 { | ||||
| 		m -= b | ||||
| 	} | ||||
| 	if a < 0 && b > 0 { | ||||
| 		m += b | ||||
| 	} | ||||
| 	return m | ||||
| } | ||||
| @ -72,6 +72,7 @@ var trackerPatternRowTextColor = color.RGBA{R: 198, G: 198, B: 198, A: 255} | ||||
| var trackerPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255} | ||||
| var trackerPatMarker = primaryColor | ||||
| var trackerCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64} | ||||
| var trackerSelectionColor = color.RGBA{R: 19, G: 40, B: 60, A: 128} | ||||
| var trackerSurfaceColor = color.RGBA{R: 18, G: 18, B: 18, A: 18} | ||||
|  | ||||
| var patternPlayColor = color.RGBA{R: 55, G: 55, B: 61, A: 255} | ||||
| @ -80,6 +81,7 @@ var patternActiveTextColor = yellow | ||||
| var patternFont = fontCollection[6].Font | ||||
| var patternFontSize = unit.Px(12) | ||||
| var patternCursorColor = color.RGBA{R: 38, G: 79, B: 120, A: 64} | ||||
| var patternSelectionColor = color.RGBA{R: 19, G: 40, B: 60, A: 128} | ||||
|  | ||||
| var inactiveBtnColor = color.RGBA{R: 61, G: 55, B: 55, A: 255} | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,7 @@ const trackRowHeight = 16 | ||||
| const trackWidth = 54 | ||||
| const patmarkWidth = 16 | ||||
|  | ||||
| func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, hex bool, cursorRow, cursorPattern, cursorCol, playRow, playPattern int) layout.Widget { | ||||
| func (t *Tracker) layoutTrack(trackNo int) layout.Widget { | ||||
| 	return func(gtx layout.Context) layout.Dimensions { | ||||
| 		gtx.Constraints.Min.X = trackWidth | ||||
| 		gtx.Constraints.Max.X = trackWidth | ||||
| @ -26,29 +26,37 @@ func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, h | ||||
| 		op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y/2)-trackRowHeight)).Add(gtx.Ops) | ||||
| 		// TODO: this is a time bomb; as soon as one of the patterns is not the same length as rest. Find a solution | ||||
| 		// to fix the pattern lengths to a constant value | ||||
| 		cursorSongRow := cursorPattern*len(patterns[0]) + cursorRow | ||||
| 		playSongRow := playPattern*len(patterns[0]) + playRow | ||||
| 		cursorSongRow := t.Cursor.Pattern*t.song.PatternRows() + t.Cursor.Row | ||||
| 		op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops) | ||||
| 		for i, s := range sequence { | ||||
| 			if cursorPattern == i && active { | ||||
| 				paint.FillShape(gtx.Ops, activeTrackColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight*len(patterns[0]))}.Op()) | ||||
| 		patternRect := SongRect{ | ||||
| 			Corner1: SongPoint{SongRow: SongRow{Pattern: t.Cursor.Pattern}, Track: t.Cursor.Track}, | ||||
| 			Corner2: SongPoint{SongRow: SongRow{Pattern: t.SelectionCorner.Pattern}, Track: t.SelectionCorner.Track}, | ||||
| 		} | ||||
| 		pointRect := SongRect{ | ||||
| 			Corner1: t.Cursor, | ||||
| 			Corner2: t.SelectionCorner, | ||||
| 		} | ||||
| 		for i, s := range t.song.Tracks[trackNo].Sequence { | ||||
| 			if patternRect.Contains(SongPoint{Track: trackNo, SongRow: SongRow{Pattern: i}}) { | ||||
| 				paint.FillShape(gtx.Ops, activeTrackColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight*t.song.PatternRows())}.Op()) | ||||
| 			} | ||||
| 			for j, c := range patterns[s] { | ||||
| 				songRow := i*len(patterns[0]) + j | ||||
| 				if songRow == playSongRow { | ||||
| 			for j, c := range t.song.Tracks[trackNo].Patterns[s] { | ||||
| 				songRow := SongRow{Pattern: i, Row: j} | ||||
| 				songPoint := SongPoint{Track: trackNo, SongRow: songRow} | ||||
| 				if songRow == t.PlayPosition && t.Playing { | ||||
| 					paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op()) | ||||
| 				} | ||||
| 				if j == 0 { | ||||
| 					paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops) | ||||
| 					widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(s)) | ||||
| 				} | ||||
| 				if songRow == cursorSongRow { | ||||
| 				if songRow == t.Cursor.SongRow { | ||||
| 					paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops) | ||||
| 				} else { | ||||
| 					paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops) | ||||
| 				} | ||||
| 				op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops) | ||||
| 				if hex { | ||||
| 				if t.TrackShowHex[trackNo] { | ||||
| 					var text string | ||||
| 					switch c { | ||||
| 					case 0: | ||||
| @ -59,13 +67,23 @@ func (t *Tracker) layoutTrack(patterns [][]byte, sequence []byte, active bool, h | ||||
| 						text = fmt.Sprintf("%02x", c) | ||||
| 					} | ||||
| 					widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(text)) | ||||
| 					if active && songRow == cursorSongRow { | ||||
| 						paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Min: image.Pt(cursorCol*10, 0), Max: image.Pt(cursorCol*10+10, trackRowHeight)}.Op()) | ||||
| 					if pointRect.Contains(songPoint) { | ||||
| 						for col := 0; col < 2; col++ { | ||||
| 							color := trackerSelectionColor | ||||
| 							if songPoint == t.Cursor && t.CursorColumn == col { | ||||
| 								color = trackerCursorColor | ||||
| 							} | ||||
| 							paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(col*10, 0), Max: image.Pt(col*10+10, trackRowHeight)}.Op()) | ||||
| 						} | ||||
| 					} | ||||
| 				} else { | ||||
| 					widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, valueAsNote(c)) | ||||
| 					if active && cursorCol == 0 && songRow == cursorSongRow { | ||||
| 						paint.FillShape(gtx.Ops, trackerCursorColor, clip.Rect{Max: image.Pt(30, trackRowHeight)}.Op()) | ||||
| 					if pointRect.Contains(songPoint) { | ||||
| 						color := trackerSelectionColor | ||||
| 						if songPoint == t.Cursor { | ||||
| 							color = trackerCursorColor | ||||
| 						} | ||||
| 						paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(30, trackRowHeight)}.Op()) | ||||
| 					} | ||||
| 				} | ||||
| 				op.Offset(f32.Pt(-patmarkWidth, trackRowHeight)).Add(gtx.Ops) | ||||
|  | ||||
| @ -19,12 +19,10 @@ type Tracker struct { | ||||
| 	Playing       bool | ||||
| 	// protects PlayPattern and PlayRow | ||||
| 	playRowPatMutex       sync.RWMutex // protects song and playing | ||||
| 	PlayPattern           int | ||||
| 	PlayRow               int | ||||
| 	CursorRow             int | ||||
| 	PlayPosition          SongRow | ||||
| 	SelectionCorner       SongPoint | ||||
| 	Cursor                SongPoint | ||||
| 	CursorColumn          int | ||||
| 	DisplayPattern        int | ||||
| 	ActiveTrack           int | ||||
| 	CurrentInstrument     int | ||||
| 	CurrentUnit           int | ||||
| 	NoteTracking          bool | ||||
| @ -73,21 +71,9 @@ func (t *Tracker) LoadSong(song sointu.Song) error { | ||||
| 	} else { | ||||
| 		t.synth = synth | ||||
| 	} | ||||
| 	if t.DisplayPattern >= song.SequenceLength() { | ||||
| 		t.DisplayPattern = song.SequenceLength() - 1 | ||||
| 	} | ||||
| 	if t.CursorRow >= song.PatternRows() { | ||||
| 		t.CursorRow = song.PatternRows() - 1 | ||||
| 	} | ||||
| 	if t.PlayPattern >= song.SequenceLength() { | ||||
| 		t.PlayPattern = song.SequenceLength() - 1 | ||||
| 	} | ||||
| 	if t.PlayRow >= song.PatternRows() { | ||||
| 		t.PlayRow = song.PatternRows() - 1 | ||||
| 	} | ||||
| 	if t.ActiveTrack >= len(song.Tracks) { | ||||
| 		t.ActiveTrack = len(song.Tracks) - 1 | ||||
| 	} | ||||
| 	t.PlayPosition.Clamp(song) | ||||
| 	t.Cursor.Clamp(song) | ||||
| 	t.SelectionCorner.Clamp(song) | ||||
| 	if t.sequencer != nil { | ||||
| 		t.sequencer.SetSynth(t.synth) | ||||
| 	} | ||||
| @ -105,8 +91,8 @@ func (t *Tracker) TogglePlay() { | ||||
| 	t.Playing = !t.Playing | ||||
| 	if t.Playing { | ||||
| 		t.NoteTracking = true | ||||
| 		t.PlayPattern = t.DisplayPattern | ||||
| 		t.PlayRow = t.CursorRow - 1 | ||||
| 		t.PlayPosition = t.Cursor.SongRow | ||||
| 		t.PlayPosition.Row-- // TODO: we advance soon to make up for this -1, but this is not very elegant way to do it | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -124,22 +110,16 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) { | ||||
| 			t.playRowPatMutex.Unlock() | ||||
| 			return nil, false | ||||
| 		} | ||||
| 		t.PlayRow++ | ||||
| 		if t.PlayRow >= t.song.PatternRows() { | ||||
| 			t.PlayRow = 0 | ||||
| 			t.PlayPattern++ | ||||
| 		} | ||||
| 		if t.PlayPattern >= t.song.SequenceLength() { | ||||
| 			t.PlayPattern = 0 | ||||
| 		} | ||||
| 		t.PlayPosition.Row++ | ||||
| 		t.PlayPosition.Wrap(t.song) | ||||
| 		if t.NoteTracking { | ||||
| 			t.DisplayPattern = t.PlayPattern | ||||
| 			t.CursorRow = t.PlayRow | ||||
| 			t.Cursor.SongRow = t.PlayPosition | ||||
| 			t.SelectionCorner.SongRow = t.PlayPosition | ||||
| 		} | ||||
| 		notes := make([]Note, 0, 32) | ||||
| 		for track := range t.song.Tracks { | ||||
| 			patternIndex := t.song.Tracks[track].Sequence[t.PlayPattern] | ||||
| 			note := t.song.Tracks[track].Patterns[patternIndex][t.PlayRow] | ||||
| 			patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern] | ||||
| 			note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row] | ||||
| 			if note == 1 { // anything but hold causes an action. | ||||
| 				continue | ||||
| 			} | ||||
| @ -240,20 +220,20 @@ func (t *Tracker) AddInstrument() { | ||||
| // SetCurrentNote sets the (note) value in current pattern under cursor to iv | ||||
| func (t *Tracker) SetCurrentNote(iv byte) { | ||||
| 	t.SaveUndo() | ||||
| 	t.song.Tracks[t.ActiveTrack].Patterns[t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern]][t.CursorRow] = iv | ||||
| 	t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] = iv | ||||
| } | ||||
|  | ||||
| func (t *Tracker) SetCurrentPattern(pat byte) { | ||||
| 	t.SaveUndo() | ||||
| 	length := len(t.song.Tracks[t.ActiveTrack].Patterns) | ||||
| 	length := len(t.song.Tracks[t.Cursor.Track].Patterns) | ||||
| 	if int(pat) >= length { | ||||
| 		tail := make([][]byte, int(pat)-length+1) | ||||
| 		for i := range tail { | ||||
| 			tail[i] = make([]byte, t.song.PatternRows()) | ||||
| 		} | ||||
| 		t.song.Tracks[t.ActiveTrack].Patterns = append(t.song.Tracks[t.ActiveTrack].Patterns, tail...) | ||||
| 		t.song.Tracks[t.Cursor.Track].Patterns = append(t.song.Tracks[t.Cursor.Track].Patterns, tail...) | ||||
| 	} | ||||
| 	t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern] = pat | ||||
| 	t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern] = pat | ||||
| } | ||||
|  | ||||
| func (t *Tracker) SetSongLength(value int) { | ||||
| @ -276,6 +256,28 @@ func (t *Tracker) SetSongLength(value int) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *Tracker) DeleteSelection() { | ||||
| 	t.SaveUndo() | ||||
| 	r1 := t.Cursor.Pattern*t.song.PatternRows() + t.Cursor.Row | ||||
| 	r2 := t.SelectionCorner.Pattern*t.song.PatternRows() + t.SelectionCorner.Row | ||||
| 	if r2 < r1 { | ||||
| 		r1, r2 = r2, r1 | ||||
| 	} | ||||
| 	t1 := t.Cursor.Track | ||||
| 	t2 := t.SelectionCorner.Track | ||||
| 	if t2 < t1 { | ||||
| 		t1, t2 = t2, t1 | ||||
| 	} | ||||
| 	for r := r1; r <= r2; r++ { | ||||
| 		for c := t1; c <= t2; c++ { | ||||
| 			s := SongRow{Row: r} | ||||
| 			s.Wrap(t.song) | ||||
| 			p := t.song.Tracks[c].Sequence[s.Pattern] | ||||
| 			t.song.Tracks[c].Patterns[p][s.Row] = 1 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func New(audioContext sointu.AudioContext) *Tracker { | ||||
| 	t := &Tracker{ | ||||
| 		Theme:                 material.NewTheme(gofont.Collection()), | ||||
|  | ||||
		Reference in New Issue
	
	Block a user