Link search Menu Expand Document

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:

  1. 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.
  2. 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
  3. 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 chimes
  • delays.length = 16 increases the length of the softcut delay length sequencer to sixteen steps
  • delays[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 the basket
  • math.random(x) generates a random number from 1 to x (in this case, 1 to the number of values in the basket)
  • basket[x] gets the xth element of the table (in our example, basket[1] would return 80)
  • 80 gets passed to engine.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 variable position
        • use position to select the current position of our notes sequins
        • query our notes sequins for the value at this random position and assign it to the temporary variable frequency
        • pass the frequency to our engine via engine.hz
      • pause for 1/10th of a second
    • after we perform the above for each count, then we pause for anywhere between 3 and 9 seconds

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 for engine.cutoff() on each wind chime
  • make K2 set delay feedback (softcut.pre_level(1,x)) to a random value between 0.2 and 0.99 (solution)

and then on to study 1: many tomorrows for a more in-depth scripting journey.

resources

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.