many tomorrows
norns studies part 1: variables, simple maths, keys + encoders
sections
terminology
Before we dive in, here is some terminology which is mentioned throughout this study:
- function: a named task or event in our script. Calling a function simply means executing or invoking that task or event. If we are defining a function, we use
end
at the end of our definition. If we are calling a function, we use parenthesis after its name to indicate a function call – we can pass arguments (see next) to the function inside of the parenthesis.- argument: functions can be written to have general knowledge of the other components of our script, or we can ask it to perform its task on specific supplied values. Supplying these values is referred to as ‘passing arguments’ into the function call. In
print("hello")
,print
is the function and"hello"
is the argument specifying what we’d like to print.
- argument: functions can be written to have general knowledge of the other components of our script, or we can ask it to perform its task on specific supplied values. Supplying these values is referred to as ‘passing arguments’ into the function call. In
- string: a sequence of characters, like every word in this sentence. A sequence of characters becomes a string when its enclosed in single quotes eg.
'hello'
, double quotes eg."hello"
, or double brackets eg.[[hello]]
. We will only use double and single quotes throughout these studies, but please be sure not to mix + match –'hello"
is not a valid string. - variable: a named storage container for data that our script can manipulate. We declare a variable and assign it a value by using a convention like
my_favorite_number = 11
. While our script runs, it will remember that our favorite number is 11. Variables can store numbers, strings, functions, and tables (which we’ll talk about in the next study). For those with prior coding experience, it might be interesting to learn that variables in Lua are, by default, global – later on, we’ll cover local vs. global variables.
hello?
Hello. Ready? Remember to stay hydrated.
First, locate yourself thus:
- connect to norns via hotspot or network
- navigate web browser to http://norns.local (or type in IP address if this fails)
- you’re looking at maiden, the editor
- create a new folder in the
code
directory: click on thecode
directory and then click the folder icon with the plus symbol to create a new folder- name your new folder something meaningful, like
my_studies
(only use alphanumeric, underscore and hyphen characters when naming)
- name your new folder something meaningful, like
- create a new file in the folder you created: locate and click on the folder and then click the + icon in the scripts toolbar
- rename the file: select the newly-created
untitled.lua
file, then click the pencil icon in the scripts toolbar- after naming it something meaningful to you (only use alphanumeric, underscore and hyphen characters when naming), select the file again to load it into the editor
The file is blank. Full of possibilities. Type the text below into the editor:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
print("the end and the beginning they are the same.")
end
Click save script (the disk icon in the upper right) and then run script (the play icon just below that).
If the code has been typed correctly and your levels are up, you’ll hear a sine tone.
what happened really
The top three lines are comments. In Lua a comment begins with two dashes. When scripting for norns, the very top comments of the main script file are special in that they are displayed as the preview text in the script selector. Try it:
- press K1 get into the norns menu
- navigate to the SELECT / SYSTEM / SLEEP screen
- press K2 on SELECT
- find your file, select it with K3
You can type as many lines at the top of a script as you want, but note that the norns screen isn’t so wide, so keep your lines short. It’s helpful to write a description about your script and list the controls. But so far this does basically nothing, so we wrote basically nothing.
But we did do one important thing! We selected an engine on line 4:
engine.name = "TestSine" -- don't run this code, it's just a reminder
This line loaded the TestSine
engine – note that this needs to be in quotes (single or double work, but don’t mix + match). This is an imperative first step.
Then, we created the init
function, which gets called at script startup (this is true of every script). Right now all it does is print. But where does it print?
robot hangout
The COMMAND LINE (aka REPL: Read-Eval-Print Loop) is where information about the currently-running script is printed. It’s the window below the editor. Messages from matron (the Lua component of norns) will appear here – you can scroll back to see what it had to say.
At the bottom is a >>
prompt which is where you can type in commands (aka command line).
Type this in and then press ENTER on your keyboard:
>> print("hello.")
…and you’ll see hello.
printed in response. norns has excellent manners.
Being able to print to the REPL is incredibly helpful, despite this being possibly the most boring intro to such an astoundingly interesting music machine.
Let’s do something even more useful now:
>> engine.list_commands()
Which will print this to the REPL:
engine.list_commands()
___ engine commands ___
amp f
hz f
<ok>
This is useful information! It lets us know that the TestSine
engine responds to two commands:
amp
: amplitudehz
: frequency
It also lets us know that both accept float (decimal) values – that’s what the f
next to each command indicates. For example, we can change the frequency of the sine by executing this in the REPL:
>> engine.hz(200)
Now is a good time to point out the UP ARROW on your keyboard. When typing into the command line use the up arrow to see the previous things you typed in. This makes rapid-changing of the frequency much easier.
Also try changing the amplitude by executing engine.amp(x)
, where x
is a value between 0.0 to 1.0 (you can certainly give it large values and it’ll happily clip).
Back to the script – if we want to have the engine start up with a particular frequency, we add it to the init
function:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
Try it. It works!
numbers and strings
Back to the command line. Let’s create some variables:
>> coins = 4
>> spell = "heal party"
We just made two variables. The first assigned the number 4 to the variable coins
. The second assigned a string to the variable spell
. You can easily confirm that it worked with:
>> print(coins)
This prints just the number to the REPL.
During debugging, you’ll find it helpful to make more informative prints by using string concatenation, which just means gluing strings together. You can glue strings together with the Lua operator ..
(two periods):
>> print("i will cast " .. spell .. " for " .. coins .. " coins.")
We can also glue commands together in the REPL, to execute many at once, using ;
(semicolon):
>> engine.hz(300); print(coins)
(Lua has a huge string library.)
maths
All of the standard arithmetic operators are available:
>> coins = 99 -- creates a variable 'coins' and assigns it 99
>> coins = coins + 1 -- now coins = 100
>> coins = coins - 10 -- now coins = 90
>> coins = coins * 2 -- now coins = 180
>> coins = coins / 4 -- now coins = 45
>> coins = coins % 2 -- now coins = 1
Modulus (%
) is perhaps unusual. It gives the remainder after a division. so: 11 % 10
would equal 1. We can use this as a trick for confining values to a range, say:
>> x = x % 10
For whatever value of x, it will be wrapped to the range 0-9.
And let’s just make sure you know about this early on:
>> coins = math.random(100)
This assigns coins
a random value up to 100. But also:
>> engine.hz(math.random(10)*50+100)
use the hardware
So far all of our interaction has been through the command prompt. This is a good way to demonstrate some basic syntax, but the point of norns is interaction – three keys and three encoders can create a lot of fun.
Let’s add a keypress function to our script, after the init()
function:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
print("key " .. n .. " == " .. z)
end
Save and rerun. Then push some keys and you’ll get some prints. You’ll notice that:
n
is the key numberz
is the key state – down (1) or up (0)
Let’s modify the keypress function in our script to do something more engaging:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
end
end
While running the above, we hear a 200hz tone when we press K3 and a 100hz tone when we release K3. This is all worked out in our key
function, where we send (100 + 100 * z)
to our engine – the value of z
depends on whether K3 is pressed (z
= 1
) or released (z
= 0
).
When we press K3, z
is equal to 1
, which sends (100 + 100 * 1)
to our engine.
When we release K3, z
is equal to 0
, which sends (100 + 100 * 0)
to our engine.
controlling flow
Code is basically a definition of flow – what should happen when and how? In a lot of ways, code is like a musical score.
In our code above, we’ve used an if
statement. if
statements test a condition and then do stuff if the condition is true:
if (condition) then
(do stuff)
end
In our code, we used n == 3
as the condition, which checked to see if the key number was equal to 3.
Other comparison operators include:
==
(is equal)~=
(not equal)>
(greater than)<
(less than)>=
(greater or equal)<=
(less than or equal)
if
statements can also be expanded with elseif
and else
, for example try changing our script’s key
function to:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
elseif n == 2 then
engine.hz(300 + 175 * z)
else
engine.hz(200 + 300 * z)
end
end
Save and re-run the script, then press K3 with or without holding K2.
all of the fun
To get data from the encoders let’s add to the end of the script:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
elseif n == 2 then
engine.hz(300 + 175 * z)
else
engine.hz(200 + 300 * z)
end
end
function enc(n,d)
print("encoder " .. n .. " == " .. d)
end
Here, d
is delta. The encoders report incremental steps of a turn: clockwise is positive, counterclockwise is negative.
make the robots mad
Let’s make a mistake on purpose.
What if we wanted to accumulate encoder turns, to keep track of an absolute position? We might try replacing the enc
function:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
elseif n == 2 then
engine.hz(300 + 175 * z)
else
engine.hz(200 + 300 * z)
end
end
function enc(n,d)
if n == 3 then
position = position + d
print("encoder 3 at position: " .. position)
end
end
Try it in your script, then save and run. Upon turning the encoder you’ll get an error!
lua: /home/we/dust/code/study/study1-manyfutures.lua:19: attempt to perform arithmetic on a nil value (global 'position')
stack traceback:
/home/we/dust/code/study/study1-manyfutures.lua:19: in function 'encoders.callback'
/home/we/norns/lua/encoders.lua:56: in function 'encoders.process'
(your line number may be different, but the error is the same)
We made a small mistake.
what is nil?
From Programming in Lua: “Lua uses nil as a kind of non-value, to represent the absence of a useful value.”
Revisiting our error, we see: attempt to perform arithmetic on a nil value (global 'position')
- we tried to perform arithmetic on a non-value
- the non-value was named
position
Look back at our code – did we ever establish a starting value for position
? No. Since we can’t meaningfully add a void to a number, we receive an error.
To get around this error, we need to establish that position
indeed exists. We can do that by adding a line inside of init
which gives position
a default value:
function init()
position = 10
engine.hz(100)
print("the end and the beginning they are the same.")
end
Now we can try that adding that new enc
function:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
position = 10
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
elseif n == 2 then
engine.hz(300 + 175 * z)
else
engine.hz(200 + 300 * z)
end
end
function enc(n,d)
if n == 3 then
position = position + d
print("encoder 3 at position: " .. position)
end
end
clamping down the fun
Though the encoders are endless, we’ll sometimes want changes made by the encoders to be clamped to a specific range. This is where special utilities built for norns come in, specifically one called util.clamp()
, which accepts three arguments:
n
: valuemin
: minumummax
: maximum
Let’s try it in our script by replacing our enc
function with a clamped version:
-- study 1
-- code exercise
-- hello
engine.name = "TestSine"
function init()
position = 10
engine.hz(100)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
elseif n == 2 then
engine.hz(300 + 175 * z)
else
engine.hz(200 + 300 * z)
end
end
function enc(n,d)
if n == 3 then
position = util.clamp(position + d,-20,20)
print("encoder 3 at position: " .. position)
end
end
Now when we turn encoder 3, we cannot go outside of -20 to 20.
make some more mistakes
To acclimate ourselves to issues, let’s make a few more common mistakes:
>> print("we like to party"
- returns:
<incomplete>
- translation: “typo. no closing parenthesis.”
>> print("we like to party')
- returns:
<incomplete>
- translation: “typo. don’t switch between single and double quotations in a single string. use pairs of one or the other.”
>> math.random(0.7)
- returns:
lua: stdin:1: bad argument #1 to 'random' (number has no integer representation)
- translation: “this function requires an integer and 0.7 is not an integer”
function key(n,z)
if n == 3 then
engine.hz(100 + 100 * z)
end
- returns:
'end' expected (to close 'function' at line 10)
- translation: “the function you started on line 10 does not have a closing
end
line, so we don’t know if you’re done defining the function”
Be sure to keep an eye on the REPL for errors.
example: many tomorrows
Putting together the concepts above, this script is demonstrated in the video up top.
-- many tomorrows
-- norns study 1
--
-- KEY 2 toggle sound on/off
-- KEY 3 toggle octave
-- ENC 2 randomize amplitude
-- ENC 3 change frequency
--
-- first turn on AUX reverb!
engine.name = "TestSine"
function init()
sound = 1
level = 1
octave = 1
f = 100
position = 0
engine.hz(f)
print("the end and the beginning they are the same.")
end
function key(n,z)
if n == 2 then
if z == 1 then
-- trick below to toggle between 0 and 1
sound = 1 - sound
engine.amp(sound * level)
end
elseif n == 3 then
octave = z + 1
engine.hz(octave * f)
end
end
function enc(n,d)
if n == 2 then
level = math.random(100) / 100
engine.amp(sound * level)
elseif n == 3 then
position = (position + d) % 11
f = 100 + position * 50
engine.hz(octave * f)
end
end
addendum: go global
This section is written for those with prior coding experience, to whom the question of ‘local’ versus ‘global’ variables in norns and Lua is curiosity-inducing. If this doesn’t describe you, that’s okay! Feel free to power through, or simply dog-ear this for later.
Variables in Lua are global by default. This means that the variable is able to be referenced and used throughout the script. Using the local
keyword makes a variable visible only to the scope within which it’s declared.
To see what we mean, execute this in the REPL:
>> local hidden_spell = "foobarbaz"
You’ll find that you can’t access hidden_spell
in the REPL:
>> print(hidden_spell)
…returns nil
as its output because the REPL only has access to global variables.
Locals declared within functions are only visible to that function (including init
):
function init()
local hidden_spell = "foobarbaz"
cast()
end
function cast()
print(hidden_spell)
end
…will print nil
to the REPL, because hidden_spell
is local to the init
function only.
Locals declared outside of functions are available to other functions within the file:
function init()
cast()
end
local hidden_spell = "foobarbaz"
function cast()
print(hidden_spell)
end
… will print foobarbaz
to the REPL, because hidden_spell
is local to the entire script since we declared it outside of any specific function.
When scripting, global variables make it incredibly easy to troubleshoot from the REPL. Besides a few very specific phrases (see the system global variable list), you should feel safe using globals in your scripts. Each time a new script runs, the previously-declared global namespace is wiped, so there’s no risk of cross-influence.
Declaring locals is often a matter of taste, but also utility + legibility:
engine.name = "TestSine"
function init()
sound = 1
level = 1
f = 650
engine.hz(f)
end
function enc(n,d)
if n == 2 then
local last_level = level
level = math.random(100) / 100
if level ~= last_level then
engine.amp(sound * level)
elseif level == last_level then
-- level didn't change, so change note
f = 650 / math.random(2,4)
engine.hz(f)
end
end
end
reference
norns-specific
engine.list_commands()
– list available engine commands (prints to command line)print(x)
– print value ofx
to REPLinit()
– function which executes at script loadkey(n,z)
– function to parse norns keypressesenc(n,d)
– function to parse norns encoder movement
general
math.random()
– generates random values, see this tutorial for detailed usageif
,else
, andelseif
– conditional statements, see Lua docs for detailed usage<
,>
,==
, etc – relational operators, see Lua docs for detailed usage
continued
- part 0: first light // learning to read and edit code
- part 1: many tomorrows
- part 2: patterning // screen drawing, for/while loops, tables
- part 3: spacetime // functions, parameters, time
- part 4: physical // grids + MIDI
- part 5: streams // system polls, OSC, file storage
- further: softcut studies // a multi-voice sample playback and recording system built into norns
community
Ask questions and share what you’re making at llllllll.co
Edits to this study welcome, see monome/docs.