diff --git a/CHANGELOG.md b/CHANGELOG.md index fb99e13..c05f842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) ### 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 upgrading oto-library to latest version. This way the tracker sound output 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 [i149]: https://github.com/vsariola/sointu/issues/149 [i150]: https://github.com/vsariola/sointu/issues/150 +[i151]: https://github.com/vsariola/sointu/issues/151 [i154]: https://github.com/vsariola/sointu/issues/154 [i158]: https://github.com/vsariola/sointu/issues/158 [i162]: https://github.com/vsariola/sointu/issues/162 diff --git a/tracker/action.go b/tracker/action.go index faa6f5b..6158c8c 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -275,18 +275,75 @@ func (m *Model) RemoveUnused() Action { }) } -func (m *Model) Rewind() Action { +func (m *Model) PlayFromCurrentPosition() Action { return Action{ - allowed: func() bool { - return m.playing || !m.instrEnlarged - }, + allowed: func() bool { return !m.instrEnlarged }, 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.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 { return Allow(func() { defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() @@ -394,8 +451,9 @@ func (m *Model) completeAction(checkSave bool) { } switch m.dialog { case NewSongChanges, NewSongSaveExplorer: - c := m.change("NewSong", SongChange|LoopChange, MajorChange) + c := m.change("NewSong", SongChange, MajorChange) m.resetSong() + m.setLoop(Loop{}) c() m.d.ChangedSinceSave = false m.dialog = NoDialog @@ -408,3 +466,17 @@ func (m *Model) completeAction(checkSave bool) { 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) + } +} diff --git a/tracker/bool.go b/tracker/bool.go index 70cf5f9..bd01ffc 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -51,8 +51,7 @@ func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) } func (m *Panic) Bool() Bool { return Bool{m} } func (m *Panic) Value() bool { return m.panic } func (m *Panic) setValue(val bool) { - m.panic = val - (*Model)(m).send(PanicMsg{val}) + (*Model)(m).setPanic(val) } 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) { m.playing = val if m.playing { + (*Model)(m).setPanic(false) (*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos}) } else { (*Model)(m).send(IsPlayingMsg{val}) @@ -98,9 +98,9 @@ func (m *CommentExpanded) Enabled() bool { return true } // NoteTracking methods 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) Enabled() bool { return m.playing } +func (m *NoteTracking) Enabled() bool { return true } // Effect methods @@ -173,16 +173,15 @@ func (m *UnitDisabled) Enabled() bool { // LoopToggle methods 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) { m := (*Model)(t) - defer m.change("SetLoopAction", LoopChange, MinorChange)() - if !val { - m.d.Loop = Loop{} - return + newLoop := Loop{} + if val { + l := m.OrderRows().List() + a, b := l.listRange() + newLoop = Loop{a, b - a + 1} } - l := m.OrderRows().List() - a, b := l.listRange() - m.d.Loop = Loop{a, b - a + 1} + m.setLoop(newLoop) } func (m *LoopToggle) Enabled() bool { return true } diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index cc16b42..35b2e59 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -121,19 +121,34 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { case "F3": t.InstrumentEditor.Focus() return - case "F5": - t.SongPanel.RewindBtn.Action.Do() - t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl)) + case "Space": + t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift)) + t.Playing().Bool().Toggle() return - case "F6", "Space": - t.SongPanel.PlayingBtn.Bool.Toggle() - t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl)) + case "F5": + t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift)) + 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 case "F7": - t.SongPanel.RecordBtn.Bool.Toggle() + t.IsRecording().Bool().Toggle() return case "F8": - t.SongPanel.NoteTracking.Bool.Toggle() + t.StopPlaying().Do() + return + case "F9": + t.NoteTracking().Bool().Toggle() return case "F12": t.Panic().Bool().Toggle() diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index 8a673f9..15501fc 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -56,7 +56,7 @@ func NewSongPanel(model *tracker.Model) *SongPanel { RecordBtn: NewBoolClickable(model.IsRecording().Bool()), NoteTracking: NewBoolClickable(model.NoteTracking().Bool()), PlayingBtn: NewBoolClickable(model.Playing().Bool()), - RewindBtn: NewActionClickable(model.Rewind()), + RewindBtn: NewActionClickable(model.PlayFromSongStart()), } ret.fileMenuItems = []MenuItem{ {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)) panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)") - rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)") - playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)") + 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 (F5 / Space)", "Stop (F8)") 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)") return layout.Flex{Axis: layout.Vertical}.Layout(gtx, diff --git a/tracker/model.go b/tracker/model.go index 44a58aa..128718e 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -36,7 +36,6 @@ type ( ChangedSinceSave bool RecoveryFilePath string ChangedSinceRecovery bool - Loop Loop } Model struct { @@ -59,6 +58,7 @@ type ( recording bool playing bool playPosition sointu.SongPos + loop Loop noteTracking bool quitted bool @@ -132,7 +132,6 @@ const ( ScoreChange BPMChange RowsPerBeatChange - LoopChange SongChange ChangeType = PatchChange | ScoreChange | BPMChange | RowsPerBeatChange ) @@ -156,7 +155,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) Loop() Loop { return m.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 } @@ -183,7 +182,7 @@ func NewModelPlayer(synther sointu.Synther, recoveryFilePath string) (*Model, *P modelMsgs: modelMessages, synther: synther, song: m.d.Song.Copy(), - loop: m.d.Loop, + loop: m.loop, avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, 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 { 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 { @@ -322,7 +318,6 @@ func (m *Model) UnmarshalRecovery(bytes []byte) { } m.d.ChangedSinceRecovery = false m.send(m.d.Song.Copy()) - m.send(m.d.Loop) m.updatePatternUseCount() } @@ -402,7 +397,6 @@ func (m *Model) resetSong() { } m.d.FilePath = "" m.d.ChangedSinceSave = false - m.d.Loop = Loop{} } // send sends a message to the player diff --git a/tracker/model_test.go b/tracker/model_test.go index 706b05d..58810d7 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -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("SubtractOctave", s.model.SubtractOctave(), 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("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed) s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)