first light
Before study, learning to see.
sections
who am I
We have ideas about what it means to be a musician and what it means to be a programmer, and these ideas shape how we approach instruments and code.
norns is a platform for customizing and creating sound instruments with code.
While norns can be used to craft ambitious sonic toolkits, it can also be used to create small compositional moments.
This study aims to show the musician the power of utilizing and editing a few lines of code, and to show the programmer that sound can be explored playfully.
what will we use
norns is comprised of three primary layers:
- SuperCollider, which is where the qualities of a norns synth engine are defined. This can be a monophonic sine oscillator, a polyphonic supersaw, a sample player with dozens of slots, a 12-voice drum synth, etc.
- Lua, which is where control and interactions are defined:
- Lua can be used to define how a performer interacts with a SuperCollider engine, eg. changing synthesis parameters
- Lua can also be used to parse MIDI to/from external gear, eg. building a MIDI sequencer which doesn’t have a specific SuperCollider target
- Lua also defines + interprets the norns hardware, eg. what gets printed to the screen and what should happen when key 3 is pressed or encoder 2 is turned
- softcut, which is a multi-voice, variable-speed sample playback and recording system built into the norns environment, controlled by Lua. This is separate from (yet complementary to) what the SuperCollider layer provides. A script can choose to ignore SuperCollider entirely and rely solely on softcut to manipulate live or prerecorded audio. Alternatively, a script can route audio generated by SuperCollider into softcut, to resample and mangle the synth they’re controlling via Lua.
Depending on your interests and goals, you may find yourself compelled toward one facet of the norns environment more than another. For the purposes of this study, we’ll gently introduce each layer by showing you a few fun and immediate ways to leverage small bits of code to make music. Don’t worry if you feel like you don’t fully understand every coding concept presented – subsequent studies go into greater detail.
a short journey
To start, let’s get norns connected to wifi and open maiden. In the maiden project manager, search for firstlight in the base
collection at the top of the available
scripts, install it, then run it on your norns.
Sit, listen.
What you’re hearing:
- a wind chime synth (a SuperCollider engine named
PolyPerc
) - you can toggle the wind chime on/off with K3 (an interaction defined in Lua)
- the wind chime is running through a simple delay effect made with a single softcut voice
- the delay length is being sequenced by a mechanism you can toggle on/off with K2 (another interaction defined in Lua)
Also, you can also play sound into the audio input.
Let’s toggle off the wind chime synth (press K3), toggle off the softcut delay length sequencer (K2), and play with some code.
nb. If you are not running version 1.1.0 of the firstlight script, please update or reinstall the script fresh from maiden. Sometimes, browsers can aggressively cache scripts, so try clearing your cache if you find you cannot access version 1.1.0
the code is alive
In this section, we’ll execute small chunks of Lua code in realtime to control different elements of the firstlight script. The goal is to introduce a few fundamental commands and make typing musical ideas feel approachable and fun!
manually playing the engine
Click on the >>
bar at the bottom of maiden. This is the matron REPL, where we’ll enter commands and press ENTER to execute them. Throughout this document, anywhere you see >>
, that lets you know the code should be executed in the matron REPL (don’t type >>
in the actual code you execute, though!).
We encourage you to type every command – copying and pasting will not be as memorable / meaningful.
The script we’re running can manipulated in real time, and that’s what we’ll do now. Try executing this in the matron REPL:
>> engine.hz(700)
This Lua chunk tells the SuperCollider engine to play a 700hz tone, which will feed into the softcut delay. Try replacing 700
with different numbers, or try multiplying 700
by different values. If you enjoy exploring pitch relationships like this, you may also enjoy reading about music and math.
Since PolyPerc
(firstlight’s SuperCollider engine) is polyphonic, we can play many tones at once. To execute many commands in a single chunk in Lua, use semicolons:
>> engine.hz(700); engine.hz(700/2); engine.hz(700*2.4)
Try executing a few chords of your own devising!
engine.hz
is just one of the commands this SuperCollider engine will respond to. This means the SuperCollider code actually contains a definition for the hz
command, which is written to accept a single argument (700
or 432
or 641.2401
) as the pitch of the tone SuperCollider will produce.
To get a full list of the commands and their expected arguments, execute:
>> engine.list_commands()
which will return:
___ engine commands ___
amp f
cutoff f
gain f
hz f
pan f
pw f
release f
This list lets us know that we can append any of those commands to engine.
and change the synth, as long as we supply the expected argument, f
, which means a ‘floating-point number’. In Lua, all numbers are floating-point, so 3
and 3.0
are the same.
To make some changes, turn on the wind chimes (K3) and try executing these lines:
>> engine.pw(0.2)
>> engine.cutoff(300)
>> engine.release(2.1)
Next, try some of your own numbers for each of these commands’ arguments!
controlling softcut
As mentioned earlier in this doc, softcut
is a 6-voice digital tape system built into norns. firstlight uses a single voice of softcut to create the delay effect (specifically, voice 1) so let’s turn the wind chimes back on (press K3) and manipulate it a bit.
If you haven’t already turned off the softcut delay length sequencer (K2), please do – it’ll help make the effect of the following exercises clearer.
Turn the delay off (by reducing voice 1’s level to 0):
>> softcut.level(1, 0)
Turn the delay on (by raising voice 1’s level to 1):
>> softcut.level(1, 1)
Set the delay to quarter-volume:
>> softcut.level(1, 0.25)
In case it’s opaque, the general pattern we’re following is:
softcut.command(voice_id, command_value)
(You don’t need a space after the comma, it’s just a bit more readable for learning.)
The full range of softcut commands can be found in the softcut API. Let’s try some of the other commands!
The following command changes the feedback level to 95%:
>> softcut.pre_level(1, 0.95)
Careful, setting softcut.pre_level
to values greater than 1.0 ( = 100%) can eventually create very loud sounds!
Like a tape machine, we can change the speed of individual softcut voices to be faster or slower. In our current softcut-as-delay configuration, changing the rate means audio recorded at the old rate will be played back faster or slower – but as you continue to feed in audio, you’ll eventually notice that pitch remains the same while overall time changes. This is because the playhead and record heads are moving faster or slower together.
To hear what we mean, try executing these commands one at a time:
>> softcut.rate(1, 2.0)
>> softcut.rate(1, 1.0)
>> softcut.rate(1, 0.5)
You’ll notice there’s a bit of a pitch ramp when you change rate – this is determined by the rate_slew_time
command. firstlight starts with a default value of 1 second.
Try a few new values and see if you like something less or more dramatic:
>> softcut.rate_slew_time(1, 0)
>> softcut.rate_slew_time(1, 5.42)
>> softcut.rate_slew_time(1, 0.23)
a few more chunks
Let’s turn on the softcut sequencer without pressing the physical K2 button:
>> sequence = true
The softcut delay length sequencer is synchronized to the global clock. You can change the clock settings via the PARAMS menu, but you can also act upon the clock this way:
>> params:set('clock_tempo',50)
The script has few other simple variables that can be changed on the fly. What happens when you execute each of the following commands?
>> chimes = false
>> delays.length = 16
>> delays[1] = 13
*answers*
chimes = false
turns off the wind chimesdelays.length = 16
increases the length of the softcut delay length sequencer to sixteen stepsdelays[1] = 13
increases the size of the first delay sequencer column, which extends the length of the delay loop
make it so
Entering commands as we did above changes the running state of the script, but the system doesn’t remember these changes if you restart. So, let’s edit the actual script so we can load our customized version.
You will want to make a copy of the original file, which you can do in maiden:
- navigate to the
code > firstlight
folder in the file viewer (don’t see the file viewer? press the piece of paper on maiden’s left menu bar) - select the
firstlight.lua
file - press the
duplicate file
icon (the two pieces of paper) in the file viewer’s top menu bar - select the newly-created
firstlight1.lua
file - press the
rename file/folder
icon (the pencil) to rename it - select your newly-renamed file and press the
save script
icon (the floppy disk) on the far-right menu bar to save your new file
The script has a few built-in places where home-editing is effective, marked by this sweet lil’ friend:
--[[ 0_0 ]]--
Let’s change a few default values to customize:
- synth sound
- delay feedback
- chime notes
To change the startup synth parameters, see line 80:
-- configure the synth --[[ 0_0 ]]--
engine.release(1)
engine.pw(0.5)
engine.cutoff(1000)
If changes result in an error, don’t worry! The REPL will tell you which lines are troublesome so you can resolve errors and run the script again.
Try changing the arguments (the numbers between parenthesis), save the file, then re-launch the script using the PLAY arrow in maiden or using the menu on the hardware.
To change the delay feedback, see line 103:
softcut.pre_level(1, 0.85) --[[ 0_0 ]]--
Try changing the second argument, save the file, then re-launch the script using the PLAY arrow in maiden or using the menu on the hardware.
To change the chime notes, see line 28:
--[[ 0_0 ]]--
notes = sequins{400,451,525,555} -- a sequencer of note values, in hz
As the annotation suggests, notes
is a table of notes. Tables can contain many things, but in this case the table is a list of numbers separated by commas and enclosed by curly braces. The notes are frequencies, just like we called with engine.hz(700)
.
But there’s something a bit unusual about this table – it has the word sequins
in front of it!
sequins
sequins
is a norns library (which is a collection of specific functions, unique to the norns ecosystem) for building sequencers and arpeggiators with very little scaffolding, using Lua tables. It was originally designed by @trentgill
for use with crow and was imported to norns by @tyleretters
.
Conceptually, sequins
is similar to a basic step sequencer: we define the values we want to use and provide a mechanism to iterate through them.
Let’s build a sequins
on the command line to learn how this library works.
Give your new sequencer a name and assign it some values, eg:
>> angles = sequins{30,60,90}
By prepending our table of values with sequins
, we endow angles
with special abilities so it becomes both a table (a storage container) and a function (an action which produces a result). For example, to step through our values, we simply need to execute the action by writing the name of our sequins
with parenthesis after it, eg:
>> angles()
30
>> angles()
60
>> angles()
90
>> angles()
30
There are a lot of other ways to manipulate and use sequins
– check out the reference docs for more examples + details.
Back to the script: let’s try changing the Hz values of the notes
table at line 28. We can commit the change by either saving the script and re-running, or we can perform our live-execution gesture (CMD+RETURN / CTRL+ENTER) to dynamically modify this one line without having to re-run the entire script.
Feel free to add as many Hz values as you want – the chime player (the enclosing wind
function) will always check the table length before playing!
now differently
Now that we’ve successfully changed some of the default values, let’s make some small changes to alter how the script actually works.
clock by hand
Instead of having the softcut delay length sequencer run on a clock, let’s have it step forward every time we push K2.
First, we’ll need to make sure the sequence doesn’t autostart when the script is loaded. We can change this default on line 34 from:
sequence = true
to:
sequence = false
Now, when the script starts and the system clock ticks, the step()
function will not be run!
Let’s try executing step()
in the matron REPL, to confirm that it will advance the sequencer:
>> step()
please note: If you need to re-execute something you just executed in the matron REPL, press the up arrow on your keyboard to recall it and press ENTER to re-execute.
assign clocking to K2
Now, let’s assign the execution of step()
to K2.
As mentioned in the intro, all norns scripts contain definitions for how the hardware should behave. try searching your version of firstlight to find where the key
function is defined (hint).
To change what happens when K2 gets pressed, we’ll edit line 154 by commenting it out by typing two dashes in front of the line and adding our step
command line below:
--[[ 0_0 ]]--
-- sequence = not sequence
step()
please note: Two dashes in front of a line of code will comment-out the line and keep it from being executed.
We want to comment-out sequence = not sequence
because we don’t want a K2 press to toggle the sequencer on/off – instead, we want it to manually step the sequence.
Save and re-run. Now, pressing K2 advances the softcut delay length sequencer!
play a random pitch
Instead of relying on our wind chime mechanism, let’s employ K3 to play a random tone through our synth engine.
First, we’ll disable the chimes by changing line 35 to:
chimes = false
We’ll revisit the key
function to reassign K3’s action. Comment-out line 151 to un-assign the chimes toggle action from K3. After this line, add a command to play a random frequency between 100 and 600:
-- chimes = not chimes
engine.hz(math.random(100,600))
We want to comment-out chimes = not chimes
because we don’t want a K3 press to toggle the chimes on/off – instead, we want it to play our SuperCollider engine at a random pitch.
Save and re-run the script to try it out. Every time you press K3, you should hear a new tone!
advanced mods
these modifications increase in complexity, so make sure you feel comfortable with the core concepts of each before moving ahead.
randomly select a pitch
Let’s make a change so that K3 plays a random selection of discrete pitches from a table, instead of randomly selecting pitches across a range.
Instead of using a sequins
, let’s use a regular Lua table. Replace the engine.hz
line from the previous exercise with:
basket = {80,201,400,555,606}
engine.hz(basket[math.random(#basket)])
basket
can be any length, so feel free to add as many frequencies as you’d like, separating them by commas.
Breaking engine.hz(basket[math.random(#basket)])
down a bit, from the inside-out:
#basket
will return the number of values in thebasket
math.random(x)
generates a random number from1
tox
(in this case, 1 to the number of values in thebasket
)basket[x]
gets thex
th element of the table (in our example,basket[1]
would return80
)80
gets passed toengine.hz()
as an argument and the note gets played at 80hz!
curly braces, brackets, and parentheses
At this point, you might be wondering why we’re using so many different symbols in our code. Simply put, it depends on a variable’s data type (eg. is it a table or a function?) and what action we’re performing (eg. are we defining a table, or are we querying a specific value within the table, or are we executing a function with specific arguments?).
Here are a few helpful bits to know as you continue to experiment with the techniques covered in this study so far:
Create a table (curly braces):
>> my_table = {1,3,8,3,5,19,-42,0}
Query the table’s length (#
):
>> #my_table
8 -- this is what the REPL will return
Query the value of the table at a specific index (square brackets):
>> my_table[6]
19 -- this is what the REPL will return
Replace the value of the table at a specific index (square brackets):
>> my_table[8] = 900
Print the table using a function (parentheses):
>> tab.print(my_table)
-- this is what the REPL will return:
1 1
2 3
3 8
4 3
5 5
6 19
7 -42
8 900
Remove the third value of the table (parentheses):
>> table.remove(my_table, 3)
8 -- this is what the REPL will return
Generate a random value using a function (parentheses):
>> math.random()
>> math.random(9,12)
>> math.random(30)/10
Generate a random value and use it to replace the value of the table at a specific index (mixed):
>> my_table[2] = math.random(7)
Query the value of the table at a random index (mixed):
>> my_table[math.random(#my_table)]
even strum
Instead of a windy chime with variation, let’s have the wind make a regular strum.
Let’s take a look at the contents of the wind
function, starting at line 58:
wind = function()
while(true) do
light = 15
if chimes then
for i = 1,notes.length do
if math.random() > 0.2 then
local position = math.random(notes.length)
notes:select(position)
local frequency = notes()
engine.hz(frequency)
end
clock.sleep(0.1)
end
end
clock.sleep(math.random(3,9))
end
end
What the inner bit does:
- while the
wind
clock is running: light
is set to full-bright (the wind lines on the screen)- if the
chimes
are enabled, then…- count from 1 to the length of our
notes
sequins. for each count, we’ll perform the following actions:- make a random value between 0.0 and 1.0 (which
math.random()
with no arguments will return / provide us) - if that random value is greater than 0.2 then…
- create a random value between 1 and the length of our
notes
sequins and assign this value to the temporary variableposition
- use
position
to select the current position of ournotes
sequins - query our
notes
sequins for the value at this random position and assign it to the temporary variablefrequency
- pass the
frequency
to our engine viaengine.hz
- create a random value between 1 and the length of our
- pause for 1/10th of a second
- make a random value between 0.0 and 1.0 (which
- after we perform the above for each count, then we pause for anywhere between 3 and 9 seconds
- count from 1 to the length of our
This creates the nice random scattered effect and creates uneven timing, with a random selection of notes each time.
We used clock.sleep
above, which allows us to specify an amount of seconds we’d like the clock to pause for until it performs the next action. Let’s make things more regularly spaced by replacing the wind
mechanism with a bpm-synced approach:
wind = function()
while(true) do
light = 15
if chimes then
for i = 1,notes.length do
local frequency = notes()
engine.hz(frequency)
clock.sync(1)
end
end
clock.sync(4)
end
end
Try changing the strum speed by altering the clock.sync
value in our for
loop (1 = one beat):
clock.sync(1/4)
Try changing the sync value between strums by editing clock.sync(4)
to another value.
sequence weirder
The sequencer step values can be used for any number of things. Instead of modulating the softcut delay length, let’s modulate the delay rate, which will re-pitch the delay line wildly.
See line 52:
softcut.loop_end(1,delays()/8)
- delays
- a
sequins
(line 25) that gets updated with the knob interface. it's the sequencer data, which is basically up to 16 steps of values 1-8 - loop_end
- we're dividing the step value by 8, so we'll set loop_end setting to between 1/8 and 1.0 (= 8/8)
Let’s comment out this line and modulate rate
instead:
--softcut.loop_end(1, delays()/8)
softcut.rate(1,delays()/8)
Save and try it out!
Since the rate jumps are very large the result is substantial. Let’s try making it more subtle:
softcut.rate(1,1+(delays()/128))
This confines the numbers to a smaller range for a subtler effect. Perhaps we’d like to try something with multiples. Let’s create a rate_seq
sequins, perhaps around the others at line 25:
rate_seq = sequins{-1.0,-0.5,0.25,0.5,1.0,2.0,4.0}
Now, we can change line 52 to:
softcut.rate(1,rate_seq())
Using sequins
, we assigned rate_seq
7 values, letting us map the range of each step. This set of numbers contains a bunch of octaves which can create sparkly-delays. It also contains negative numbers which make for some nice reversals.
Try setting softcut.rate_slew_time(1,0)
down around line 98 (the line might’ve shifted up or down during edits), which will make rate changes instantaneous rather than sliding.
We also can reverse the direction of rate_seq
:
>> rate_seq:step(-1)
from here
suggested exercises:
- make a
sequins
forengine.cutoff()
on each wind chime - make K2 set delay feedback (
softcut.pre_level(1,x)
) to a random value between0.2
and0.99
(solution)
and then on to study 1: many tomorrows for a more in-depth scripting journey.
resources
- script reference - lists of norns API commands and how to use them
- firstlight walkthrough - a recorded livestream of a firstlight study walkthrough
continued
- part 0: first light
- part 1: many tomorrows // variables, simple maths, keys + encoders
- 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.