params and paramset
Sets of parameters can be defined with the paramset
library. The global variable params
is a special instance of a paramset
object which contains the PARAMETERS menu, so acting on params
will change the PARAMETERS menu.
sections
functions
Syntax | Description |
---|---|
params.new(id, name) | Create a parameter from scratch (not typical, but useful to have) |
params:add_separator(id, name) | Create a horizontal line with a title in the parameters menu to demarcate a section of controls (users can press K3 on any selected separator to jump through the menu’s additional separators) |
params:add_group(id, name, n) | Group n controls under name , to help collapse parameter menu navigation. Note that groups cannot be made within groups (no nesting). |
params:add(args) | Create a parameter by passing an argument table |
params:add_number(id, name, min, max, default, formatter, wrap) | Create a parameter set for numbers |
params:add_option(id, name, options, default) | Create a parameter set for option selection (options must be passed as a table) |
params:add_control(id, name, controlspec, formatter) | Create a parameter set for controlspec formatting (see controlspec for more details) |
params:add_file(id, name, path) | Create a parameter set for file selection |
params:add_text(id, name, text) | Create a parameter set for text entry |
params:add_taper(id, name, min, max, default, k, units) | Create a parameter set for non-linear movement |
params:add_trigger(id, name) | Create a parameter set for an “on/off” action trigger |
params:add_binary(id, name, behavior, default) | Create a parameter set for either momentary, toggle, or trigger behavior with a default state |
params:print() | Print the index, name, and value for each parameter to the REPL |
params:list() | Print the names of each parameter to the REPL |
params:get_id(index) | Returns the string id of a given parameter’s index |
params:lookup_param(index) | Returns the param object at index; useful for useful for meta-programming tasks like changing a param once created |
params:string(id) | Returns the string associated with the current value for a given parameter’s id |
params:set(id,val,silent) | Set a parameter’s value, with optional action execution (boolean) |
params:set_raw (index, v, silent) | Set a parameter’s controlspec raw value, with optional action execution (boolean) |
params:set_save(id, state) | Set whether a parameter is automatically saved as part of a .pset file (boolean) |
params:set_action(id, function) | Assign an action to a parameter’s changes |
params:get(id) | Returns a parameter’s current number value |
params:get_raw(id) | Returns a parameter’s controlspec raw value |
params:t(index) | Returns a given parameter’s type |
params:get_range(index) | Returns the min/max range of a parameter |
params:get_allow_pmap(index) | Returns whether a parameter is able to be MIDI-mapped (boolean) |
params:hide(id) | Hides the specified parameter in the UI menu, but parameter index and data is still retained. Use _menu.rebuild_params() after hiding to dynamically rebuild the menu UI. |
params:show(id) | Shows the specified parameter in the UI menu, after it is hidden (not required for default parameter building). Use _menu.rebuild_params() after hiding to dynamically rebuild the menu UI. |
params:visible(id) | Returns whether a parameter is visible in the UI menu (boolean) |
params:write(filename, name) | Save a .pset file of all parameters’ current states to disk |
params:read(filename, silent) | Read a .pset file from disk, restoring saved parameter states, with an option to avoid triggering parameters’ associated actions (boolean) |
params:delete(filename, name, pset_number) | Delete a .pset file from disk |
params:default() | Read the default .pset file from disk, if available |
params:bang() | Trigger all parameters’ associated actions |
params:clear() | Clear all parameters (system toolkit, not for script usage) |
additional tools
The function calls listed above are supplemented by additional helpers + extensions, which are formatted differently.
Syntax | Description |
---|---|
params.lookup[id] | A table call (note the square brackets) which returns the index of a given parameter’s string id |
params.action_write = function(filename, name, pset_number) | User script callback whenever a parameter write occurs, which passes the .pset ’s filename + UI name + PSET number |
params.action_read = function(filename, silent, pset_number) | User script callback whenever a parameter read occurs, which passes the .pset ’s filename + silent state + PSET number |
params.action_delete = function(filename, name, pset_number) | User script callback whenever a parameter delete occurs, which passes the .pset ’s filename + name + PSET number |
example
MusicUtil = require "musicutil"
math.randomseed(os.time())
function init()
params:add_separator("test_script_header", "test script")
params:add_group("example_group", "example group", 3)
for i = 1,3 do
params:add{
type = "option",
id = "example_"..i,
name = "parameter "..i,
options = {"hi","hello","bye"},
default = i
}
end
params:add_number(
"note_number", -- id
"notes with wrap", -- name
0, -- min
127, -- max
60, -- default
function(param) return MusicUtil.note_num_to_name(param:get(), true) end, -- formatter
true -- wrap
)
local groceries = {"green onions","shitake","brown rice","pop tarts","chicken thighs","apples"}
params:add_option("grocery_list","grocery list",groceries,1)
params:add_control("frequency","frequency",controlspec.FREQ)
params:add_file("clip_sample", "clip sample")
params:set_action("clip_sample", function(file) load_sample(file) end)
params:add_text("named_thing", "my name is:", "")
params:add_taper("taper_example", "taper", 0.5, 6.2, 3.3, 0, "%")
params:add_separator()
params:add_trigger("trig", "press K3 here")
params:set_action("trig",function() print("boop!") end)
params:add_binary("momentary", "press K3 here", "momentary")
params:set_action("momentary",function(x) print(x) end)
params:add_binary("toggle", "press K3 here", "toggle",1)
params:set_action("toggle",function(x)
if x == 0 then
params:show("secrets")
elseif x == 1 then
params:hide("secrets")
end
_menu.rebuild_params()
end)
params:add_text("secrets","secret!!")
params:hide("secrets")
params:print()
random_grocery()
end
function load_sample(file)
print(file)
end
function random_grocery()
local ranged_list = params:get_range("grocery_list")
params:set("grocery_list",math.random(ranged_list[1],ranged_list[2]))
end
parameter types
norns can support many types of parameter declarations, to facilitate the ‘right’ type of control for the musical task at hand.
The types, with a few linked examples, are:
IDs + collisions
Every parameter has its own ID, as well as a display string, which helps differentiate scripting syntax from what you see in the PARAMETERS
UI menu.
eg.
params:add_number(
'a_special_number', -- ID, used for scripting
'*special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
127, -- max
63 -- default
)
A parameter’s ID is special – it’s how the norns system communicates back and forth with the parameter the ID represents.
If a script overwrites a parameter’s ID, norns will make some noise about it because some aspects of the previous instance of the ID are overwritten, which can cause unexpected results in a script. For example:
function init()
params:add_number(
'a_special_number', -- ID, used for scripting
'*special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
127, -- max
63 -- default
)
params:add_number(
'a_special_number', -- ID, used for scripting
'*another special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
10, -- max
4 -- default
)
print(params:get('a_special_number'))
end
Will cause the following to print to matron:
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!! ERROR: parameter ID collision: a_special_number
! please contact the script maintainer - this will cause a load failure in future updates
! BEWARE! clobbering a script or mod param
But more importantly, print(params:get('a_special_number'))
will never reference the earlier instance of a_special_number
– it returns 4
at the script load, because a_special_number
’s original default value of 63 was overwritten by a new default value of 4.
Separators and groups also have ID’s, to assist with visibility (using params:hide
and params:show
, in conjunction with _menu.rebuild_params()
). Since these parameter types don’t reference actions, if IDs for either of these two parameter types are reused for another separator or group, norns will automatically ‘steal’ the ID and print a notice to matron. For example:
function init()
params:add_separator('number_separator', 'first number')
params:add_number(
'a_special_number', -- ID, used for scripting
'*special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
127, -- max
63 -- default
)
params:add_separator('number_separator', 'second number')
params:add_number(
'another_special_number', -- ID, used for scripting
'*another special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
10, -- max
4 -- default
)
print(params:get('a_special_number'), params:get('another_special_number'))
end
Will print:
! stealing separator ID <number_separator> from earlier separator
If the separator or group uses a parameter ID already established for another parameter type, norns will not steal it. For example:
function init()
params:add_separator('number_separator', 'first number')
params:add_number(
'a_special_number', -- ID, used for scripting
'*special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
127, -- max
63 -- default
)
params:add_separator('reverb', 'second number') -- uses a system parameter ID!
params:add_number(
'another_special_number', -- ID, used for scripting
'*another special number*', -- name, displayed in PARAMETERS UI menu
0, -- min
10, -- max
4 -- default
)
print(params:get('a_special_number'), params:get('another_special_number'))
end
Will print:
! separator ID <reverb> collides with a non-separator parameter, will not overwrite
nb. while norns allows spaces in a parameter ID, it is best practice to use underscores
PSET save/load/delete callback
Parameters are designed to make MIDI mapping and saving control values for a script very straightforward, using the PMAP and PSET functionality. However, you may find that you need to generate and save data which doesn’t fit the parameters model, like tables of sequencer steps (though awake
does show how to efficiently work with patterns as parameters).
If you wish to perform additional actions when a PSET is saved, loaded or deleted, such as managing non-params data into your script, params.action_write
/ params.action_read
/ params.action_delete
are script-definable callback functions which are triggered whenever a PSET is saved, loaded, or deleted. Here’s a quick overview of their use:
function init()
params.action_write = function(filename,name,number)
print("finished writing '"..filename.."' as '"..name.."' and PSET number: "..number)
end
params.action_read = function(filename,silent,number)
print("finished reading '"..filename.."' as PSET number: "..number)
end
params.action_delete = function(filename,name,number)
print("finished deleting '"..filename.."' as '"..name.."' and PSET number: "..number)
end
Run the above and save a few PSETs in the PARAMETERS > PSET
menu. You’ll see the action_write
callback print after each is saved. Try loading them back and you’ll see the action_read
callback print after each is read. Try deleting any and you’ll see the action_delete
callback print after each is deleted.
When paired with other norns utilities, like tab.save
and tab.load
, params.action_write
/ params.action_read
/ params.action_delete
can help you manage script-generated tables while keeping the parameters UI clear, eg:
MusicUtil = require 'musicutil' -- see https://monome.org/docs/norns/reference/lib/musicutil
engine.name = 'PolyPerc' -- an included norns engine
s = require 'sequins' -- see https://monome.org/docs/norns/reference/lib/sequins
function init()
base_note = math.random(30,50)
my_seq = s{}
notes_array = MusicUtil.generate_scale_of_length(base_note, "dorian", 16)
generate_random_notes()
play = false
-- here, we set our PSET callbacks:
params.action_write = function(filename,name,number)
print("finished writing '"..filename.."' as '"..name.."'", number)
os.execute("mkdir -p "..norns.state.data.."/"..number.."/")
tab.save(note_data,norns.state.data.."/"..number.."/note.data")
end
params.action_read = function(filename,silent,number)
print("finished reading '"..filename.."'", number)
note_data = tab.load(norns.state.data.."/"..number.."/note.data")
my_seq:settable(note_data) -- send this restored table to the sequins
end
params.action_delete = function(filename,name,number)
print("finished deleting '"..filename, number)
norns.system_cmd("rm -r "..norns.state.data.."/"..number.."/")
end
end
function generate_random_notes()
note_data = {}
for j = 1,64 do
-- auto-generate 64 steps of notes:
note_data[j] = MusicUtil.snap_note_to_array(math.random(base_note, base_note+28),notes_array)
end
my_seq:settable(note_data) -- send this table of notes to the sequins
end
function play_notes()
while true do
clock.sync(1/3)
engine.hz(MusicUtil.note_num_to_freq(my_seq())) -- PolyPerc accepts 'hz' arguments
redraw()
end
end
function key(n,z)
if n == 3 and z == 1 then
-- a simple start/stop mechanism
-- can also be set to MIDI/Link transport: https://monome.org/docs/norns/clocks/#transport-callbacks
play = not play
if play then
sequencer = clock.run(play_notes)
else
clock.cancel(sequencer)
my_seq:reset()
end
redraw()
end
end
function redraw()
screen.clear()
screen.move(120,55)
screen.text_right(play and "K3: stop" or "K3: play")
screen.move(30,30)
screen.text(my_seq.ix..": "..note_data[my_seq.ix])
screen.update()
end
bundling PSETs with a script
When sharing a script with others, it may be desirable to bundle it with pre-made PSETs to give artists a collection of known starting points.
As you ready your script for sharing, you can use the standard PSET save UI in norns to build and name PSETs – they will be saved under your device’s dust/data/<script_name>
folder. Then, create a dust/code/<script_name>/data
folder and copy the generated PSET files from dust/data/<script_name>
into it.
Behind the scenes, norns checks if a dust/data/<script_name>
folder already exists on a script’s first run – if it doesn’t, norns then checks if a dust/code/<script_name>/data
folder exists. If it does and the folder has any .pset
files in it, norns copies them from code/<script_name>/data
to dust/data/<script_name>
, which surfaces the PSETs in the on-screen UI menu.
As the PSET-bundled script is first loaded onto a norns which doesn’t already have a corresponding dust/data/<script_name>
folder, you will see the following print to matron:
# script clear
# script load: /home/we/dust/code/<script_name>/<script_name>.lua
### initializing data folder
### copied default psets
# script run
To test, you can simply delete your dust/data/<script_name>
folder after copying the .pset
files to dust/code/<script_name>/data
, and a fresh boot of the script will copy all the bundled PSETs into dust/data/<script_name>
.
loading a PSET at script load
By default, norns will load a script with a blank slate, unless the script specifies otherwise. This means that parameter values will default to their initial state, which is often useful for first-time exploration but not always for live performances where one might want to reliably load a modified state as a starting point.
There are two avenues which we can travel: loading the last-saved PSET and loading a specific PSET.
loading the last-saved PSET
Whenever a PSET is saved, the slot number is stored under dust/<script_name>/pset-last.txt
. In the norns UI, this determines the placement of the asterisk for the PSET save/load/delete menu, so the most-recently saved PSET is always starred.
To load this last-saved PSET, use params:default()
. We recommend placing this at the end of your init()
function, or executing it on-the-fly through matron’s command line.
Once params:default()
is executed, matron will confirm the file, eg:
pset >> read: /home/we/dust/data/<script_name>/<script_name>-02.pset
loading a specific PSET
To load a specific PSET, we’ll use params:read(x)
, where x
is either:
-
a PSET slot number, eg.
params:read(4)
will load the 4th PSET -
a PSET filename, eg.
params:read('/home/we/dust/data/<script_name>/<script_name>-03.pset')
will load the 3rd PSET file in<script_name>
’s data folder
getting the currently-loaded PSET’s name
You might have noticed that PSETs are indexed numerically. Though numeric indexing is helpful from a scripting perspective, every PSET is also given a name when saved, which can be queried using params.name
. This can be useful for displaying which PSET file is loaded on-screen, eg:
function redraw()
screen.clear()
screen.move(64,32)
screen.text_center('loaded PSET: '..params.name)
screen.update()
end
adjusting parameter attributes after creation
Any created parameter can be adjusted after it has been built by using params:lookup_param(index)
.
For example:
function init()
params:add{
type = 'control',
id = 'loop_start',
name = 'loop start',
controlspec = controlspec.def{
min = 0,
max = 10,
warp = 'lin',
step = 1/100,
default = 0,
units = 's',
quantum = 0.01
}
}
end
Using maiden’s command line, we can query the loop_start
parameter object:
>> tab.print(params:lookup_param('loop_start'))
action function: 0x44d328
t 3
allow_pmap true
name loop start
controlspec table: 0x425aa8
raw 0
save true
id loop_start
And we can further inspect the parameter’s controlspec
def:
>> tab.print(params:lookup_param('loop_start').controlspec)
wrap false
maxval 10
quantum 0.01
minval 0
step 0.01
warp table: 0x490218
units s
default 0
We can then modify the object with scripting (also, switching to params:add_control
to reduce verbosity):
function init()
params:add_option('loop_length','loop length', {'10 seconds','60 seconds'}, 1)
params:set_action('loop_length',
function(x)
if x == 1 then
params:lookup_param('loop_end').controlspec.maxval = 10
params:lookup_param('loop_end').controlspec.quantum = 1/100
elseif x == 2 then
params:lookup_param('loop_end').controlspec.maxval = 60
params:lookup_param('loop_end').controlspec.quantum = 1/600
end
end
)
params:add_control('loop_start', 'loop start', controlspec.new(0,10,'lin',1/100,0,'s',1/100))
params:add_control('loop_end', 'loop end', controlspec.new(0,10,'lin',1/100,10,'s',1/100))
end
While scripting parameters after the script is running can be tons of fun, it’s important to be mindful of the nuances of the object that you’re modifying!
In the above example, we want changes to the loop_end
parameter to delta +/- 0.1 seconds. When the controlspec’s maxval
is 10, our quantum
of 1/100 yields 0.1 second changes (since 10 * 0.01 is 0.1). But when we change our maxval
to 60, a quantum
of 1/100 means each delta will increment or decrement by 1/100 of the full 60 second range – this yields 0.6 second changes. So, we need to adjust the quantum
to 1/600
when the range is 60 seconds, and we need to return the quantum
to 1/100
when the range is 10 seconds.
Here’s another example:
function init()
color_options = {
{'Red','Yellow','Blue'}, -- color_options[1]
{'Orange', 'Green', 'Violet'}, -- color_options[2]
{'Red-Orange', 'Yellow-Orange', 'Yellow-Green', 'Blue-Green', 'Blue-Violet', 'Red-Violet'}, -- color_options[3]
}
params:add_option('colors', 'colors', color_options[1], 1)
params:add_option('color_set', 'color set', {'Primary','Secondary','Tertiary'}, 1)
params:set_action('color_set',
function(x)
local color_param = params:lookup_param('colors')
color_param.options = color_options[x]
color_param.count = #color_options[x]
color_param.selected = 1
end
)
end
Again, it’s important to be mindful of the nuances of the object that you’re modifying! For example:
-
if we don’t change the
color_param.count
, we can’t select past the third color in ourTertiary
color set -
if we don’t reset
color_param.selected
, then we will receive errors if our currently selected color is beyond the number of options in our new color set
When in doubt, use tab.print
to understand the parameter object you’re hoping to modify, or study the params source code.
description
Parameters are a fundamental component of the norns toolkit. They allow you to associate controls and data to variables and functions within your scripts. They offer MIDI mapping and OSC addresses, as well as state save and restore.
For more information about UI interactions with params, see the play docs.
For more information about scripting with params, see study 3: spacetime.