Norns Script Saving Script State

How would I go about saving the state of a script such that it can be recalled the next time the script is run?

Is there a mechanism (short of immediately writing all parameter changes to disk as soon as they happen) that will ensure state is saved when a script is closed?

One option would be to store all the data you’d like to recall in parameters. Then it’s just saving and loading a pset. You can save a pset with params:write(), and load a pset with params:read(). You’ll need a params:bang() after reading.

Another option would be to write the data to a file on script close, and read it back in on script init.
I did this in foulplay, way down at the bottom.

3 Likes

Cool, thanks @Justmat. Are there advantages and disadvantages to each approach?

I can’t seem to find anything on ParamSet beyond the reference page which doesn’t provide much in the way of context.

i think that it depends on the complexity of a script and how you want to manage it. with the new hide and add_group functions, you can definitely (for example) break out all the steps in a sequence as parameters that the user won’t ever see, but will collect with PSET creation. super straightforward for the user and depending on your scripting style, can really help with variable management.

for scripts that have many tables of information that your user will want to persist across sessions, i think external files are primo. @Justmat’s example is a great starting point :slight_smile:

for cheat codes, i handle saving/restoring 100’s of variables within “collections” – savestate and loadstate functions that grab data from parameters, variables, filepaths, etc. this breaks the user experience a bit, though – a user needs to use the “collections” function to save things rather than relying on the system PSET.

i do wonder if there isn’t a way to execute a custom savestate function as a callback to creating a PSET, tho. that feels like it’d offer the best flexibility.

1 Like

Thanks @dan_derks that’s useful. So ParamSet is there so that script authors and users can create their own presets for your scripts?

My current (first) script is pretty simple, at the moment, so I’m not sure it really needs preset save and recall, so maybe just dumping the current state to a file in /lib would suffice.

On the other hand, I’d like to know how to do presets, too.

paramset provides a few things:

  • an easy way to structure scripts without using redundant variables (param:get and param:set are super useful!)
  • MIDI + OSC endpoints for straightforward external control over these values – using MAP in the PARAMETERS menu, you can easily assign these
  • all paramset values are captured by PSETs without additional configuration

more on presets + MAP from a “play” standpoint here: https://monome.org/docs/norns/play/#pset

1 Like

Ah, that does sound pretty useful. I will definitely look into using ParamSet, then, as I do want to make params mappable to MIDI controllers.

1 Like

sounds good!

icymi, here’s the section of norns study 3 that introduces best practices for param handling: https://monome.org/docs/norns/study-3/#parameters

1 Like

Great, I will look at that! I must admit, I skipped the Studies. Will have to go back to them. Thank you for reminding me.

1 Like

I’m a bit confused…

I’ve managed to add various params, but I’m confused about the order I should do things in. This is what I have currently in my init() function:

function init()
  
  -- Params
  
  params:add_control("tempo","tempo",controlspec.new(40,200,'lin',0,110,'bpm'))
  params:set_action("tempo", function(t) clk:bpm_change(t) end)
  
  params:add_control("xpos","xpos",controlspec.new(1,5,'lin',0,3))
  params:set_action("xpos", function(x) patterngen.set_xy(x, params:get('ypos')) end)
  
  params:add_control("ypos","ypos",controlspec.new(1,5,'lin',0,3))
  params:set_action("ypos", function(y) patterngen.set_xy(params:get('xpos'),y) end)
  
  params:read("luagridstate.pset")
  params:bang()
  
  metro_save = metro.init(function(stage) params:write("luagridstate.pset") end, 10)
  metro_save:start()

  -- OTHER STUFF
end

But I get

pset >> write: luagridstate.pset
pset: BAD FILENAME

every time metro attempts to save the pset.

searching in the norns lua stack reveals some clues


--- write to disk.
-- @param filename either an absolute path, a number (to write [scriptname]-[number].pset to local data folder) or nil (to write default [scriptname].pset to local data folder)
-- @tparam string name
function ParamSet:write(filename, name)
  filename = filename or 1
  if type(filename) == "number" then
    local n = filename
    filename = norns.state.data .. norns.state.shortname
    filename = filename .. "-" .. string.format("%02d",n) .. ".pset"
  end
  print("pset >> write: "..filename)
  local fd = io.open(filename, "w+")
  if fd then
    io.output(fd)
    if name then io.write("-- "..name.."\n") end
    for _,param in pairs(self.params) do
      if param.id and param.save and param.t ~= self.tTRIGGER then
        io.write(string.format("%s: %s\n", quote(param.id), param:get()))
      end
    end
    io.close(fd)
  else print("pset: BAD FILENAME") end
end

same clue is revealed by API doc for paramset.write
http://monome.org/norns/classes/paramset.html#ParamSet:write

2 Likes

Study 3 should probably be updated, as it doesn’t work as described, in that case.

With

function init()
  
  -- Params
  
  params:add_control("tempo","tempo",controlspec.new(40,200,'lin',0,110,'bpm'))
  params:set_action( "tempo", function(t) clk:bpm_change(t) end)
  
  params:add_control("xpos","xpos",controlspec.new(1,5,'lin',0,3))
  params:set_action( "xpos", function(x) patterngen.set_xpos(x) end)
  
  params:add_control("ypos","ypos",controlspec.new(1,5,'lin',0,3))
  params:set_action( "ypos", function(y) patterngen.set_ypos(y) end)
  
  params:add_control("density_kick","density_kick",controlspec.new(0,1,'lin',0,0.5))
  params:set_action( "density_kick", function(x) patterngen.set_threshold_kick(x) end)
  
  params:add_control("density_snare","density_snare",controlspec.new(0,1,'lin',0,0.5))
  params:set_action( "density_snare", function(x) patterngen.set_threshold_snare(x) end)
  
  params:add_control("density_hat","density_hat",controlspec.new(0,1,'lin',0,0.5))
  params:set_action( "density_hat", function(x) patterngen.set_threshold_hat(x) end)
  
  params:read(nil)
  params:bang()
  
  metro_save = metro.init(function(stage) params:write(nil) end, 10)
  metro_save:start()
  
  ---etc.
end

I now get

pset >> write: /home/we/dust/data/luagrid/luagrid-01.pset

The preset seems to contain the value I expect, after running the script and tweaking params.

However, while tempo seems to be currently recalled, when re-running the script, the value for ‘xpos’ and ‘ypos’ always get their default values, rather than those previously written to the pset (and these values are re-written into the pset the first time it auto-saves, assuming I haven’t changed them manually at this point).

UPDATE

My bad- a “reset()” function was being called later on in init(), that was resetting the xpos and ypos params.

Removed that, and all appears to be working as expected, now!

1 Like

I came here looking for the right syntax to pass to params:write. What I came away with, from searching the norns scripts installed on mine was:

params:write(_path.data .. "myscript/myfile.pset","pset-name")

Another thing I’ve noticed, and I might be wrong here, but it seems that it’s much more efficient to put your pset save in the cleanup function, rather than in a clock or metro. I put together a little “autosave” example for myself which works that way:

init = function ()
  -- set up params first
  params:add_number("numtest", "numtest", 5)
  -- load latest autosave values
  params:read(_path.data .. "myscript/autosave.pset")
end

cleanup = function ()
  -- save params before unloading
  params:write(_path.data .. "myscript/autosave.pset","autosave")
end

I kind of like it, because you can still save out psets manually to “snapshot” the current state. then if you were to load that pset, it’s what would be saved to the autosave state at cleanup.

Edit: One thing I misjudged, if you save a pset through the pset menu, the script is now set to use that pset next time you load, which makes my loading of autosave there not work.

3 Likes