I’ve struggled getting my head around the PARAMS concepts for a few days, but I have now successfully implemented a bunch of them in my script and they’re quite convenient for handling those numbers indeed! Yay!

But:

  • When adjusting params in the EDIT menu, my encoders always step them up/down by 1. I know I can hold E3 for fine tuning, but I was wondering wether I can set a default step size for each param when adjusted via the menu?

  • I tried to use wrap=true for a param with a range from 0 to 3. There was some strange behavior though when wrapping the values then. When going above the max value, it would wrap around to min+1. E.g. 3+1 was 1 and not 0. Vice versa for 0-1, which would result in 2, instead of 3. Is this a bug or did I do something wrong?

for fractional values control is best and you can set the increment with quantum in the controlspec (measured in the fraction of the total range incrimented on each delta). you’ll also want to pay attention to step which rounds down whatever that end value happens to be (0 to turn it off)

which param type were you using for this ?

2 Likes

10000%

just to add a few more waypoints, from https://monome.org/docs/norns/reference/controlspec: controlspec.new(min, max, warp, step, default, units, quantum, wrap)

these can get folded into parameters in a few ways…

you can keep it terse:

function init()
  params:add_control("pan","pan",controlspec.new(-1,1,'lin',0.1,0,'',0.01,true))
  -- this gives 0.1 changes without K3, from -1 to 1, starting at 0, with wrapping
end

or use the controlspec preset for panning:

function init()
  my_pan = controlspec.PAN
  params:add_control("panning","panning",my_pan)
end

or roll your own controlspec definition:

function init()
  my_pan = controlspec.def{
    min=-1,
    max=1,
    warp='lin',
    step=0.1,
    default=0,
    quantum=0.01,
    wrap=true,
    units=''
  }
  params:add_control("panning","panning",my_pan)
end

i’m not able to reproduce the wrap issue with control-type params, but will be happy to dig in if you have a specific case!

2 Likes

Thank you @andrew and @dani_derks! I was using wrap on a number type. I’ll look into using control then :slight_smile:

oh! rad, thank you – seems like a bug!

if anyone has more brainspace than i do this very second, feel free to submit a fix!

3 Likes

Hm, I’m not having any luck with control wrapping either :thinking:

In my script I have four states 1, 2, 3 and 4 through which I want to cycle with an encoder, by adding a delta of +/-1. I tried the following controlspec, but this behaves exactly like when wrapping a number.

params:add_control("wrap", "wrap", controlspec.new(1, 4, "lin", 1, 1, "", 0.25, true))

If I switch wrapping off, it also behaves strangely. It will go 1 → 2 → 3 but then I will need two encoder clicks to reach 4.

1 Like

ah! will test control further, but ultimately whole number states are totally a case where number is perfect.

until the wrapping is fixed, here’s a dumb workaround for a wrapping number param:

function init()
  params:add{type = "number", id = "rnd", name = "stuff", min = 0, max = 5, default = 1} -- one lower and one higher than we need
  params:set_action("rnd",function(x)
    if x == 5 then
      params:set("rnd",1,true) -- 'true' silently updates the param
      x = 1 -- set 5 to 1
    elseif x == 0 then
      params:set("rnd",4,true) -- 'true' silently updates the param
      x = 4 -- set 0 to 4
    end
    print("do stuff with "..x)
  end
  )
end
2 Likes

@NightMachines
@dani_derks

wrapping on number is bugged by what looks like a simple typo. (or zero-based thinking)
[ fix number wrap (issue 1353) by catfact · Pull Request #1354 · monome/norns · GitHub ]

wrapping control seems to works as expected.

the confusion here is how quantum works. in this example it should be 1/3, not 1/4. i think quantum and step are inherently confusing, which is one reason number or option are more convenient for ordinals. i posted about this in depth further up the thread

2 Likes

Hi together,
i have the following problem regarding the handling of tables in lua. In my program I often create tables in functions and save the returned table in a variable.

function new_table ()
  -- local t = {}
  t = {}
  
  t.name = "test123"
  t.clock_id = nil
  
  t.run = function (self)
    self.clock_id = clock.run(
      function (s)
        while true do
          print(s.name)
          clock.sleep(1)
        end
      end, self)
  end
  
  return t
end

function init ()
  print("lua test project")
  
  obj = new_table()
  
  obj:run()
end

If I execute the above example I see the expected behavior, the print command is executed every second, printing the name field of obj.

If I try to delete obj via the console with obj = nil, I would expect that an error message should occur since the running clock function refers to a table which not longer exists, instead the “test123” is further printed.

Next, I tried to directly delete t since I assumed that obj is rather a “reference” to the “real” table. However, the behavior continues. If I check if t still exists, I can not longer reference t.

What is going on here? Is clock still referencing the original memory slot and therefore, still runs without errors? Is the memory trashed with seemingly deleted tables, which are created in functions?

I am aware that I simply should cancel the clock, but I am really interested in what is happening with the table to be deleted. While this is a super easy example to illustrate this problem, I found it in the context of deleting a big sequence of note data and the sequencer kept running in the background …

So it would be pretty bad if I would unconsciously overflow the memory, by not being able to delete such tables properly.

EDIT:
To fix the problem I tried to eliminate to pass the table to be deleted as a variable to the clock.run function.

function new_table ()
  t = {}
  
  t.name = "test123"
  t.clock_id = nil
  
  t.run = function (self)
    self.clock_id = clock.run(
      function (s)
        while true do
          print(s.name)
          clock.sleep(1)
        end
      end, self)
  end
  
  return t
end

function init ()
  obj = new_table()
  
  --obj:run()
  clock.run(function () while true do print(obj.name) clock.sleep(1) end end)
end

if I now delete obj with obj = nil, an error occurs.

Eliminating the variable in which the table is stored in new_table does not solve the problem.

function new_table ()
  return {
    name = "test123",
    clock_id = nil,
  
    run = function (self)
      self.clock_id = clock.run(
        function (s)
          while true do
            print(s, s.name)
            clock.sleep(1)
          end
        end, self)
    end
  }
end

function init ()
  obj = new_table()
  print(obj)
  
  obj:run()
  -- clock.run(function () while true do print(obj.name) clock.sleep(1) end end)
end

If I print the table, the same address is returned. No matter if I refer to obj or to s inside the passed function.

Since passing variables to run is a feature, I think this could be a major issue for a lot of use cases.

Thanks for your help and your input!

1 Like

The Lua garbage collector will only delete object/values if they aren’t being referred to. In the original example, if the clock function continues to run without error after one seemingly destroys the only variable which is holding onto the table then that is a good indicator that another variable somewhere is still holding onto the table. Looking at the run function specifically:

  t.run = function (self)
    self.clock_id = clock.run(
      function (s)
        while true do
          print(s.name)
          clock.sleep(1)
        end
      end, self)
  end

…and then later the run function is used:

The obj:run() syntax is sugar for obj.run(obj) - essentially when the run function is called with method syntax another variable (the argument to run, self) is given a reference to the table. Further the implementation of run passes that table value to clock.run which stores it in order to pass it as s to the clock coroutine (function):

When defining one function inside another we are creating what is generally called a closure. Closures can be used in lots of creative ways, for example:

function make_shifter(delta)
	return function(n)
		return n + delta
	end
end

shift_two = make_shifter(2)
shift_four = make_shifter(4)

shift_two(5)
-- returns 7

shift_four(-6)
-- returns -2

In the above example make_shifter returns a closure (function) which captures (closes over) the value of delta each time it is called. A potentially more compelling using of closures is to give them internal state like so:

function make_counter(initial, delta)
	local next_value = initial - delta
	return function()
		next_value = next_value + delta
		return next_vale
	end
end

up = make_counter(0, 1)
down = make_counter(10, -1)

up() -- returns 0
up() -- returns 1

down() -- returns 10
down() -- returns 9

Returning to the original concern

…based on the code presented Lua is behaving normally. Closures and co-routines can be hard to wrap ones head around at first but in time they can become a powerful tool.

5 Likes

@ngwese thanks for the detailed answer, I think I can better grasp the problem now. Still, I have no idea how to delete the table correctly.

To illustrate the problem better I made the example closer to my real use case.

function new_seq ()
  return {
    seq_data = {1,2,3,4,5,6,7,8},
    pos = 1,
    clock_id = nil,
  
    run = function (self)
      self.clock_id = clock.run(
        function (s)
          while true do
            print(s.seq_data[s.pos])
            s.pos = s.pos+1
            if s.pos>#s.seq_data then s.pos = 1 end
            clock.sleep(1)
          end
        end, self)
    end
  }
end

function init ()
  seq = new_seq()
  seq:run()
  
  -- [Option1]
  -- seq = nil -> keeps running
  
  -- [Option2]
  -- clock.cancel(seq.clock_id) -> stops running
  -- seq = nil -> does it still exist?
end

The question which is haunting me is, if the table keeps existing when following [Option 2]?

Assuming that clock.cancel(...) itself destroys the co-routine (a strong likelihood) then stopping the clock should allow the garbage collector to reclaim the space consumed by the table. Short of looking at the code for the clock implementation I don’t know a good way off the top of my head to validate that table is truly gone.

If you wanted to have a cleaner abstraction then Option2 consider adding a stop function to the table like the run function.

t.stop = function(self)
   clock.cancel(self.clock_id)
end

…then the usage would look like:

seq = new_seq()
seq:run()

...stuff...

seq:stop()
-- if `seq` goes out of scope then it will be collected
1 Like

Hi, I’m working on a script that has some quite complex settings. To help people understand what is going on I want to include a range of example “projects” that people can load and play with to get an understanding of the script. My aim is to make these available under the main “DATA” folder under a sub directory named after the script.

My question is, how do I get the maiden installation (e.g ;install https://github.com/kevinlindley/myscript) to include these “projects” under the “DATA” folder ?
From what I can see the maiden installation only seems to install anything under the “CODE” folder. Is this correct, or is there a way of getting maiden to install items under the “DATA” folder as well?

The alternative would be to create a sub folder under my “CODE” folder but that feels a wrong approach.

I would like to hear from an script developers who has an opinion or who knows how to bundle “DATA” with “CODE” for installation by maiden.

Kind Regards
Kevin

on first run, the script’s bundled data folder (ie /dust/code/x/data) gets copied to /dust/data/x

so, exactly what you want, it seems?

see here

4 Likes

would it make sense to extend this to copy everything in data/, not just psets ? that would be handy !

1 Like

Excellent, thanks for the feedback on this really appreciated. Will give this a try now.

Kind Regards
Kevin

yes that makes sense, agreed

edit: i take it back. scripts should be able to keep some “local” arbitrary data that doesn’t need to be copied to /dust/data (in case it’s large, such as an audio sample).

if we want arbitrary initial data (besides psets) to be copied to /dust/data we should add an init folder to a script’s data/ folder (or some similar solution).

Is there a way to “pause” a synced clock? I basically want to write clock.sync(1/0), which of course doesn’t work, but which would be very convenient :smiley:

hi hi!

if i’m not looking to full-out cancel the clock and recreate it, then i often use a state flag which can act as a gate:

function init()
  is_running = false
  screen_pos = 10
  the_action()
  clock.run(do_this)
end

function do_this()
  while true do
    clock.sync(1/4)
    if is_running then
      the_action()
    end
  end
end

function the_action()
  screen.clear()
  screen.level(15)
  screen_pos = util.wrap(screen_pos+1,10,48)
  screen.move(10,screen_pos)
  screen.text("*")
  screen.update()
end

function key(n,z)
  if n == 3 and z == 1 then
    is_running = not is_running
  end
end

bonus of this approach is the “un-pause” picks up the action on the next beat-sync

5 Likes

Thanks!!!


EDIT: All figured out :slight_smile: