mirror of
				https://github.com/vsariola/sointu.git
				synced 2025-10-30 15:35:41 -04:00 
			
		
		
		
	feat(tracker): add ability to loop part of song during playback
Closes #128.
This commit is contained in:
		
						parent
						
							aa7a2e56fa
						
					
				
				
					commit
					dc12f58082
				
			| @ -20,6 +20,7 @@ type ( | ||||
| 	NoteTracking    Model | ||||
| 	UnitSearching   Model | ||||
| 	UnitDisabled    Model | ||||
| 	LoopToggle      Model | ||||
| ) | ||||
|  | ||||
| func (v Bool) Toggle() { | ||||
| @ -43,6 +44,7 @@ func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m | ||||
| func (m *Model) NoteTracking() *NoteTracking       { return (*NoteTracking)(m) } | ||||
| func (m *Model) UnitSearching() *UnitSearching     { return (*UnitSearching)(m) } | ||||
| func (m *Model) UnitDisabled() *UnitDisabled       { return (*UnitDisabled)(m) } | ||||
| func (m *Model) LoopToggle() *LoopToggle           { return (*LoopToggle)(m) } | ||||
|  | ||||
| // Panic methods | ||||
|  | ||||
| @ -161,3 +163,20 @@ func (m *UnitDisabled) Enabled() bool { | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // LoopToggle methods | ||||
|  | ||||
| func (m *LoopToggle) Bool() Bool  { return Bool{m} } | ||||
| func (m *LoopToggle) Value() bool { return m.d.Loop.Length > 0 } | ||||
| func (t *LoopToggle) setValue(val bool) { | ||||
| 	m := (*Model)(t) | ||||
| 	defer m.change("SetLoopAction", LoopChange, MinorChange)() | ||||
| 	if !val { | ||||
| 		m.d.Loop = Loop{} | ||||
| 		return | ||||
| 	} | ||||
| 	l := m.OrderRows().List() | ||||
| 	a, b := l.listRange() | ||||
| 	m.d.Loop = Loop{a, b - a + 1} | ||||
| } | ||||
| func (m *LoopToggle) Enabled() bool { return true } | ||||
|  | ||||
| @ -69,6 +69,11 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) { | ||||
| 				t.Model.UnitDisabled().Bool().Toggle() | ||||
| 				return | ||||
| 			} | ||||
| 		case "L": | ||||
| 			if e.Modifiers.Contain(key.ModShortcut) { | ||||
| 				t.Model.LoopToggle().Bool().Toggle() | ||||
| 				return | ||||
| 			} | ||||
| 		case "N": | ||||
| 			if e.Modifiers.Contain(key.ModShortcut) { | ||||
| 				t.NewSong().Do() | ||||
|  | ||||
| @ -202,9 +202,13 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D { | ||||
| 		pat := j / rpp | ||||
| 		row := j % rpp | ||||
| 		w := pxPatMarkWidth + pxRowMarkWidth | ||||
| 		paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) | ||||
| 		defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop() | ||||
| 		if row == 0 { | ||||
| 			color := rowMarkerPatternTextColor | ||||
| 			if l := t.Loop(); pat >= l.Start && pat < l.Start+l.Length { | ||||
| 				color = loopMarkerColor | ||||
| 			} | ||||
| 			paint.ColorOp{Color: color}.Add(gtx.Ops) | ||||
| 			widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", pat)), op.CallOp{}) | ||||
| 		} | ||||
| 		defer op.Offset(image.Pt(pxPatMarkWidth, 0)).Push(gtx.Ops).Pop() | ||||
|  | ||||
| @ -81,11 +81,15 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { | ||||
| 	} | ||||
|  | ||||
| 	rowTitle := func(gtx C, j int) D { | ||||
| 		w := gtx.Dp(unit.Dp(30)) | ||||
| 		if playPos := t.PlayPosition(); t.SongPanel.PlayingBtn.Bool.Value() && j == playPos.OrderRow { | ||||
| 			paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op()) | ||||
| 		} | ||||
| 		w := gtx.Dp(unit.Dp(30)) | ||||
| 		paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) | ||||
| 		color := rowMarkerPatternTextColor | ||||
| 		if l := t.Loop(); j >= l.Start && j < l.Start+l.Length { | ||||
| 			color = loopMarkerColor | ||||
| 		} | ||||
| 		paint.ColorOp{Color: color}.Add(gtx.Ops) | ||||
| 		defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop() | ||||
| 		widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{}) | ||||
| 		return D{Size: image.Pt(w, patternCellHeight)} | ||||
|  | ||||
| @ -26,6 +26,7 @@ type SongPanel struct { | ||||
| 	RecordBtn    *BoolClickable | ||||
| 	NoteTracking *BoolClickable | ||||
| 	PanicBtn     *BoolClickable | ||||
| 	LoopBtn      *BoolClickable | ||||
|  | ||||
| 	// File menu items | ||||
| 	fileMenuItems  []MenuItem | ||||
| @ -50,6 +51,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel { | ||||
| 		Step:           NewNumberInput(model.Step().Int()), | ||||
| 		SongLength:     NewNumberInput(model.SongLength().Int()), | ||||
| 		PanicBtn:       NewBoolClickable(model.Panic().Bool()), | ||||
| 		LoopBtn:        NewBoolClickable(model.LoopToggle().Bool()), | ||||
| 		RecordBtn:      NewBoolClickable(model.IsRecording().Bool()), | ||||
| 		NoteTracking:   NewBoolClickable(model.NoteTracking().Bool()), | ||||
| 		PlayingBtn:     NewBoolClickable(model.Playing().Bool()), | ||||
| @ -106,6 +108,7 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { | ||||
| 	playBtnStyle := ToggleIcon(tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)") | ||||
| 	recordBtnStyle := ToggleIcon(tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)") | ||||
| 	noteTrackBtnStyle := ToggleIcon(tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)") | ||||
| 	loopBtnStyle := ToggleIcon(tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)") | ||||
|  | ||||
| 	return layout.Flex{Axis: layout.Vertical}.Layout(gtx, | ||||
| 		layout.Rigid(func(gtx C) D { | ||||
| @ -174,6 +177,7 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { | ||||
| 				layout.Rigid(playBtnStyle.Layout), | ||||
| 				layout.Rigid(recordBtnStyle.Layout), | ||||
| 				layout.Rigid(noteTrackBtnStyle.Layout), | ||||
| 				layout.Rigid(loopBtnStyle.Layout), | ||||
| 			) | ||||
| 		}), | ||||
| 		layout.Rigid(panicBtnStyle.Layout), | ||||
|  | ||||
| @ -44,6 +44,7 @@ var twoBeatHighlight = color.NRGBA{R: 31, G: 51, B: 53, A: 255} | ||||
| var patternPlayColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255} | ||||
| var patternTextColor = primaryColor | ||||
| var patternCellColor = color.NRGBA{R: 255, G: 255, B: 255, A: 3} | ||||
| var loopMarkerColor = color.NRGBA{R: 252, G: 186, B: 3, A: 255} | ||||
|  | ||||
| var instrumentHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255} | ||||
| var instrumentNameColor = color.NRGBA{R: 255, G: 255, B: 255, A: 255} | ||||
|  | ||||
| @ -72,7 +72,7 @@ func NewTracker(model *tracker.Model) *Tracker { | ||||
| 		OctaveNumberInput: NewNumberInput(model.Octave().Int()), | ||||
| 		InstrumentVoices:  NewNumberInput(model.InstrumentVoices().Int()), | ||||
|  | ||||
| 		TopHorizontalSplit:    &Split{Ratio: -.6}, | ||||
| 		TopHorizontalSplit:    &Split{Ratio: -.5}, | ||||
| 		BottomHorizontalSplit: &Split{Ratio: -.6}, | ||||
| 		VerticalSplit:         &Split{Axis: layout.Vertical}, | ||||
|  | ||||
|  | ||||
| @ -36,6 +36,7 @@ type ( | ||||
| 		ChangedSinceSave        bool | ||||
| 		RecoveryFilePath        string | ||||
| 		ChangedSinceRecovery    bool | ||||
| 		Loop                    Loop | ||||
| 	} | ||||
|  | ||||
| 	Model struct { | ||||
| @ -81,6 +82,12 @@ type ( | ||||
| 		sointu.SongPos | ||||
| 	} | ||||
|  | ||||
| 	// Loop identifier the order rows, which are the loop positions | ||||
| 	// Length = 0 means no loop is chosen, regardless of start | ||||
| 	Loop struct { | ||||
| 		Start, Length int | ||||
| 	} | ||||
|  | ||||
| 	Explore struct { | ||||
| 		IsSave       bool         // true if this is a save operation, false if open operation | ||||
| 		IsSong       bool         // true if this is a song, false if instrument | ||||
| @ -125,6 +132,7 @@ const ( | ||||
| 	ScoreChange | ||||
| 	BPMChange | ||||
| 	RowsPerBeatChange | ||||
| 	LoopChange | ||||
| 	SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange | ||||
| ) | ||||
|  | ||||
| @ -148,6 +156,7 @@ const maxUndo = 64 | ||||
| func (m *Model) AverageVolume() Volume        { return m.avgVolume } | ||||
| func (m *Model) PeakVolume() Volume           { return m.peakVolume } | ||||
| func (m *Model) PlayPosition() sointu.SongPos { return m.playPosition } | ||||
| func (m *Model) Loop() Loop                   { return m.d.Loop } | ||||
| func (m *Model) PlaySongRow() int             { return m.d.Song.Score.SongRow(m.playPosition) } | ||||
| func (m *Model) ChangedSinceSave() bool       { return m.d.ChangedSinceSave } | ||||
| func (m *Model) Dialog() Dialog               { return m.dialog } | ||||
| @ -233,6 +242,9 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( | ||||
| 			if m.changeType&RowsPerBeatChange != 0 { | ||||
| 				m.send(RowsPerBeatMsg{m.d.Song.RowsPerBeat}) | ||||
| 			} | ||||
| 			if m.changeType&LoopChange != 0 { | ||||
| 				m.send(m.d.Loop) | ||||
| 			} | ||||
| 			m.undoSkipCounter++ | ||||
| 			var limit int | ||||
| 			switch m.changeSeverity { | ||||
| @ -385,6 +397,7 @@ func (m *Model) resetSong() { | ||||
| 	} | ||||
| 	m.d.FilePath = "" | ||||
| 	m.d.ChangedSinceSave = false | ||||
| 	m.d.Loop = Loop{} | ||||
| } | ||||
|  | ||||
| // send sends a message to the player | ||||
|  | ||||
| @ -24,6 +24,7 @@ type ( | ||||
| 		peakVolumeMeter VolumeAnalyzer         // the volume analyzer used to calculate the peak volume | ||||
| 		voiceLevels     [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice | ||||
| 		voices          [vm.MAX_VOICES]voice | ||||
| 		loop            Loop | ||||
|  | ||||
| 		recState  recState  // is the recording off; are we waiting for a note; or are we recording | ||||
| 		recording Recording // the recorded MIDI events and BPM | ||||
| @ -192,6 +193,10 @@ func (p *Player) advanceRow() { | ||||
| 		return | ||||
| 	} | ||||
| 	p.songPos.PatternRow++ // advance row (this is why we subtracted one in Play()) | ||||
| 	if p.loop.Length > 0 && p.songPos.PatternRow >= p.song.Score.RowsPerPattern && p.songPos.OrderRow == p.loop.Start+p.loop.Length-1 { | ||||
| 		p.songPos.PatternRow = 0 | ||||
| 		p.songPos.OrderRow = p.loop.Start | ||||
| 	} | ||||
| 	p.songPos = p.song.Score.Wrap(p.songPos) | ||||
| 	p.send(nil) // just send volume and song row information | ||||
| 	lastVoice := 0 | ||||
| @ -230,6 +235,8 @@ loop: | ||||
| 				p.compileOrUpdateSynth() | ||||
| 			case sointu.Score: | ||||
| 				p.song.Score = m | ||||
| 			case Loop: | ||||
| 				p.loop = m | ||||
| 			case IsPlayingMsg: | ||||
| 				p.playing = bool(m.bool) | ||||
| 				if !p.playing { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user