Norns: set ParamSet parameter limits dynamically

I’m working on a Norns app involving circles. The radii of these circles are set with ParamSet parameters. I would like them to remain in the radius order I set - otherwise, things get too confusing. However, I also don’t want to artificially limit their radii. The most elegant solution would be to modify the parameters thusly:

  • inner circle, minimum radius: absolute minimum
  • inner circle maximum radius: middle circle radius - 1
  • middle circle minimum radius: inner circle radius + 1
  • middle circle maximum radius: outer circle radius - 1
  • outer circle minimum radius: middle circle radius + 1
  • outer circle maximum radius absolute maximum

Is it possible to set the limits of a Norns ParamSet parameter dynamically like this? That is, can I update the other circle’s minimums and maximums when one circle’s radius is updated?

4 Likes

ooo this is a super cool q!!!

parameter limits can be dynamically shifted, by futzing with the min and max values for that specific param. the path is a little wonky, as it requires looking up the parameter you want to adjust after it’s declared:

> params.lookup["OSC_name_of_parameter"]
45
> tab.print(params.params[45]) -- gives us the parameter definition
action	function: 0x5230e8
t	1
min	1
id	inner
allow_pmap	true
value	19
save	true
wrap	false
default	19
max	19
range	120
name	OSC_name_of_parameter

so if we execute params.params[45].max = 12, that parameter would no longer be able to cross past 12 – it would stay at its current value, though, until interacted with. since the problem also asks for us to cascade these changes, then we’ll need to do some lookups, some checking for current state, and force some changes depending on which parameter we’re adjusting – but that’s all totally possible!

all to say, i think this should get you started?

function init()
  --inner circle, minimum radius: absolute minimum
  local abs_min = 1

  --middle circle minimum radius: inner circle radius + 1
  params:add_number("inner","inner",abs_min,120,10)
  params:set_action("inner", function(x)
    local circ_look = params.lookup["middle"]
    params.params[circ_look].min = x+1
  end)
  
  --inner circle maximum radius: middle circle radius - 1
  params:add_number("middle","middle",1,120,18)
  params:set_action("middle", function(x)
    local circ_look = params.lookup["inner"]
    params.params[circ_look].max = x-1
  end)
  
  params:bang() -- process the actions at script start
end
3 Likes

This is exactly what I was looking for, thank you! This answers a few other questions of mine, too.

Fortunately, I don’t think that will be an issue here, as the values can’t cross in either direction!

Where are these fields of the ParamSet entries documented? I think I must have been looking in the wrong place.

1 Like

ah, rad! glad i could assist!

and yes, totally on about them not crossing – i only brought up the params.params[45].max = 12 as a pre-emptive clarification for this sort of situation:

> params:set("the_45_one",19)
> params.params[45].max = 12
> params:get("the_45_one")
19 <~~~ does not reflect the updated max
> params:delta("the_45_one",1) <~~~ refreshes the param
> params:get("the_45_one")
12 <~~~ the updated max!

they’re generally all annotated in the reference (in progress but robust) and the API (more or less complete but minimal), tho there isn’t a good illustration of when you’d use the lookup id to dynamically adjust ranges, so i think i’ll add this question / example in the ref tomorrow. thank you again for asking it!

2 Likes

(also @dan_derks ) Regarding documentation, I’d say that any time you end up having to use lookup and directly editing the fields of the resultant table, you are going outside of any formally-supported API. There might be unintended side effects (or more relevant, a lack thereof; for example, here you may - or may not - want to manually bang the edited parameter after adjusting its range.)

If these functions are broadly useful, then they can be trivially added to “formal” API for paramset and accessed by param ID. That is the point where it makes sense to expand documention and tutorials.

3 Likes

makes sense!

i think that’d be helpful! early on, I avoided parameters because it seemed like relying on variables would be the most flexible choice, but have come to love params as a very powerful + legible approach – plus all the MIDI-mapping and PSET stuff you get for free! formally supporting these types of changes to what I think can be seen as a bit of a mysterious/rigid container would be rad!


maybe i’m overthinking but is the alternative to this direct change approach just chaining if/then statements inside of each parameter, to check for the state of the other parameters (using params:get) and then also checking the state of the parameter itself? that feels familiar but i went this route since all that logic is inside of the parameter code anyway – but this would def be a worthwhile challenge to explore!

2 Likes

Oh yes! The reference is what I was looking for, wonderful. I had been going off the API docs only this whole time, possibly because I’m a bit spoiled by how comprehensive Rust’s autogenerated docs tend to be.

This definitely makes sense. Certainly, I think it might be useful to add the function params:set_range(id, min, max) or so, but there are design considerations there. For instance, as you say, whether or not this function auto-bangs the parameter, and whether or how you can unlimit a parameter (by passing nil, perhaps.)

Either way, thanks, I’m going to go off and write some more code now!

2 Likes

It’s tangentially related to this, but I’ve been working on a script that has two params, which are the start and end of a sequence (which affects the sequence length that plays back). Here, they can’t cross and must be constrained by each other. What I’ve done (which seems a little inelegant but does the trick) is to just define a function outside of function init() and then call that function in each of the params. I’ve set out the relevant extracts below! As I said, I think it’s a little chunky (and there’s probably a neater way to do it even if you’re just chaining if/then statements, but it’s worked for me so far.

I think the same approach could be used to address what @NoraCodes was referring to in the first post, but it might get quite clunky indeed. Hope that contributes something to the conversation!

For those interested:
function init()
...
  params:add{type = "number", id = "main_start", name = "main start", min = 1, max = 8, default = 1, action = function() min_max_main_start() redraw() end}
  params:add{type = "number", id = "main_end", name = "main end", min = 1, max = 8, default = 8, action = function() min_max_main_end() redraw() end}
...
end

function min_max_main_start()
  if params:get("main_start") > params:get("main_end") 
    then params:set("main_start",params:get("main_end"))
  end
end

function min_max_main_end()
  if params:get("main_start") > params:get("main_end")
    then params:set("main_end",params:get("main_start"))
  end
end

EDIT: Thought this was quite a fun little exercise to try, so here’s my attempt at using functions instead of the internal logic of params. It seems to work for me. One of the benefits is that this is quite easily scalable for however many circles you want to have.

See here for my attempt:
function init()

	--absolute minimum and maximums
	local abs_min = 1
	local abs_max = 120

	-- parameters for each circle's radius
	for i=1,3 do
		params:add_number("radius_circle_"..i,"radius circle "..i,abs_min,abs_max,10*i)
	end
	
	-- actions to constrain radii
	params:set_action("radius_circle_1", function() maxradius(1,2) end)
	params:set_action("radius_circle_2", function() minradius(1,2) maxradius(2,3) end)
	params:set_action("radius_circle_3", function() minradius(2,3) end)
	
end

-- function for minimum radius, takes integers for the id of the circles
function minradius(smaller,larger)
	local smaller_id = "radius_circle_"..smaller
	local larger_id = "radius_circle_"..larger
	if params:get(larger_id) < params:get(smaller_id) + 1
		then params:set(larger_id, params:get(smaller_id)+1)
	end
end

-- function for maximum radius, takes integers for the id of the circles
function maxradius(smaller,larger)
	local smaller_id = "radius_circle_"..smaller
	local larger_id = "radius_circle_"..larger
	if params:get(smaller_id) > params:get(larger_id) - 1
		then params:set(smaller_id, params:get(larger_id)-1)
	end
end
4 Likes

ayyy! this is the stuff – i was thinking wayyy too literally about the problem of setting the actual parameter limits dynamically that i totally ignored the elegance to be found in pattern-driven naming like radius_circle_x and simple abstractions.

upvote x 1000, this is definitely a better way to go as it doesn’t rely on undocumented workflows and the core functionality is abstracted for use elsewhere in the code.

great work, @Fardles !

5 Likes