Link

spacetime

norns studies part 3: functions, parameters, time

we function together

so far we’ve seen a few ways to run commands:

  • the command line, for single lines
  • the init function which is run at startup of a script
  • enc and key functions which are executed when you touch an encoder or key

i just said function a few times, and you may have seen in study 1 we didn’t explain the first word of function init(). a function is a block of code that can be called and conditionally accept and return parameters. simplest example:

function greeting()
  print("hello there!")
end

in the command line you can type greeting() and this function will run, printing hello there!. but this is a silly example. functions are useful when you have some code you need to run frequently or perhaps from different places. they are very good for organizing and making your scripts readable and reusable. a better example:

function midi_to_hz(note)
  local hz = (440 / 32) * (2 ^ ((note - 9) / 12))
  return hz
end

it’s just a bunch of math that you likely don’t want to remember. but we introduce a couple of things:

  • we pass an argument to the function, which is the variable note
  • we make a local variable called hz, do some math with note for the conversion
  • we return hz

let’s use it:

drone = midi_to_hz(30)

here’s what happens:

  • midi_to_hz is called with a value of 30, which is assigned to note
  • the function runs and returns a value which is assigned to drone

zoom out

but where do you put the function definition? check this out:

-- spacetime
-- norns study 3

engine.name = "PolyPerc"

function init()
  engine.amp(0.5)
end

function key(n,z)
  local whatever = 30 + math.random(24)*2
  engine.hz(midi_to_hz(whatever))
end

function midi_to_hz(note)
  local hz = (440 / 32) * (2 ^ ((note - 9) / 12))
  return hz
end

we simply put our function at the bottom. when you execute the script, the entire file is processed. so even though the key function comes earlier and references midi_to_hz(), everything is fine because now the whole global scope knows about midi_to_hz.

you’ll also see that we did a little shortcut with function calling. you can nest them:

drone = midi_to_hz(60)
engine.hz(drone)

is the same as:

engine.hz(midi_to_hz(60))

many to many

functions can have many arguments:

function stack_notes(root, interval, number)
  local note = root
  for i=1,number do
    engine.hz(midi_to_hz(note))
    note = note + interval
  end
end

this function takes three arguments: a root note, a note interval, and a number of notes. using a loop it plays a stack of notes. try this:

stack_notes(40,7,6)

it’ll play these 6 midi notes, which start at 40 and increment by 7 each time: 40 47 54 61 68 75. but what if we leave off an argument, ie stack_notes(40,7)? you’ll get an error when the function tries to use nil as number in the loop. in functions we can define a default value like this:

function stack_notes(root, interval, number)
  number = number or 4
  interval = interval or 7
  note = root or 50
  ...

now you can even call stack_notes() and you’ll get something much more pleasant than an error. the number = number or 4 trick is the same as if number == nil then number = 4 end.

functions can also return many values:

function whereami()
  local a = math.random(128)
  local b = math.random(64)
  return a,b
end

and the get the values:

x,y = whereami()
x,y,z = whereami()
x = whereami()

in the second line z will be nil. in the third line the second returned value is thrown away.

try this:

function redraw()
  screen.clear()
  screen.level(15)
  screen.move(whereami())
  screen.text("here!")
  screen.update()
end

then enter/exit menu mode.

tangle and detangle

lua lets us easily make functions that point at other functions. observe:

function happy()
  engine.hz(midi_to_hz(60))
  engine.hz(midi_to_hz(64))
  engine.hz(midi_to_hz(67))
end

function sad()
  engine.hz(midi_to_hz(60))
  engine.hz(midi_to_hz(63))
  engine.hz(midi_to_hz(67))
end

function key(n,z)
  if z == 1 then
    go = happy
  else
    go = sad
  end
  go()
end

the trick happens in the key function. see how go is getting reassigned to one of the other functions? for the puzzle lovers let’s make it even more complicated:

feelings = {sad,happy}

function key(n,z)
  feelings[z+1]()
end

what? it’s a table of functions! why would you want to do this??

the default way of thinking about decisions is perhaps to make a big if-else statement:

function key(n,z)
  if z == 1 then
    happy()
  elseif z == 0 then
    sad()
  end
end

makes sense, totally readable. but what if your program could change itself while it ran?

feelings[2] = sad

(oh no!! always sad now!)

but really. imagine adding some complexity to these functions, having more of them, and design a process where they dynamically inform one another.

pause. really consider the possibilities, and i hope your mind explodes a tiny bit. this is why programming in a musical context is so incredibly powerful and interesting.

parameters

managing numbers is of primary concern. we usually want them to stay in a certain range and behave a certain way, and this means typically making a lot of repetitive code. but we’ve created some structures to help you keep your numbers together and scripts looking clean:

params:add_number("tempo","tempo",20,240,88)

we just created an entry in the default parameter set which is called params. this is the parameter set that you see in the system menu under PARAMETERS. go there now and you’ll see “tempo” which you can change with the menu interface. here’s what we did:

  • parameter id = “tempo”
  • name = “tempo”
  • minimum = 20
  • maximum = 240
  • default = 88

min, max, and default are all optional. we could add an unbounded number parameter that defaults to 0 by simply writing params:add_number("anything"). the name field is required.

besides editing in the menu, let’s do some things with code:

params:set("tempo", 110)
print("tempo is " .. params:get("tempo"))
params:delta("tempo", -100)
print("tempo is now " .. params:get("tempo"))

note the colon (:) for the parameter functions (these are class functions). the lines above did some pretty handy things:

  • set the value
  • get the value
  • delta the value

that last delta we did was clamped into the range, so you’ll that the final tempo value is 20 (which is the minimum we specified above).

usually when a parameter changes we want something to happen. what if we could automatically call a function whenever a parameter changed, via set or delta?

function print_bpm_to_ms(bpm)
  print(bpm .. " bpm is a " .. 60/bpm .. "second interval.")
end

params:set_action("tempo", function(x) print_bpm_to_ms(x) end)

indeed indeed! now whenever the value of tempo is touched you’ll be informed of the interval time. the value x is the value of the parameter, and we pass it to the print_bpm_to_ms() function.

there’s a shortcut to make parameter creation more readable:

params:add{type="number", id="tempo", min=20, max=240, default=88,
  action=function(x) print_bpm_to_ms(x) end}

note that we’re using a new syntax style with curly brackets. this passes a table to the function which creates the new parameter. we’re able to specify the attribute names (ie, min, max which makes it more readable, in addition to specifying the action in the same line. you can use either method, but this way is generally recommended.

more sound please

add more parameters with multiple lines of params:add_number(), but all parameters are not just basic numbers. there is a control parameter that maps a “control” range (think 0-100) to a specified min/max, with linear and exponential scaling. we use these frequently with engine parameters:

params:add_control("cutoff","cutoff",controlspec.new(50,5000,'exp',0,555,'hz'))

the third argument of the params:add_control function is a control spec. we use controlspec.new() to create a new spec with arguments:

  • min = 50
  • max = 5000
  • curve = exp (can also be lin)
  • step = 0
  • default = 555
  • unit = hz (for printing)

if we wanted to define this control spec once and then assign it to many controls, we could do it like this:

filter_cs = controlspec.new(50,5000,'exp',0,555,'hz')
params:add_control("f1","f1",filter_cs)
params:add_control("f2","f2",filter_cs)
params:add_control("f3","f3",filter_cs)

it is easy to then directly attach the parameter to an engine parameter:

params:set_action("cutoff", function(x) engine.cutoff(x) end)

now we can use the system menu to directly change an engine parameter. or much more fun, include it in our own script function:

function enc(n,d)
  if n == 3 then
    params:delta("cutoff",d)
  end
end

with this short few lines we have an exponential, range-limited parameter control! and what’s more:

params:write("later.pset")

this saves the current values of the parameter set to disk, in the file later.pset under /dust/data/. you can similarly load with:

params:read("later.pset")
params:bang()

after a read you’ll want to call params:bang() which will activate every parameter’s action function.

lastly, we’ve been using the default parameter set throughout, but we can create as many of our own parameter sets as desired. they won’t be hooked up to the system menu, but we can manipulate them in all of the same ways. here’s how to create one:

others = paramset.new()
others:add_number("first","My First Parameter")

this creates a new parameter set called others and adds a number.

it’s about time

until now we haven’t considered time. how do we become aware of time?

util.time()

this returns something like: 1529498027.7441. it’s the system time, which is useful as a marker. now we will measure the length of a key press:

down_time = 0

function key(n,z)
  if n == 3 then
    if z == 1 then
      down_time = util.time()
    else
      hold_time = util.time() - down_time
      print("held for " .. hold_time .. " seconds")
    end
  end
end

try it out. press key 3, you’ll get a time measurement. we can use something like this to create a tap-tempo, by sampling the time interval between key-downs, storing those values in a table, and averaging the values.

time again

in addition to using keys and encoders to trigger functions, we can also make time-based metronomes which trigger functions.

function init()
  position = 0
  counter = metro.init()
  counter.time = 1
  counter.count = -1
  counter.event = count
  counter:start()
end

function count(c)
  position = position + 1
  print(c .. "> " .. position)
end

this init function creates a metro called counter:

  • set interval time to 1 (second)
  • set count to -1, which means never stop. (we could set this to a target number to auto-stop).
  • set the event function (like a param action function).
  • start the metronome counting. (note this is a class function, use a colon).

on each tick of the counter, the count function is executed. the value c is the stage of the metro. we create a position variable which is counted up. try the following one by one on the command line:

counter.time = 0.5
position = 0
counter:stop()
counter.count = 5
counter:start()

this demonstrates how we’re able to manipulate counter on the fly. here’s a quick script that creates a simple ascending strum pattern:

-- strum

engine.name = "PolyPerc"

function init()
  strum = metro.init(note, 0.05, 8)
end

function key(n,z)
  if z == 1 then
    strum:stop()
    root = 40 + math.random(12) * 2
    engine.hz(midi_to_hz(root))
    strum:start()
  end
end

function note(stage)
  engine.hz(midi_to_hz(root + stage * 5))
end

function midi_to_hz(note)
  return (440 / 32) * (2 ^ ((note - 9) / 12))
end

we use a shortcut for initializing the metro by putting the event function, interval time, and number of stages in the metro.init() function arguments. when we push any key down, a random root note is selected and played and then the metro is started. it will trigger 8 times, and on each function call we will sound a new note that is 5 semi-tones above the previous note. try modulating the metro interval, number of stages, and stage multiplier! for example, change 8 to 1 (for a single note) and 5 to 12 (for an octave shift).

example: spacetime

putting together concepts above. this script is demonstrated in the video up top.

-- spacetime
-- norns study 3
--
-- ENC 1 - sweep filter
-- ENC 2 - select edit position
-- ENC 3 - choose command
-- KEY 3 - randomize command set
--
-- spacetime is a weird function sequencer.
-- it plays a note on each step.
-- each step is a symbol for the action.
-- + = increase note
-- - = decrease note
-- < = go to bottom note
-- > = go to top note
-- * = random note
-- M = fast metro
-- m = slow metro
-- # = jump random position
--
-- augment/change this script with new functions!

engine.name = "PolyPerc"

note = 40
position = 1
step = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
STEPS = 16
edit = 1

function inc() note = util.clamp(note + 5, 40, 120) end
function dec() note = util.clamp(note - 5, 40, 120) end
function bottom() note = 40 end
function top() note = 120 end
function rand() note = math.random(80) + 40 end
function metrofast() counter.time = 0.125 end
function metroslow() counter.time = 0.25 end
function positionrand() position = math.random(STEPS) end

act = {inc, dec, bottom, top, rand, metrofast, metroslow, positionrand}
COMMANDS = 8
label = {"+", "-", "<", ">", "*", "M", "m", "#"}

function init()
  params:add_control("cutoff","cutoff",controlspec.new(50,5000,'exp',0,555,'hz'))
  params:set_action("cutoff", function(x) engine.cutoff(x) end)
  counter = metro.init(count, 0.125, -1)
  counter:start()
end

function count()
  position = (position % STEPS) + 1
  act[step[position]]()
  engine.hz(midi_to_hz(note))
  redraw()
end

function redraw()
  screen.clear()
  for i=1,16 do
    screen.level((i == edit) and 15 or 2)
    screen.move(i*8-8,40)
    screen.text(label[step[i]])
    if i == position then
      screen.move(i*8-8, 45)
      screen.line_rel(6,0)
      screen.stroke()
    end
  end
  screen.update()
end

function enc(n,d)
  if n == 1 then
    params:delta("cutoff",d)
  elseif n == 2 then
    edit = util.clamp(edit + d, 1, STEPS)
  elseif n == 3 then
    step[edit] = util.clamp(step[edit]+d, 1, COMMANDS)
  end
  redraw()
end

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

function midi_to_hz(note)
  return (440 / 32) * (2 ^ ((note - 9) / 12))
end

function randomize_steps()
  for i=1,16 do
    step[i] = math.random(COMMANDS)
  end
end

reference

  • metro class is in http://norns.local/doc (must be connected to norns)
  • util (also in the docs) includes many other helper functions, such as util.clamp

continued

  • part 1: many tomorrows // variables, simple maths, keys + encoders
  • part 2: patterning // screen drawing, for/while loops, tables
  • part 3: spacetime
  • part 4: physical // grids + midi
  • part 5: streams // system polls, osc, file storage

community

ask questions and share what you’re making at llllllll.co

edits to this study welcome, see monome/docs