Norns: code review

thanks so much! to be honest, it’s not enough dissonance for me. have been trying to think of more playable ways to do key changes with the midi keyboard. the simplest would be to make the black keys something like the secondary dominate of of the white keys, but it doesn’t seem like it would be very intuitive. if you want to experiment, i tried to set up the function for key_change to support modulation by any number of semitones! (the code is looking so janky to me now, really need to find some time to start the next version…)

Here’s my first addition to the review. It should work fine on your norns.

Suggestions would be appreciated. Still studying though the many examples you all have provided in dust. Much to learn.

-- strum v0.52
--
-- tap grid pads to change pitch
-- double tap to add a rest
--
-- ENC 1: adjusts direction
-- ENC 2: adjusts tempo
-- ENC 3: sets scale
-- KEY 3: pauses/restarts
-- MIDI : transposes key
--
-- synth params can be changed
--
-- based on norns study #4
--
-- @carvingcode (Randy Brown)
--

engine.name = 'KarplusRings'

local cs = require 'controlspec'
music = require 'mark_eats/musicutil'
beatclock = require 'beatclock'

version = "v0.5"
name = ":: strum :: "

steps = {}
playmode = {"Onward","Aft","Sway","Joy"}
playchoice = 1
position = 1
transpose = 0
direction = 1
k3_state = 0

mode = math.random(#music.SCALES)
scale = music.generate_scale_of_length(60,music.SCALES[mode].name,8)

clk = beatclock.new()
clk_midi = midi.connect()
clk_midi.event = clk.process_midi

--
-- setup
--
function init()

    for i=1,16 do
        table.insert(steps,1)
    end
    grid_redraw()
    redraw()

    clk.on_step = handle_step
    clk.on_select_internal = function() clk:start() end
    clk.on_select_external = function() print("external") end
    clk:add_clock_params()

    params:add_separator()

    cs.AMP = cs.new(0,1,'lin',0,0.5,'')
    params:add_control("amp", "amp", cs.AMP)
    params:set_action("amp",
        function(x) engine.amp(x) end)

    cs.DECAY = cs.new(0.1,15,'lin',0,3.6,'s')
    params:add_control("damping", "damping", cs.DECAY)
    params:set_action("damping",
        function(x) engine.decay(x) end)

    cs.COEF = cs.new(0.2,0.9,'lin',0,0.2,'')
    params:add_control("brightness", "brightness", cs.COEF)
    params:set_action("brightness",
        function(x) engine.coef(x) end)

    cs.LPF_FREQ = cs.new(100,10000,'lin',0,4500,'')
    params:add_control("lpf_freq", "lpf_freq", cs.LPF_FREQ)
    params:set_action("lpf_freq",
        function(x) engine.lpf_freq(x) end)

    cs.LPF_GAIN = cs.new(0,3.2,'lin',0,0.5,'')
    params:add_control("lpf_gain", "lpf_gain", cs.LPF_GAIN)
    params:set_action("lpf_gain",
        function(x) engine.lpf_gain(x) end)

    cs.BPF_FREQ = cs.new(100,4000,'lin',0,0.5,'')
    params:add_control("bpf_freq", "bpf_freq", cs.BPF_FREQ)
    params:set_action("bpf_freq",
        function(x) engine.bpf_freq(x) end)

    cs.BPF_RES = cs.new(0,3,'lin',0,0.5,'')
    params:add_control("bpf_res", "bpf_res", cs.BPF_RES)
    params:set_action("bpf_res",
        function(x) engine.bpf_res(x) end)

    params:bang()

    clk:start()

end

--
-- each step
--
function handle_step()
    --print(playmode[playchoice])
    if playmode[playchoice] == "Onward" then
        position = (position % 16) + 1
    elseif playmode[playchoice] == "Aft" then
        position = position - 1
        if position == 0 then
            position = 16
        end
    elseif playmode[playchoice] == "Sway" then
        if direction == 1 then
            position = (position % 16) + 1
            if position == 16 then
                direction = 0
            end
        else
            position = position - 1
            if position == 1 then
                direction = 1
            end
        end
    else
        position = math.random(1,16)
    end

    if steps[position] ~= 0 then
        vel = math.random(1,100) / 100 -- random velocity values
        --print(vel)
        engine.amp(vel)
        engine.hz(music.note_num_to_freq(scale[steps[position]] + transpose))
    end
    grid_redraw()
end

--
-- norns keys
--
function key(n,z)
    --if n == 2 and z == 1 then
    --prompt = "key 2 pressed"
    --end
    if n == 3 and z == 1 then
        --prompt = "key 3 pressed"
        if k3_state == 0 then
            clk:stop()
            g.all(0)
            g.refresh()
            k3_state = 1
        else
            clk:start()
            k3_state = 0
        end
    end
    --if z == 0 then
    --prompt = "key released"
    --end
    redraw()
end

--
-- norns encoders
--
function enc(n,d)
    if n == 1 then          -- sequence direction
        playchoice = util.clamp(playchoice + d, 1, #playmode)
        print (playchoice)
    elseif n == 2 then      -- tempo
        params:delta("bpm",d)
    elseif n == 3 then      -- scale
        mode = util.clamp(mode + d, 1, #music.SCALES)
        scale = music.generate_scale_of_length(60,music.SCALES[mode].name,8)
    end
    redraw()
end

--
-- norns screen display
--
function redraw()
    screen.clear()
    screen.move(44,10)
    screen.level(5)
    screen.text(name)
    screen.move(0,20)
    screen.text("---------------------------------")
    screen.move(0,30)
    screen.level(5)
    screen.text("Path: ")
    screen.move(30,30)
    screen.level(15)
    screen.text(playmode[playchoice])
    screen.move(0,40)
    screen.level(5)
    screen.text("Tempo: ")
    screen.move(30,40)
    screen.level(15)
    screen.text(params:get("bpm").." bpm")
    screen.move(0,50)
    screen.level(5)
    screen.text("Scale: ")
    screen.move(30,50)
    screen.level(15)
    screen.text(music.SCALES[mode].name)
    --screen.move(64,60)
    --screen.level(2)
    --screen.text(":: carvingcode ::")
    screen.update()
end

--
-- grid functions
--
g = grid.connect()

g.event = function(x,y,z)
    --print(x,y,z)
    if z == 1 then
        if steps[x] == y then
            steps[x] = 0
        else
            steps[x] = y
        end
        grid_redraw()
    end
    redraw()
end

function grid_redraw()
    g.all(0)
    for i=1,16 do
        if steps[i] ~= 0 then
            for j=0,7 do
                g.led(i,steps[i]+j,i==position and 12 or (2+j))
            end
        end
    end
    g.refresh()
end

--
-- midi functions
--
k = midi.connect(1)
k.event = function(data)
    local d = midi.to_msg(data)
    if d.type == "note_on" then
        transpose = d.note - 60
    end
end
5 Likes

Hello everyone… I’m trying to get my head around norns engines.

First off - I should say I HAVE NO IDEA WHAT I’M DOING
But… my usual technique of “copy-and-paste, then rewrite stuff until it works” seems to be working ok so far. :grin:

So - here’s an engine based on one of the f0plugins Ugens by fredrik olofsson. I’ve done one for SN76489 and Atari2600 so far. Atari2600 engine copied here for review.

EDIT - I should mention - I used PolyPerc as a template and changed a couple things around.

Would anyone with more sc/norns-engine experience give me comments on how this looks so far?

if you want to try this I have compiled the f0plugin ugens here.

6 Likes

This is great - all working good here.

Some ideas

  • Not sure how to transpose works but via the parameters section so I can assign midi CC would be great. Being able to use a slider to switch transpose would be handy
  • Some sort of pattern saving so you can manually switch between patterns - I would look at the takt app in the bedtime folder as that has pattern chaining and a song mode in there on different pages. Or better still, check the Kria app in the Junklight folder, which is 4 channels with different pages of sequences and patterns. This has individual transpose per step not global. Both use a pattern selection across the top row.
  • midi out would be useful also.
  • I would clamp the upper range on bpf-freq and mainly the bpf-res to avoid it self oscillating - you get a nasty clicks that can blow your speakers when both are set high together and the audio cuts out all together
1 Like

Thanks for feedback. Will look into these. Especially clamp to avoid speaker damage. KarplusRings is not my work. Will need to investigate.

Edit:

Per suggestions, I made adjustments to the parameters to be less extreme. See below, but I also updated them in the full script posted above. Please let me know your thoughts.

cs.AMP = cs.new(0,1,'lin',0,0.5,'')
    params:add_control("amp", "amp", cs.AMP)
    params:set_action("amp",
        function(x) engine.amp(x) end)

    cs.DECAY = cs.new(0.1,15,'lin',0,3.6,'s')
    params:add_control("damping", "damping", cs.DECAY)
    params:set_action("damping",
        function(x) engine.decay(x) end)

    cs.COEF = cs.new(0.2,0.9,'lin',0,0.2,'')
    params:add_control("brightness", "brightness", cs.COEF)
    params:set_action("brightness",
        function(x) engine.coef(x) end)

    cs.LPF_FREQ = cs.new(100,10000,'lin',0,4500,'')
    params:add_control("lpf_freq", "lpf_freq", cs.LPF_FREQ)
    params:set_action("lpf_freq",
        function(x) engine.lpf_freq(x) end)

    cs.LPF_GAIN = cs.new(0,3.2,'lin',0,0.5,'')
    params:add_control("lpf_gain", "lpf_gain", cs.LPF_GAIN)
    params:set_action("lpf_gain",
        function(x) engine.lpf_gain(x) end)

    cs.BPF_FREQ = cs.new(100,4000,'lin',0,0.5,'')
    params:add_control("bpf_freq", "bpf_freq", cs.BPF_FREQ)
    params:set_action("bpf_freq",
        function(x) engine.bpf_freq(x) end)

    cs.BPF_RES = cs.new(0,3,'lin',0,0.5,'')
    params:add_control("bpf_res", "bpf_res", cs.BPF_RES)
    params:set_action("bpf_res",
        function(x) engine.bpf_res(x) end)

Hi! This is my first Norns script - it’s called “Bletchley Park.”

This script started out as a straightforward Turing Machine clone in Max4Live about 8 months ago, then gradually evolved as I learned Marbles and ported the script to Norns. Now, it’s a 2-voice, 16-step shift register, with adjustable loop length and repetition, and independent probabilistic triggers per voice.

I put together a quick demo on IG:


The big design difference vs Marbles are:

  1. It’s a true shift register, which adds a smoothness to the generated notes
  2. The registers are displayed in their entirety at all times, no hidden state
  3. The 2 voices have fully independent controls

The quirky/different thing is that it uses 3-bit integer math throughout, which maps very well onto a 16 x 8 grid - each step holds a value between 1 and 8, which can be displayed in 8 rows or in compactly in 3. I mostly use MIDI out with CVpal but it does have an internal PolyPerc engine also. More details on how it works are in the code comments.

I’m feeling OK about the code, although I’m not sure how much I’ll need to change for Norns 2.0 - the thing I’m most confused about is pattern/preset storage, but I’d appreciate any style tips, totally new to Lua and I haven’t worked in a dynamic language in a while.

Most of all, I’d be most interested in a “design review” - I’m really new to the Monome ecosystem, and I’m not sure what obvious best practices I’m missing. In particular, I’m not sure if the horizontal-fader UI approach makes sense, is legible at all, or is too dense. I’d actually like to add a lot more features, but don’t know how to fit them in - more pages, contextual/button combos, something else?

Also really curious how other folks are handling MIDI vs built-in sounds - is it worth it to support both, or better to decide on one or the other?

Finally, huge thanks to @dan_derks and @andrew for giving me early feedback and moral support on this, as well as for being inspirational artists and instrument designers.

16 Likes

agh thanks R ! it’s sounding really lovely so far. this generative bit math stuff goes way over my head but I can’t wait to try it out someday

2 Likes

I’ve followed your Insta posts for the past several days and have been intrigued. Will explore your code over the next couple.

You mention wanting to explore some ‘how to best do things’ ideas: I might suggest taking a look at the “loom” script in the 'mark_eats" folder. He handles saving/loading presets, paging, and a lot of other interesting things which may be of interest.

Great work.

1 Like

You can sneak a look at syntax changes here. mostly it’s quick/easy changes to metro and grid functions.

4 Likes

thanks

missed this in another thread

anybody willing to glance at my minor tweak of a script by @Tyler ??
(hopefully not stepping on their toes…just wanted to try updating a small app for 2.0)

4 Likes

will check this out later, I love ekombi!

Love cranes still! Any idea how difficult it would be to add a display of the current time of the buffer? Say start is at 2 seconds and end is at 8. It would be cool to know where in that span the play head is at.

edit : not sure where is the best place to post this, please move as needed

there is a poll for current phase of each softcut voice, arbitrarily quantized

cool, thanks! I’ll have to see if I can poke around and display it.

I had somewhat missed Traffic! It sounds gorgeous! Thanks @carvingcode for directing me to it. @ypxkap Will you update it for norns 2.0? Lovely lovely work.

4 Likes

thanks! it’s up for 2.0 over here—

something is broken in my hacky mod of the traffic speeds, tbd if it should be fixed or removed. really want to redo the sequencer and integrate the arc in…

3 Likes

(I hope this is still the right place for this post… last one was March 19.)

Is this the best way to detect when encoders stop?

I added the visual indicators to make it easier to grok.

3 Likes

got another puzzle if anyone can spare some cycles for me on this fine friday:

-- say we have a metatable Foo...
Foo = {}

function Foo:new()
  local f = setmetatable({}, { __index = Foo })
  f.items = {}
  bar_mixin.init(self)
  return f
end

-- and we have this concept items that mixins register:
function Foo:register_mixin_item(key, value)
  self.items[key] = value
end

-- which we then want to return dynamically with the key
function Foo:get_item(key)
  return self.items[key]
end

-- ...that has the bar "mixin"
bar_mixin = {}

bar_mixin.init = function(self)

  self.register_mixin_item("baz", self:get_baz()) -- (issue is here)

  self.fnord = 1337

  self.get_baz = function(self)
    return self.fnord * 2
  end

end

-- the issue
foo = Foo:new()
foo:get_item("baz") --[[

### SCRIPT ERROR: load fail
/home/we/dust/code/dev/dynamic.lua:25: attempt to call a nil value (method 'get_baz')

]]

(EDIT was missing a return f in the init())

seems like this should work. do i need to use assert() or load() or something with string concatenation? i need to be able to call functions from strings.

i’m not 100% following but i think you’re calling .get_baz before it’s defined, no ?

ohh are you trying to send the function into the Foo object ? in that case flip the order so the function is defined but send in the variable for the function w/o calling it —> self.get_baz