feat: save recovery data to disk and/or DAW project

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2023-10-15 15:28:35 +03:00
parent 97a1b2f766
commit 462faf5f4e
7 changed files with 126 additions and 95 deletions

View File

@ -57,16 +57,20 @@ type Tracker struct {
lastVolume tracker.Volume
wavFilePath string
quitChannel chan struct{}
quitWG sync.WaitGroup
errorChannel chan error
quitted bool
synthService sointu.SynthService
wavFilePath string
quitChannel chan struct{}
quitWG sync.WaitGroup
errorChannel chan error
quitted bool
unmarshalRecoveryChannel chan []byte
marshalRecoveryChannel chan (chan []byte)
synthService sointu.SynthService
*tracker.Model
*trackerModel
}
type trackerModel = tracker.Model
func (t *Tracker) UnmarshalContent(bytes []byte) error {
var units []sointu.Unit
if errJSON := json.Unmarshal(bytes, &units); errJSON == nil {
@ -143,7 +147,10 @@ func NewTracker(model *tracker.Model, synthService sointu.SynthService) *Tracker
errorChannel: make(chan error, 32),
synthService: synthService,
Model: model,
trackerModel: model,
marshalRecoveryChannel: make(chan (chan []byte)),
unmarshalRecoveryChannel: make(chan []byte),
}
t.Theme.Palette.Fg = primaryColor
t.Theme.Palette.ContrastFg = black
@ -213,6 +220,10 @@ mainloop:
}
case <-recoveryTicker.C:
t.SaveRecovery()
case retChn := <-t.marshalRecoveryChannel:
retChn <- t.MarshalRecovery()
case bytes := <-t.unmarshalRecoveryChannel:
t.UnmarshalRecovery(bytes)
}
}
w.Perform(system.ActionClose)
@ -220,6 +231,18 @@ mainloop:
t.quitWG.Done()
}
// thread safe, executed in the GUI thread
func (t *Tracker) SafeMarshalRecovery() []byte {
retChn := make(chan []byte)
t.marshalRecoveryChannel <- retChn
return <-retChn
}
// thread safe, executed in the GUI thread
func (t *Tracker) SafeUnmarshalRecovery(data []byte) {
t.unmarshalRecoveryChannel <- data
}
func (t *Tracker) sendQuit() {
select {
case t.quitChannel <- struct{}{}:

View File

@ -13,7 +13,6 @@ import (
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
)
// Model implements the mutable state for the tracker program GUI.
@ -26,25 +25,27 @@ import (
type (
// modelData is the part of the model that gets save to recovery file
modelData struct {
Song sointu.Song
SelectionCorner SongPoint
Cursor SongPoint
LowNibble bool
InstrIndex int
UnitIndex int
ParamIndex int
Octave int
NoteTracking bool
UsedIDs map[int]bool
MaxID int
FilePath string
ChangedSinceSave bool
PatternUseCount [][]int
Panic bool
Playing bool
Recording bool
PlayPosition SongRow
InstrEnlarged bool
Song sointu.Song
SelectionCorner SongPoint
Cursor SongPoint
LowNibble bool
InstrIndex int
UnitIndex int
ParamIndex int
Octave int
NoteTracking bool
UsedIDs map[int]bool
MaxID int
FilePath string
ChangedSinceSave bool
PatternUseCount [][]int
Panic bool
Playing bool
Recording bool
PlayPosition SongRow
InstrEnlarged bool
RecoveryFilePath string
ChangedSinceRecovery bool
PrevUndoType string
UndoSkipCounter int
@ -116,63 +117,76 @@ const (
const maxUndo = 64
const RECOVERY_FILE = ".sointu_recovery"
func NewModel(modelMessages chan<- interface{}, playerMessages <-chan PlayerMessage) *Model {
func NewModel(modelMessages chan<- interface{}, playerMessages <-chan PlayerMessage, recoveryFilePath string) *Model {
ret := new(Model)
ret.modelMessages = modelMessages
ret.PlayerMessages = playerMessages
ret.setSongNoUndo(defaultSong.Copy())
ret.d.Octave = 4
ret.d.RecoveryFilePath = recoveryFilePath
if recoveryFilePath != "" {
if bytes2, err := os.ReadFile(ret.d.RecoveryFilePath); err == nil {
json.Unmarshal(bytes2, &ret.d)
}
}
return ret
}
func LoadRecovery(modelMessages chan<- interface{}, playerMessages <-chan PlayerMessage) (*Model, error) {
homeDir, err := os.UserHomeDir()
func (m *Model) MarshalRecovery() []byte {
out, err := json.Marshal(m.d)
if err != nil {
return nil, fmt.Errorf("could not get user home directory: %w", err)
return nil
}
filePath := filepath.Join(homeDir, RECOVERY_FILE)
b, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("could not read recovery file: %w", err)
if m.d.RecoveryFilePath != "" {
os.Remove(m.d.RecoveryFilePath)
}
var ret Model
err = json.Unmarshal(b, &ret.d)
if err != nil {
err = yaml.Unmarshal(b, &ret.d)
if err != nil {
return nil, fmt.Errorf("could not unmarshal recovery file: %w", err)
}
}
ret.modelMessages = modelMessages
ret.PlayerMessages = playerMessages
ret.notifyPatchChange()
ret.notifySamplesPerRowChange()
ret.notifyScoreChange()
return &ret, nil
m.d.ChangedSinceRecovery = false
return out
}
func (m *Model) SaveRecovery() error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("could not get user home directory: %w", err)
if !m.d.ChangedSinceRecovery {
return nil
}
if m.d.RecoveryFilePath == "" {
return errors.New("no backup file path")
}
out, err := json.Marshal(m.d)
if err != nil {
return fmt.Errorf("could not marshal the model: %w", err)
return fmt.Errorf("could not marshal recovery data: %w", err)
}
filePath := filepath.Join(homeDir, RECOVERY_FILE)
file, err := os.Create(filePath)
dir := filepath.Dir(m.d.RecoveryFilePath)
if _, err := os.Stat(dir); os.IsNotExist(err) {
os.MkdirAll(dir, os.ModePerm)
}
file, err := os.Create(m.d.RecoveryFilePath)
if err != nil {
return fmt.Errorf("could not open recovery file: %w", err)
return fmt.Errorf("could not create recovery file: %w", err)
}
_, err = file.Write(out)
if err != nil {
return fmt.Errorf("could not write recovery file: %w", err)
}
m.d.ChangedSinceRecovery = false
return nil
}
func (m *Model) UnmarshalRecovery(bytes []byte) {
err := json.Unmarshal(bytes, &m.d)
if err != nil {
return
}
if m.d.RecoveryFilePath != "" { // check if there's a recovery file on disk and load it instead
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
json.Unmarshal(bytes2, &m.d)
}
}
m.d.ChangedSinceRecovery = false
m.notifyPatchChange()
m.notifySamplesPerRowChange()
m.notifyScoreChange()
}
func (m *Model) FilePath() string {
return m.d.FilePath
}
@ -1416,6 +1430,7 @@ func (m *Model) notifySamplesPerRowChange() {
func (m *Model) saveUndo(undoType string, undoSkipping int) {
m.d.ChangedSinceSave = true
m.d.ChangedSinceRecovery = true
if m.d.PrevUndoType == undoType && m.d.UndoSkipCounter < undoSkipping {
m.d.UndoSkipCounter++
return