feat(tracker): change keyboard shortcuts to mimic old trackers

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2024-10-11 13:44:06 +03:00
parent b4a63ce362
commit 91b7850bf7
7 changed files with 122 additions and 39 deletions

View File

@ -28,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Empty patch should not crash the native synth ([#148][i148]) - Empty patch should not crash the native synth ([#148][i148])
### Changed ### Changed
- The keyboard shortcuts are now again closer to what they were old trackers
([#151][i151])
- The stand-alone apps now output floating point sound, as made possible by - The stand-alone apps now output floating point sound, as made possible by
upgrading oto-library to latest version. This way the tracker sound output upgrading oto-library to latest version. This way the tracker sound output
matches the compiled output better, as usually compiled intros output sound in matches the compiled output better, as usually compiled intros output sound in
@ -237,6 +239,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[i148]: https://github.com/vsariola/sointu/issues/148 [i148]: https://github.com/vsariola/sointu/issues/148
[i149]: https://github.com/vsariola/sointu/issues/149 [i149]: https://github.com/vsariola/sointu/issues/149
[i150]: https://github.com/vsariola/sointu/issues/150 [i150]: https://github.com/vsariola/sointu/issues/150
[i151]: https://github.com/vsariola/sointu/issues/151
[i154]: https://github.com/vsariola/sointu/issues/154 [i154]: https://github.com/vsariola/sointu/issues/154
[i158]: https://github.com/vsariola/sointu/issues/158 [i158]: https://github.com/vsariola/sointu/issues/158
[i162]: https://github.com/vsariola/sointu/issues/162 [i162]: https://github.com/vsariola/sointu/issues/162

View File

@ -275,18 +275,75 @@ func (m *Model) RemoveUnused() Action {
}) })
} }
func (m *Model) Rewind() Action { func (m *Model) PlayFromCurrentPosition() Action {
return Action{ return Action{
allowed: func() bool { allowed: func() bool { return !m.instrEnlarged },
return m.playing || !m.instrEnlarged
},
do: func() { do: func() {
m.setPanic(false)
m.setLoop(Loop{})
m.playing = true
m.send(StartPlayMsg{m.d.Cursor.SongPos})
},
}
}
func (m *Model) PlayFromSongStart() Action {
return Action{
allowed: func() bool { return !m.instrEnlarged },
do: func() {
m.setPanic(false)
m.setLoop(Loop{})
m.playing = true m.playing = true
m.send(StartPlayMsg{}) m.send(StartPlayMsg{})
}, },
} }
} }
func (m *Model) PlaySelected() Action {
return Action{
allowed: func() bool { return !m.instrEnlarged },
do: func() {
m.setPanic(false)
m.playing = true
l := m.OrderRows().List()
a, b := l.listRange()
newLoop := Loop{a, b - a + 1}
m.setLoop(newLoop)
m.send(StartPlayMsg{sointu.SongPos{OrderRow: a, PatternRow: 0}})
},
}
}
func (m *Model) PlayFromLoopStart() Action {
return Action{
allowed: func() bool { return !m.instrEnlarged },
do: func() {
m.setPanic(false)
if m.loop == (Loop{}) {
m.PlaySelected().Do()
return
}
m.playing = true
m.send(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}})
},
}
}
func (m *Model) StopPlaying() Action {
return Action{
allowed: func() bool { return true },
do: func() {
if !m.playing {
m.setPanic(true)
m.setLoop(Loop{})
return
}
m.playing = false
(*Model)(m).send(IsPlayingMsg{false})
},
}
}
func (m *Model) AddOrderRow(before bool) Action { func (m *Model) AddOrderRow(before bool) Action {
return Allow(func() { return Allow(func() {
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
@ -394,8 +451,9 @@ func (m *Model) completeAction(checkSave bool) {
} }
switch m.dialog { switch m.dialog {
case NewSongChanges, NewSongSaveExplorer: case NewSongChanges, NewSongSaveExplorer:
c := m.change("NewSong", SongChange|LoopChange, MajorChange) c := m.change("NewSong", SongChange, MajorChange)
m.resetSong() m.resetSong()
m.setLoop(Loop{})
c() c()
m.d.ChangedSinceSave = false m.d.ChangedSinceSave = false
m.dialog = NoDialog m.dialog = NoDialog
@ -408,3 +466,17 @@ func (m *Model) completeAction(checkSave bool) {
m.dialog = NoDialog m.dialog = NoDialog
} }
} }
func (m *Model) setPanic(val bool) {
if m.panic != val {
m.panic = val
m.send(PanicMsg{val})
}
}
func (m *Model) setLoop(newLoop Loop) {
if m.loop != newLoop {
m.loop = newLoop
m.send(newLoop)
}
}

View File

@ -51,8 +51,7 @@ func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
func (m *Panic) Bool() Bool { return Bool{m} } func (m *Panic) Bool() Bool { return Bool{m} }
func (m *Panic) Value() bool { return m.panic } func (m *Panic) Value() bool { return m.panic }
func (m *Panic) setValue(val bool) { func (m *Panic) setValue(val bool) {
m.panic = val (*Model)(m).setPanic(val)
(*Model)(m).send(PanicMsg{val})
} }
func (m *Panic) Enabled() bool { return true } func (m *Panic) Enabled() bool { return true }
@ -74,6 +73,7 @@ func (m *Playing) Value() bool { return m.playing }
func (m *Playing) setValue(val bool) { func (m *Playing) setValue(val bool) {
m.playing = val m.playing = val
if m.playing { if m.playing {
(*Model)(m).setPanic(false)
(*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos}) (*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos})
} else { } else {
(*Model)(m).send(IsPlayingMsg{val}) (*Model)(m).send(IsPlayingMsg{val})
@ -98,9 +98,9 @@ func (m *CommentExpanded) Enabled() bool { return true }
// NoteTracking methods // NoteTracking methods
func (m *NoteTracking) Bool() Bool { return Bool{m} } func (m *NoteTracking) Bool() Bool { return Bool{m} }
func (m *NoteTracking) Value() bool { return m.playing && m.noteTracking } func (m *NoteTracking) Value() bool { return m.noteTracking }
func (m *NoteTracking) setValue(val bool) { m.noteTracking = val } func (m *NoteTracking) setValue(val bool) { m.noteTracking = val }
func (m *NoteTracking) Enabled() bool { return m.playing } func (m *NoteTracking) Enabled() bool { return true }
// Effect methods // Effect methods
@ -173,16 +173,15 @@ func (m *UnitDisabled) Enabled() bool {
// LoopToggle methods // LoopToggle methods
func (m *LoopToggle) Bool() Bool { return Bool{m} } func (m *LoopToggle) Bool() Bool { return Bool{m} }
func (m *LoopToggle) Value() bool { return m.d.Loop.Length > 0 } func (m *LoopToggle) Value() bool { return m.loop.Length > 0 }
func (t *LoopToggle) setValue(val bool) { func (t *LoopToggle) setValue(val bool) {
m := (*Model)(t) m := (*Model)(t)
defer m.change("SetLoopAction", LoopChange, MinorChange)() newLoop := Loop{}
if !val { if val {
m.d.Loop = Loop{}
return
}
l := m.OrderRows().List() l := m.OrderRows().List()
a, b := l.listRange() a, b := l.listRange()
m.d.Loop = Loop{a, b - a + 1} newLoop = Loop{a, b - a + 1}
}
m.setLoop(newLoop)
} }
func (m *LoopToggle) Enabled() bool { return true } func (m *LoopToggle) Enabled() bool { return true }

View File

@ -121,19 +121,34 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
case "F3": case "F3":
t.InstrumentEditor.Focus() t.InstrumentEditor.Focus()
return return
case "F5": case "Space":
t.SongPanel.RewindBtn.Action.Do() t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl)) t.Playing().Bool().Toggle()
return return
case "F6", "Space": case "F5":
t.SongPanel.PlayingBtn.Bool.Toggle() t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl)) if e.Modifiers.Contain(key.ModCtrl) {
t.Model.PlayFromSongStart().Do()
} else {
t.Model.PlayFromCurrentPosition().Do()
}
return
case "F6":
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
if e.Modifiers.Contain(key.ModCtrl) {
t.Model.PlayFromLoopStart().Do()
} else {
t.Model.PlaySelected().Do()
}
return return
case "F7": case "F7":
t.SongPanel.RecordBtn.Bool.Toggle() t.IsRecording().Bool().Toggle()
return return
case "F8": case "F8":
t.SongPanel.NoteTracking.Bool.Toggle() t.StopPlaying().Do()
return
case "F9":
t.NoteTracking().Bool().Toggle()
return return
case "F12": case "F12":
t.Panic().Bool().Toggle() t.Panic().Bool().Toggle()

View File

@ -56,7 +56,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
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()),
RewindBtn: NewActionClickable(model.Rewind()), RewindBtn: NewActionClickable(model.PlayFromSongStart()),
} }
ret.fileMenuItems = []MenuItem{ ret.fileMenuItems = []MenuItem{
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()}, {IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
@ -105,10 +105,10 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
in := layout.UniformInset(unit.Dp(1)) in := layout.UniformInset(unit.Dp(1))
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)") panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)") rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(Ctrl+F5)")
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)") playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F5 / Space)", "Stop (F8)")
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)") recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)") noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff", "Follow\nOn")
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)") loopBtnStyle := ToggleIcon(gtx, 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,

View File

@ -36,7 +36,6 @@ type (
ChangedSinceSave bool ChangedSinceSave bool
RecoveryFilePath string RecoveryFilePath string
ChangedSinceRecovery bool ChangedSinceRecovery bool
Loop Loop
} }
Model struct { Model struct {
@ -59,6 +58,7 @@ type (
recording bool recording bool
playing bool playing bool
playPosition sointu.SongPos playPosition sointu.SongPos
loop Loop
noteTracking bool noteTracking bool
quitted bool quitted bool
@ -132,7 +132,6 @@ const (
ScoreChange ScoreChange
BPMChange BPMChange
RowsPerBeatChange RowsPerBeatChange
LoopChange
SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange
) )
@ -156,7 +155,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) Loop() Loop { return m.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 }
@ -183,7 +182,7 @@ func NewModelPlayer(synther sointu.Synther, recoveryFilePath string) (*Model, *P
modelMsgs: modelMessages, modelMsgs: modelMessages,
synther: synther, synther: synther,
song: m.d.Song.Copy(), song: m.d.Song.Copy(),
loop: m.d.Loop, loop: m.loop,
avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20}, avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20},
peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20}, peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20},
} }
@ -244,9 +243,6 @@ 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 {
@ -322,7 +318,6 @@ func (m *Model) UnmarshalRecovery(bytes []byte) {
} }
m.d.ChangedSinceRecovery = false m.d.ChangedSinceRecovery = false
m.send(m.d.Song.Copy()) m.send(m.d.Song.Copy())
m.send(m.d.Loop)
m.updatePatternUseCount() m.updatePatternUseCount()
} }
@ -402,7 +397,6 @@ 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

View File

@ -82,7 +82,7 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed) s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed) s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed) s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
s.IterateAction("Rewind", s.model.Rewind(), yield, seed) s.IterateAction("Rewind", s.model.PlayFromSongStart(), yield, seed)
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed) s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed) s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed) s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)