mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat(tracker): add ability to loop part of song during playback
Closes #128.
This commit is contained in:
parent
aa7a2e56fa
commit
dc12f58082
@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
### Added
|
### Added
|
||||||
|
- Ability to loop certain section of the song when playing. The loop can be set
|
||||||
|
by using the toggle button in the song panel, or by hitting Ctrl+L.
|
||||||
|
([#128][i128])
|
||||||
- Disable units temporarily. The disabled units are shown in gray and are not
|
- Disable units temporarily. The disabled units are shown in gray and are not
|
||||||
compiled into the patch and are considered for all purposes non-existent.
|
compiled into the patch and are considered for all purposes non-existent.
|
||||||
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
|
Hitting Ctrl-D disables/re-enables the selected unit(s). The yaml file has
|
||||||
@ -139,5 +142,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||||
|
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||||
[i129]: https://github.com/vsariola/sointu/issues/129
|
[i129]: https://github.com/vsariola/sointu/issues/129
|
||||||
[i130]: https://github.com/vsariola/sointu/issues/130
|
[i130]: https://github.com/vsariola/sointu/issues/130
|
||||||
|
@ -20,6 +20,7 @@ type (
|
|||||||
NoteTracking Model
|
NoteTracking Model
|
||||||
UnitSearching Model
|
UnitSearching Model
|
||||||
UnitDisabled Model
|
UnitDisabled Model
|
||||||
|
LoopToggle Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Bool) Toggle() {
|
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) NoteTracking() *NoteTracking { return (*NoteTracking)(m) }
|
||||||
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
|
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
|
||||||
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
|
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
|
||||||
|
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
|
||||||
|
|
||||||
// Panic methods
|
// Panic methods
|
||||||
|
|
||||||
@ -161,3 +163,20 @@ func (m *UnitDisabled) Enabled() bool {
|
|||||||
}
|
}
|
||||||
return true
|
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()
|
t.Model.UnitDisabled().Bool().Toggle()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "L":
|
||||||
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||||||
|
t.Model.LoopToggle().Bool().Toggle()
|
||||||
|
return
|
||||||
|
}
|
||||||
case "N":
|
case "N":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||||||
t.NewSong().Do()
|
t.NewSong().Do()
|
||||||
|
@ -202,9 +202,13 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
pat := j / rpp
|
pat := j / rpp
|
||||||
row := j % rpp
|
row := j % rpp
|
||||||
w := pxPatMarkWidth + pxRowMarkWidth
|
w := pxPatMarkWidth + pxRowMarkWidth
|
||||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
|
||||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||||
if row == 0 {
|
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{})
|
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()
|
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 {
|
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 {
|
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())
|
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||||
}
|
}
|
||||||
w := gtx.Dp(unit.Dp(30))
|
color := rowMarkerPatternTextColor
|
||||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
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()
|
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{})
|
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||||
return D{Size: image.Pt(w, patternCellHeight)}
|
return D{Size: image.Pt(w, patternCellHeight)}
|
||||||
|
@ -26,6 +26,7 @@ type SongPanel struct {
|
|||||||
RecordBtn *BoolClickable
|
RecordBtn *BoolClickable
|
||||||
NoteTracking *BoolClickable
|
NoteTracking *BoolClickable
|
||||||
PanicBtn *BoolClickable
|
PanicBtn *BoolClickable
|
||||||
|
LoopBtn *BoolClickable
|
||||||
|
|
||||||
// File menu items
|
// File menu items
|
||||||
fileMenuItems []MenuItem
|
fileMenuItems []MenuItem
|
||||||
@ -50,6 +51,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
|||||||
Step: NewNumberInput(model.Step().Int()),
|
Step: NewNumberInput(model.Step().Int()),
|
||||||
SongLength: NewNumberInput(model.SongLength().Int()),
|
SongLength: NewNumberInput(model.SongLength().Int()),
|
||||||
PanicBtn: NewBoolClickable(model.Panic().Bool()),
|
PanicBtn: NewBoolClickable(model.Panic().Bool()),
|
||||||
|
LoopBtn: NewBoolClickable(model.LoopToggle().Bool()),
|
||||||
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
||||||
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
||||||
PlayingBtn: NewBoolClickable(model.Playing().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)")
|
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)")
|
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)")
|
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,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
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(playBtnStyle.Layout),
|
||||||
layout.Rigid(recordBtnStyle.Layout),
|
layout.Rigid(recordBtnStyle.Layout),
|
||||||
layout.Rigid(noteTrackBtnStyle.Layout),
|
layout.Rigid(noteTrackBtnStyle.Layout),
|
||||||
|
layout.Rigid(loopBtnStyle.Layout),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(panicBtnStyle.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 patternPlayColor = color.NRGBA{R: 55, G: 55, B: 61, A: 255}
|
||||||
var patternTextColor = primaryColor
|
var patternTextColor = primaryColor
|
||||||
var patternCellColor = color.NRGBA{R: 255, G: 255, B: 255, A: 3}
|
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 instrumentHoverColor = color.NRGBA{R: 30, G: 31, B: 38, A: 255}
|
||||||
var instrumentNameColor = color.NRGBA{R: 255, G: 255, B: 255, 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()),
|
OctaveNumberInput: NewNumberInput(model.Octave().Int()),
|
||||||
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
|
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
|
||||||
|
|
||||||
TopHorizontalSplit: &Split{Ratio: -.6},
|
TopHorizontalSplit: &Split{Ratio: -.5},
|
||||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ type (
|
|||||||
ChangedSinceSave bool
|
ChangedSinceSave bool
|
||||||
RecoveryFilePath string
|
RecoveryFilePath string
|
||||||
ChangedSinceRecovery bool
|
ChangedSinceRecovery bool
|
||||||
|
Loop Loop
|
||||||
}
|
}
|
||||||
|
|
||||||
Model struct {
|
Model struct {
|
||||||
@ -81,6 +82,12 @@ type (
|
|||||||
sointu.SongPos
|
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 {
|
Explore struct {
|
||||||
IsSave bool // true if this is a save operation, false if open operation
|
IsSave bool // true if this is a save operation, false if open operation
|
||||||
IsSong bool // true if this is a song, false if instrument
|
IsSong bool // true if this is a song, false if instrument
|
||||||
@ -125,6 +132,7 @@ const (
|
|||||||
ScoreChange
|
ScoreChange
|
||||||
BPMChange
|
BPMChange
|
||||||
RowsPerBeatChange
|
RowsPerBeatChange
|
||||||
|
LoopChange
|
||||||
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
|
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -148,6 +156,7 @@ const maxUndo = 64
|
|||||||
func (m *Model) AverageVolume() Volume { return m.avgVolume }
|
func (m *Model) AverageVolume() Volume { return m.avgVolume }
|
||||||
func (m *Model) PeakVolume() Volume { return m.peakVolume }
|
func (m *Model) PeakVolume() Volume { return m.peakVolume }
|
||||||
func (m *Model) PlayPosition() sointu.SongPos { return m.playPosition }
|
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) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playPosition) }
|
||||||
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
|
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
|
||||||
func (m *Model) Dialog() Dialog { return m.dialog }
|
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 {
|
if m.changeType&RowsPerBeatChange != 0 {
|
||||||
m.send(RowsPerBeatMsg{m.d.Song.RowsPerBeat})
|
m.send(RowsPerBeatMsg{m.d.Song.RowsPerBeat})
|
||||||
}
|
}
|
||||||
|
if m.changeType&LoopChange != 0 {
|
||||||
|
m.send(m.d.Loop)
|
||||||
|
}
|
||||||
m.undoSkipCounter++
|
m.undoSkipCounter++
|
||||||
var limit int
|
var limit int
|
||||||
switch m.changeSeverity {
|
switch m.changeSeverity {
|
||||||
@ -385,6 +397,7 @@ func (m *Model) resetSong() {
|
|||||||
}
|
}
|
||||||
m.d.FilePath = ""
|
m.d.FilePath = ""
|
||||||
m.d.ChangedSinceSave = false
|
m.d.ChangedSinceSave = false
|
||||||
|
m.d.Loop = Loop{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send sends a message to the player
|
// send sends a message to the player
|
||||||
|
@ -24,6 +24,7 @@ type (
|
|||||||
peakVolumeMeter VolumeAnalyzer // the volume analyzer used to calculate the peak volume
|
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
|
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
|
||||||
voices [vm.MAX_VOICES]voice
|
voices [vm.MAX_VOICES]voice
|
||||||
|
loop Loop
|
||||||
|
|
||||||
recState recState // is the recording off; are we waiting for a note; or are we recording
|
recState recState // is the recording off; are we waiting for a note; or are we recording
|
||||||
recording Recording // the recorded MIDI events and BPM
|
recording Recording // the recorded MIDI events and BPM
|
||||||
@ -192,6 +193,10 @@ func (p *Player) advanceRow() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.songPos.PatternRow++ // advance row (this is why we subtracted one in Play())
|
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.songPos = p.song.Score.Wrap(p.songPos)
|
||||||
p.send(nil) // just send volume and song row information
|
p.send(nil) // just send volume and song row information
|
||||||
lastVoice := 0
|
lastVoice := 0
|
||||||
@ -230,6 +235,8 @@ loop:
|
|||||||
p.compileOrUpdateSynth()
|
p.compileOrUpdateSynth()
|
||||||
case sointu.Score:
|
case sointu.Score:
|
||||||
p.song.Score = m
|
p.song.Score = m
|
||||||
|
case Loop:
|
||||||
|
p.loop = m
|
||||||
case IsPlayingMsg:
|
case IsPlayingMsg:
|
||||||
p.playing = bool(m.bool)
|
p.playing = bool(m.bool)
|
||||||
if !p.playing {
|
if !p.playing {
|
||||||
|
Loading…
Reference in New Issue
Block a user