@spunoza can’t wait to try this out, looks great

ok i promise to figure out how to use github soon, but in the meantime here is my new take on sequencing key changes in a musical way. i’ve stripped down loom (thanks mark!) to only include the major/minor scales. losing all the modes/weird scales opens room up to control the tonal center independently from the root note.

how it works:
-random scale is assigned on launch
-encoders 1/2 move the key center but do not adjust the root note.
-key 2 + enc 2 adjusts the position of the notes

since the notes in the sequence are never moving more than a whole step between modulations you can get away with some pretty weird changes (button 3 picks a random scale). but you have to be careful, it crosses into jazz fusiony excess pretty quickly.

two big problems i am stuck on:
-sometimes redraw only gets 15 notes of the scale instead of the full 16 and refuses to draw. the scale is definitely sending 16 notes. probably some inefficiency somewhere is not letting it get all the notes in time
-the functions to change key only go a half step up (should go up or down or stay within some predetermined range). this should be easy to fix but for some reason i have not been able to do it.

next up:
-rewrite this all again to try to find/fix those two problems
-add in concept of measures, allow pattern recording of scale changes over measures to create chord progressions of x length
-add midi in to set scales based on keyboard, old 2000s yamaha backing track style

@okyeron here is the code i was referring to last week.

13 Likes

work in progress of meadowphysics port. mostly looking for feedback on if my library type implementation is correct. also any suggestions on the design are welcome.

create new mp like this

local meadowphysics = equire "ansible/meadowphysics"

mp = meadowphysics.new()

when a meadowphysics trigger occurs there is a callback function mp_event. user can implement their own function for this. passed in parameter is the row where the trigger occurred.

mp.mp_event = play_note

function play_note(i)
  --do something
end

user implements their own clock, and calls the mp clock function at each tick. this might need updating as I haven’t gotten too much into the meadowphysics speed code.

clk = metro.alloc()
clk.time = 60 / 240
clk.count = -1
clk.callback = tick
clk:start()

function tick()
  mp:clock()
end

user calls the gridredraw method passing the grid object g. maybe there’s no need for this

mp:gridredraw(g)

user calls the grid event method passing x, y, and z

function g.event(x, y, z)
  mp:gridevent(x, y, z)
end

library implementation

new

local mp = {}
mp.__index = mp

function mp.new()
  local m = {}
  setmetatable(m, mp)
  m.foo = bar

  return m
end

function mp:clock()
  --do clock stuff
end

function mp:gridevent(x, y, z)
  --do grid stuff
end

function mp:gridredraw(g)
  gridbuf:render(g)
  g.refresh()
end

return mp
4 Likes

Im sure that could be done much more elegant, but anyway -

https://gist.github.com/w30n/84b5512012aef967fd1b7c10f56fcd56

Any tips appreciated!

edit: updated gist

4 Likes

looking forward to this - you seem to have gone a similar way than I did with Kria (presumably intentionally) which is cool

After the Kria epic I’ve had to get back to paid work - need to earn a living :wink: - but will hopefully get some more Norns/code time soon and will dive in

I’ve got a couple of things I want to make with Meadowphysics!

2 Likes

yes! tried to keep the design consistent with the good work you did for kria. bit of a ways to go until it’s finished, but your kria library has and will be a great help.

1 Like

Here is a simple isomorphic “in-key” MIDI keyboard for Norns and Grid. There are two MIDI channels, one for each hand. Only notes in the selected scale are shown, with the tonic highlighted. The notes are arranged with five semitones between each row (each note is a fourth from the note below it).

Of course you’ll need a midi interface (something like this) to send the MIDI to an external synth or DAW.

11 Likes

I made a really vanilla 16 step sample sequencer!

The main thing I was trying to achieve is a clean decoupling of state, actions, and UI (my limited front-end js experience definitely guiding here).

I also actually developed the whole script as in the literate programming style with org-babel. So you can see the code interspersed with my thoughts/motivations here.

I’d love feedback on the architecture of the script (though it is pretty tiny) as well as on the format of the blog post. Also if you have any feature ideas that would be interesting or challenging to implement, they’re also very welcome.

9 Likes

Thanks very much for this, very helpful for a newbie like me!

Glad to hear it’s useful :smiley: Please reach out if anythings unclear or if there’s anything you’re interested in implementing or digging into.

1 Like

Still struggling with this in pairs function, so I wanted to ask if my following understanding is correct.

In “Running our beat” you are using
for instrument, triggered in pairs(state.steps[state.position]) do
without having defined instrument and triggered before. I assume this works because:

pairs looks at the four values in the array state.steps (e.g. false, true, true, false) at the current state.position (e.g. 2), and returns each of these values as a key-value pair (e.g. key 1 = value false, key 2 = value true, key 3 = value true, key 4 = value false). Then for each of these key-value pairs the local variable instrument is set to key and the local variable triggered is set to value, and the code block between do and end is executed. Both instrument and triggered are only valid inside this for…do statement.

Did I understand this correctly?

1 Like

I haven’t looked through all of the code, but I am fairly sure that in this case, instrument and triggered are just variables for the loop. You could write it this way too…

for i, j in pairs(some table of stuff)

or, if you do not need the index…

for _, j in pairs(blah blah)

It just looks at the index ( i in my case, instrument in the example) and the value (j or triggered) in pairs.

3 Likes

In regards to variable scoping yes, instrument and triggered are only valid inside this for loop, and will be replaced each iteration of the loop.

To be honest I haven’t totally grokked variable scoping in lua yet :sweat_smile:

update on traffic.lua, my reworking of loom to decouple a sequence from its harmonic content.

the recent changes to the app have produced some music that’s got me feeling introspective so this will be needlessly long.

awhile ago i read that when pt anderson decided to adapt pynchon’s ‘inherent vice’, the first thing he did was type up the entire book, line by line, in screenplay format, to get a feel of what it had felt like to write the book. i thought this seemed like a good idea for my next version of loom, to teach myself vim and “truly understand” how the script worked.

this in retrospect was probably not a good use of my time because i almost immediately forgot how the app worked after i was done and i also introduced a bunch of typos that took forever to find.

but i’ve got 98% of the kinks worked out with my scale changing functions. i’ve added in tehn’s pattern recording to allow for the repetition of a sequence of scales of transpositions of arbitrary length, and i’ve added support for transposing within the key via midi.

i also tried to decouple the triggers from the notes but this proved to be both a lot harder and less useful than i thought, so i only wound up including 4 modes for trigger speed: normal, slower, paused, reverse.

i have gotten to the point where i don’t actually know where to go next with this code, aside from tweaking some edge case bugs as they pop up i am probably done with this idea in its current form.

the impulse with this project was to handle changing chords and modulations in a more dynamic way without interrupting flow (eg during a performance). but i’m not a performer, and what i find most handy about the script is its ability to quickly generate motifs and then refine them using harmony until until they’re something interesting. so for my next exploration, i want push the refinement possibilities further without losing the immediacy of the generative aspects.

some broad design goals:
—design a new grid sequencer that facilitates easy reassignment of individual notes and triggers without breaking a chord tone.
—bring wacom integration to sequencer. if the information can be processed without too big of a cpu hit this seems like an ideal way of refining sequences. map grid to tablet 1:1, select notes with pen, process with arc…
—if you hear a happy accident it should be possible to get back to it. on the norns screen, some abstraction of a timeline with options to set loop points within the previous events, ala the party van. maybe should be able to set sequencer to rewind mode to undo changes.
—should be a tab on norns’ display for synth paramaters.
—synth scenes that can be morphed between over a defined period of time.
—never use the brightest LED level for more than, say, 8 LEDs at a time. LED noise on this was a constant battle but too closely entwined with the sequencer for me to wrap my head around.

anyway here’s the script!

if you try it out and something isn’t working or is unclear please let me know! thanks again everyone.

19 Likes

Great music - looking forward to trying out the code :thinking:

1 Like

Will this be released into the dust repository or whatever its successor might be? I’d love to give it a spin.

not 100% sure this was directed at me but just in case—i am still too github impaired to figure out how to do it, and have sort have been waiting to figure out what actually is going to happen to dust before i invest any time in it.

even though i called it done, after having used it for a couple of weeks i already have some more “small tweaks” i want to change. in the process i’ve discover the fundamental problem with this hobby:

—it takes time to build an instrument
—you need to play an instrument to know what it needs
—it takes time to learn to play an instrument
—doing one causes you to forget the other

it should “just work” if you copy/paste it into maiden though unless i’ve done something really wrong

4 Likes

Just coming across this now. Great mind map you left for us to read/learn from. Thanks!

1 Like

Quite an interesting instrument you’ve built. First play with it this evening. Really like the harmonic constraints you built in. Some may want more dissonances, but IMB one can’t go wrong with he Circle of 5ths. Looking forward to spending more time.

1 Like

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