norns: scripting

ok, thank you. i’ll experiment.

1 Like

I haven’t done this exactly, but I would probably go about it like this now:

softcut.event_phase(function update_positions(i,x)
 -- keep track of voice (i) current position (x) and last position
 -- if not reversing and current position < last position, then its looped
 -- if reversing and current position > last position, then its looped
 -- set last position to current position
end)
softcut.poll_start_phase()

in oooooo I actually just keep track of time (duration playing loop / loop duration) which works well for recording but wouldn’t use generally for playback (it is a missing feature actually in oooooo, which I might add soon so loop resets can trigger other loops without having to quantize to the clock).

@zebra - I’m interested in implementing something like this for softcut! like via a callback that gets fired when the loop turns over. just finished the Stroustrup book and took a look at softcut to get a feel for how to do it. if its within scope I’m happy to give it a try. however, I go back and forth thinking that maybe the phase poll (like above) is just well suited to do it too.

2 Likes

thanks @infinitedigits! i’ll give this a try.

EDIT: after much head scratching I got this working for loops going forwards and backwards. thanks again for the help!

EDIT 2: at the moment, the only time the approach isn’t working is when I have voices going in opposite directions but I imagine with a little thought/assistance or a lot of random code generation I can get this to work.

1 Like

I also go back and forth. Implementing it on crone side is basically simple. But there are edge cases and limitations that are not so great:

  • updates happen at most once per audio block. So no gain in timing resolution over phase poll…

  • on the other hand, loops in softcut can be arbitrarily small (1 sample…) Of course we can’t fire OSC packets and callbacks that fast, and even once per block would likely overwhelm the network stack and lead to dropped commands, borked timing, and possible soft locks of norns ui

I tend to think in practice it will work just as well to take a phase poll and look for sign changes in delta(phase)… And at the same time script author can choose appropriate quantum for the application… But i have not actually tried building on this assumptions, maybe it would be onerous. And certainly can imagine times when you want to act on loop points, but an appropriately small phase poll quantum would be exorbitant. (this last possibility would be the main motivation to adding a dedicated OSC trigger from the audio process.)

2 Likes

the warp option is for changing the scaling of one or both axes, which is probably not what you want. what you probably do want if you’re using Graph.new is to set style to spline.

under the hood this uses screen.curve to connect points by bezier splines. screen.arc may also be of interest to you.

and finally, if you do want to just generate 128 rendered points directly, look into interpolation methods. specifically, interpolation by cubic polynomials

-- interpolate between y1 and y2, x in [0, 1]
function interp_cubic(x, y0, y1, y2, y3)
-- 4-point, 3rd-order hermite interpolant:
-- [[    
-- all steps:
            local c0 = y1
            local c1 = 0.5 * (y2 - y0)
            local c2 = y0 - 2.5 * y1 + 2. * y2 - 0.5 * y3
            local c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2)
            return ((c3 * x + c2) * x + c1) * x + c0
--]]
-- inlined:
            return (((0.5 * (y3 - y0) + 1.5 * (y1 - y2)) * x + (y0 - 2.5 * y1 + 2. * y2 - 0.5 * y3)) * x + 0.5 * (y2 - y0)) * x + y1
end

or maybe by cosines:

-- interpolate between y0 and y1, x in [0, 1]
function interp_cos(x, y0, y1) 
    local f = math.cos((x+ 1) * math.pi) * 0.5 + 0.5
    return y0 + (y1 - y0) * f
end
2 Likes

– original post –
Flora attempts to extend Graph to create a class called ArbitraryGraph that allows shapes with an arbitrary number of curve segments. Flora uses this to create multisegment envelopes (see Envelope.lua for those details).

I’m not sure if this is useful to you, but just wanted to point out another example that leverages @markeats super awesome code.

– new post on a new topic –
I am writing a new script that includes rerouting the norns’ connection graph and I’d like to share the results of my (somewhat meager) explorations.

Rerouting norns connections has been discussed in a couple of other places:

The secret sauce @dan_derks is referring to is found in @infinitedigits stonesoup script. This script forms much of the foundation for the script I’m writing. Combining @infinitedigits ‘secret sauce’ with the suggestion given by @zebra in the first quote above, I’ve been able to create a parameter that provides three options for how the audio connections to SuperCollider:in are connected:

  1. audio in + softcut output → supercollider
  2. audio in → supercollider
  3. softcut output → supercollider
Here's the code:
rerouting_audio = false
function route_audio()
  clock.sleep(0.5)
  local selected_route = params:get("audio_routing")
  if rerouting_audio == true then
    rerouting_audio = false
    if selected_route == 1 then -- audio in + softcut output -> supercollider
      --print(1)
      os.execute("jack_connect crone:output_5 SuperCollider:in_1;")  
      os.execute("jack_connect crone:output_6 SuperCollider:in_2;")
      os.execute("jack_connect softcut:output_1 SuperCollider:in_1;")  
      os.execute("jack_connect softcut:output_2 SuperCollider:in_2;")      
    elseif selected_route == 2 then --just audio in -> supercollider
      --print(2)
      os.execute("jack_connect crone:output_5 SuperCollider:in_1;")  
      os.execute("jack_connect crone:output_6 SuperCollider:in_2;")
      os.execute("jack_disconnect softcut:output_1 SuperCollider:in_1;")  
      os.execute("jack_disconnect softcut:output_2 SuperCollider:in_2;")
    elseif selected_route == 3 then -- just softcut output -> supercollider
      --print(3)
      os.execute("jack_disconnect crone:output_5 SuperCollider:in_1;")  
      os.execute("jack_disconnect crone:output_6 SuperCollider:in_2;")
      os.execute("jack_connect softcut:output_1 SuperCollider:in_1;")  
      os.execute("jack_connect softcut:output_2 SuperCollider:in_2;")        
    end
  end
end

params:add{
  type = "option", id = "audio_routing", name = "audio routing", 
  options = {"in+cut->eng","in->eng","cut->eng"},
  min = 1, max = 3, default = 1,
  action = function(value) 
    rerouting_audio = true
    clock.run(route_audio)
  end
}

Also, anyone doing similar rewiring in a script should reset the connections to their defaults in the script’s cleanup function, like:

Cleanup
function cleanup ()
  os.execute("jack_disconnect softcut:output_1 SuperCollider:in_1;")  
  os.execute("jack_disconnect softcut:output_2 SuperCollider:in_2;")
  os.execute("jack_connect crone:output_5 SuperCollider:in_1;")  
  os.execute("jack_connect crone:output_6 SuperCollider:in_2;")
end

This code was just written in the past couple of hours and hasn’t yet been tested super extensively but it appears to do what I was intending.

8 Likes

I’m new to coding and attempting to modify scripts, and am having trouble understanding when to make a function/variable global vs. local. It seems like scripts don’t like it when I try to use local functions in init or with a metro, but I don’t really understand what’s going on, I just change things to global when Maiden tells me it’s getting nil for a global value. Hoping someone can point me in the right direction. Thanks!

Without reading your scripts I can only recommend reading into the concept of scoping.
LUA is a bit weird in that variables are global by default, unless explicitly declared local witch the local keyword.
The key to understanding is getting a feeling for the blocks and their border.
Examples: a local declared outside init but above init, will be known inside init.
A local declared after init, will not be known inside init.
A local declared inside a function above init, will not be known inside init.
And so on.

1 Like

Is there any reason not to put a bunch of local functions before init?

1 Like

maybe it would help to think about global & local as the “lifespans” of a function or variable. local variables “die” once they hit the end statement of the block1 they were declared in, while global variables last the entire lifespan of your program.

use local variables when you only need a variable for a short time (useful because you can reuse the name of the variable in another block). use global variables when you have a value that you’ll be using throughout & across different sections of the program (the name of any global variable will be “taken” so you shouldn’t create another variable with the same name)

no reason not to ! but globals are useful at the top of the script because they can be accessed from the repl or other lua files included into your script.


1(a block is a function, loop or if/else statement - they always end in end)

3 Likes

Hello! Random question. Trying to get the “mods” menu setup on my Norns for a few of the really handy sounding ones. But I’m running into a little bit of trouble with the initial setup. Is there a how to guide anywhere?

Here’s the link for reference.

hi hi! hope all’s well :slight_smile:

this is a beta feature + is still being developed, so no official docs yet. if you’re looking to script some mods, there’s active conversation on the Mod Ideas thread in the norns study discord + if you can identify reproducible issues please share them to the norns github.

if you're looking to utilize the current community mods in beta, here are a few setup steps -- hope this helps!

community mods:

using mods

  • install a mod via typical ;install github_url_of_mod_you_want_to_install method for non-maiden-catalog-scripts
  • installed mods will populate under SYSTEM > MODS
    • for uninitiated mods, E3 CW on the mod name will add a + to the right of the mod, which indicates it’s queued for load-up on the next restart
  • active mods will have a to their left
    • E3 CCW will add a - to the right of the mod, which indicates it’s queued for deactivation on the next restart
  • use the RESTART action on norns or execute ;restart in matron’s REPL to restart and activate/deactivate the queued mods
1 Like

Awesome! For some reason when I did

;install

The mod menu doesn’t show under system on the Norns screen. Is there anything I specific I need to do to enable that??

ah, gotcha — apologies, that part of the instructions is for actually installing mods once you’re on the beta (for the adventurous: highlight SYSTEM > UPDATE, hold K1 and press K3 and you’ll see the beta). stuff is still in progress, tho, so come on by the discord channel and we’ll get ya sorted! :slight_smile:

2 Likes

Attempting to use the pattern library, got everything working smoothly with one pattern, then tried to add a second pattern and am having a bug I can’t figure out. I can record a pattern, and then play through the pattern once, but when it hits the end of the pattern I get this error:

lua: /home/we/norns/lua/lib/pattern_time.lua:128: attempt to perform arithmetic on a nil value (field
 '?')
stack traceback:
        /home/we/norns/lua/lib/pattern_time.lua:128: in function 'pattern_time.next_event'
        /home/we/norns/lua/lib/pattern_time.lua:21: in field 'event'
        /home/we/norns/lua/core/metro.lua:165: in function </home/we/norns/lua/core/metro.lua:162>

I’m having trouble telling what’s going wrong, as the stack doesn’t traceback into my code. The 2 patterns have separate initializers, but their processes both refer to the same function.

Here is the full code that’s causing this: https://github.com/evannjohnson/orison/blob/1b0b6fdb364be9ebbcdf099a32ec75daaeda44bb/isohel.lua

@Zop , happy to take a peek but repo is set to private :sweat_smile:

(also, icymi and it helps: pattern_time | monome/docs)

1 Like

i was trying to look as well! didn’t know what was happening lol

1 Like

Whoops! Fixed. Thanks

edit: I feel silly - on lines 212 and 219, I was checking if pattern.rec == 2 instead of == 1.

1 Like

they are just control changes, so nothing too special is needed. you just need to know the NRPN numbers you’re interested in, split up your 14-bit values and follow the spec.

i haven’t tested this on hardware but this is what i would try.

-- @function set_nrpn: set a 14-bit NRPN value
-- @param dev: a MIDI device
-- @param num: an NRPN number
-- @param val: an NRPN value
-- @param ch: MIDI channel
function set_nrpn(dev, num, val, ch)
    dev:cc(0x63, num >> 7,   ch); -- NRPN MSB
    dev:cc(0x62, num & 0x7f, ch); -- NRPN LSB
    dev:cc(0x06, val >> 7,   ch); -- Data Entry MSB
    dev:cc(0x26, val & 0x7f, ch); -- Data Entry LSB 
    -- not required by MIDI spec, but good practice:
    -- set the active NRPN number to zero/null when we are done
    -- (this avoids unintended value changes from later use of the device) 
    dev:cc(0x63, 0, ch);
    dev:cc(0x62, 0, ch);
end

-- @function inc_nrpn: increment NRPN by 1
-- @param dev: a MIDI device
-- @param num: an NRPN number
-- @param ch: MIDI channel
function inc_nrpn(dev, num, val, ch)
    dev:cc(0x63, num >> 7,   ch); -- NRPN MSB
    dev:cc(0x62, num & 0x7f, ch); -- NRPN LSB
    dev:cc(0x60, 0, ch);          -- Data Increment
    dev:cc(0x63, 0, ch);
    dev:cc(0x62, 0, ch);
end

-- @function dec_nrpn: decrement NRPN by 1
-- @param dev: a MIDI device
-- @param num: an NRPN number
-- @param ch: MIDI channel
function dec_nrpn(dev, num, val, ch)    
    dev:cc(0x63, num >> 7,   ch); -- NRPN MSB
    dev:cc(0x62, num & 0x7f, ch); -- NRPN LSB
    dev:cc(0x61, 0, ch);          -- Data Decrement
    dev:cc(0x63, 0, ch);
    dev:cc(0x62, 0, ch);
 end

[mod note: i might move this thread to norns:scripting or something.)

6 Likes

I am not sure if this is the right place to ask, but is it possible to use 2 grids simultaneously in one script? And of yes, how would one do this? Thanks in advance!