From 462faf5f4eea92cf651171107b77bd4532d9c665 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:28:35 +0300 Subject: [PATCH] feat: save recovery data to disk and/or DAW project --- CHANGELOG.md | 6 +- cmd/sointu-track/main.go | 8 ++- cmd/sointu-vsti/main.go | 20 ++++++- go.mod | 4 +- go.sum | 29 +--------- tracker/gioui/tracker.go | 39 ++++++++++--- tracker/model.go | 115 ++++++++++++++++++++++----------------- 7 files changed, 126 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c78bb..3f02934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased ### Added - Save the GUI state periodically to a recovery file and load it on - startup of the app, if present. The recovery file is located in the - home directory of the user. + startup of the app, if present. The recovery files are located in the + app config directory (e.g. AppData/Roaming/Sointu on Windows). +- Save the VSTI GUI state to the DAW project file, through GetChunk / + SetChunk mechanisms. - Instrument presets. The presets are embedded in the executable and there's a button to open a menu to load one of the presets. - Frequency modulation target for oscillator, as it was in 4klang diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index 82ffe85..c1bfd5b 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "path/filepath" "runtime" "runtime/pprof" @@ -50,10 +51,11 @@ func main() { defer audioContext.Close() modelMessages := make(chan interface{}, 1024) playerMessages := make(chan tracker.PlayerMessage, 1024) - model, err := tracker.LoadRecovery(modelMessages, playerMessages) - if err != nil { - model = tracker.NewModel(modelMessages, playerMessages) + recoveryFile := "" + if configDir, err := os.UserConfigDir(); err == nil { + recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery") } + model := tracker.NewModel(modelMessages, playerMessages, recoveryFile) player := tracker.NewPlayer(cmd.DefaultService, playerMessages, modelMessages) tracker := gioui.NewTracker(model, cmd.DefaultService) output := audioContext.Output() diff --git a/cmd/sointu-vsti/main.go b/cmd/sointu-vsti/main.go index 305feeb..3aafb2e 100644 --- a/cmd/sointu-vsti/main.go +++ b/cmd/sointu-vsti/main.go @@ -3,6 +3,11 @@ package main import ( + "crypto/rand" + "encoding/hex" + "os" + "path/filepath" + "github.com/vsariola/sointu/cmd" "github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker/gioui" @@ -49,10 +54,13 @@ func init() { vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) { modelMessages := make(chan interface{}, 1024) playerMessages := make(chan tracker.PlayerMessage, 1024) - model, err := tracker.LoadRecovery(modelMessages, playerMessages) - if err != nil { - model = tracker.NewModel(modelMessages, playerMessages) + recoveryFile := "" + if configDir, err := os.UserConfigDir(); err == nil { + randBytes := make([]byte, 16) + rand.Read(randBytes) + recoveryFile = filepath.Join(configDir, "Sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes)) } + model := tracker.NewModel(modelMessages, playerMessages, recoveryFile) player := tracker.NewPlayer(cmd.DefaultService, playerMessages, modelMessages) tracker := gioui.NewTracker(model, cmd.DefaultService) tracker.SetInstrEnlarged(true) // start the vsti with the instrument editor enlarged @@ -102,6 +110,12 @@ func init() { tracker.Quit(true) tracker.WaitQuitted() }, + GetChunkFunc: func(isPreset bool) []byte { + return tracker.SafeMarshalRecovery() + }, + SetChunkFunc: func(data []byte, isPreset bool) { + tracker.SafeUnmarshalRecovery(data) + }, } } diff --git a/go.mod b/go.mod index d97425b..aaa2cb7 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,11 @@ require ( gioui.org/x v0.1.0 github.com/Masterminds/sprig v2.22.0+incompatible github.com/hajimehoshi/oto v0.6.6 + golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 - pipelined.dev/audio/vst2 v0.10.1-0.20230718065422-be1242fca4ab + pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826 ) require ( @@ -28,7 +29,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/stretchr/testify v1.6.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect golang.org/x/image v0.7.0 // indirect golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f // indirect golang.org/x/sys v0.7.0 // indirect diff --git a/go.sum b/go.sum index fcb2a63..cb7ca48 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,4 @@ -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= -eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= -gioui.org v0.1.0/go.mod h1:a3hz8FyrPMkt899D9YrxMGtyRzpPrJpz1Lzbssn81vI= -gioui.org v0.2.1-0.20230823193131-cf5ae4aad92e h1:EdWCy7gaaBZoVFF0OCdo2Tj/LCJ4AI6HOGG8p5Zcljs= -gioui.org v0.2.1-0.20230823193131-cf5ae4aad92e/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4= gioui.org v0.3.0 h1:xZty/uLl1+/HNKpumX60JPQd46n8Zy6lc5T3IRMKoR4= gioui.org v0.3.0/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= @@ -13,7 +8,6 @@ gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/x v0.1.0 h1:CvphvaQSroRaNEZ+JbXBkV3J3klA76U3JpieyEwHFX4= gioui.org/x v0.1.0/go.mod h1:5qZxjtK/TVznMlcEOyn8OheiCZlArxF3IKnLqSehKXQ= -git.sr.ht/~jackmordaunt/go-toast v1.0.0/go.mod h1:aIuRX/HdBOz7yRS8rOVYQCwJQlFS7DbYBTpUV0SHeeg= git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI= git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -23,22 +17,13 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0/go.mod h1:ccdDYaY5+gO+cbnQdFxEXqfy0RkoV25H3jLXUDNM3wg= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/esiqveland/notify v0.11.0/go.mod h1:63UbVSaeJwF0LVJARHFuPgUAoM7o1BEvCZyknsuonBc= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= -github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc/go.mod h1:RaqFwjcYyM5BjbYGwON0H5K0UqwO3sJlo9ukKha80ZE= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= -github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hajimehoshi/oto v0.6.6 h1:HYSZ8cYZqOL4iHugvbcfhNN2smiSOsBMaoSBi4nnWcw= @@ -47,7 +32,6 @@ github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/jezek/xgb v1.0.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -71,8 +55,6 @@ golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFd golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= @@ -90,7 +72,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -98,25 +79,19 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -135,8 +110,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -pipelined.dev/audio/vst2 v0.10.1-0.20230718065422-be1242fca4ab h1:rhVxuLIl32iwa3IaGwLKmEsPCV0XK3czP3pD4TDy/Ik= -pipelined.dev/audio/vst2 v0.10.1-0.20230718065422-be1242fca4ab/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8= +pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826 h1:4c7O6PJ/Zl677O2VhXHUZK7LJyVBhUI7Q39+ri+gKUs= +pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8= pipelined.dev/pipe v0.10.0/go.mod h1:aIt+NPlW0QLYByqYniG77lTxSvl7OtCNLws/m+Xz5ww= pipelined.dev/pipe v0.11.0 h1:yRrbntKdqw/nbFqkz9dPaSHBoM7pK1LRHHDqdBJuqtc= pipelined.dev/pipe v0.11.0/go.mod h1:aIt+NPlW0QLYByqYniG77lTxSvl7OtCNLws/m+Xz5ww= diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index 7751c1b..1881e0f 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -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{}{}: diff --git a/tracker/model.go b/tracker/model.go index 7123de9..27e510a 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -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