Link search Menu Expand Document

streams

norns studies part 5: system polls, OSC, file storage

sections

numerical superstorm

So far we’ve explored many ways of pushing data around towards musical ends: metronomes and clocks, spreadsheet-like tables, MIDI and grids, even typing commands directly. There is more.

Polls report data from the audio subsystem, such as amplitude envelope and pitch detection. To see the available polls, execute:

>> poll.list_names()

You’ll see:

--- polls ---
amp_in_l
amp_in_r
amp_out_l
amp_out_r
cpu_avg
cpu_peak
pitch_in_l
pitch_in_r
------

These are the basic system-wide polls. Engines may add their own polls – for example, softcut adds polls for buffer playback position.

Let’s set a poll to track the amplitude of the left input:

>> p = poll.set("amp_in_l")
>> p.callback = function(val) print("in > "..string.format("%.2f",val)) end
>> p.time = 0.25
>> p:start()

Play some sound into the left input and you’ll see the printed numbers change with the sound level. Try changing p.time to update the poll interval.

Bonus: when we want numbers displayed a specific way we can use string.format. Here we use the format string "%.2f" which means we always want two decimals shown. See the printf reference for more formatting methods.

To stop the poll:

>> p:stop()

We can also request a single immediate value from the poll:

>> p:update()

Of course instead of just printing out the value of the poll, we should be using it for something more interesting and musical. We’ll do that in the example in the end, but first let’s smash together even more data.

numbers through air

Open Sound Control (OSC) is a network protocol for sending messages supported by numerous sound and media applications. OSC also is how Lua communicates with SuperCollider within the norns ecosystem.

OSC messages look like this:

/cutoff 500

The first part, /cutoff, is the path. A series of values and/or strings can come after the path which is the data. In this way, an OSC message can be somewhat self-describing: we could assume the message above is to set the cutoff to 500.

We can use OSC in our scripts to interface with the outside world via WiFi. For the following example you’ll need to be connected to hotspot or a network. First let’s receive a message from Max/MSP:

norns listens to OSC on port 10111.

This simple max patch sends the message /hello 42. Note that you’ll need to change the udpsend box to match your norns’ IP address (which you can find in the SYSTEM menu).

If you’ve gone through the previous studies:

  • open your uniquely-named study folder in the maiden file browser
  • create a new file in your study folder: 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
If you haven't gone through the previous studies
  • create a new folder in the code directory: click on the code 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)
  • 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 5
-- code exercise
-- numbers through air

function osc_in(path, args, from)
  if path == "/hello" then
    print("hi!")
  elseif path == "/x" then
    x = args[1]
  elseif path == "/y" then
    y = args[1]
  elseif path == "/xy" then
    x = args[1]
    y = args[2]
  else
    print(path)
    tab.print(args)
  end
  print("osc from " .. from[1] .. " port " .. from[2])
end

osc.event = osc_in

Executing the message from Max will print the following to maiden’s REPL:

hi!
osc from 192.168.0.109 port 60092

In Max, try sending OSC messages with /x and /y as paths and a single number as data. Path /xy will accept two numbers and set both values. This is how we map OSC paths to functionality within our script!

Notice that we can also extract the address (from[1]) and port (from[2]) of the sender. The receiving port will typically be different, so if your OSC client doesn’t allow receiving port definition, check its ports.

Let’s send a message back to Max from our script:

Above we set up a receive port on 10101. Here’s how we send to it:

>> dest = {"192.168.1.12",10101}
>> osc.send(dest, "/soup", {1,10})

dest is the destination we’re sending to, so change the IP address to match the address where you received the messages earlier. The second argument is the path, followed by a table (curly brackets) with the data. Please note that even if you want to send a single value, it still has to be inside a table!

If everything is set up correctly, you should see /soup 1. 10. appear in the message box in Max.

norns is also auto-discoverable as an OSC device. For example, using TouchOSC is very straightforward as “norns” should show up in the config list if both are connected to the same network.

long term number storage

There will come a time when you have collected too many numbers, and they are precious and you want to save them for later. norns has a filesystem that can store a ton of numbers. Here’s the easy way:

>> my_secret_bits = {2,-1,21,0}
>> tab.save(my_secret_bits, _path.data.."secret.txt")

tab.save is a function which saves a table to disk. We specify the file as secret.txt inside the folder _path.data (which is a global for /home/we/dust/data/, see more in the reference).

Let’s now load the same file to a different table:

>> summoned_bits = tab.load(_path.data.."secret.txt")

A quick check via tab.print(summoned_bits) will show that the read was successful:

1	2
2	-1
3	21
4	0

Let’s do some more complex file operations. Here’s how you get a folder listing:

>> listing = util.scandir(_path.home)
>> tab.print(listing)

You’ll see something resembling this:

1    bin/ 
2    dust/
3    maiden/
4    norns/
5    norns-image/
6    update/
7    changelog.txt
8    version.txt

util.scandir takes one argument which is a folder path, and then it returns a table the folder contents. Too see how file loading works, let’s load one of these files and print it out:

-- study 5
-- long term number storage

function print_file(filepath)
  local f=io.open(filepath,"r")
  if f==nil then
    print("file not found: "..filepath)
  else
    f:close()
    for line in io.lines(filepath) do
      -- this is where you would do something useful!
      -- but for now we'll just print each line
      print(line)
    end
  end
end

Let’s test it:

>> folder = _path.home
>> listing = util.scandir(folder)
>> print_file(folder.."/"..listing[7])

The file changelog.txt should be printed! Stepping through the print_file function:

  • argument is a file with path
  • checks if the file exists
  • uses a for loop to iterate on each line of the file

Writing a file is not much more complex:

f=io.open(_path.data .. "other_test.txt","w+")
f:write("dear diary,\n")
f:write("10011010\n")
f:close(f)

example: streams

Putting together concepts above. This script is demonstrated in the video up top.

Here’s a Max patch that uses a pictslider object for 2D control. This script is also compatible with the TouchOSC “simple” template.

-- streams
-- norns study 5
--
-- KEY2 - clear pitch table
-- KEY3 - capture pitch to table
--
-- OSC patterns:
-- /x i : noise 0-127
-- /y i : cut 0-127
-- /3/xy f f : noise 0-1 cut 0-1

engine.name = 'PolySub'

collection = {}
last = -1

function init()
  screen.level(4)
  screen.aa(0)
  screen.line_width(1)

  params:add_control("shape","shape", controlspec.new(0,1,"lin",0,0,""))
  params:set_action("shape", function(x) engine.shape(x) end)
  params:add_control("timbre","timbre", controlspec.new(0,1,"lin",0,0.5,""))
  params:set_action("timbre", function(x) engine.timbre(x) end)
  params:add_control("noise","noise", controlspec.new(0,1,"lin",0,0,""))
  params:set_action("noise", function(x) engine.noise(x) end)
  params:add_control("cut","cut", controlspec.new(0,32,"lin",0,8,""))
  params:set_action("cut", function(x) engine.cut(x) end)
  params:add_control("fgain","fgain", controlspec.new(0,6,"lin",0,0,""))
  params:set_action("fgain", function(x) engine.fgain(x) end)
  params:add_control("cutEnvAmt","cutEnvAmt", controlspec.new(0,1,"lin",0,0,""))
  params:set_action("cutEnvAmt", function(x) engine.cutEnvAmt(x) end)
  params:add_control("detune","detune", controlspec.new(0,1,"lin",0,0,""))
  params:set_action("detune", function(x) engine.detune(x) end)
  params:add_control("ampAtk","ampAtk", controlspec.new(0.01,10,"lin",0,1.5,""))
  params:set_action("ampAtk", function(x) engine.ampAtk(x) end)
  params:add_control("ampDec","ampDec", controlspec.new(0,2,"lin",0,0.1,""))
  params:set_action("ampDec", function(x) engine.ampDec(x) end)
  params:add_control("ampSus","ampSus", controlspec.new(0,1,"lin",0,1,""))
  params:set_action("ampSus", function(x) engine.ampSus(x) end)
  params:add_control("ampRel","ampRel", controlspec.new(0.01,10,"lin",0,1,""))
  params:set_action("ampRel", function(x) engine.ampRel(x) end)
  params:add_control("cutAtk","cutAtk", controlspec.new(0.01,10,"lin",0,0.05,""))
  params:set_action("cutAtk", function(x) engine.cutAtk(x) end)
  params:add_control("cutDec","cutDec", controlspec.new(0,2,"lin",0,0.1,""))
  params:set_action("cutDec", function(x) engine.cutDec(x) end)
  params:add_control("cutSus","cutSus", controlspec.new(0,1,"lin",0,1,""))
  params:set_action("cutSus", function(x) engine.cutSus(x) end)
  params:add_control("cutRel","cutRel", controlspec.new(0.01,10,"lin",0,1,""))
  params:set_action("cutRel", function(x) engine.cutRel(x) end)
  params:bang()

  engine.level(0.02)

  pitch_tracker = poll.set("pitch_in_l")
  pitch_tracker.callback = function(x)
    if x > 0 then
      table.insert(collection,x)
      engine.start(#collection,x)
      last = x
      redraw()
    end
  end
end

function key(n,z)
  if n==2 and z==1 then
    engine.stopAll()
    collection = {}
    last = -1
    redraw()
  elseif n==3 and z==1 and #collection < 16 then
    pitch_tracker:update()
  end
end

local osc_in = function(path, args, from)
  if path == "/x" then
    params:set_raw("noise",args[1]/127)
  elseif path == "/y" then
    params:set_raw("cut",args[1]/127)
  elseif path == "/3/xy" then
    params:set_raw("noise",args[1])
    params:set_raw("cut",1-args[2])
  else
    print(path)
    tab.print(args)
  end
end

osc.event = osc_in

function redraw()
  screen.clear()
  if last ~= -1 then
    screen.move(0,10)
    screen.text(#collection .. " > " .. string.format("%.2f",last))
  else
    screen.move(128,10)
    screen.text_right("...")
  end
  for i,y in pairs(collection) do
    screen.move(4+(i-1)*8,60)
    screen.line_rel(0,-(8 * (math.log(collection[i]))-30))
    screen.stroke()
  end
  screen.update()
end

continued

  • 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
  • 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.